docs/reference/architecture.md
An overview of how the spec attributes pipeline turns your source code into an OpenAPI document.
For the internals โ how nesting is resolved, why the DTOs are shaped the way they are โ see Spec pipeline internals.
Source files โ Assembler โ Specification โ Resolver โ Augmenters โ Compiler โ OpenAPI document
The Assembler reads spec attributes off your classes, methods, properties and parameters,
and works out which ones belong inside which. An #[OA\Response] on a method ends up
inside that method's operation; an #[OA\Property] on a class property ends up inside the
class's schema.
Each attribute declares where it can go, rather than the Assembler hard-coding the rules. That is what lets you introduce your own attributes and have them nest correctly. The declaration mechanism is described in Spec pipeline internals.
Whatever is left once nesting is resolved is added to the 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.
Between assembly and augmentation, the Resolver looks for classes the specification refers
to but does not contain โ a model used in a $ref, or the type of a property on a schema โ
and hands each one to a chain of ResolverInterface implementations.
Resolver\Reflection is registered by default: it reflects the class and collects it with
the assembler already in use. A class carrying no spec attributes contributes nothing, so
resolution reports failure and the next resolver in the chain gets a turn โ which is where
something generating schemas for unannotated classes would slot in.
Wiring resolvers into a build is covered in Resolver configuration; discovery and the convergence loop are in Spec pipeline internals.
namespace OpenApi\Contracts;
interface ResolverInterface
{
public function resolve(string $fqcn, Assembler $assembler): bool;
}
Resolvers are handed the Assembler that built the specification, so collecting a reflector
with it adds the result straight into the specification in progress. A resolver that
assembles differently can add to $assembler->getSpecification() directly. Return true to
mark the FQCN handled and stop the chain for it.
Augmenters form a grouped pipeline that enriches the Specification in three ordered phases:
| Phase | Purpose |
|---|---|
| Resolve | Infer data from PHP reflection and cross-bucket relationships |
| Reduce | Filter or remove entries |
| Augment | Add derived metadata |
Each augmenter implements PipeInterface and receives the full Specification, and those
within a phase run in registration order. The Augmenters reference
lists which augmenters belong to each phase, in the order they run.
$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\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;
}
}
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.
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 walk a single nested annotation tree in one chain. Spec augmenters operate on a flat Specification of typed buckets, grouped into explicit phases. Both mutate their attributes in place โ the pipelines differ in shape and ordering, not in mutability.