Skip to main content
For a ready-to-run script and CI wiring, see Generate OpenAPI and docblocks.

How it Works

The generator leverages the existing framework infrastructure to discover and process your API metadata:
  1. Route Discovery: It uses the Router to find all registered routes and their corresponding controller classes.
  2. Metadata Extraction: For each route, it takes the request class from the controller’s __invoke() parameter and the response classes from the new expressions its body returns, then calls getDocumentation() on those classes to obtain the declared ApivalkRequestDocumentation / ApivalkResponseDocumentation. Route-level metadata (filters, sortings, pagination, rate limit, authorization) is merged in from the Route object itself. See Documented Responses for how both are derived. That scan is syntactic: a response your controller builds in a helper or receives from a service is not documented, and only a controller the scan finds no response in at all aborts generation with a LogicException.
  3. Object Mapping: It maps the Apivalk Property system to OpenAPI objects (Schemas, Parameters, RequestBodies, Responses).
  4. Serialization: It assembles these objects into a final OpenAPI object and serializes it to JSON.

Excluding Routes

Routes marked with Route::excludeFromDocumentation() are skipped during route discovery and never appear in paths. If every method of a URL is excluded, the path itself is omitted entirely.
The exclusion is documentation-only. The endpoint stays reachable at runtime, so use routeAuthorization() to actually protect it. See Route for details. forceIncludeExcludedRoutes() opts back in, so the same route definitions can produce a public and an internal document:
It takes an optional boolean (forceIncludeExcludedRoutes($isInternalBuild)) if the decision comes from a CLI flag or env var.

Filtering by Tag

onlyWithTags() restricts the document to routes carrying at least one of the given tag names. The default, an empty list, documents every route.
  • Tag names are matched exactly and case sensitively against the TagObject names set via Route::tags().
  • A route without tags never matches a non-empty list and drops out. Resource controllers take their tags from AbstractResource::tags(), which returns [] unless you override it.
  • A tag name that no documented route carries throws an InvalidArgumentException naming the known tags. Without that check a typo would silently produce a document without paths, which is not a valid OpenAPI document.
  • A matching tag never brings an excluded route back on its own. It still needs forceIncludeExcludedRoutes(), and a tag that only excluded routes carry counts as unknown until that option is on.

Core Logic

The OpenAPI Object

The OpenAPI class represents the root of the specification. It contains sub-objects for:
  • InfoObject: API title, version, description.
  • ServerObject: Base URLs for your API.
  • PathsObject: Map of endpoints and their methods.
  • ComponentsObject: Reusable schemas and security schemes.

The Generation Process

The OpenAPIGenerator coordinates several specialized generators:
  • PathsGenerator: Processes the list of routes.
  • PathItemGenerator: Handles a single URL (which may have multiple HTTP methods).
  • OperationGenerator: Handles a single HTTP method (GET, POST, etc.) on a path.
  • ParameterGenerator: Converts query and path properties to OpenAPI parameters.
  • RequestBodyGenerator: Converts body properties to a JSON request body schema.
  • ResponseGenerator: Converts response documentation to OpenAPI response schemas.

Usage Example

For a copy-paste bin/generate-openapi script and a complementary docblock-generation script, see the how-to: generate OpenAPI + docblocks.

Security Schemes and Components

The ComponentsObject is where you define reusable elements for your OpenAPI specification, such as security schemes, common schemas, or parameters.

How Security Schemes Connect to Routes

The key concept: the name you give a SecuritySchemeObject is the same name you pass to RouteAuthorization on your routes. This is how Apivalk knows which security scheme protects which route, and how Swagger UI knows when to show the “Authorize” button.
When the OpenAPI spec is generated, Apivalk matches these names to produce the correct security entries per operation. Swagger UI then renders the “Authorize” button and applies the right credentials to the right endpoints.

Factory Methods

SecuritySchemeObject ships four static factories — one per scheme type. Each factory only exposes the parameters that are valid for that type, so you can’t accidentally pass bearerFormat to an apiKey scheme or in to an http scheme. The raw constructor still works but requires you to pass every field positionally, including nulls for fields that don’t apply. Use SecuritySchemeObject::TYPE_* constants wherever you need to reference a type string in your own code.

SecuritySchemeObject::http($name, $scheme, $description, $bearerFormat)

For HTTP authentication schemes (Basic, Bearer, Digest, …). The scheme value must be a registered IANA HTTP Authentication Scheme and is placed in the Authorization header by the client. bearerFormat is optional — it is a documentation hint only and has no effect on validation.
Emits:
name and in are not emitted — they are invalid for http per the OpenAPI spec. The name is used only as the key in components.securitySchemes and to match RouteAuthorization.

SecuritySchemeObject::apiKey($name, $in, $description)

For raw API keys passed as a header, query parameter, or cookie. in must be one of "header", "query", or "cookie". The name is emitted in the spec as the actual header/parameter name the client must send (e.g. X-Api-Key).
Emits:
Note that for apiKey, name is emitted — it is the required field that tells the client which header or parameter to set.

SecuritySchemeObject::oauth2($name, $flows, $description)

For OAuth2. Requires an OAuthFlowsObject describing which grant types are supported. Each flow has its own URLs and scopes; pass null for flows you don’t support.
Emits:

SecuritySchemeObject::openIdConnect($name, $openIdConnectUrl, $description)

For OpenID Connect. Requires the well-known discovery URL of the provider. Swagger UI uses this URL to fetch scopes automatically.
Emits:

End-to-End Example: JWT Bearer

Step 1: Define the security scheme in your ComponentsObject.
Step 2: Reference the same name in your route’s RouteAuthorization.
Step 3: Pass the components to the generator.
The generated OpenAPI JSON will contain the security scheme under components.securitySchemes.api, and the /rest/v1/contract POST operation will have security: [{"api": ["contract"]}]. Swagger UI renders this as an “Authorize” button with a JWT bearer input.

End-to-End Example: Raw API Key Header

Not every API uses JWT. If your API authenticates via a raw API key sent in a custom header (e.g., X-API-Key), use the apiKey type: Step 1: Define the security scheme.
Step 2: Reference it in your route.
Swagger UI will show an “Authorize” button that prompts for an API key value, and will send it as the X-API-Key header on matching requests.

End-to-End Example: OAuth2

For OAuth2, you can define full flows (authorization URL, token URL, scopes) so that Swagger UI can perform the OAuth2 flow directly: Step 1: Define the security scheme with OAuth2 flows.
Step 2: Reference it in your route.

Multiple Security Schemes

You can define multiple security schemes in a single ComponentsObject and use different ones on different routes:

Automatic Security Scheme Generation (Fallback)

If you use security requirements in your routes but haven’t defined them in the ComponentsObject, the OpenAPIGenerator will attempt to automatically generate a base authorization part for you:
  • If the scheme name contains “bearer”, it defaults to an http type with a bearer scheme.
  • If the scheme name contains “oauth2”, it defaults to an oauth2 type with a basic password flow.
  • If the scheme name contains “fido”, it defaults to an apiKey type in the header.
  • Otherwise, it defaults to an apiKey type in the header.
This fallback exists as a convenience, but it is recommended to define your schemes explicitly so you have full control over descriptions, flows, and how Swagger UI renders the authorization interface.
Keep in mind: Future Auto-Discovery. Apivalk is moving towards full auto-discovery of security schemes. In the future, it will be able to automatically detect and document your security configuration (including names and versions) directly from your middleware and authenticators.

Automated Error Responses

On top of what your controller returns, the generator documents the errors the framework itself produces. Each one is added only where the route can actually answer with it: A response your controller returns wins over the framework entry for the same status code, so a custom 404 body is documented instead of the generic one.

Automated Headers

Locale Headers (Accept-Language / Content-Language)

By default, the generator documents localization headers on every operation:
  • Request: An optional Accept-Language header parameter (BCP 47 language tag).
  • Response: A Content-Language header on every response, indicating the resolved locale.
This matches the runtime behavior of the MiddlewareStack, which always resolves the locale from the Accept-Language header and returns Content-Language. You can disable locale header documentation by passing false for the $documentLocaleHeaders parameter:

Filter Documentation Style

By default, the form of a filter parameter follows the number of operators the field declares. A field with exactly one operator becomes a flat query parameter whose description states how it matches. Clients send ?status=active.
A single IN operator is documented as an array with style: form and explode: false, which serialises as ?status=draft,active and keeps the item enum. A single NULL operator is documented as a boolean. See Filtering for the full shape. A field with two or more operators becomes its own deepObject parameter whose properties are the operators that field declares. Clients send ?amount[gte]=10&amount[lte]=100.
One bracket level with primitive properties is the case OpenAPI defines for deepObject, which is why filters are not nested under a shared filter key. Multi-operator fields can be flattened as well, at the cost of not documenting which operator a bare value resolves to. Pass true for the $flatFilters parameter:
With flatFilters: false (default):
  • Single-operator fields are flat, with the operator stated in the description
  • Multi-operator fields are one parameter per field, with their allowed operators as sub-inputs
  • Clients send ?status=active&amount[gte]=10
With flatFilters: true:
  • One flat input per field, no operator choice documented and no operator stated in the description
  • Clients send ?status=active&amount=10, resolving to each field’s first declared operator
Both notations are accepted at runtime regardless of which style you document. Both formats are accepted by the server at runtime regardless of this setting. This option only controls how the filters are presented in the OpenAPI spec.

Rate Limit Headers

When a route has a rate limit defined (via Route::rateLimit()), the generator automatically documents the following response headers on every response for that operation: The header descriptions include the configured window duration from the RateLimitInterface. Routes without a rate limit will not have these headers in their documentation.

Automated Pagination Envelope

When a route has a pagination strategy attached via Route::pagination(...), the generator automatically wraps the matching list response’s schema in the standard pagination envelope (data, plus a pagination object whose shape matches the chosen strategy — page/page_size/total_pages for Pagination::page(), limit/offset/total for Pagination::offset(), limit/current_cursor/next_cursor for Pagination::cursor()). At runtime, attach the matching PaginationResponseInterface to your response via setPaginationResponse(...) — see pagination for the end-to-end flow. Only 2xx responses are wrapped. An error schema describes an error, never a page of results.