feat: add Swagger integration with OpenAPI annotations and endpoints
All checks were successful
🧪✨ Tests Workflow / 📝 ✨ Code Lint (push) Successful in 4m0s
🧪✨ Tests Workflow / 🛡️ 🔒 Library Audit (push) Successful in 4m11s
🧪✨ Tests Workflow / 🛡️ 🔒 License Check (push) Successful in 4m22s
🧪✨ Tests Workflow / 🧪 ✨ Database Migrations (push) Successful in 4m42s
🧪✨ Tests Workflow / 🐙 🔍 Code Sniffer (push) Successful in 4m27s
🧪✨ Tests Workflow / 🧪 ✅ Unit Tests (push) Successful in 2m55s

This commit is contained in:
2025-12-01 10:21:04 -05:00
parent cd49507140
commit 99493d4264
10 changed files with 546 additions and 145 deletions

View File

@@ -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());

View File

@@ -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
{
/**

View File

@@ -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 {

View File

@@ -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']);

View File

@@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
namespace Siteworxpro\App\Controllers;
use Nyholm\Psr7\Response;
use Nyholm\Psr7\ServerRequest;
use OpenApi\Generator;
use Psr\Http\Message\ResponseInterface;
class SwaggerController extends Controller
{
public function get(ServerRequest $request): ResponseInterface
{
$openapi = new Generator()->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');
}
}

10
src/Helpers/Version.php Normal file
View File

@@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace Siteworxpro\App\Helpers;
class Version
{
public const string VERSION = 'dev-version';
}

View File

@@ -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";