Enable Discovery debug logging for production troubleshooting
- Add DISCOVERY_LOG_LEVEL=debug - Add DISCOVERY_SHOW_PROGRESS=true - Temporary changes for debugging InitializerProcessor fixes on production
This commit is contained in:
23
src/Framework/OpenApi/Attributes/ApiEndpoint.php
Normal file
23
src/Framework/OpenApi/Attributes/ApiEndpoint.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\OpenApi\Attributes;
|
||||
|
||||
use Attribute;
|
||||
|
||||
/**
|
||||
* Marks a controller method as an API endpoint for OpenAPI documentation
|
||||
*/
|
||||
#[Attribute(Attribute::TARGET_METHOD)]
|
||||
final readonly class ApiEndpoint
|
||||
{
|
||||
public function __construct(
|
||||
public string $summary,
|
||||
public string $description = '',
|
||||
public array $tags = [],
|
||||
public bool $deprecated = false,
|
||||
public ?string $operationId = null,
|
||||
) {
|
||||
}
|
||||
}
|
||||
26
src/Framework/OpenApi/Attributes/ApiParameter.php
Normal file
26
src/Framework/OpenApi/Attributes/ApiParameter.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\OpenApi\Attributes;
|
||||
|
||||
use Attribute;
|
||||
|
||||
/**
|
||||
* Describes a parameter for an API endpoint
|
||||
*/
|
||||
#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
|
||||
final readonly class ApiParameter
|
||||
{
|
||||
public function __construct(
|
||||
public string $name,
|
||||
public string $in = 'query', // query, path, header, cookie
|
||||
public string $description = '',
|
||||
public bool $required = false,
|
||||
public string $type = 'string',
|
||||
public ?string $format = null,
|
||||
public mixed $example = null,
|
||||
public array $enum = [],
|
||||
) {
|
||||
}
|
||||
}
|
||||
23
src/Framework/OpenApi/Attributes/ApiRequestBody.php
Normal file
23
src/Framework/OpenApi/Attributes/ApiRequestBody.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\OpenApi\Attributes;
|
||||
|
||||
use Attribute;
|
||||
|
||||
/**
|
||||
* Describes the request body for an API endpoint
|
||||
*/
|
||||
#[Attribute(Attribute::TARGET_METHOD)]
|
||||
final readonly class ApiRequestBody
|
||||
{
|
||||
public function __construct(
|
||||
public string $description,
|
||||
public bool $required = true,
|
||||
public string $contentType = 'application/json',
|
||||
public ?string $schemaRef = null,
|
||||
public ?array $example = null,
|
||||
) {
|
||||
}
|
||||
}
|
||||
24
src/Framework/OpenApi/Attributes/ApiResponse.php
Normal file
24
src/Framework/OpenApi/Attributes/ApiResponse.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\OpenApi\Attributes;
|
||||
|
||||
use Attribute;
|
||||
|
||||
/**
|
||||
* Describes a response for an API endpoint
|
||||
*/
|
||||
#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
|
||||
final readonly class ApiResponse
|
||||
{
|
||||
public function __construct(
|
||||
public int $statusCode,
|
||||
public string $description,
|
||||
public ?string $contentType = 'application/json',
|
||||
public ?string $schemaRef = null,
|
||||
public ?array $example = null,
|
||||
public array $headers = [],
|
||||
) {
|
||||
}
|
||||
}
|
||||
20
src/Framework/OpenApi/Attributes/ApiSecurity.php
Normal file
20
src/Framework/OpenApi/Attributes/ApiSecurity.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\OpenApi\Attributes;
|
||||
|
||||
use Attribute;
|
||||
|
||||
/**
|
||||
* Describes security requirements for an API endpoint
|
||||
*/
|
||||
#[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)]
|
||||
final readonly class ApiSecurity
|
||||
{
|
||||
public function __construct(
|
||||
public string $scheme,
|
||||
public array $scopes = [],
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\OpenApi\Commands;
|
||||
|
||||
use App\Framework\Console\ConsoleCommand;
|
||||
use App\Framework\Console\ConsoleInput;
|
||||
use App\Framework\Console\ConsoleOutput;
|
||||
use App\Framework\OpenApi\MarkdownGenerator;
|
||||
use App\Framework\OpenApi\OpenApiGenerator;
|
||||
use App\Framework\OpenApi\OpenApiInfo;
|
||||
|
||||
/**
|
||||
* Console command to generate Markdown documentation
|
||||
*/
|
||||
final readonly class GenerateMarkdownDocsCommand
|
||||
{
|
||||
public function __construct(
|
||||
private OpenApiGenerator $generator,
|
||||
private MarkdownGenerator $markdownGenerator,
|
||||
) {
|
||||
}
|
||||
|
||||
#[ConsoleCommand('docs:markdown', 'Generate Markdown documentation from OpenAPI specification')]
|
||||
public function __invoke(ConsoleInput $input, ConsoleOutput $output): void
|
||||
{
|
||||
$output->writeLine('Generating Markdown documentation...');
|
||||
|
||||
$info = new OpenApiInfo(
|
||||
title: 'Michael Schiemer API',
|
||||
version: '1.0.0',
|
||||
description: 'API documentation for Michael Schiemer\'s custom PHP framework',
|
||||
);
|
||||
|
||||
$spec = $this->generator->generate($info);
|
||||
$markdown = $this->markdownGenerator->generate($spec);
|
||||
|
||||
$outputFile = $input->getOption('output') ?? 'docs/api.md';
|
||||
$format = $input->getOption('format') ?? 'markdown';
|
||||
|
||||
if (! is_dir(dirname($outputFile))) {
|
||||
mkdir(dirname($outputFile), 0755, true);
|
||||
}
|
||||
|
||||
if ($format === 'html') {
|
||||
$renderer = new \App\Framework\Markdown\MarkdownRenderer(
|
||||
new \App\Framework\Markdown\MarkdownConverter()
|
||||
);
|
||||
$html = $renderer->render($markdown, 'api', [
|
||||
'title' => 'API Documentation',
|
||||
'syntaxHighlighting' => true,
|
||||
]);
|
||||
$outputFile = str_replace('.md', '.html', $outputFile);
|
||||
file_put_contents($outputFile, $html);
|
||||
} else {
|
||||
file_put_contents($outputFile, $markdown);
|
||||
}
|
||||
|
||||
$output->writeSuccess("Documentation generated: {$outputFile}");
|
||||
}
|
||||
}
|
||||
72
src/Framework/OpenApi/Commands/GenerateOpenApiCommand.php
Normal file
72
src/Framework/OpenApi/Commands/GenerateOpenApiCommand.php
Normal file
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\OpenApi\Commands;
|
||||
|
||||
use App\Framework\Console\ConsoleCommand;
|
||||
use App\Framework\Console\ConsoleInput;
|
||||
use App\Framework\Console\ConsoleOutput;
|
||||
use App\Framework\OpenApi\OpenApiContact;
|
||||
use App\Framework\OpenApi\OpenApiGenerator;
|
||||
use App\Framework\OpenApi\OpenApiInfo;
|
||||
use App\Framework\OpenApi\OpenApiLicense;
|
||||
|
||||
/**
|
||||
* Console command to generate OpenAPI specification
|
||||
*/
|
||||
final readonly class GenerateOpenApiCommand
|
||||
{
|
||||
public function __construct(
|
||||
private OpenApiGenerator $generator,
|
||||
) {
|
||||
}
|
||||
|
||||
#[ConsoleCommand('openapi:generate', 'Generate OpenAPI specification from controller attributes')]
|
||||
public function __invoke(ConsoleInput $input, ConsoleOutput $output): void
|
||||
{
|
||||
$output->writeLine('Generating OpenAPI specification...');
|
||||
|
||||
$info = new OpenApiInfo(
|
||||
title: 'Michael Schiemer API',
|
||||
version: '1.0.0',
|
||||
description: 'API documentation for Michael Schiemer\'s custom PHP framework',
|
||||
contact: new OpenApiContact(
|
||||
name: 'Michael Schiemer',
|
||||
email: 'contact@michaelschiemer.dev',
|
||||
),
|
||||
license: new OpenApiLicense(
|
||||
name: 'MIT',
|
||||
url: 'https://opensource.org/licenses/MIT',
|
||||
),
|
||||
);
|
||||
|
||||
$servers = [
|
||||
[
|
||||
'url' => 'http://localhost',
|
||||
'description' => 'Local development server (HTTP)',
|
||||
],
|
||||
[
|
||||
'url' => 'https://localhost',
|
||||
'description' => 'Local development server (HTTPS)',
|
||||
],
|
||||
[
|
||||
'url' => 'https://api.michaelschiemer.dev',
|
||||
'description' => 'Production server',
|
||||
],
|
||||
];
|
||||
|
||||
$spec = $this->generator->generate($info, $servers);
|
||||
|
||||
$outputFile = $input->getOption('output') ?? 'public/openapi.json';
|
||||
|
||||
if (! is_dir(dirname($outputFile))) {
|
||||
mkdir(dirname($outputFile), 0755, true);
|
||||
}
|
||||
|
||||
file_put_contents($outputFile, $spec->toJson());
|
||||
|
||||
$output->writeSuccess("OpenAPI specification generated: {$outputFile}");
|
||||
$output->writeLine("View at: https://editor.swagger.io/?url=" . urlencode("https://localhost/{$outputFile}"));
|
||||
}
|
||||
}
|
||||
60
src/Framework/OpenApi/MarkdownGenerator.php
Normal file
60
src/Framework/OpenApi/MarkdownGenerator.php
Normal file
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\OpenApi;
|
||||
|
||||
/**
|
||||
* Generates Markdown documentation from OpenAPI specification
|
||||
*/
|
||||
final readonly class MarkdownGenerator
|
||||
{
|
||||
public function generate(OpenApiSpec $spec): string
|
||||
{
|
||||
$markdown = "# {$spec->info->title}\n\n";
|
||||
$markdown .= "{$spec->info->description}\n\n";
|
||||
$markdown .= "**Version:** {$spec->info->version}\n\n";
|
||||
|
||||
if (! empty($spec->servers)) {
|
||||
$markdown .= "## Servers\n\n";
|
||||
foreach ($spec->servers as $server) {
|
||||
$markdown .= "- **{$server['description']}**: `{$server['url']}`\n";
|
||||
}
|
||||
$markdown .= "\n";
|
||||
}
|
||||
|
||||
$markdown .= "## Endpoints\n\n";
|
||||
|
||||
foreach ($spec->paths as $path => $pathItem) {
|
||||
foreach ($pathItem as $method => $operation) {
|
||||
$markdown .= "### " . strtoupper($method) . " " . $path . "\n\n";
|
||||
$markdown .= $operation['summary'] . "\n\n";
|
||||
|
||||
if (! empty($operation['description'])) {
|
||||
$markdown .= $operation['description'] . "\n\n";
|
||||
}
|
||||
|
||||
if (! empty($operation['parameters'])) {
|
||||
$markdown .= "**Parameters:**\n\n";
|
||||
foreach ($operation['parameters'] as $param) {
|
||||
$required = $param['required'] ? '**required**' : 'optional';
|
||||
$markdown .= "- `{$param['name']}` ({$param['in']}) - {$param['description']} ({$required})\n";
|
||||
}
|
||||
$markdown .= "\n";
|
||||
}
|
||||
|
||||
if (! empty($operation['responses'])) {
|
||||
$markdown .= "**Responses:**\n\n";
|
||||
foreach ($operation['responses'] as $code => $response) {
|
||||
$markdown .= "- `{$code}` - {$response['description']}\n";
|
||||
}
|
||||
$markdown .= "\n";
|
||||
}
|
||||
|
||||
$markdown .= "---\n\n";
|
||||
}
|
||||
}
|
||||
|
||||
return $markdown;
|
||||
}
|
||||
}
|
||||
37
src/Framework/OpenApi/OpenApiContact.php
Normal file
37
src/Framework/OpenApi/OpenApiContact.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\OpenApi;
|
||||
|
||||
final readonly class OpenApiContact
|
||||
{
|
||||
public function __construct(
|
||||
public ?string $name = null,
|
||||
public ?string $url = null,
|
||||
public ?string $email = null,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
$contact = [];
|
||||
|
||||
if ($this->name !== null) {
|
||||
$contact['name'] = $this->name;
|
||||
}
|
||||
|
||||
if ($this->url !== null) {
|
||||
$contact['url'] = $this->url;
|
||||
}
|
||||
|
||||
if ($this->email !== null) {
|
||||
$contact['email'] = $this->email;
|
||||
}
|
||||
|
||||
return $contact;
|
||||
}
|
||||
}
|
||||
277
src/Framework/OpenApi/OpenApiGenerator.php
Normal file
277
src/Framework/OpenApi/OpenApiGenerator.php
Normal file
@@ -0,0 +1,277 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\OpenApi;
|
||||
|
||||
use App\Framework\Core\ValueObjects\ClassName;
|
||||
use App\Framework\OpenApi\Attributes\ApiEndpoint;
|
||||
use App\Framework\OpenApi\Attributes\ApiParameter;
|
||||
use App\Framework\OpenApi\Attributes\ApiRequestBody;
|
||||
use App\Framework\OpenApi\Attributes\ApiResponse;
|
||||
use App\Framework\OpenApi\Attributes\ApiSecurity;
|
||||
use App\Framework\Reflection\ReflectionProvider;
|
||||
use App\Framework\Router\CompiledRoutes;
|
||||
use ReflectionMethod;
|
||||
|
||||
/**
|
||||
* Generates OpenAPI specification from controller attributes
|
||||
*/
|
||||
final readonly class OpenApiGenerator
|
||||
{
|
||||
public function __construct(
|
||||
private CompiledRoutes $compiledRoutes,
|
||||
private ReflectionProvider $reflectionProvider,
|
||||
) {
|
||||
}
|
||||
|
||||
public function generate(OpenApiInfo $info, array $servers = []): OpenApiSpec
|
||||
{
|
||||
$paths = [];
|
||||
$tags = [];
|
||||
$components = [
|
||||
'securitySchemes' => [
|
||||
'bearerAuth' => [
|
||||
'type' => 'http',
|
||||
'scheme' => 'bearer',
|
||||
'bearerFormat' => 'JWT',
|
||||
],
|
||||
'apiKey' => [
|
||||
'type' => 'apiKey',
|
||||
'in' => 'header',
|
||||
'name' => 'X-API-Key',
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
$staticRoutes = $this->compiledRoutes->getStaticRoutes();
|
||||
|
||||
foreach ($staticRoutes as $method => $methodRoutes) {
|
||||
foreach ($methodRoutes as $path => $route) {
|
||||
$className = ClassName::create($route->controller);
|
||||
$reflectionMethod = $this->reflectionProvider->getNativeMethod($className, $route->action);
|
||||
|
||||
$apiEndpoint = $this->getApiEndpointAttribute($reflectionMethod);
|
||||
if ($apiEndpoint === null) {
|
||||
continue; // Skip non-API routes
|
||||
}
|
||||
|
||||
$pathItem = $this->generatePathItem($route, $reflectionMethod, $apiEndpoint);
|
||||
|
||||
// Group paths by path pattern
|
||||
$pathPattern = $route->path;
|
||||
if (! isset($paths[$pathPattern])) {
|
||||
$paths[$pathPattern] = [];
|
||||
}
|
||||
|
||||
$paths[$pathPattern][strtolower($method)] = $pathItem;
|
||||
|
||||
// Collect tags
|
||||
foreach ($apiEndpoint->tags as $tag) {
|
||||
if (! in_array($tag, array_column($tags, 'name'), true)) {
|
||||
$tags[] = ['name' => $tag];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new OpenApiSpec(
|
||||
info: $info,
|
||||
servers: $servers,
|
||||
paths: $paths,
|
||||
components: $components,
|
||||
tags: $tags,
|
||||
);
|
||||
}
|
||||
|
||||
private function getApiEndpointAttribute(ReflectionMethod $method): ?ApiEndpoint
|
||||
{
|
||||
$attributes = $method->getAttributes(ApiEndpoint::class);
|
||||
|
||||
return ! empty($attributes) ? $attributes[0]->newInstance() : null;
|
||||
}
|
||||
|
||||
private function generatePathItem(object $route, ReflectionMethod $method, ApiEndpoint $apiEndpoint): array
|
||||
{
|
||||
$operation = [
|
||||
'summary' => $apiEndpoint->summary,
|
||||
'operationId' => $apiEndpoint->operationId ?? $method->getDeclaringClass()->getShortName() . '_' . $method->getName(),
|
||||
];
|
||||
|
||||
if ($apiEndpoint->description !== '') {
|
||||
$operation['description'] = $apiEndpoint->description;
|
||||
}
|
||||
|
||||
if (! empty($apiEndpoint->tags)) {
|
||||
$operation['tags'] = $apiEndpoint->tags;
|
||||
}
|
||||
|
||||
if ($apiEndpoint->deprecated) {
|
||||
$operation['deprecated'] = true;
|
||||
}
|
||||
|
||||
// Parameters
|
||||
$parameters = $this->extractParameters($method);
|
||||
if (! empty($parameters)) {
|
||||
$operation['parameters'] = $parameters;
|
||||
}
|
||||
|
||||
// Request body
|
||||
$requestBody = $this->extractRequestBody($method);
|
||||
if ($requestBody !== null) {
|
||||
$operation['requestBody'] = $requestBody;
|
||||
}
|
||||
|
||||
// Responses
|
||||
$responses = $this->extractResponses($method);
|
||||
$operation['responses'] = $responses;
|
||||
|
||||
// Security
|
||||
$security = $this->extractSecurity($method);
|
||||
if (! empty($security)) {
|
||||
$operation['security'] = $security;
|
||||
}
|
||||
|
||||
return $operation;
|
||||
}
|
||||
|
||||
private function extractParameters(ReflectionMethod $method): array
|
||||
{
|
||||
$parameters = [];
|
||||
|
||||
foreach ($method->getAttributes(ApiParameter::class) as $attribute) {
|
||||
$param = $attribute->newInstance();
|
||||
|
||||
$parameter = [
|
||||
'name' => $param->name,
|
||||
'in' => $param->in,
|
||||
'required' => $param->required,
|
||||
'schema' => [
|
||||
'type' => $param->type,
|
||||
],
|
||||
];
|
||||
|
||||
if ($param->description !== '') {
|
||||
$parameter['description'] = $param->description;
|
||||
}
|
||||
|
||||
if ($param->format !== null) {
|
||||
$parameter['schema']['format'] = $param->format;
|
||||
}
|
||||
|
||||
if ($param->example !== null) {
|
||||
$parameter['example'] = $param->example;
|
||||
}
|
||||
|
||||
if (! empty($param->enum)) {
|
||||
$parameter['schema']['enum'] = $param->enum;
|
||||
}
|
||||
|
||||
$parameters[] = $parameter;
|
||||
}
|
||||
|
||||
return $parameters;
|
||||
}
|
||||
|
||||
private function extractRequestBody(ReflectionMethod $method): ?array
|
||||
{
|
||||
$attributes = $method->getAttributes(ApiRequestBody::class);
|
||||
if (empty($attributes)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$requestBody = $attributes[0]->newInstance();
|
||||
|
||||
$body = [
|
||||
'required' => $requestBody->required,
|
||||
'content' => [
|
||||
$requestBody->contentType => [],
|
||||
],
|
||||
];
|
||||
|
||||
if ($requestBody->description !== '') {
|
||||
$body['description'] = $requestBody->description;
|
||||
}
|
||||
|
||||
if ($requestBody->schemaRef !== null) {
|
||||
$body['content'][$requestBody->contentType]['schema'] = [
|
||||
'$ref' => '#/components/schemas/' . $requestBody->schemaRef,
|
||||
];
|
||||
}
|
||||
|
||||
if ($requestBody->example !== null) {
|
||||
$body['content'][$requestBody->contentType]['example'] = $requestBody->example;
|
||||
}
|
||||
|
||||
return $body;
|
||||
}
|
||||
|
||||
private function extractResponses(ReflectionMethod $method): array
|
||||
{
|
||||
$responses = [];
|
||||
|
||||
foreach ($method->getAttributes(ApiResponse::class) as $attribute) {
|
||||
$response = $attribute->newInstance();
|
||||
|
||||
$responseData = [
|
||||
'description' => $response->description,
|
||||
];
|
||||
|
||||
if ($response->contentType !== null) {
|
||||
$content = [];
|
||||
|
||||
if ($response->schemaRef !== null) {
|
||||
$content['schema'] = [
|
||||
'$ref' => '#/components/schemas/' . $response->schemaRef,
|
||||
];
|
||||
}
|
||||
|
||||
if ($response->example !== null) {
|
||||
$content['example'] = $response->example;
|
||||
}
|
||||
|
||||
if (! empty($content)) {
|
||||
$responseData['content'] = [
|
||||
$response->contentType => $content,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if (! empty($response->headers)) {
|
||||
$responseData['headers'] = $response->headers;
|
||||
}
|
||||
|
||||
$responses[(string) $response->statusCode] = $responseData;
|
||||
}
|
||||
|
||||
// Ensure at least one response exists
|
||||
if (empty($responses)) {
|
||||
$responses['200'] = [
|
||||
'description' => 'Successful response',
|
||||
];
|
||||
}
|
||||
|
||||
return $responses;
|
||||
}
|
||||
|
||||
private function extractSecurity(ReflectionMethod $method): array
|
||||
{
|
||||
$security = [];
|
||||
|
||||
// Check method-level security
|
||||
foreach ($method->getAttributes(ApiSecurity::class) as $attribute) {
|
||||
$sec = $attribute->newInstance();
|
||||
$security[] = [$sec->scheme => $sec->scopes];
|
||||
}
|
||||
|
||||
// Check class-level security if no method-level security
|
||||
if (empty($security)) {
|
||||
foreach ($method->getDeclaringClass()->getAttributes(ApiSecurity::class) as $attribute) {
|
||||
$sec = $attribute->newInstance();
|
||||
$security[] = [$sec->scheme => $sec->scopes];
|
||||
}
|
||||
}
|
||||
|
||||
return $security;
|
||||
}
|
||||
}
|
||||
47
src/Framework/OpenApi/OpenApiInfo.php
Normal file
47
src/Framework/OpenApi/OpenApiInfo.php
Normal file
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\OpenApi;
|
||||
|
||||
/**
|
||||
* Represents the info object in OpenAPI specification
|
||||
*/
|
||||
final readonly class OpenApiInfo
|
||||
{
|
||||
public function __construct(
|
||||
public string $title,
|
||||
public string $version,
|
||||
public string $description = '',
|
||||
public ?string $termsOfService = null,
|
||||
public ?OpenApiContact $contact = null,
|
||||
public ?OpenApiLicense $license = null,
|
||||
) {
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
$info = [
|
||||
'title' => $this->title,
|
||||
'version' => $this->version,
|
||||
];
|
||||
|
||||
if ($this->description !== '') {
|
||||
$info['description'] = $this->description;
|
||||
}
|
||||
|
||||
if ($this->termsOfService !== null) {
|
||||
$info['termsOfService'] = $this->termsOfService;
|
||||
}
|
||||
|
||||
if ($this->contact !== null) {
|
||||
$info['contact'] = $this->contact->toArray();
|
||||
}
|
||||
|
||||
if ($this->license !== null) {
|
||||
$info['license'] = $this->license->toArray();
|
||||
}
|
||||
|
||||
return $info;
|
||||
}
|
||||
}
|
||||
25
src/Framework/OpenApi/OpenApiLicense.php
Normal file
25
src/Framework/OpenApi/OpenApiLicense.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\OpenApi;
|
||||
|
||||
final readonly class OpenApiLicense
|
||||
{
|
||||
public function __construct(
|
||||
public string $name,
|
||||
public ?string $url = null,
|
||||
) {
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
$license = ['name' => $this->name];
|
||||
|
||||
if ($this->url !== null) {
|
||||
$license['url'] = $this->url;
|
||||
}
|
||||
|
||||
return $license;
|
||||
}
|
||||
}
|
||||
53
src/Framework/OpenApi/OpenApiService.php
Normal file
53
src/Framework/OpenApi/OpenApiService.php
Normal file
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\OpenApi;
|
||||
|
||||
/**
|
||||
* Service for programmatic access to OpenAPI specification
|
||||
*/
|
||||
final readonly class OpenApiService
|
||||
{
|
||||
public function __construct(
|
||||
private OpenApiGenerator $generator,
|
||||
) {
|
||||
}
|
||||
|
||||
public function getSpecification(): OpenApiSpec
|
||||
{
|
||||
$info = new OpenApiInfo(
|
||||
title: 'API',
|
||||
version: '1.0.0',
|
||||
);
|
||||
|
||||
return $this->generator->generate($info);
|
||||
}
|
||||
|
||||
public function getEndpoints(): array
|
||||
{
|
||||
$spec = $this->getSpecification();
|
||||
$endpoints = [];
|
||||
|
||||
foreach ($spec->paths as $path => $pathItem) {
|
||||
foreach ($pathItem as $method => $operation) {
|
||||
$endpoints[] = [
|
||||
'method' => strtoupper($method),
|
||||
'path' => $path,
|
||||
'summary' => $operation['summary'] ?? '',
|
||||
'tags' => $operation['tags'] ?? [],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $endpoints;
|
||||
}
|
||||
|
||||
public function getEndpointsByTag(string $tag): array
|
||||
{
|
||||
return array_filter(
|
||||
$this->getEndpoints(),
|
||||
fn ($endpoint) => in_array($tag, $endpoint['tags'])
|
||||
);
|
||||
}
|
||||
}
|
||||
57
src/Framework/OpenApi/OpenApiSpec.php
Normal file
57
src/Framework/OpenApi/OpenApiSpec.php
Normal file
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\OpenApi;
|
||||
|
||||
/**
|
||||
* Represents a complete OpenAPI specification
|
||||
*/
|
||||
final readonly class OpenApiSpec
|
||||
{
|
||||
public function __construct(
|
||||
public OpenApiInfo $info,
|
||||
public string $openapi = '3.0.3',
|
||||
public array $servers = [],
|
||||
public array $paths = [],
|
||||
public array $components = [],
|
||||
public array $security = [],
|
||||
public array $tags = [],
|
||||
) {
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
$spec = [
|
||||
'openapi' => $this->openapi,
|
||||
'info' => $this->info->toArray(),
|
||||
];
|
||||
|
||||
if (! empty($this->servers)) {
|
||||
$spec['servers'] = $this->servers;
|
||||
}
|
||||
|
||||
if (! empty($this->paths)) {
|
||||
$spec['paths'] = $this->paths;
|
||||
}
|
||||
|
||||
if (! empty($this->components)) {
|
||||
$spec['components'] = $this->components;
|
||||
}
|
||||
|
||||
if (! empty($this->security)) {
|
||||
$spec['security'] = $this->security;
|
||||
}
|
||||
|
||||
if (! empty($this->tags)) {
|
||||
$spec['tags'] = $this->tags;
|
||||
}
|
||||
|
||||
return $spec;
|
||||
}
|
||||
|
||||
public function toJson(): string
|
||||
{
|
||||
return json_encode($this->toArray(), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user