๐งช Spec Pipeline Architecture โ
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.
Pipeline overview โ
Source files โ Assembler โ Specification โ Augmenters โ Compiler โ OpenAPI document- Assembler โ scans source files, instantiates attributes from reflection, resolves nesting via two-pass slot maps
- Specification โ a flat, typed container holding all collected attributes in buckets
- Augmenters โ enrich the specification with inferred data (types, refs, tags, etc.) via a grouped pipeline
- Compiler โ transforms the specification into a versioned OpenAPI document array
- Builder โ the unified entry point that orchestrates the pipeline
Assembler โ
The Assembler collects spec attributes from source files and resolves their nesting relationships.
Slot-map driven nesting โ
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 absorbs
Slots use [] suffix for collection append ('parameters[]'), bare name for scalar assignment ('requestBody').
Two-pass assembly โ
The Assembler runs two distinct passes:
- Sibling merge โ attributes on the same reflector compose via
merge()maps - Hierarchy resolution โ attributes from inner reflectors (method โ class, property โ class) are absorbed via
contains()maps
After both passes, only root attributes remain and are added to the Specification.
Root attributes โ
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.
Specification โ
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 โ
Augmenters form a grouped pipeline that enriches the Specification in three ordered phases:
Pipeline phases โ
| Phase | Purpose | Examples |
|---|---|---|
| Resolve | Infer data from PHP reflection and cross-bucket relationships | Inheritance, Names, Enums, PathItems, Types, Refs, Shortcuts |
| Reduce | Filter or remove entries | PathFilter, Cleanup |
| Augment | Add derived metadata | MediaTypes, Docblocks, OperationIds, Tags, EnumDescriptions |
Each augmenter implements PipeInterface and receives the full Specification. Augmenters within a phase run in registration order.
Configuring augmenters โ
$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);
});Writing a custom augmenter โ
A custom augmenter implements PipeInterface:
use OpenApi\Utils\PipeInterface;
use OpenApi\Specification;
use OpenApi\Spec as OA;
class CustomAugmenter implements PipeInterface
{
public function group(): string|\BackedEnum
{
return \OpenApi\Augmenter\Group::Augment;
}
public function __invoke(mixed $payload): mixed
{
foreach ($payload->schemas as $schema) {
// enrich schemas...
}
// or
// the walker will walk all attributes (including nested) of the specification
$payload->getWalker()->visit(OA\Property::class, function (OA\Property $property) {
// ...
});
// or walk all attributes with $ref set
$payload->getWalker()->eachRef(function () {
// $attribute->ref = ...
});
return $payload;
}
}Compilers โ
Each OpenAPI version has its own compiler that handles version-specific output differences:
| Compiler | Version | Key differences |
|---|---|---|
OpenApi30Compiler | 3.0.x | nullable as property, exclusiveMinimum as boolean |
OpenApi31Compiler | 3.1.x | nullable via type array, exclusiveMinimum as number, webhooks |
OpenApi32Compiler | 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.
Design principles โ
Normalise early, store simple โ
When a property has both a rich input type (e.g. a PHP enum) and a plain serialization type (e.g. string), accept both on input but normalise to the plain type immediately in the constructor. Properties always store the simple form โ enums, objects, and convenience types are input sugar only. This keeps downstream code (augmenters, compilers, serialization) free of type-checking branches.
// Example: FlowType enum accepted on input, stored as string
public function __construct(string|FlowType|null $flow = null) {
parent::__construct([
'flow' => $flow instanceof \BackedEnum ? $flow->value : $flow,
]);
}Reflectors as glue โ
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:
- PathItem โ Operation binding โ PathItem is placed on a class; operations on methods of that class. The
PathItemsaugmenter walksReflectionMethod::getDeclaringClass()to find which PathItem governs an operation. - Prefix composition via inheritance โ PathItems on parent classes contribute prefixes. The augmenter walks
ReflectionClass::getParentClass()to compose the full path prefix chain. - OperationId generation โ the reflector provides class/method name context for auto-generated identifiers.
- Type inference โ the
Typesaugmenter 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.
DTO class tree โ
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\Schema\Items
โ โโโ 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\MediaType\Json
โ โโโ OA\MediaType\Xml
โโโ 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\AttachableTyped 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.
Classic processor mapping โ
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 | Shortcuts | resolve |
| MergeXmlContent | Shortcuts | resolve |
| 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.