diff --git a/.gitea/workflows/build.yml b/.gitea/workflows/build.yml index 30d9bd7..31c19b5 100644 --- a/.gitea/workflows/build.yml +++ b/.gitea/workflows/build.yml @@ -26,6 +26,12 @@ jobs: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} + - name: Write Version File + run: | + echo $GITEA_REF_NAME > VERSION + sed -i "s/dev-version/${GITEA_REF_NAME}/g" src/Helpers/Version.php + + - name: 🏗️ 🔧 Set up Docker Buildx uses: docker/setup-buildx-action@v3 diff --git a/composer.json b/composer.json index c0b8af1..2ca465d 100644 --- a/composer.json +++ b/composer.json @@ -24,7 +24,8 @@ "monolog/monolog": "^3.9", "react/promise": "^3", "react/async": "^4", - "guzzlehttp/guzzle": "^7.10" + "guzzlehttp/guzzle": "^7.10", + "zircote/swagger-php": "^5.7" }, "require-dev": { "phpunit/phpunit": "^12.4", diff --git a/composer.lock b/composer.lock index 2c208b5..eb3ac31 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "8c2444c3a25a3469cf369de1c085ad01", + "content-hash": "f12aaf0dae6930c226e719a5705e3f91", "packages": [ { "name": "adhocore/cli", @@ -1559,6 +1559,64 @@ }, "time": "2018-02-13T20:26:39+00:00" }, + { + "name": "nikic/php-parser", + "version": "v5.6.2", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "3a454ca033b9e06b63282ce19562e892747449bb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/3a454ca033b9e06b63282ce19562e892747449bb", + "reference": "3a454ca033b9e06b63282ce19562e892747449bb", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-json": "*", + "ext-tokenizer": "*", + "php": ">=7.4" + }, + "require-dev": { + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^9.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v5.6.2" + }, + "time": "2025-10-21T19:32:17+00:00" + }, { "name": "nyholm/psr7", "version": "1.8.2", @@ -1637,6 +1695,53 @@ ], "time": "2024-09-09T07:06:30+00:00" }, + { + "name": "phpstan/phpdoc-parser", + "version": "2.3.0", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpdoc-parser.git", + "reference": "1e0cd5370df5dd2e556a36b9c62f62e555870495" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/1e0cd5370df5dd2e556a36b9c62f62e555870495", + "reference": "1e0cd5370df5dd2e556a36b9c62f62e555870495", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "doctrine/annotations": "^2.0", + "nikic/php-parser": "^5.3.0", + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^9.6", + "symfony/process": "^5.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "PHPStan\\PhpDocParser\\": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPDoc parser with support for nullable, intersection and generic types", + "support": { + "issues": "https://github.com/phpstan/phpdoc-parser/issues", + "source": "https://github.com/phpstan/phpdoc-parser/tree/2.3.0" + }, + "time": "2025-08-30T15:50:23+00:00" + }, { "name": "predis/predis", "version": "v3.2.0", @@ -3154,6 +3259,157 @@ ], "time": "2024-09-25T14:21:43+00:00" }, + { + "name": "symfony/finder", + "version": "v8.0.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/finder.git", + "reference": "7598dd5770580fa3517ec83e8da0c9b9e01f4291" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/finder/zipball/7598dd5770580fa3517ec83e8da0c9b9e01f4291", + "reference": "7598dd5770580fa3517ec83e8da0c9b9e01f4291", + "shasum": "" + }, + "require": { + "php": ">=8.4" + }, + "require-dev": { + "symfony/filesystem": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Finder\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Finds files and directories via an intuitive fluent interface", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/finder/tree/v8.0.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-11-05T14:36:47+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "provide": { + "ext-ctype": "*" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, { "name": "symfony/polyfill-mbstring", "version": "v1.33.0", @@ -3661,6 +3917,82 @@ ], "time": "2025-07-15T13:41:35+00:00" }, + { + "name": "symfony/yaml", + "version": "v7.4.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/yaml.git", + "reference": "6c84a4b55aee4cd02034d1c528e83f69ddf63810" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/yaml/zipball/6c84a4b55aee4cd02034d1c528e83f69ddf63810", + "reference": "6c84a4b55aee4cd02034d1c528e83f69ddf63810", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-ctype": "^1.8" + }, + "conflict": { + "symfony/console": "<6.4" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0|^8.0" + }, + "bin": [ + "Resources/bin/yaml-lint" + ], + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Yaml\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Loads and dumps YAML files", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/yaml/tree/v7.4.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-11-16T10:14:42+00:00" + }, { "name": "voku/portable-ascii", "version": "2.0.3", @@ -3734,6 +4066,94 @@ } ], "time": "2024-11-21T01:49:47+00:00" + }, + { + "name": "zircote/swagger-php", + "version": "5.7.5", + "source": { + "type": "git", + "url": "https://github.com/zircote/swagger-php.git", + "reference": "9a37739401485b42d779495e70548309820d11d6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/zircote/swagger-php/zipball/9a37739401485b42d779495e70548309820d11d6", + "reference": "9a37739401485b42d779495e70548309820d11d6", + "shasum": "" + }, + "require": { + "ext-json": "*", + "nikic/php-parser": "^4.19 || ^5.0", + "php": ">=7.4", + "phpstan/phpdoc-parser": "^2.0", + "psr/log": "^1.1 || ^2.0 || ^3.0", + "symfony/deprecation-contracts": "^2 || ^3", + "symfony/finder": "^5.0 || ^6.0 || ^7.0 || ^8.0", + "symfony/yaml": "^5.4 || ^6.0 || ^7.0 || ^8.0" + }, + "conflict": { + "symfony/process": ">=6, <6.4.14" + }, + "require-dev": { + "composer/package-versions-deprecated": "^1.11", + "doctrine/annotations": "^2.0", + "friendsofphp/php-cs-fixer": "^3.62.0", + "phpstan/phpstan": "^1.6 || ^2.0", + "phpunit/phpunit": "^9.0", + "rector/rector": "^1.0 || ^2.0", + "vimeo/psalm": "^4.30 || ^5.0" + }, + "suggest": { + "doctrine/annotations": "^2.0", + "radebatz/type-info-extras": "^1.0.2" + }, + "bin": [ + "bin/openapi" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "OpenApi\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Robert Allen", + "email": "zircote@gmail.com" + }, + { + "name": "Bob Fanger", + "email": "bfanger@gmail.com", + "homepage": "https://bfanger.nl" + }, + { + "name": "Martin Rademacher", + "email": "mano@radebatz.net", + "homepage": "https://radebatz.net" + } + ], + "description": "Generate interactive documentation for your RESTful API using PHP attributes (preferred) or PHPDoc annotations", + "homepage": "https://github.com/zircote/swagger-php", + "keywords": [ + "api", + "json", + "rest", + "service discovery" + ], + "support": { + "issues": "https://github.com/zircote/swagger-php/issues", + "source": "https://github.com/zircote/swagger-php/tree/5.7.5" + }, + "time": "2025-11-28T23:22:21+00:00" } ], "packages-dev": [ @@ -4027,64 +4447,6 @@ ], "time": "2025-08-01T08:46:24+00:00" }, - { - "name": "nikic/php-parser", - "version": "v5.6.2", - "source": { - "type": "git", - "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "3a454ca033b9e06b63282ce19562e892747449bb" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/3a454ca033b9e06b63282ce19562e892747449bb", - "reference": "3a454ca033b9e06b63282ce19562e892747449bb", - "shasum": "" - }, - "require": { - "ext-ctype": "*", - "ext-json": "*", - "ext-tokenizer": "*", - "php": ">=7.4" - }, - "require-dev": { - "ircmaxell/php-yacc": "^0.0.7", - "phpunit/phpunit": "^9.0" - }, - "bin": [ - "bin/php-parse" - ], - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "5.x-dev" - } - }, - "autoload": { - "psr-4": { - "PhpParser\\": "lib/PhpParser" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Nikita Popov" - } - ], - "description": "A PHP parser written in PHP", - "keywords": [ - "parser", - "php" - ], - "support": { - "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v5.6.2" - }, - "time": "2025-10-21T19:32:17+00:00" - }, { "name": "phar-io/manifest", "version": "2.0.4", @@ -5821,89 +6183,6 @@ ], "time": "2025-11-04T01:21:42+00:00" }, - { - "name": "symfony/polyfill-ctype", - "version": "v1.33.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638", - "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638", - "shasum": "" - }, - "require": { - "php": ">=7.2" - }, - "provide": { - "ext-ctype": "*" - }, - "suggest": { - "ext-ctype": "For best performance" - }, - "type": "library", - "extra": { - "thanks": { - "url": "https://github.com/symfony/polyfill", - "name": "symfony/polyfill" - } - }, - "autoload": { - "files": [ - "bootstrap.php" - ], - "psr-4": { - "Symfony\\Polyfill\\Ctype\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Gert de Pagter", - "email": "BackEndTea@gmail.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill for ctype functions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "ctype", - "polyfill", - "portable" - ], - "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.33.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://github.com/nicolas-grekas", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-09-09T11:45:10+00:00" - }, { "name": "symfony/polyfill-intl-grapheme", "version": "v1.33.0", @@ -6370,7 +6649,7 @@ "prefer-stable": false, "prefer-lowest": false, "platform": { - "php": "^8.4" + "php": "^8.5" }, "platform-dev": {}, "plugin-api-version": "2.9.0" diff --git a/docker-compose.yml b/docker-compose.yml index 38e0e6f..5d91921 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -37,6 +37,20 @@ services: environment: PHP_IDE_CONFIG: serverName=localhost + swagger-ui: + labels: + - "traefik.enable=true" + - "traefik.http.routers.swagger-ui.entrypoints=web-secure" + - "traefik.http.routers.swagger-ui.rule=Host(`localhost`) && PathPrefix(`/docs`)" + - "traefik.http.routers.swagger-ui.tls=true" + - "traefik.http.routers.swagger-ui.service=swagger-ui" + - "traefik.http.services.swagger-ui.loadbalancer.server.port=8080" + image: swaggerapi/swagger-ui:latest + container_name: swagger-ui + environment: + BASE_URL: /docs + URL: /.well-known/swagger.yaml + migration-container: volumes: - ./db/migrations:/app/db/migrations diff --git a/src/Api.php b/src/Api.php index 4393069..c3816a7 100644 --- a/src/Api.php +++ b/src/Api.php @@ -6,10 +6,12 @@ namespace Siteworxpro\App; use League\Route\Http\Exception\MethodNotAllowedException; use League\Route\Http\Exception\NotFoundException; +use League\Route\RouteGroup; use League\Route\Router; use Nyholm\Psr7\Factory\Psr17Factory; use Siteworxpro\App\Controllers\HealthcheckController; use Siteworxpro\App\Controllers\IndexController; +use Siteworxpro\App\Controllers\OpenApiController; use Siteworxpro\App\Http\JsonResponseFactory; use Siteworxpro\App\Http\Middleware\CorsMiddleware; use Siteworxpro\App\Http\Middleware\JwtMiddleware; @@ -69,8 +71,14 @@ class Api $this->router = new Router(); $this->router->get('/', IndexController::class . '::get'); + $this->router->post('/', IndexController::class . '::post'); $this->router->get('/healthz', HealthcheckController::class . '::get'); + $this->router->group('/.well-known', function (RouteGroup $router) { + $router->get('/swagger.yaml', OpenApiController::class . '::get'); + $router->get('/swagger.json', OpenApiController::class . '::get'); + }); + $this->router->middleware(new CorsMiddleware()); $this->router->middleware(new JwtMiddleware()); $this->router->middleware(new ScopeMiddleware()); diff --git a/src/Controllers/Controller.php b/src/Controllers/Controller.php index f74147f..aa579d0 100644 --- a/src/Controllers/Controller.php +++ b/src/Controllers/Controller.php @@ -6,7 +6,9 @@ namespace Siteworxpro\App\Controllers; use League\Route\Http\Exception\NotFoundException; use Nyholm\Psr7\ServerRequest; +use OpenApi\Attributes as OA; use Psr\Http\Message\ResponseInterface; +use Siteworxpro\App\Helpers\Version; /** * Class Controller @@ -15,6 +17,18 @@ use Psr\Http\Message\ResponseInterface; * * @package Siteworxpro\App\Controllers */ +#[OA\Info( + version: Version::VERSION, + description: "This is a template API built using Siteworxpro framework.", + title: "Siteworxpro Template API", + contact: new OA\Contact( + name: "Siteworxpro", + url: "https://www.siteworxpro.com", + email: "support@siteworxpro.com" + ), + license: new OA\License('MIT', 'https://opensource.org/licenses/MIT') +)] +#[OA\Server(url: "https://localhost", description: "Local Server")] abstract class Controller implements ControllerInterface { /** diff --git a/src/Controllers/HealthcheckController.php b/src/Controllers/HealthcheckController.php index c6f6ddf..ac93f20 100644 --- a/src/Controllers/HealthcheckController.php +++ b/src/Controllers/HealthcheckController.php @@ -8,9 +8,11 @@ use Illuminate\Database\PostgresConnection; use Nyholm\Psr7\ServerRequest; use Psr\Http\Message\ResponseInterface; use Siteworxpro\App\Http\JsonResponseFactory; +use Siteworxpro\App\Http\Responses\GenericResponse; use Siteworxpro\App\Models\Model; use Siteworxpro\App\Services\Facades\Redis; use Siteworxpro\HttpStatus\CodesEnum; +use OpenApi\Attributes as OA; /** * Class HealthcheckController @@ -22,8 +24,13 @@ use Siteworxpro\HttpStatus\CodesEnum; class HealthcheckController extends Controller { /** + * Handles the GET request for health check. + * * @throws \JsonException */ + #[OA\Get(path: '/healthz', tags: ['Healthcheck'])] + #[OA\Response(response: '200', description: 'Healthcheck OK')] + #[OA\Response(response: '503', description: 'Healthcheck Failed')] public function get(ServerRequest $request): ResponseInterface { try { @@ -47,7 +54,7 @@ class HealthcheckController extends Controller } return JsonResponseFactory::createJsonResponse( - ['status_code' => 200, 'message' => 'Healthcheck OK'] + new GenericResponse('Healthcheck OK', CodesEnum::OK->value) ); } } diff --git a/src/Controllers/IndexController.php b/src/Controllers/IndexController.php index 1547df5..e9a9059 100644 --- a/src/Controllers/IndexController.php +++ b/src/Controllers/IndexController.php @@ -7,7 +7,11 @@ namespace Siteworxpro\App\Controllers; use Nyholm\Psr7\ServerRequest; use Psr\Http\Message\ResponseInterface; use Siteworxpro\App\Attributes\Guards; +use Siteworxpro\App\Docs\TokenSecurity; +use Siteworxpro\App\Docs\UnauthorizedResponse; use Siteworxpro\App\Http\JsonResponseFactory; +use OpenApi\Attributes as OA; +use Siteworxpro\App\Http\Responses\GenericResponse; /** * Class IndexController @@ -24,18 +28,34 @@ class IndexController extends Controller #[Guards\Jwt] #[Guards\Scope(['get.index', 'status.check'])] #[Guards\RequireAllScopes] + #[OA\Get(path: '/', security: [new TokenSecurity()], tags: ['Examples'])] + #[OA\Response( + response: '200', + description: 'An Example Response', + content: new OA\JsonContent(ref: '#/components/schemas/GenericResponse') + )] + #[UnauthorizedResponse] public function get(ServerRequest $request): ResponseInterface { - return JsonResponseFactory::createJsonResponse(['status_code' => 200, 'message' => 'Server is running']); + return JsonResponseFactory::createJsonResponse(new GenericResponse('Server is running')); } /** + * Handles the POST request for the index route. + * * @throws \JsonException */ #[Guards\Jwt] #[Guards\Scope(['post.index'])] + #[OA\Post(path: '/', security: [new TokenSecurity()], tags: ['Examples'])] + #[OA\Response( + response: '200', + description: 'An Example Response', + content: new OA\JsonContent(ref: '#/components/schemas/GenericResponse') + )] + #[UnauthorizedResponse] public function post(ServerRequest $request): ResponseInterface { - return JsonResponseFactory::createJsonResponse(['status_code' => 200, 'message' => 'Server is running']); + return JsonResponseFactory::createJsonResponse(new GenericResponse('POST request received')); } } diff --git a/src/Controllers/OpenApiController.php b/src/Controllers/OpenApiController.php new file mode 100644 index 0000000..85b5513 --- /dev/null +++ b/src/Controllers/OpenApiController.php @@ -0,0 +1,41 @@ +generate([ + __DIR__ . '/../Controllers', + __DIR__ . '/../Models', + __DIR__ . '/../Http/Responses', + ]); + + $response = new Response(); + + if ( + $request->getHeaderLine('Accept') === 'application/json' || + str_contains($request->getUri()->getPath(), '.json') + ) { + $response->getBody()->write($openapi->toJson()); + return $response->withHeader('Content-Type', 'application/json'); + } + + $response->getBody()->write($openapi->toYaml()); + return $response->withHeader('Content-Type', 'application/x-yaml'); + } +} diff --git a/src/Docs/TokenSecurity.php b/src/Docs/TokenSecurity.php new file mode 100644 index 0000000..7376ab6 --- /dev/null +++ b/src/Docs/TokenSecurity.php @@ -0,0 +1,19 @@ +toArray(); + } + return new Response( status: $statusCode->value, headers: [ diff --git a/src/Http/Responses/GenericResponse.php b/src/Http/Responses/GenericResponse.php new file mode 100644 index 0000000..93d01e6 --- /dev/null +++ b/src/Http/Responses/GenericResponse.php @@ -0,0 +1,32 @@ + $this->message, + 'status_code' => $this->statusCode, + ]; + } +} diff --git a/src/Models/User.php b/src/Models/User.php index 53cca25..db0d2ee 100644 --- a/src/Models/User.php +++ b/src/Models/User.php @@ -5,11 +5,13 @@ declare(strict_types=1); namespace Siteworxpro\App\Models; use Carbon\Carbon; +use OpenApi\Attributes as OA; +use Siteworxpro\App\Helpers\Ulid; /** * Class User * - * @property string $id + * @property-read string $id * @property string $first_name * @property string $last_name * @property string $email @@ -19,6 +21,23 @@ use Carbon\Carbon; * @property-read string $full_name * @property-read string $formatted_email */ +#[OA\Schema( + schema: "User", + properties: [ + new OA\Property( + property: "id", + description: "Unique identifier for the user", + type: "string", + format: "ulid", + readOnly: true, + example: '01KBD5WPZKYD77BYM2QD9NKG99' + ), + new OA\Property(property: "first_name", type: "string"), + new OA\Property(property: "last_name", type: "string"), + new OA\Property(property: "email", type: "string", format: "email"), + new OA\Property(property: "created_at", type: "string", format: "date-time"), + ] +)] class User extends Model { protected $casts = [ @@ -36,6 +55,12 @@ class User extends Model 'password', ]; + public function __construct(array $attributes = []) + { + parent::__construct($attributes); + $this->attributes['id'] = $this->attributes['id'] ?? Ulid::generate(); + } + public function getFullNameAttribute(): string { return "$this->first_name $this->last_name"; diff --git a/tests/Controllers/IndexControllerTest.php b/tests/Controllers/IndexControllerTest.php index 2639a6d..67cff2d 100644 --- a/tests/Controllers/IndexControllerTest.php +++ b/tests/Controllers/IndexControllerTest.php @@ -20,7 +20,7 @@ class IndexControllerTest extends AbstractController $response = $controller->get($this->getMockRequest()); $this->assertEquals(200, $response->getStatusCode()); - $this->assertEquals('{"status_code":200,"message":"Server is running"}', (string)$response->getBody()); + $this->assertEquals('{"message":"Server is running","status_code":200}', (string)$response->getBody()); } /** @@ -35,6 +35,6 @@ class IndexControllerTest extends AbstractController $response = $controller->post($this->getMockRequest()); $this->assertEquals(200, $response->getStatusCode()); - $this->assertEquals('{"status_code":200,"message":"Server is running"}', (string)$response->getBody()); + $this->assertEquals('{"message":"POST request received","status_code":200}', (string)$response->getBody()); } }