docs/reference/architecture.md
This page documents the internals of the spec attributes pipeline โ for users who want to understand how it works, write custom augmenters, or debug pipeline behavior.
Source files โ Assembler โ Specification โ Augmenters โ Compiler โ OpenAPI document
The Assembler collects spec attributes from source files and resolves their nesting relationships.
Each attribute declares its relationships via two methods:
merge() returns [ParentClass => 'slot'] โ how this attribute composes into siblings on the same reflectorcontains() returns [ChildClass => 'slot'] โ which child attributes from inner reflectors this attribute absorbsSlots use [] suffix for collection append ('parameters[]'), bare name for scalar assignment ('requestBody').
The Assembler runs two distinct passes:
merge() mapscontains() mapsAfter both passes, only root attributes remain and are added to the Specification.
A "root" attribute is one that can exist independently in the Specification โ it has its own bucket and doesn't require a parent container.
Always root: Schema, Operation, PathItem, OpenApi, Info, Tag, Server, ExternalDocumentation, SecurityScheme, Components, Attachable
Conditionally root: Response (when response key is set), RequestBody (when request key is set)
Never root: Parameter, Header, Link, Example, MediaType, Property, etc. โ these must be nested inside a parent or wrapped in a Components container.
The Specification is a flat, typed container with one bucket per root attribute type. It holds all attributes collected by the Assembler, organized by type (schemas, operations, pathItems, tags, etc.).
Augmenters read from and write to the Specification's buckets. The container is deliberately simple โ no tree structure, no parent pointers. Cross-bucket relationships are resolved by augmenters using reflectors.
Augmenters form a grouped pipeline that enriches the Specification in three ordered phases:
| Phase | Purpose | Examples |
|---|---|---|
| Resolve | Infer data from PHP reflection and cross-bucket relationships | Types, Refs, PathItems |
| Reduce | Filter or remove entries | PathFilter, Cleanup |
| Augment | Add derived metadata | Docblocks, OperationIds, Tags, Inheritance, Names |
Each augmenter implements PipeInterface and receives the full Specification. Augmenters within a phase run in registration order.
$builder->withAugmenters(function (\OpenApi\Utils\Pipeline $pipeline) {
// Get a typed reference to configure
$pipeline->get(Augmenter\OperationIds::class)?->setHash(true);
// Enable/disable
$pipeline->get(Augmenter\Cleanup::class)?->setEnabled(false);
// Insert before another
$pipeline->insert(new CustomAugmenter(), Augmenter\Inheritance::class);
// Remove entirely
$pipeline->remove(Augmenter\EnumDescriptions::class);
});
A custom augmenter implements PipeInterface:
use OpenApi\Augmenter\PipeInterface;
use OpenApi\Spec\Specification;
class CustomAugmenter implements PipeInterface
{
public function pipe(Specification $specification): Specification
{
foreach ($specification->schemas as $schema) {
// enrich schemas...
}
// or
// the walker will walk all attributes (including nested) of the specification
$specification->getWalker()->visit(OA\Property::class, function (OA\Property $property) {
// ...
});
// or walk all attributes with $ref set
$specification->getWalker()->eachRef(funtion () {
// $attribute->ref = ...
});
return $specification;
}
}
Each OpenAPI version has its own compiler that handles version-specific output differences:
| Compiler | Version | Key differences |
|---|---|---|
Compiler30 | 3.0.x | nullable as property, exclusiveMinimum as boolean |
Compiler31 | 3.1.x | nullable via type array, exclusiveMinimum as number, webhooks |
Compiler32 | 3.2.x | Extends 3.1 (currently without additional features) |
The compiler transforms a Specification into a plain PHP array representing the OpenAPI document. Version selection is automatic based on Builder::setVersion() or the #[OA\OpenApi(version: '...')] attribute.
Every root DTO carries its originating reflector (ReflectionClass, ReflectionMethod, etc.). This is the fundamental mechanism for resolving cross-bucket relationships at augmentation time.
Key applications:
PathItems augmenter walks ReflectionMethod::getDeclaringClass() to find which PathItem governs an operation.ReflectionClass::getParentClass() to compose the full path prefix chain.Types augmenter reads PHP type declarations from property/parameter reflectors.This design keeps the Assembler simple (just collect into buckets) and makes cross-cutting relationships resolvable without coupling DTOs to each other.
All spec attributes extend AbstractAttribute and live in the OpenApi\Spec namespace:
OA\AbstractAttribute
โโโ OA\Components
โโโ OA\OpenApi
โโโ OA\Info
โ โโโ OA\Contact
โ โโโ OA\License
โโโ OA\Server
โ โโโ OA\ServerVariable
โโโ OA\Tag
โโโ OA\ExternalDocumentation
โโโ OA\Operation
โ โโโ OA\Operation\Get
โ โโโ OA\Operation\Post
โ โโโ OA\Operation\Put
โ โโโ OA\Operation\Delete
โ โโโ OA\Operation\Patch
โ โโโ OA\Operation\Head
โ โโโ OA\Operation\Options
โ โโโ OA\Operation\Trace
โโโ OA\PathItem
โโโ OA\Schema
โ โโโ OA\Property
โโโ OA\Parameter
โ โโโ OA\Parameter\Path
โ โโโ OA\Parameter\Query
โ โโโ OA\Parameter\Header
โ โโโ OA\Parameter\Cookie
โโโ OA\RequestBody
โโโ OA\Response
โโโ OA\Header
โโโ OA\MediaType
โ โโโ OA\Encoding
โโโ OA\Link
โโโ OA\Example
โโโ OA\Discriminator
โโโ OA\Xml
โโโ OA\Flow
โ โโโ OA\Flow\Implicit
โ โโโ OA\Flow\Password
โ โโโ OA\Flow\ClientCredentials
โ โโโ OA\Flow\AuthorizationCode
โโโ OA\Security
โ โโโ OA\Security\Requirement
โ โโโ OA\Security\Scheme
โ โโโ OA\Security\Scheme\Http
โ โโโ OA\Security\Scheme\ApiKey
โ โโโ OA\Security\Scheme\OAuth2
โ โโโ OA\Security\Scheme\OpenIdConnect
โ โโโ OA\Security\Scheme\MutualTls
โโโ OA\Attachable
Typed subclasses (e.g. Operation\Get, Parameter\Path) pre-fill fields that the base class requires explicitly. The base class can always be used directly for full control.
How each classic processor maps to the new pipeline:
| Classic Processor | Spec Equivalent | Stage |
|---|---|---|
| ExpandClasses | Inheritance + Assembler | augment + assembly |
| ExpandTraits | Inheritance + Assembler | augment + assembly |
| ExpandInterfaces | Inheritance + Assembler | augment + assembly |
| ExpandEnums | Enums | augment |
| MergeIntoOpenApi | Assembler | assembly |
| MergeIntoComponents | Compiler | compile |
| MergeJsonContent | N/A (attribute eliminated) | โ |
| MergeXmlContent | N/A (attribute eliminated) | โ |
| BuildPaths | Compiler | compile |
| AugmentSchemas | Names + Types + Assembler + Compiler | mixed |
| AugmentProperties | Types | resolve |
| AugmentParameters | Types | resolve |
| AugmentItems | Types | resolve |
| AugmentRequestBody | Types | resolve |
| AugmentRefs | Refs | resolve |
| AugmentDiscriminators | Refs | resolve |
| AugmentTags | Tags | augment |
| AugmentMediaType | MediaTypes | augment |
| DocBlockDescriptions | Docblocks | augment |
| OperationId | OperationIds | augment |
| CleanUnmerged | Assembler (orphan validation) | assembly |
| CleanUnusedComponents | Cleanup | reduce |
| PathFilter | PathFilter | reduce |
The key architectural difference: classic processors operate on a mutable annotation tree in a single chain. Spec augmenters operate on an immutable Specification with typed buckets, grouped into explicit phases.