Search by

crabphp / openapi

bangnokia

OpenAPI documents generated from Crab routes and action signatures

Package info

github.com/crabphp/openapi

pkg:composer/crabphp/openapi

Statistics

Installs: 2

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v0.1.1 2026-11-07 05:11 UTC

This package is auto-updated.

Last update: 2026-11-07 05:13:21 UTC


README

Generate an OpenAPI 3.1 document from a Crab application's route table, validation schemas and action signatures.

The package runs at build time. It adds a console command and configuration defaults; it adds no middleware and no work to a request.

Installation

Requires PHP 8.4 or later and crabphp/framework 0.6.x.

composer require crabphp/openapi

Register the plugin in app.php:

use App\Setup;
use Crab\App;
use Crab\OpenApi\OpenApiPlugin;

return App::configure(__DIR__)
    ->withPlugins([
        OpenApiPlugin::class,
    ])
    ->withSetup(Setup::class)
    ->withRouting(routes: 'routes/web.php')
    ->create();

Create config/openapi.php:

return [
    'title' => 'Shop API',
    'version' => '1.0.0',
    'output' => 'openapi.json',
    'strict' => true,
];

Generate and check the document:

php cua openapi:generate
php cua openapi:generate --check
php cua openapi:generate --stdout

--check regenerates in memory and exits non-zero when the file differs. Generation starts no server and does not call actions or construct their services. Loading app.php still runs the application's setup and route declarations.

Use --output=path/to/openapi.json to choose a local file, or --output=- for standard output. The directory must already exist. A complete document replaces the file in one rename; a failed write leaves the previous document and removes the temporary file. --strict and --no-strict override the configured strict mode.

What is inferred

Inputs come from the same schemas the router uses to validate a request:

  • path parameters, including their types and rules;
  • flattened query fields, defaults and required fields;
  • body classes, nested objects, enums, dates, lists and files;
  • multipart/form-data when a body contains a file.

Query lists use bracketed names such as ids[] with form serialization and explode: true, so ids[]=1&ids[]=2 reaches Crab as a list. Query objects with scalar properties use deepObject, such as filter[name]=crab. Nested query collections and untyped arrays have no portable OpenAPI serialization; strict generation refuses them and non-strict generation reports a warning.

Responses are inferred only where Crab's runtime makes the result unambiguous:

Return type Documented response
void, null 204
string 200 text/plain
typed JSON scalar/class 200 application/json
nullable typed value both the typed 200 and 204

A DTO's public properties become its JSON Schema. Property unions accept any of their member types, including overlapping types such as int|float. Inherited properties resolve self and parent in the class that declared them. A backed enum becomes an enum and a date becomes an ISO date-time. Recursive classes become component $refs.

The generator records a warning and, in strict mode, stops when the signature cannot prove the response: a PSR-7 ResponseInterface, a bare array, mixed, never, a stream's event shape, JsonSerializable (including an enum that implements it), or a union or intersection return type. Declare those responses explicitly.

Declaring responses

Attributes can go on an action method, action class or closure:

use Crab\OpenApi\Attributes\Operation;
use Crab\OpenApi\Attributes\Response;

#[Operation(tags: ['orders'])]
final readonly class PlaceOrder
{
    #[Response(201, body: CreatedOrder::class, headers: ['Location'])]
    public function handle(CreateOrder $input): \Psr\Http\Message\ResponseInterface
    {
        // ...
    }
}

The generator constructs only this package's attributes. Other action metadata is ignored, and a #[Hidden] action is excluded before its response declarations are read.

Class attributes come from the class registered on the route, even when it inherits its action method. An inherited method returning static is described as that registered class.

body accepts:

  • one class name for a JSON object;
  • [OrderView::class] for a JSON list of that class;
  • an inline JSON Schema array;
  • [] for an explicitly unconstrained JSON value;
  • null for no body.

Repeated #[Response] declarations for the same status keep each media type. Schemas for the same media type are combined with anyOf. Informational responses and statuses 204, 205 and 304 have no content; declaring a body for one fails strict generation.

A declared error status without a body uses the shared Crab Error component:

#[Response(404, description: 'No such order.')]

Other attributes are:

use Crab\OpenApi\Attributes\Hidden;
use Crab\OpenApi\Attributes\Operation;
use Crab\OpenApi\Attributes\Security;

#[Security('bearer')]              // one alternative requiring bearer
#[Security('oauth2', 'apiKey')]    // another alternative requiring both
#[Operation(operationId: 'orders.show', summary: 'Show an order', deprecated: true)]
#[Hidden]

For streams and downloads, set contentType and describe the text or binary schema:

#[Response(
    body: ['type' => 'string'],
    contentType: 'text/event-stream',
    description: 'Stock updates as server-sent events.',
)]

Configuration

The plugin declares and checks its settings when the document builder is first used, including from the console. Unknown settings and values of the wrong type are refused even with --no-strict.

Setting Default
openapi.title app.name, then API Document title
openapi.version 1.0.0 API version
openapi.description null Document description
openapi.license null A license name or object with name and optionally url or identifier, never both
openapi.output openapi.json Local file, relative to app.path unless absolute; - writes to standard output
openapi.strict true Fail on response details PHP cannot prove
openapi.servers app.url Server URLs or server objects
openapi.exclude [] Route keys (GET /path), names or action classes
openapi.security [] OpenAPI security scheme objects, by name
openapi.middleware [] Documented middleware effects, by class
openapi.error_schema Crab's error object Schema for an application's custom ErrorRenderer

Middleware effects stay explicit because a class name does not say what it does:

use App\Http\AdminsOnly;
use Crab\Auth\TokenAuth;
use Crab\Http\RateLimit;

return [
    'security' => [
        'bearer' => ['type' => 'http', 'scheme' => 'bearer'],
    ],
    'middleware' => [
        TokenAuth::class => ['security' => 'bearer', 'responses' => [401]],
        RateLimit::class => ['responses' => [429]],
        AdminsOnly::class => ['responses' => [403]],
    ],
];

Every security name used by middleware or #[Security] must have a matching entry in openapi.security; strict generation reports a missing scheme.

Path, query and body input automatically add 422. A required identity parameter or mapped security scheme adds 401. Error responses document both application/json and Crab's plain-text negotiation.

Mapped middleware statuses below 400 do not use the error schema. A status that allows content needs an explicit #[Response] declaration; statuses that forbid content are documented without a body.

Components and determinism

A class named App\Http\Order becomes the component App.Http.Order. Components are sorted and repeated classes are registered once. Output has no timestamps, uses stable route order and is safe to commit. x-crab-rules, x-crab-min-bytes, x-crab-max-bytes and x-crab-media-types retain validation details with no OpenAPI keyword.

Input fields keep their own nullability, defaults and rules beside the component reference. A nullable use of a class or enum does not make every other use nullable.

When one class is used for both request validation and a response view, it gets separate .Input and .Output components so validation constraints never depend on route order. Mapped middleware on the application's global stack is applied to every operation; middleware stacked on one route requires all of its mapped security schemes.

Limits

  • Response bodies are not inspected at runtime. An attribute states what the action promises.
  • Docblock types are not parsed into schemas; use DTOs or #[Response].
  • Promoted or defaulted public DTO properties are required. A public property without a default may be uninitialized, and is therefore optional in the response schema because PHP's JSON serialization omits it.
  • OpenAPI 3.1 is the output format. A 3.0 or YAML renderer is not included.
  • Equivalent templated paths must use the same parameter names across methods. For example, use /orders/{id} for both GET and PUT; /orders/{name} is an OpenAPI path collision even when its HTTP method differs.
  • A custom ErrorRenderer needs openapi.error_schema, or strict generation reports that the error body is unknown.

Tests

composer test
composer format:check
composer lint

For development, Composer uses the framework checkout at ../framework. CI runs these checks on Linux and macOS against framework v0.6.1 and main. The sample application exercises the package against a shop API.