feat: add ArrayShapeDescriber for precise array shape documentation#2696
feat: add ArrayShapeDescriber for precise array shape documentation#2696lacatoire wants to merge 12 commits intonelmio:5.xfrom
Conversation
Add a dedicated TypeDescriber for Symfony's ArrayShapeType (7.3+) that generates OpenAPI object schemas with explicit properties, required fields, and additionalProperties based on the array shape definition. Previously, ArrayShapeType was handled by DictionaryDescriber (as it extends CollectionType), which only produced a generic object with additionalProperties, losing all shape information. - Add ArrayShapeDescriber with support for sealed/unsealed shapes - Exclude ArrayShapeType from DictionaryDescriber and ListDescriber - Conditionally register service only when ArrayShapeType class exists - Add functional test with fixture controller and entity Closes nelmio#2675
This reverts commit 9f5734d.
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## 5.x #2696 +/- ##
==========================================
+ Coverage 95.68% 95.70% +0.02%
==========================================
Files 94 95 +1
Lines 3079 3097 +18
==========================================
+ Hits 2946 2964 +18
Misses 133 133 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
Auto-generated fixture validating the OpenAPI output of the ArrayShapeDescriber for sealed and unsealed array shapes.
There was a problem hiding this comment.
Pull request overview
Adds first-class OpenAPI schema generation for Symfony TypeInfo ArrayShapeType (Symfony 7.3+), so array-shape PHPDoc produces explicit object properties/required fields instead of a generic dictionary schema.
Changes:
- Introduce
ArrayShapeDescriberto convertArrayShapeTypeinto an OpenAPI object schema withproperties,required, andadditionalProperties. - Exclude
ArrayShapeTypefromDictionaryDescriberandListDescriberso it isn’t described as a generic collection. - Add functional coverage (controller + entity + conditional test case) and conditionally register the describer service when
ArrayShapeTypeexists.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/Functional/Entity/ArrayShapeTypes.php | Adds an entity with array-shape PHPDoc to drive functional schema generation. |
| tests/Functional/ControllerTest.php | Adds a conditional functional test case for array-shape handling on Symfony 7.3+. |
| tests/Functional/Controller/ArrayShapeController.php | Adds a functional endpoint that returns a model using array-shape properties. |
| src/TypeDescriber/ListDescriber.php | Attempts to prevent list handling from matching ArrayShapeType. |
| src/TypeDescriber/DictionaryDescriber.php | Attempts to prevent dictionary handling from matching ArrayShapeType. |
| src/TypeDescriber/ArrayShapeDescriber.php | New describer implementing precise OpenAPI schema generation for ArrayShapeType. |
| phpstan-baseline.neon | Adds new PHPStan baseline entries related to ArrayShapeType availability/typing. |
| config/services.php | Conditionally registers the new describer service when ArrayShapeType is present. |
| public function supports(Type $type, array $context = []): bool | ||
| { | ||
| return $type instanceof CollectionType | ||
| && !$type instanceof Type\ArrayShapeType |
There was a problem hiding this comment.
The instanceof Type\ArrayShapeType check will throw a runtime Error on Symfony versions where Symfony\\Component\\TypeInfo\\Type\\ArrayShapeType doesn't exist (because instanceof against a missing class is fatal). Use a safe check (e.g., class_exists(...) && is_a($type, 'Symfony\\Component\\TypeInfo\\Type\\ArrayShapeType')) or compare against a string class name to avoid hard-referencing a non-existent class.
| && !$type instanceof Type\ArrayShapeType | |
| && !(class_exists(Type\ArrayShapeType::class) | |
| && is_a($type, Type\ArrayShapeType::class)) |
| public function supports(Type $type, array $context = []): bool | ||
| { | ||
| return $type instanceof CollectionType | ||
| && !$type instanceof Type\ArrayShapeType |
There was a problem hiding this comment.
The instanceof Type\ArrayShapeType exclusion is a runtime-fatal check when the ArrayShapeType class isn't present (Symfony < 7.3). Replace it with a guarded class_exists + is_a(...) check, or use the fully-qualified class name string, so the describer remains compatible across supported Symfony versions.
| && !$type instanceof Type\ArrayShapeType | |
| && ( | |
| !class_exists(Type\ArrayShapeType::class) | |
| || !is_a($type, Type\ArrayShapeType::class) | |
| ) |
|
|
||
| - | ||
| message: '#^Class Symfony\\Component\\TypeInfo\\Type\\ArrayShapeType not found\.$#' | ||
| identifier: class.notFound | ||
| count: 1 | ||
| path: src/TypeDescriber/DictionaryDescriber.php | ||
|
|
||
| - | ||
| message: '#^Class Symfony\\Component\\TypeInfo\\Type\\ArrayShapeType not found\.$#' | ||
| identifier: class.notFound | ||
| count: 1 | ||
| path: src/TypeDescriber/ListDescriber.php |
There was a problem hiding this comment.
The new baseline entries for DictionaryDescriber/ListDescriber are currently required only because the code hard-references Symfony\\Component\\TypeInfo\\Type\\ArrayShapeType. Once the describers use a safe string-based/guarded check, these ignores should be removed to avoid permanently masking real analysis issues in those files.
| - | |
| message: '#^Class Symfony\\Component\\TypeInfo\\Type\\ArrayShapeType not found\.$#' | |
| identifier: class.notFound | |
| count: 1 | |
| path: src/TypeDescriber/DictionaryDescriber.php | |
| - | |
| message: '#^Class Symfony\\Component\\TypeInfo\\Type\\ArrayShapeType not found\.$#' | |
| identifier: class.notFound | |
| count: 1 | |
| path: src/TypeDescriber/ListDescriber.php |
- Remove `?? false` fallback on `optional` key since ArrayShapeType::getShape() guarantees its presence - Add test cases for nested shapes, numeric keys, and nullable values - Regenerate fixture with new test entities
…ne entry - Use 'symfony-7.3' fixture suffix for Symfony 7.3 where the unsealed array shape extra value type is resolved differently (oneOf vs nullable) - Remove stale LegacyTypeConverter baseline entry that was erroneously added and doesn't exist on 5.x
Summary
ArrayShapeDescriberfor Symfony'sArrayShapeType(7.3+) that generates OpenAPI object schemas with explicit properties, required fields, andadditionalPropertiesbased on the array shape definitionArrayShapeTypewas handled byDictionaryDescriber(since it extendsCollectionType), producing only a genericobjectwithadditionalProperties— losing all shape information (property names, types, optionality)ArrayShapeTypefromDictionaryDescriberandListDescriberto prevent incorrect handlingArrayShapeTypeclass exists (Symfony 7.3+)Example
For
array{name: string, age: int, email?: string}, the describer now generates:{ "type": "object", "required": ["name", "age"], "properties": { "name": { "type": "string" }, "age": { "type": "integer" }, "email": { "type": "string" } }, "additionalProperties": false }Closes #2675