Back to Swagger Php

๐Ÿงช Spec Pipeline Architecture

docs/reference/architecture.md

6.5.09.5 KB
Original Source

๐Ÿงช 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
  1. Assembler โ€” scans source files, instantiates attributes from reflection, resolves nesting via two-pass slot maps
  2. Specification โ€” a flat, typed container holding all collected attributes in buckets
  3. Augmenters โ€” enrich the specification with inferred data (types, refs, tags, etc.) via a grouped pipeline
  4. Compiler โ€” transforms the specification into a versioned OpenAPI document array
  5. 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 reflector
  • contains() 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:

  1. Sibling merge โ€” attributes on the same reflector compose via merge() maps
  2. 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

PhasePurposeExamples
ResolveInfer data from PHP reflection and cross-bucket relationshipsTypes, Refs, PathItems
ReduceFilter or remove entriesPathFilter, Cleanup
AugmentAdd derived metadataDocblocks, OperationIds, Tags, Inheritance, Names

Each augmenter implements PipeInterface and receives the full Specification. Augmenters within a phase run in registration order.

Configuring augmenters

php
$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:

php
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;
    }
}

Compilers

Each OpenAPI version has its own compiler that handles version-specific output differences:

CompilerVersionKey differences
Compiler303.0.xnullable as property, exclusiveMinimum as boolean
Compiler313.1.xnullable via type array, exclusiveMinimum as number, webhooks
Compiler323.2.xExtends 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.

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 PathItems augmenter walks ReflectionMethod::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 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.

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\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.

Classic processor mapping

How each classic processor maps to the new pipeline:

Classic ProcessorSpec EquivalentStage
ExpandClassesInheritance + Assembleraugment + assembly
ExpandTraitsInheritance + Assembleraugment + assembly
ExpandInterfacesInheritance + Assembleraugment + assembly
ExpandEnumsEnumsaugment
MergeIntoOpenApiAssemblerassembly
MergeIntoComponentsCompilercompile
MergeJsonContentN/A (attribute eliminated)โ€”
MergeXmlContentN/A (attribute eliminated)โ€”
BuildPathsCompilercompile
AugmentSchemasNames + Types + Assembler + Compilermixed
AugmentPropertiesTypesresolve
AugmentParametersTypesresolve
AugmentItemsTypesresolve
AugmentRequestBodyTypesresolve
AugmentRefsRefsresolve
AugmentDiscriminatorsRefsresolve
AugmentTagsTagsaugment
AugmentMediaTypeMediaTypesaugment
DocBlockDescriptionsDocblocksaugment
OperationIdOperationIdsaugment
CleanUnmergedAssembler (orphan validation)assembly
CleanUnusedComponentsCleanupreduce
PathFilterPathFilterreduce

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.