docs/reference/inheritance.md
This page documents the decision rules and mechanics of how PHP class hierarchy maps to OpenAPI composition in the spec pipeline. For usage examples see Using Spec Attributes.
Two augmenters handle inheritance: Inheritance (schema composition via allOf) and PathItems (prefix composition and metadata cloning).
Augmenter: OpenApi\Augmenter\Inheritance · Phase: Resolve
The Inheritance augmenter walks the PHP class hierarchy for every schema that has a class reflector and expands it into OpenAPI allOf composition or inline property merging.
For each schema, the augmenter processes three relationship types in order:
getParentClass() chainFor each ancestor encountered:
Ancestor has #[Schema]? | Action | Continue walking? |
|---|---|---|
| Yes | Add $ref to the schema's allOf | Stop (parents only) |
| No | Merge ancestor's #[OA\Property] members inline | Continue |
class C extends B extends A
C.allOf gets $ref: B, walk stopsC.allOf gets $ref: AThe "stop at first schema ancestor" rule prevents redundant references — B's schema already composes A if needed.
Traits are collected from two sources:
This ensures that when a non-schema parent uses a trait with #[Schema], the composition is still captured.
Only direct interfaces of the schema's class are processed. Unlike parents, there is no stop-on-first-schema rule — all direct interfaces with schemas contribute a $ref.
When an ancestor has no schema, its #[OA\Property] members are merged into the current schema. Deduplication is by property name — if the schema already declares a property with the same name, the ancestor's version is skipped. Merged properties are prepended to the schema's property list.
After expansion, if a schema has both allOf entries and its own properties, the compiler moves the properties into a dedicated allOf entry (an anonymous schema with type: object). This keeps the output clean:
User:
allOf:
- $ref: '#/components/schemas/BaseModel'
- type: object
properties:
email:
type: string
If you explicitly declare an allOf entry that matches one the augmenter would add (e.g. you extend a class and also manually reference it), the compiler deduplicates — only one $ref survives.
Augmenter: OpenApi\Augmenter\PathItems · Phase: Resolve
The PathItems augmenter resolves how #[OA\PathItem] attributes on controller classes compose via PHP inheritance to build operation paths and share metadata.
Each PathItem may declare a prefix. The augmenter composes prefixes by walking up the class hierarchy:
#[OA\PathItem(prefix: '/api/v1')]
class BaseController {}
#[OA\PathItem(prefix: '/users')]
class UserController extends BaseController {}
Resolution walks from the class to root, collects prefixes in ancestor order (root first), and joins them:
/api/v1 + /users → /api/v1/users
The resolved prefix is prepended to each operation's path. An operation with path: '/{id}' in UserController becomes /api/v1/users/{id}.
An operation's "governing" PathItem is found by walking up from the operation's declaring class until a class with #[PathItem] is found. Operations in a class without #[PathItem] can still inherit from an ancestor's PathItem.
The augmenter clones metadata from PathItem (and its ancestors) to operations:
| Property | Merge behavior |
|---|---|
tags | Accumulated from all ancestors, deduplicated, appended to operation's existing tags |
security | Accumulated from all ancestors, deduplicated by scheme name |
responses | Accumulated from all ancestors, deduplicated by response code — operation's own responses take precedence |
All three accumulate additively up the hierarchy — every ancestor's PathItem contributes.
PathItem parameters are inherited from ancestor PathItems and are emitted at the path level in the OpenAPI output. Deduplication is by name + in combination. The child's parameters take precedence over ancestors.
A PathItem with parameters, summary, description, or servers produces path-level output in the OpenAPI document. The augmenter resolves which operation paths map to this PathItem and emits the path-level properties for each.