docs/adr/002-analysis-registries-and-tree-structure.md
Accepted
The OpenAPI specification is represented as a tree of annotation objects rooted at OpenApi. Processors need to both query annotations by type ("find all Schemas") and traverse structural relationships ("what refs does this subtree contain"). The system maintains two flat registries alongside the tree to support efficient queries.
$analysis->annotations (SplObjectStorage)A flat index of all annotations keyed by object identity, with Context as the attached value.
Populated by: Analysis::addAnnotation(), which recursively registers the annotation and all its nested children at the time of the call.
Read by:
getAnnotationsOfType() — query all annotations of a given class (used by ~15 processors)MergeIntoOpenApi, MergeIntoComponents, DocBlockDescriptions, CleanUnmerged)AugmentRefs, CleanUnusedComponents)Analysis::merged() / Analysis::unmerged() / Analysis::split()$context->annotations (array)A per-source-location list of annotations declared at that context.
Populated by: Analysis::addAnnotation(), which appends to the context's annotations array.
Read by:
getAnnotationForSource() — finds the source-declared annotation for a FQDN. Critical for ExpandClasses, ExpandTraits, ExpandInterfaces, AugmentSchemas, AugmentDiscriminators, and type resolvers.The annotation tree rooted at $analysis->openapi is the source of truth for what appears in the output. Annotations reachable from this root via non-blacklisted properties are serialized and validated.
The registries are complete — every annotation in the tree is also in the registry. This is guaranteed by the combination of:
addAnnotation() on all discovered annotations, which recursively registers their children.mergeAnnotations() (or addAnnotation() directly) when creating or placing annotations, ensuring new annotations are always registered.Since addAnnotation() is idempotent (early-returns if the annotation already exists), calling it on already-registered annotations is a safe no-op. This eliminates the need for processors to track whether an annotation is "new" or "already known."
mergeAnnotations MethodPreviously, processors that created new annotations needed two separate operations:
$parent->merge([$annotation]) — place the annotation in the tree$analysis->addAnnotation($annotation, ...) — register it in the indexThis split was error-prone: some processors forgot step 2.
Analysis::mergeAnnotations($parent, $annotations, $ignore) combines both:
$parent->merge($annotations, $ignore) to place annotations in the treeaddAnnotation() for each annotation to ensure registration| Situation | Method |
|---|---|
| Processor merges annotations into a parent | $analysis->mergeAnnotations($parent, [...]) |
| Processor creates an annotation and places it directly (not via merge) | $analysis->addAnnotation($annotation, $context) + direct assignment |
| Processor relocates an annotation (removes from old parent, adds to new) | $analysis->removeAnnotation() + direct assignment + addAnnotation() |
Processors must use $analysis->mergeAnnotations() when merging annotations into the tree. This guarantees registry completeness without requiring callers to remember a separate registration step.
The registries are complete and can be relied upon for type-based queries and ref scanning. Iterating $analysis->annotations is sufficient to find all annotations, including those created by processors.
Processors that need type-based queries should use getAnnotationsOfType() — it's correct and efficient.
AbstractAnnotation::merge() should not be called directly by processors when Analysis is available. Direct merge() is acceptable only in contexts where annotations are already registered (e.g., trait methods operating on scan-time annotations without access to Analysis).