Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -465,10 +465,11 @@ Generate OpenAPI documentation from registered routes:
```sh
./shift openapi
./shift openapi --output=docs/openapi.json
./shift openapi --validate
./shift openapi --live
```

The generator reads module routes from the same router used by the HTTP runtime and emits OpenAPI 3.0 JSON. Live mode starts a local documentation server at `http://127.0.0.1:8088` by default. Use `--host=` and `--port=` to change the binding.
The generator reads module routes from the same router used by the HTTP runtime and emits OpenAPI 3.0 JSON. It understands routing attributes, parameter binding attributes, request DTO rules, response metadata, and OpenAPI attributes such as `#[Summary]`, `#[Description]`, `#[Tag]`, `#[Response]`, `#[Deprecated]`, `#[Security]`, and `#[Schema]`. Live mode starts a local documentation server at `http://127.0.0.1:8088` by default. Use `--host=` and `--port=` to change the binding.

Run the example module command:

Expand Down
1 change: 1 addition & 0 deletions REFACTORING.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ ShiftPHP is moving toward an API-only modular monolith. View templates, compiled
- [x] Developer documentation organized into a Laravel-like guide.
- [x] CLI quality gate with `shift lint` and `shift qa`.
- [x] OpenAPI JSON generation from registered module routes with `shift openapi`.
- [x] OpenAPI documentation attributes, validation, and live Swagger-like viewer.
- [x] Removal of view storage and example page assets from runtime.
- [x] Removal of legacy `application/controllers` and `application/routes.php`.
- [x] Domain-oriented framework namespaces:
Expand Down
27 changes: 26 additions & 1 deletion docs/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -694,15 +694,40 @@ <h2>OpenAPI</h2>

<pre><code>./shift openapi
./shift openapi --output=docs/openapi.json
./shift openapi --validate
./shift openapi --live</code></pre>

<p>The generator reads route paths, HTTP methods, controller handlers, path parameters, query parameters, request body attributes, request DTO rules, response status attributes, and response header attributes.</p>

<pre><code>use Shift\OpenApi\Attributes\Description;
use Shift\OpenApi\Attributes\Response;
use Shift\OpenApi\Attributes\Schema;
use Shift\OpenApi\Attributes\Security;
use Shift\OpenApi\Attributes\Summary;
use Shift\OpenApi\Attributes\Tag;

#[Tag('Users')]
final class UserController
{
#[Summary('Create a user')]
#[Description('Creates a user account from a JSON request body.')]
#[Security('bearerAuth')]
#[Response(201, 'User created')]
public function store(
#[Schema(format: 'email')]
string $email
): array {
return ['email' =&gt; $email];
}
}</code></pre>

<p>Available OpenAPI attributes include <code>Summary</code>, <code>Description</code>, <code>Tag</code>, <code>Response</code>, <code>Deprecated</code>, <code>Security</code>, and <code>Schema</code>. Validation mode checks the generated document for required OpenAPI fields, unique operation ids, responses, and declared path parameters.</p>

<pre><code>./shift help openapi
./shift api:docs --output=storage/openapi.json
./shift openapi --live --host=127.0.0.1 --port=8088</code></pre>

<p>Live mode writes the generated JSON into a temporary directory and starts a local documentation server with a lightweight Swagger-like HTML viewer.</p>
<p>Live mode writes the generated JSON into a temporary directory and starts a local documentation server with a lightweight Swagger-like HTML viewer, endpoint search, tag filtering, dark mode, and JSON download.</p>
</section>

<section id="database">
Expand Down
24 changes: 22 additions & 2 deletions src/Console/Commands/OpenApi.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use Shift\Modules\ModuleLoader;
use Shift\OpenApi\OpenApiGenerator;
use Shift\OpenApi\OpenApiLivePage;
use Shift\OpenApi\OpenApiValidator;
use Shift\Routing\Router\Router;

#[\Shift\Console\Attributes\Command('openapi', aliases: ['api:docs'], group: 'documentation')]
Expand All @@ -27,12 +28,31 @@ public function execute(mixed ...$args): void

$outputPath = $this->outputPath($args);
$live = $this->hasOption($args, '--live');
$validate = $this->hasOption($args, '--validate');

if ($outputPath === null && !$live) {
if ($validate) {
$errors = (new OpenApiValidator())->validate($document);

if ($errors !== []) {
$cli = new Cli();

foreach ($errors as $error) {
$cli->error($error);
}

exit(1);
}
}

if ($outputPath === null && !$live && !$validate) {
echo $json . PHP_EOL;
return;
}

if ($validate) {
(new Cli())->success('OpenAPI document is valid.');
}

if ($outputPath !== null) {
$this->writeFile($outputPath, $json . PHP_EOL);
(new Cli())->success('OpenAPI document written to ' . $outputPath);
Expand All @@ -45,7 +65,7 @@ public function execute(mixed ...$args): void

public function getHelp(): string
{
return 'Usage: ./shift openapi [--output=docs/openapi.json] [--live] [--host=127.0.0.1] [--port=8088]';
return 'Usage: ./shift openapi [--output=docs/openapi.json] [--validate] [--live] [--host=127.0.0.1] [--port=8088]';
}

public function getDescription(): string
Expand Down
29 changes: 2 additions & 27 deletions src/Console/Quality/QualityChecks.php
Original file line number Diff line number Diff line change
Expand Up @@ -96,34 +96,9 @@ public function routeList(): CheckResult

public function openApiDocument(): CheckResult
{
$command = escapeshellarg(PHP_BINARY) . ' ' . escapeshellarg($this->projectRoot() . '/shift') . ' openapi 2>&1';
$previousDirectory = getcwd();

if ($previousDirectory !== false) {
chdir($this->projectRoot());
}

try {
exec($command, $output, $exitCode);
} finally {
if ($previousDirectory !== false) {
chdir($previousDirectory);
}
}

if ($exitCode !== 0) {
$details = trim(implode(' ', array_slice($output, -3)));

return CheckResult::fail('OpenAPI document', $details !== '' ? $details : 'Command failed with exit code ' . $exitCode);
}

$document = json_decode(implode("\n", $output), true);

if (!is_array($document) || ($document['openapi'] ?? null) !== '3.0.3') {
return CheckResult::fail('OpenAPI document', 'Generated document is not valid OpenAPI JSON');
}
$command = escapeshellarg(PHP_BINARY) . ' ' . escapeshellarg($this->projectRoot() . '/shift') . ' openapi --validate 2>&1';

return CheckResult::ok('OpenAPI document', './shift openapi passed');
return $this->runShellCheck('OpenAPI document', $command, './shift openapi --validate passed');
}

private function runShellCheck(string $name, string $command, string $successDetails): CheckResult
Expand Down
10 changes: 10 additions & 0 deletions src/OpenApi/Attributes/Deprecated.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

namespace Shift\OpenApi\Attributes;

use Attribute;

#[Attribute(Attribute::TARGET_METHOD)]
class Deprecated
{
}
13 changes: 13 additions & 0 deletions src/OpenApi/Attributes/Description.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

namespace Shift\OpenApi\Attributes;

use Attribute;

#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD | Attribute::TARGET_PARAMETER | Attribute::TARGET_PROPERTY)]
class Description
{
public function __construct(public readonly string $text)
{
}
}
16 changes: 16 additions & 0 deletions src/OpenApi/Attributes/Response.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

namespace Shift\OpenApi\Attributes;

use Attribute;

#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
class Response
{
public function __construct(
public readonly int $status,
public readonly string $description = 'Response',
public readonly ?string $type = 'object'
) {
}
}
23 changes: 23 additions & 0 deletions src/OpenApi/Attributes/Schema.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

namespace Shift\OpenApi\Attributes;

use Attribute;

#[Attribute(Attribute::TARGET_PARAMETER | Attribute::TARGET_PROPERTY)]
class Schema
{
/**
* @param list<string> $enum
*/
public function __construct(
public readonly ?string $type = null,
public readonly ?string $format = null,
public readonly ?string $description = null,
public readonly ?string $itemsType = null,
public readonly array $enum = [],
public readonly ?bool $required = null,
public readonly ?bool $nullable = null
) {
}
}
18 changes: 18 additions & 0 deletions src/OpenApi/Attributes/Security.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

namespace Shift\OpenApi\Attributes;

use Attribute;

#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
class Security
{
/**
* @param list<string> $scopes
*/
public function __construct(
public readonly string $name,
public readonly array $scopes = []
) {
}
}
13 changes: 13 additions & 0 deletions src/OpenApi/Attributes/Summary.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

namespace Shift\OpenApi\Attributes;

use Attribute;

#[Attribute(Attribute::TARGET_METHOD)]
class Summary
{
public function __construct(public readonly string $text)
{
}
}
13 changes: 13 additions & 0 deletions src/OpenApi/Attributes/Tag.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

namespace Shift\OpenApi\Attributes;

use Attribute;

#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
class Tag
{
public function __construct(public readonly string $name)
{
}
}
Loading
Loading