From 99493d42647d3f52c557e8d9a0d92814b269ce4a Mon Sep 17 00:00:00 2001 From: Ron Rise Date: Mon, 1 Dec 2025 10:21:04 -0500 Subject: [PATCH 1/6] feat: add Swagger integration with OpenAPI annotations and endpoints --- .gitea/workflows/build.yml | 6 + composer.json | 3 +- composer.lock | 565 ++++++++++++++++------ src/Api.php | 7 + src/Controllers/Controller.php | 14 + src/Controllers/HealthcheckController.php | 4 + src/Controllers/IndexController.php | 21 + src/Controllers/SwaggerController.php | 34 ++ src/Helpers/Version.php | 10 + src/Models/User.php | 27 +- 10 files changed, 546 insertions(+), 145 deletions(-) create mode 100644 src/Controllers/SwaggerController.php create mode 100644 src/Helpers/Version.php 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/src/Api.php b/src/Api.php index 4393069..77d81bd 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\SwaggerController; use Siteworxpro\App\Http\JsonResponseFactory; use Siteworxpro\App\Http\Middleware\CorsMiddleware; use Siteworxpro\App\Http\Middleware\JwtMiddleware; @@ -71,6 +73,11 @@ class Api $this->router->get('/', IndexController::class . '::get'); $this->router->get('/healthz', HealthcheckController::class . '::get'); + $this->router->group('/.well-known', function (RouteGroup $router) { + $router->get('/swagger.yaml', SwaggerController::class . '::get'); + $router->get('/swagger.json', SwaggerController::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..c65bb28 100644 --- a/src/Controllers/HealthcheckController.php +++ b/src/Controllers/HealthcheckController.php @@ -11,6 +11,7 @@ use Siteworxpro\App\Http\JsonResponseFactory; use Siteworxpro\App\Models\Model; use Siteworxpro\App\Services\Facades\Redis; use Siteworxpro\HttpStatus\CodesEnum; +use OpenApi\Attributes as OA; /** * Class HealthcheckController @@ -24,6 +25,9 @@ class HealthcheckController extends Controller /** * @throws \JsonException */ + #[OA\Get(path: '/healthz')] + #[OA\Response(response: '200', description: 'Healthcheck OK')] + #[OA\Response(response: '503', description: 'Healthcheck Failed')] public function get(ServerRequest $request): ResponseInterface { try { diff --git a/src/Controllers/IndexController.php b/src/Controllers/IndexController.php index 1547df5..e320deb 100644 --- a/src/Controllers/IndexController.php +++ b/src/Controllers/IndexController.php @@ -8,6 +8,7 @@ use Nyholm\Psr7\ServerRequest; use Psr\Http\Message\ResponseInterface; use Siteworxpro\App\Attributes\Guards; use Siteworxpro\App\Http\JsonResponseFactory; +use OpenApi\Attributes as OA; /** * Class IndexController @@ -24,6 +25,15 @@ class IndexController extends Controller #[Guards\Jwt] #[Guards\Scope(['get.index', 'status.check'])] #[Guards\RequireAllScopes] + #[OA\Get(path: '/')] + #[OA\Response( + response: '200', + description: 'An Example Response', + content: new OA\JsonContent(properties: [ + new OA\Property('status_code', type: 'integer'), + new OA\Property('message', type: 'string'), + ]) + )] public function get(ServerRequest $request): ResponseInterface { return JsonResponseFactory::createJsonResponse(['status_code' => 200, 'message' => 'Server is running']); @@ -34,6 +44,17 @@ class IndexController extends Controller */ #[Guards\Jwt] #[Guards\Scope(['post.index'])] + #[OA\Post(path: '/')] + #[OA\Response( + response: '200', + description: 'An Example Response', + content: new OA\JsonContent( + properties: [ + new OA\Property('status_code', type: 'integer'), + new OA\Property('message', type: 'string'), + ] + ) + )] public function post(ServerRequest $request): ResponseInterface { return JsonResponseFactory::createJsonResponse(['status_code' => 200, 'message' => 'Server is running']); diff --git a/src/Controllers/SwaggerController.php b/src/Controllers/SwaggerController.php new file mode 100644 index 0000000..0b03260 --- /dev/null +++ b/src/Controllers/SwaggerController.php @@ -0,0 +1,34 @@ +generate([ + __DIR__ . '/../Controllers', + __DIR__ . '/../Models', + ]); + + $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/Helpers/Version.php b/src/Helpers/Version.php new file mode 100644 index 0000000..51bfeda --- /dev/null +++ b/src/Helpers/Version.php @@ -0,0 +1,10 @@ +attributes['id'] = $this->attributes['id'] ?? Ulid::generate(); + } + public function getFullNameAttribute(): string { return "$this->first_name $this->last_name"; -- 2.49.1 From a397177896925e00620e27d5ca6c8a5678189141 Mon Sep 17 00:00:00 2001 From: Ron Rise Date: Mon, 1 Dec 2025 10:31:20 -0500 Subject: [PATCH 2/6] feat: add Swagger UI service to docker-compose and update OpenAPI annotations in controllers --- docker-compose.yml | 14 ++++++++++++++ src/Controllers/HealthcheckController.php | 4 +++- src/Controllers/IndexController.php | 6 ++++-- 3 files changed, 21 insertions(+), 3 deletions(-) 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/Controllers/HealthcheckController.php b/src/Controllers/HealthcheckController.php index c65bb28..f1884fa 100644 --- a/src/Controllers/HealthcheckController.php +++ b/src/Controllers/HealthcheckController.php @@ -23,9 +23,11 @@ use OpenApi\Attributes as OA; class HealthcheckController extends Controller { /** + * Handles the GET request for health check. + * * @throws \JsonException */ - #[OA\Get(path: '/healthz')] + #[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 diff --git a/src/Controllers/IndexController.php b/src/Controllers/IndexController.php index e320deb..5fbbcda 100644 --- a/src/Controllers/IndexController.php +++ b/src/Controllers/IndexController.php @@ -25,7 +25,7 @@ class IndexController extends Controller #[Guards\Jwt] #[Guards\Scope(['get.index', 'status.check'])] #[Guards\RequireAllScopes] - #[OA\Get(path: '/')] + #[OA\Get(path: '/', tags: ['Examples'])] #[OA\Response( response: '200', description: 'An Example Response', @@ -40,11 +40,13 @@ class IndexController extends Controller } /** + * Handles the POST request for the index route. + * * @throws \JsonException */ #[Guards\Jwt] #[Guards\Scope(['post.index'])] - #[OA\Post(path: '/')] + #[OA\Post(path: '/', tags: ['Examples'])] #[OA\Response( response: '200', description: 'An Example Response', -- 2.49.1 From abc4fcf5447996012a98839a6649fc0376167aa8 Mon Sep 17 00:00:00 2001 From: Ron Rise Date: Mon, 1 Dec 2025 10:34:38 -0500 Subject: [PATCH 3/6] feat: add Swagger UI service to docker-compose and update OpenAPI annotations in controllers --- src/Api.php | 6 +++--- .../{SwaggerController.php => OpenApiController.php} | 8 +++++++- 2 files changed, 10 insertions(+), 4 deletions(-) rename src/Controllers/{SwaggerController.php => OpenApiController.php} (80%) diff --git a/src/Api.php b/src/Api.php index 77d81bd..99f7bdc 100644 --- a/src/Api.php +++ b/src/Api.php @@ -11,7 +11,7 @@ use League\Route\Router; use Nyholm\Psr7\Factory\Psr17Factory; use Siteworxpro\App\Controllers\HealthcheckController; use Siteworxpro\App\Controllers\IndexController; -use Siteworxpro\App\Controllers\SwaggerController; +use Siteworxpro\App\Controllers\OpenApiController; use Siteworxpro\App\Http\JsonResponseFactory; use Siteworxpro\App\Http\Middleware\CorsMiddleware; use Siteworxpro\App\Http\Middleware\JwtMiddleware; @@ -74,8 +74,8 @@ class Api $this->router->get('/healthz', HealthcheckController::class . '::get'); $this->router->group('/.well-known', function (RouteGroup $router) { - $router->get('/swagger.yaml', SwaggerController::class . '::get'); - $router->get('/swagger.json', SwaggerController::class . '::get'); + $router->get('/swagger.yaml', OpenApiController::class . '::get'); + $router->get('/swagger.json', OpenApiController::class . '::get'); }); $this->router->middleware(new CorsMiddleware()); diff --git a/src/Controllers/SwaggerController.php b/src/Controllers/OpenApiController.php similarity index 80% rename from src/Controllers/SwaggerController.php rename to src/Controllers/OpenApiController.php index 0b03260..0fdeda9 100644 --- a/src/Controllers/SwaggerController.php +++ b/src/Controllers/OpenApiController.php @@ -9,8 +9,14 @@ use Nyholm\Psr7\ServerRequest; use OpenApi\Generator; use Psr\Http\Message\ResponseInterface; -class SwaggerController extends Controller +class OpenApiController extends Controller { + /** + * Handles the GET request to generate and return the OpenAPI specification. + * + * @param ServerRequest $request + * @return ResponseInterface + */ public function get(ServerRequest $request): ResponseInterface { $openapi = new Generator()->generate([ -- 2.49.1 From 3a82c5028da5d75192a5b496aa5208f5fc01843e Mon Sep 17 00:00:00 2001 From: Ron Rise Date: Mon, 1 Dec 2025 10:48:40 -0500 Subject: [PATCH 4/6] feat: add POST endpoint and security annotations to IndexController, implement TokenSecurity and UnauthorizedResponse --- src/Api.php | 1 + src/Controllers/IndexController.php | 8 ++++++-- src/Docs/TokenSecurity.php | 19 +++++++++++++++++++ src/Docs/UnauthorizedResponse.php | 26 ++++++++++++++++++++++++++ 4 files changed, 52 insertions(+), 2 deletions(-) create mode 100644 src/Docs/TokenSecurity.php create mode 100644 src/Docs/UnauthorizedResponse.php diff --git a/src/Api.php b/src/Api.php index 99f7bdc..c3816a7 100644 --- a/src/Api.php +++ b/src/Api.php @@ -71,6 +71,7 @@ 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) { diff --git a/src/Controllers/IndexController.php b/src/Controllers/IndexController.php index 5fbbcda..c5d1c70 100644 --- a/src/Controllers/IndexController.php +++ b/src/Controllers/IndexController.php @@ -7,6 +7,8 @@ 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; @@ -25,7 +27,7 @@ class IndexController extends Controller #[Guards\Jwt] #[Guards\Scope(['get.index', 'status.check'])] #[Guards\RequireAllScopes] - #[OA\Get(path: '/', tags: ['Examples'])] + #[OA\Get(path: '/', security: [new TokenSecurity()], tags: ['Examples'])] #[OA\Response( response: '200', description: 'An Example Response', @@ -34,6 +36,7 @@ class IndexController extends Controller new OA\Property('message', type: 'string'), ]) )] + #[UnauthorizedResponse] public function get(ServerRequest $request): ResponseInterface { return JsonResponseFactory::createJsonResponse(['status_code' => 200, 'message' => 'Server is running']); @@ -46,7 +49,7 @@ class IndexController extends Controller */ #[Guards\Jwt] #[Guards\Scope(['post.index'])] - #[OA\Post(path: '/', tags: ['Examples'])] + #[OA\Post(path: '/', security: [new TokenSecurity()], tags: ['Examples'])] #[OA\Response( response: '200', description: 'An Example Response', @@ -57,6 +60,7 @@ class IndexController extends Controller ] ) )] + #[UnauthorizedResponse] public function post(ServerRequest $request): ResponseInterface { return JsonResponseFactory::createJsonResponse(['status_code' => 200, 'message' => 'Server is running']); diff --git a/src/Docs/TokenSecurity.php b/src/Docs/TokenSecurity.php new file mode 100644 index 0000000..2998de2 --- /dev/null +++ b/src/Docs/TokenSecurity.php @@ -0,0 +1,19 @@ + Date: Mon, 1 Dec 2025 11:05:29 -0500 Subject: [PATCH 5/6] feat: implement GenericResponse class and update controllers to use it for consistent JSON responses --- src/Controllers/HealthcheckController.php | 3 ++- src/Controllers/IndexController.php | 17 ++++-------- src/Controllers/OpenApiController.php | 1 + src/Http/JsonResponseFactory.php | 13 ++++++--- src/Http/Responses/GenericResponse.php | 32 +++++++++++++++++++++++ 5 files changed, 50 insertions(+), 16 deletions(-) create mode 100644 src/Http/Responses/GenericResponse.php diff --git a/src/Controllers/HealthcheckController.php b/src/Controllers/HealthcheckController.php index f1884fa..ac93f20 100644 --- a/src/Controllers/HealthcheckController.php +++ b/src/Controllers/HealthcheckController.php @@ -8,6 +8,7 @@ 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; @@ -53,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 c5d1c70..5ccb26e 100644 --- a/src/Controllers/IndexController.php +++ b/src/Controllers/IndexController.php @@ -11,6 +11,7 @@ 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 @@ -31,15 +32,12 @@ class IndexController extends Controller #[OA\Response( response: '200', description: 'An Example Response', - content: new OA\JsonContent(properties: [ - new OA\Property('status_code', type: 'integer'), - new OA\Property('message', type: 'string'), - ]) + 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(['message' => 'Server is running']); } /** @@ -53,16 +51,11 @@ class IndexController extends Controller #[OA\Response( response: '200', description: 'An Example Response', - content: new OA\JsonContent( - properties: [ - new OA\Property('status_code', type: 'integer'), - new OA\Property('message', type: 'string'), - ] - ) + 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 index 0fdeda9..85b5513 100644 --- a/src/Controllers/OpenApiController.php +++ b/src/Controllers/OpenApiController.php @@ -22,6 +22,7 @@ class OpenApiController extends Controller $openapi = new Generator()->generate([ __DIR__ . '/../Controllers', __DIR__ . '/../Models', + __DIR__ . '/../Http/Responses', ]); $response = new Response(); diff --git a/src/Http/JsonResponseFactory.php b/src/Http/JsonResponseFactory.php index 005412f..69eb779 100644 --- a/src/Http/JsonResponseFactory.php +++ b/src/Http/JsonResponseFactory.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace Siteworxpro\App\Http; +use Illuminate\Contracts\Support\Arrayable; use Nyholm\Psr7\Response; use Siteworxpro\HttpStatus\CodesEnum; @@ -17,13 +18,19 @@ class JsonResponseFactory /** * Create a JSON response with the given data and status code. * - * @param array $data The data to include in the response. + * @param array|Arrayable $data The data to include in the response. * @param CodesEnum $statusCode The HTTP status code for the response. * @return Response The JSON response. * @throws \JsonException */ - public static function createJsonResponse(array $data, CodesEnum $statusCode = CodesEnum::OK): Response - { + public static function createJsonResponse( + array|Arrayable $data, + CodesEnum $statusCode = CodesEnum::OK + ): Response { + if ($data instanceof Arrayable) { + $data = $data->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, + ]; + } +} -- 2.49.1 From af2f1645465b173d43122c0931b029e65197e9cb Mon Sep 17 00:00:00 2001 From: Ron Rise Date: Mon, 1 Dec 2025 11:16:59 -0500 Subject: [PATCH 6/6] feat: implement GenericResponse class and update controllers to use it for consistent JSON responses --- src/Controllers/IndexController.php | 2 +- src/Docs/TokenSecurity.php | 2 +- tests/Controllers/IndexControllerTest.php | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Controllers/IndexController.php b/src/Controllers/IndexController.php index 5ccb26e..e9a9059 100644 --- a/src/Controllers/IndexController.php +++ b/src/Controllers/IndexController.php @@ -37,7 +37,7 @@ class IndexController extends Controller #[UnauthorizedResponse] public function get(ServerRequest $request): ResponseInterface { - return JsonResponseFactory::createJsonResponse(['message' => 'Server is running']); + return JsonResponseFactory::createJsonResponse(new GenericResponse('Server is running')); } /** diff --git a/src/Docs/TokenSecurity.php b/src/Docs/TokenSecurity.php index 2998de2..7376ab6 100644 --- a/src/Docs/TokenSecurity.php +++ b/src/Docs/TokenSecurity.php @@ -16,4 +16,4 @@ class TokenSecurity extends OA\SecurityScheme scheme: 'bearer' ); } -} \ No newline at end of file +} 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()); } } -- 2.49.1