Back to Swagger Php

๐Ÿงช Spec Pipeline Architecture

docs/reference/architecture.md

6.7.18.8 KB
Original Source

๐Ÿงช Spec Pipeline Architecture

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.

Pipeline overview

Source files โ†’ Assembler โ†’ Specification โ†’ Resolver โ†’ Augmenters โ†’ Compiler โ†’ OpenAPI document
  1. Assembler โ€” scans source files, instantiates attributes from reflection, resolves nesting via slot maps (merge + hierarchical absorb)
  2. Specification โ€” a flat, typed container holding all collected attributes in buckets
  3. Resolver โ€” discovers and resolves unresolved FQCNs (e.g. unannotated model classes) before augmentation
  4. Augmenters โ€” enrich the specification with inferred data (types, refs, tags, etc.) via a grouped pipeline
  5. Compiler โ€” transforms the specification into a versioned OpenAPI document array
  6. Builder โ€” the unified entry point that orchestrates the pipeline

Assembler

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.

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.

Resolver

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.

Writing a resolver

php
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

Augmenters form a grouped pipeline that enriches the Specification in three ordered phases:

Pipeline phases

PhasePurpose
ResolveInfer data from PHP reflection and cross-bucket relationships
ReduceFilter or remove entries
AugmentAdd 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.

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

CompilerVersionKey differences
OpenApi30Compiler3.0.xnullable as property, exclusiveMinimum as boolean
OpenApi31Compiler3.1.xnullable via type array, exclusiveMinimum as number, webhooks
OpenApi32Compiler3.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.

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
MergeJsonContentShortcutsresolve
MergeXmlContentShortcutsresolve
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 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.