Back to Swagger Php

ADR-001: Context Keys and Annotation Lifecycle

docs/adr/001-context-keys-and-annotation-lifecycle.md

6.2.08.7 KB
Original Source

ADR-001: Context Keys and Annotation Lifecycle

Status

Accepted

Context

The Context class uses dynamic properties (via __get with prototypical inheritance) to carry metadata about where an annotation was found or created. Understanding these keys is essential for writing processors that create, move, or remove annotations.

Context Keys

Source location keys

Set by the analyser, inherited via the parent chain.

KeyTypeMeaning
filenamestringAbsolute path to the PHP file
lineintLine number
characterintColumn offset
namespacestringPHP namespace
usesarrayImport aliases (['Alias' => 'Full\\Class'])
classstringEnclosing class name
interfacestringEnclosing interface name
traitstringEnclosing trait name
enumstringEnclosing enum name
methodstringEnclosing method name
propertystringEnclosing property name
staticboolWhether the method/property is static
extendsstring|arrayParent class or interfaces extended
implementsarrayInterfaces implemented
commentstringThe raw PHP DocComment
reflector\ReflectorReflection object for the element
scannedarrayDetails from file scanner (ReflectionAnalyser)

Annotation relationship keys

KeyTypeMeaning
nestedAbstractAnnotation|nullThe parent annotation this one is nested inside. null means explicitly not nested (top-level for merge purposes). Absent (not set) means the same as null but via inheritance — is('nested') returns false.
annotationslist<AbstractAnnotation>All annotations registered on this context. Shared by annotations at the same source location.

Processing keys

KeyTypeMeaning
generatedboolThe annotation/context was created by a processor, type resolver, or serializer (not from source scan).
versionstringThe OpenAPI version in use (set on root context).
loggerLoggerInterfacePSR logger (guaranteed set when using Generator).
otherlist<AbstractAnnotation>Non-OpenApi annotations found at this location.

The nested Key

How is('nested') works

Context::is() calls property_exists() — it checks whether the property is set directly on this context instance (not inherited from parent). This distinction drives processor behaviour:

  • is('nested') === true: The property exists on this context. The annotation has an explicit nesting declaration.
  • is('nested') === false: The property is not set. MergeIntoOpenApi/MergeIntoComponents treat this as "top-level, merge into root".

Values

Valueis('nested')Meaning
AbstractAnnotation instancetrueThis annotation is a child of that parent
nulltrueExplicitly marked as having no parent (e.g. parameter-level attributes that should not be merged into root)
(not set)falseTop-level — eligible for merge into OpenApi/Components

Where nested is set

  1. AbstractAnnotation::__construct() (line 110): Creates a child context ['nested' => $this] for annotations passed as constructor properties.

  2. AbstractAnnotation::merge() (line 156): Same pattern for annotations merged into _unmerged.

  3. AttributeAnnotationFactory (line 76): Sets 'nested' => null for parameter-level attributes (#[Property], #[Parameter], #[RequestBody] on method parameters) that should not be merged into root.

  4. Processors (e.g. MergeJsonContent): Should update _context when relocating an annotation to reflect its new parent.

How processors use nested

  • MergeJsonContent/MergeXmlContent: Read $annotation->_context->nested to find the parent (Response/RequestBody/Parameter) and check instanceof.
  • MergeIntoOpenApi/MergeIntoComponents: Check $annotation->_context->is('nested') === false to find top-level annotations eligible for merging into root.

Annotation Lifecycle

Registration

Analysis::addAnnotation($annotation, $context) registers in two places:

  1. $this->annotations (SplObjectStorage) — keyed by annotation, value is context
  2. $context->annotations[] — array on the context object

Removal

Analysis::removeAnnotation($annotation) removes from both:

  1. The SplObjectStorage
  2. The context's annotations array (context is retrieved from the SplObjectStorage before removal)

Creating annotations in a processor

When a processor creates a new annotation, it must register it with the analysis via addAnnotation(). This ensures the annotation is discoverable by subsequent processors (via getAnnotationsOfType()) and that its context is properly linked into the tree.

php
$context = new Context(['nested' => $parent, 'generated' => true], $parent->_context);
$annotation = new OA\Schema(['_context' => $context, ...]);
$analysis->addAnnotation($annotation, $context);

Key points:

  • Create a new Context for each annotation — never share a single context instance across multiple annotations. Each annotation needs its own context because addAnnotation() appends to $context->annotations[]. Sharing a context causes unrelated annotations to appear in each other's context, which confuses validation and cleanup. The context's parent chain provides inheritance of location keys (file, class, method), so per-annotation contexts are lightweight.
  • Always pass _context with 'generated' => true and 'nested' => $parent so the annotation is correctly positioned for validation.
  • Call addAnnotation() — this registers in both the SplObjectStorage (making it findable by type) and $context->annotations (linking it to its source location).
  • Use $parent->merge([$annotation]) if the annotation should be nested into the parent's $_nested mapping or end up in _unmerged. This is the normal path for annotations that the parent "owns".
  • Call addAnnotation() directly (without merge) when the annotation will be consumed by a later processor (e.g., creating a JsonContent that MergeJsonContent will transform). The later processor is responsible for cleanup.

If you only call addAnnotation() without placing the annotation into the tree (i.e., it's not reachable from the root OpenApi object via properties or _unmerged), it will be findable via getAnnotationsOfType() but won't be validated or serialized.

When processors relocate annotations

When a processor transforms an annotation (e.g., JsonContent becomes a Schema inside a MediaType), it must:

  1. Update _context — set a new context with nested pointing to the new parent, so tree-walking validation sees it in the correct location.
  2. Remove from parent's _unmerged — so the old parent's validation doesn't warn about unexpected children.
  3. Remove from analysis registry — via $analysis->removeAnnotation() so it's no longer discoverable via getAnnotationsOfType() and the old context's annotations array is cleaned up.

Example (from MergeJsonContent):

php
// Create the new parent
$mediaType = new OA\MediaType(['schema' => $jsonContent, ...]);

// Update context to reflect new position in tree
$jsonContent->_context = new Context(['nested' => $mediaType, 'generated' => true], $mediaType->_context);

// Remove from old parent's _unmerged
array_splice($parent->_unmerged, $index, 1);

// Remove from analysis registry (cleans up SplObjectStorage + old context->annotations)
$analysis->removeAnnotation($jsonContent);

Validation and Tree Walking

Analysis::validate() does NOT iterate the SplObjectStorage. It calls collectAnnotations() which walks the annotation tree starting from $this->openapi, following all non-blacklisted object properties recursively. This means:

  • An annotation removed from the registry but still reachable via the tree will be validated.
  • The _context->nested value determines whether validation considers the annotation correctly placed.
  • Annotations with $_parents = [] (like JsonContent) have no valid parent — if still reachable during tree walking, their context must point to a valid parent or they should be transformed into a type that has valid parents.

Decision

Processors that consume/transform annotations must perform full cleanup:

  1. Update _context to reflect the annotation's new position in the tree
  2. Remove from old parent's _unmerged
  3. Remove from analysis registry via removeAnnotation()

The nested context key should use null (not false) to indicate "explicitly no parent" — this matches the declared @property OA\AbstractAnnotation|null type.