How it Works
The generator leverages the existing framework infrastructure to discover and process your API metadata:- Route Discovery: It uses the
Routerto find all registered routes and their corresponding controller classes. - Metadata Extraction: For each route, it takes the request class from the controller’s
__invoke()parameter and the response classes from thenewexpressions its body returns, then callsgetDocumentation()on those classes to obtain the declaredApivalkRequestDocumentation/ApivalkResponseDocumentation. Route-level metadata (filters, sortings, pagination, rate limit, authorization) is merged in from theRouteobject 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 aLogicException. - Object Mapping: It maps the Apivalk
Propertysystem to OpenAPI objects (Schemas, Parameters, RequestBodies, Responses). - Serialization: It assembles these objects into a final
OpenAPIobject and serializes it to JSON.
Excluding Routes
Routes marked withRoute::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.
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:
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
TagObjectnames set viaRoute::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
InvalidArgumentExceptionnaming the known tags. Without that check a typo would silently produce a document withoutpaths, 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
TheOpenAPIGenerator 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-pastebin/generate-openapi script and a complementary docblock-generation script, see the how-to: generate OpenAPI + docblocks.
Security Schemes and Components
TheComponentsObject 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: thename 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.
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.
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).
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.
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.
End-to-End Example: JWT Bearer
Step 1: Define the security scheme in yourComponentsObject.
RouteAuthorization.
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.
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.Multiple Security Schemes
You can define multiple security schemes in a singleComponentsObject 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 theComponentsObject, the OpenAPIGenerator will attempt to automatically generate a base authorization part for you:
- If the scheme name contains “bearer”, it defaults to an
httptype with abearerscheme. - If the scheme name contains “oauth2”, it defaults to an
oauth2type with a basic password flow. - If the scheme name contains “fido”, it defaults to an
apiKeytype in theheader. - Otherwise, it defaults to an
apiKeytype in theheader.
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-Languageheader parameter (BCP 47 language tag). - Response: A
Content-Languageheader on every response, indicating the resolved locale.
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.
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.
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:
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
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
Rate Limit Headers
When a route has a rate limit defined (viaRoute::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 viaRoute::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.