From 9c0489c26ea567e6efce9ddd3a7e6f3a897b1e43 Mon Sep 17 00:00:00 2001 From: lacatoire Date: Fri, 20 Feb 2026 16:58:28 +0100 Subject: [PATCH 01/11] feat: add ArrayShapeDescriber for precise array shape documentation 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 #2675 --- config/services.php | 8 +++ src/TypeDescriber/ArrayShapeDescriber.php | 59 +++++++++++++++++++ src/TypeDescriber/DictionaryDescriber.php | 1 + src/TypeDescriber/ListDescriber.php | 1 + .../Controller/ArrayShapeController.php | 32 ++++++++++ tests/Functional/ControllerTest.php | 13 ++++ tests/Functional/Entity/ArrayShapeTypes.php | 23 ++++++++ 7 files changed, 137 insertions(+) create mode 100644 src/TypeDescriber/ArrayShapeDescriber.php create mode 100644 tests/Functional/Controller/ArrayShapeController.php create mode 100644 tests/Functional/Entity/ArrayShapeTypes.php diff --git a/config/services.php b/config/services.php index ba18fff23..7992c751b 100644 --- a/config/services.php +++ b/config/services.php @@ -251,4 +251,12 @@ ->private() ->tag('nelmio_api_doc.type_describer', ['priority' => -1000]) ; + + if (class_exists(\Symfony\Component\TypeInfo\Type\ArrayShapeType::class)) { + $container->services() + ->set('nelmio_api_doc.type_describer.array_shape', \Nelmio\ApiDocBundle\TypeDescriber\ArrayShapeDescriber::class) + ->private() + ->tag('nelmio_api_doc.type_describer', ['priority' => -990]) + ; + } }; diff --git a/src/TypeDescriber/ArrayShapeDescriber.php b/src/TypeDescriber/ArrayShapeDescriber.php new file mode 100644 index 000000000..38b8e7003 --- /dev/null +++ b/src/TypeDescriber/ArrayShapeDescriber.php @@ -0,0 +1,59 @@ + + * + * @internal + */ +final class ArrayShapeDescriber implements TypeDescriberInterface, TypeDescriberAwareInterface +{ + use TypeDescriberAwareTrait; + + public function describe(Type $type, Schema $schema, array $context = []): void + { + $schema->type = 'object'; + $required = []; + + foreach ($type->getShape() as $key => $shapeValue) { + $property = Util::getProperty($schema, (string) $key); + $this->describer->describe($shapeValue['type'], $property, $context); + + if (!($shapeValue['optional'] ?? false)) { + $required[] = (string) $key; + } + } + + if ([] !== $required) { + $schema->required = $required; + } + + if (!$type->isSealed()) { + $additionalProperties = Util::getChild($schema, OA\AdditionalProperties::class); + $this->describer->describe($type->getExtraValueType(), $additionalProperties, $context); + } else { + $schema->additionalProperties = false; + } + } + + public function supports(Type $type, array $context = []): bool + { + return $type instanceof ArrayShapeType; + } +} diff --git a/src/TypeDescriber/DictionaryDescriber.php b/src/TypeDescriber/DictionaryDescriber.php index aab49aa8c..3ef3e7484 100644 --- a/src/TypeDescriber/DictionaryDescriber.php +++ b/src/TypeDescriber/DictionaryDescriber.php @@ -38,6 +38,7 @@ public function describe(Type $type, Schema $schema, array $context = []): void public function supports(Type $type, array $context = []): bool { return $type instanceof CollectionType + && !$type instanceof Type\ArrayShapeType && $type->getCollectionKeyType() instanceof Type\BuiltinType && TypeIdentifier::STRING === $type->getCollectionKeyType()->getTypeIdentifier(); } diff --git a/src/TypeDescriber/ListDescriber.php b/src/TypeDescriber/ListDescriber.php index c9061b150..b9fd751cc 100644 --- a/src/TypeDescriber/ListDescriber.php +++ b/src/TypeDescriber/ListDescriber.php @@ -38,6 +38,7 @@ public function describe(Type $type, Schema $schema, array $context = []): void public function supports(Type $type, array $context = []): bool { return $type instanceof CollectionType + && !$type instanceof Type\ArrayShapeType && $type->getCollectionKeyType() instanceof Type\BuiltinType && TypeIdentifier::INT === $type->getCollectionKeyType()->getTypeIdentifier(); } diff --git a/tests/Functional/Controller/ArrayShapeController.php b/tests/Functional/Controller/ArrayShapeController.php new file mode 100644 index 000000000..9386d6664 --- /dev/null +++ b/tests/Functional/Controller/ArrayShapeController.php @@ -0,0 +1,32 @@ + [ + 'ArrayShapeController', + null, + [], + [ + 'nelmio_api_doc' => [ + 'type_info' => true, + ], + ], + ]; + } + yield 'Custom model names' => [ 'CustomModelNameController', ]; diff --git a/tests/Functional/Entity/ArrayShapeTypes.php b/tests/Functional/Entity/ArrayShapeTypes.php new file mode 100644 index 000000000..cb3723a22 --- /dev/null +++ b/tests/Functional/Entity/ArrayShapeTypes.php @@ -0,0 +1,23 @@ + Date: Fri, 20 Feb 2026 17:03:58 +0100 Subject: [PATCH 02/11] feat: add support for zircote/swagger-php 6 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 3aec2546e..321f050ab 100644 --- a/composer.json +++ b/composer.json @@ -33,7 +33,7 @@ "symfony/property-info": "^6.4 || ^7.2 || ^8.0", "symfony/routing": "^6.4 || ^7.2 || ^8.0", "symfony/type-info": "^7.2 || ^8.0", - "zircote/swagger-php": "^4.11.1 || ^5.0" + "zircote/swagger-php": "^4.11.1 || ^5.0 || ^6.0" }, "require-dev": { "api-platform/core": "^3.2 || ^4.0", From 5f063c470059e5bdb98c571de5615bb65936f703 Mon Sep 17 00:00:00 2001 From: lacatoire Date: Fri, 20 Feb 2026 17:06:53 +0100 Subject: [PATCH 03/11] fix: add PHPStan baseline entries for ArrayShapeType (Symfony 7.3+) --- phpstan-baseline.neon | 60 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 9ca3cd965..bf3f7f8d1 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -47,3 +47,63 @@ parameters: identifier: argument.templateType count: 1 path: src/Util/LegacyTypeConverter.php + + - + message: '#^PHPDoc tag `@implements` has invalid type Symfony\\\\Component\\\\TypeInfo\\\\Type\\\\ArrayShapeType\.$#' + identifier: class.notFound + count: 1 + path: src/TypeDescriber/ArrayShapeDescriber.php + + - + message: '#^Type Symfony\\\\Component\\\\TypeInfo\\\\Type\\\\ArrayShapeType in generic type Nelmio\\\\ApiDocBundle\\\\TypeDescriber\\\\TypeDescriberInterface\ in PHPDoc tag `@implements` is not subtype of template type T of Symfony\\\\Component\\\\TypeInfo\\\\Type of interface Nelmio\\\\ApiDocBundle\\\\TypeDescriber\\\\TypeDescriberInterface\.$#' + identifier: class.notFound + count: 1 + path: src/TypeDescriber/ArrayShapeDescriber.php + + - + message: '#^Parameter \$type of method Nelmio\\\\ApiDocBundle\\\\TypeDescriber\\\\ArrayShapeDescriber\:\:describe\(\) has invalid type Symfony\\\\Component\\\\TypeInfo\\\\Type\\\\ArrayShapeType\.$#' + identifier: class.notFound + count: 1 + path: src/TypeDescriber/ArrayShapeDescriber.php + + - + message: '#^Call to an undefined method Symfony\\\\Component\\\\TypeInfo\\\\Type\:\:getShape\(\)\.$#' + identifier: method.notFound + count: 1 + path: src/TypeDescriber/ArrayShapeDescriber.php + + - + message: '#^Call to an undefined method Symfony\\\\Component\\\\TypeInfo\\\\Type\:\:isSealed\(\)\.$#' + identifier: method.notFound + count: 1 + path: src/TypeDescriber/ArrayShapeDescriber.php + + - + message: '#^Call to an undefined method Symfony\\\\Component\\\\TypeInfo\\\\Type\:\:getExtraValueType\(\)\.$#' + identifier: method.notFound + count: 1 + path: src/TypeDescriber/ArrayShapeDescriber.php + + - + message: '#^Parameter \$type of method Nelmio\\\\ApiDocBundle\\\\TypeDescriber\\\\ArrayShapeDescriber\:\:supports\(\) has invalid type Symfony\\\\Component\\\\TypeInfo\\\\Type\\\\ArrayShapeType\.$#' + identifier: class.notFound + count: 1 + path: src/TypeDescriber/ArrayShapeDescriber.php + + - + message: '#^Class Symfony\\\\Component\\\\TypeInfo\\\\Type\\\\ArrayShapeType not found\.$#' + identifier: class.notFound + count: 1 + path: src/TypeDescriber/ArrayShapeDescriber.php + + - + 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 From 2243c1330832b6f9a4372f3c74d0115abbea6107 Mon Sep 17 00:00:00 2001 From: lacatoire Date: Fri, 20 Feb 2026 17:17:13 +0100 Subject: [PATCH 04/11] Revert "feat: add support for zircote/swagger-php 6" This reverts commit 9f5734d9c22161ebcb1cfeb481823c920992dbac. --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 321f050ab..3aec2546e 100644 --- a/composer.json +++ b/composer.json @@ -33,7 +33,7 @@ "symfony/property-info": "^6.4 || ^7.2 || ^8.0", "symfony/routing": "^6.4 || ^7.2 || ^8.0", "symfony/type-info": "^7.2 || ^8.0", - "zircote/swagger-php": "^4.11.1 || ^5.0 || ^6.0" + "zircote/swagger-php": "^4.11.1 || ^5.0" }, "require-dev": { "api-platform/core": "^3.2 || ^4.0", From cb056e9803d4c63c601b9a9c426c161043fb2c80 Mon Sep 17 00:00:00 2001 From: lacatoire Date: Fri, 20 Feb 2026 17:18:39 +0100 Subject: [PATCH 05/11] fix: PHPStan baseline escaping and ArrayShape test condition for Symfony < 7.3 --- phpstan-baseline.neon | 20 ++++++++++---------- tests/Functional/ControllerTest.php | 2 +- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index bf3f7f8d1..d658df653 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -49,61 +49,61 @@ parameters: path: src/Util/LegacyTypeConverter.php - - message: '#^PHPDoc tag `@implements` has invalid type Symfony\\\\Component\\\\TypeInfo\\\\Type\\\\ArrayShapeType\.$#' + message: '#^PHPDoc tag `@implements` has invalid type Symfony\\Component\\TypeInfo\\Type\\ArrayShapeType\.$#' identifier: class.notFound count: 1 path: src/TypeDescriber/ArrayShapeDescriber.php - - message: '#^Type Symfony\\\\Component\\\\TypeInfo\\\\Type\\\\ArrayShapeType in generic type Nelmio\\\\ApiDocBundle\\\\TypeDescriber\\\\TypeDescriberInterface\ in PHPDoc tag `@implements` is not subtype of template type T of Symfony\\\\Component\\\\TypeInfo\\\\Type of interface Nelmio\\\\ApiDocBundle\\\\TypeDescriber\\\\TypeDescriberInterface\.$#' + message: '#^Type Symfony\\Component\\TypeInfo\\Type\\ArrayShapeType in generic type Nelmio\\ApiDocBundle\\TypeDescriber\\TypeDescriberInterface\ in PHPDoc tag `@implements` is not subtype of template type T of Symfony\\Component\\TypeInfo\\Type of interface Nelmio\\ApiDocBundle\\TypeDescriber\\TypeDescriberInterface\.$#' identifier: class.notFound count: 1 path: src/TypeDescriber/ArrayShapeDescriber.php - - message: '#^Parameter \$type of method Nelmio\\\\ApiDocBundle\\\\TypeDescriber\\\\ArrayShapeDescriber\:\:describe\(\) has invalid type Symfony\\\\Component\\\\TypeInfo\\\\Type\\\\ArrayShapeType\.$#' + message: '#^Parameter \$type of method Nelmio\\ApiDocBundle\\TypeDescriber\\ArrayShapeDescriber\:\:describe\(\) has invalid type Symfony\\Component\\TypeInfo\\Type\\ArrayShapeType\.$#' identifier: class.notFound count: 1 path: src/TypeDescriber/ArrayShapeDescriber.php - - message: '#^Call to an undefined method Symfony\\\\Component\\\\TypeInfo\\\\Type\:\:getShape\(\)\.$#' + message: '#^Call to an undefined method Symfony\\Component\\TypeInfo\\Type\:\:getShape\(\)\.$#' identifier: method.notFound count: 1 path: src/TypeDescriber/ArrayShapeDescriber.php - - message: '#^Call to an undefined method Symfony\\\\Component\\\\TypeInfo\\\\Type\:\:isSealed\(\)\.$#' + message: '#^Call to an undefined method Symfony\\Component\\TypeInfo\\Type\:\:isSealed\(\)\.$#' identifier: method.notFound count: 1 path: src/TypeDescriber/ArrayShapeDescriber.php - - message: '#^Call to an undefined method Symfony\\\\Component\\\\TypeInfo\\\\Type\:\:getExtraValueType\(\)\.$#' + message: '#^Call to an undefined method Symfony\\Component\\TypeInfo\\Type\:\:getExtraValueType\(\)\.$#' identifier: method.notFound count: 1 path: src/TypeDescriber/ArrayShapeDescriber.php - - message: '#^Parameter \$type of method Nelmio\\\\ApiDocBundle\\\\TypeDescriber\\\\ArrayShapeDescriber\:\:supports\(\) has invalid type Symfony\\\\Component\\\\TypeInfo\\\\Type\\\\ArrayShapeType\.$#' + message: '#^Parameter \$type of method Nelmio\\ApiDocBundle\\TypeDescriber\\ArrayShapeDescriber\:\:supports\(\) has invalid type Symfony\\Component\\TypeInfo\\Type\\ArrayShapeType\.$#' identifier: class.notFound count: 1 path: src/TypeDescriber/ArrayShapeDescriber.php - - message: '#^Class Symfony\\\\Component\\\\TypeInfo\\\\Type\\\\ArrayShapeType not found\.$#' + message: '#^Class Symfony\\Component\\TypeInfo\\Type\\ArrayShapeType not found\.$#' identifier: class.notFound count: 1 path: src/TypeDescriber/ArrayShapeDescriber.php - - message: '#^Class Symfony\\\\Component\\\\TypeInfo\\\\Type\\\\ArrayShapeType not found\.$#' + 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\.$#' + message: '#^Class Symfony\\Component\\TypeInfo\\Type\\ArrayShapeType not found\.$#' identifier: class.notFound count: 1 path: src/TypeDescriber/ListDescriber.php diff --git a/tests/Functional/ControllerTest.php b/tests/Functional/ControllerTest.php index 48ad83c12..d2b31741f 100644 --- a/tests/Functional/ControllerTest.php +++ b/tests/Functional/ControllerTest.php @@ -714,7 +714,7 @@ static function (RoutingConfigurator $routes) { ]; } - if (class_exists(\Symfony\Component\TypeInfo\Type\ArrayShapeType::class)) { + if (version_compare(Kernel::VERSION, '7.3.0', '>=') && class_exists(TypeInfoType\ArrayShapeType::class)) { yield 'Array shape types' => [ 'ArrayShapeController', null, From 15f5a303830dd1910edf739929fd736ff2784307 Mon Sep 17 00:00:00 2001 From: lacatoire Date: Fri, 20 Feb 2026 17:22:11 +0100 Subject: [PATCH 06/11] fix: PHPStan baseline identifiers and CS-Fixer bracket formatting --- phpstan-baseline.neon | 2 -- tests/Functional/Controller/JMSController.php | 12 ++++-------- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index d658df653..9b3a411d5 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -50,13 +50,11 @@ parameters: - message: '#^PHPDoc tag `@implements` has invalid type Symfony\\Component\\TypeInfo\\Type\\ArrayShapeType\.$#' - identifier: class.notFound count: 1 path: src/TypeDescriber/ArrayShapeDescriber.php - message: '#^Type Symfony\\Component\\TypeInfo\\Type\\ArrayShapeType in generic type Nelmio\\ApiDocBundle\\TypeDescriber\\TypeDescriberInterface\ in PHPDoc tag `@implements` is not subtype of template type T of Symfony\\Component\\TypeInfo\\Type of interface Nelmio\\ApiDocBundle\\TypeDescriber\\TypeDescriberInterface\.$#' - identifier: class.notFound count: 1 path: src/TypeDescriber/ArrayShapeDescriber.php diff --git a/tests/Functional/Controller/JMSController.php b/tests/Functional/Controller/JMSController.php index edce7f5f9..03994fca7 100644 --- a/tests/Functional/Controller/JMSController.php +++ b/tests/Functional/Controller/JMSController.php @@ -132,8 +132,7 @@ public function minUserNestedAction(): void response: 200, description: 'Success', content: new Model(type: Article81::class) - ) - ] + )] public function enum(): void { } @@ -143,8 +142,7 @@ public function enum(): void response: 200, description: 'Success', content: new Model(type: JMSAbstractUser::class) - ) - ] + )] public function discriminatorMapAction(): void { } @@ -154,8 +152,7 @@ public function discriminatorMapAction(): void response: 200, description: 'Success', content: new Model(type: JMSEnum::class) - ) - ] + )] public function enumArrayAction(): void { } @@ -165,8 +162,7 @@ public function enumArrayAction(): void response: 200, description: 'Success', content: new Model(type: JMSIgnoredProperty::class) - ) - ] + )] public function ignoredProperty(): void { } From f9d910f3df3870f60454e00bf01d0c2222b90db1 Mon Sep 17 00:00:00 2001 From: lacatoire Date: Fri, 20 Feb 2026 17:25:25 +0100 Subject: [PATCH 07/11] fix: use wildcard for backticks in PHPStan baseline patterns --- phpstan-baseline.neon | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 9b3a411d5..bcefe36fe 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -49,12 +49,12 @@ parameters: path: src/Util/LegacyTypeConverter.php - - message: '#^PHPDoc tag `@implements` has invalid type Symfony\\Component\\TypeInfo\\Type\\ArrayShapeType\.$#' + message: '#^PHPDoc tag .@implements. has invalid type Symfony\\Component\\TypeInfo\\Type\\ArrayShapeType\.$#' count: 1 path: src/TypeDescriber/ArrayShapeDescriber.php - - message: '#^Type Symfony\\Component\\TypeInfo\\Type\\ArrayShapeType in generic type Nelmio\\ApiDocBundle\\TypeDescriber\\TypeDescriberInterface\ in PHPDoc tag `@implements` is not subtype of template type T of Symfony\\Component\\TypeInfo\\Type of interface Nelmio\\ApiDocBundle\\TypeDescriber\\TypeDescriberInterface\.$#' + message: '#^Type Symfony\\Component\\TypeInfo\\Type\\ArrayShapeType in generic type Nelmio\\ApiDocBundle\\TypeDescriber\\TypeDescriberInterface\ in PHPDoc tag .@implements. is not subtype of template type T of Symfony\\Component\\TypeInfo\\Type of interface Nelmio\\ApiDocBundle\\TypeDescriber\\TypeDescriberInterface\.$#' count: 1 path: src/TypeDescriber/ArrayShapeDescriber.php From 18acb3b88cb29caf2b500dc33fbde603fda5e88e Mon Sep 17 00:00:00 2001 From: lacatoire Date: Fri, 20 Feb 2026 17:30:33 +0100 Subject: [PATCH 08/11] fix: remove backticks from PHPStan baseline patterns (GitHub Actions formatting artifact) --- phpstan-baseline.neon | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index bcefe36fe..3541cf5fe 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -49,12 +49,12 @@ parameters: path: src/Util/LegacyTypeConverter.php - - message: '#^PHPDoc tag .@implements. has invalid type Symfony\\Component\\TypeInfo\\Type\\ArrayShapeType\.$#' + message: '#^PHPDoc tag @implements has invalid type Symfony\\Component\\TypeInfo\\Type\\ArrayShapeType\.$#' count: 1 path: src/TypeDescriber/ArrayShapeDescriber.php - - message: '#^Type Symfony\\Component\\TypeInfo\\Type\\ArrayShapeType in generic type Nelmio\\ApiDocBundle\\TypeDescriber\\TypeDescriberInterface\ in PHPDoc tag .@implements. is not subtype of template type T of Symfony\\Component\\TypeInfo\\Type of interface Nelmio\\ApiDocBundle\\TypeDescriber\\TypeDescriberInterface\.$#' + message: '#^Type Symfony\\Component\\TypeInfo\\Type\\ArrayShapeType in generic type Nelmio\\ApiDocBundle\\TypeDescriber\\TypeDescriberInterface\ in PHPDoc tag @implements is not subtype of template type T of Symfony\\Component\\TypeInfo\\Type of interface Nelmio\\ApiDocBundle\\TypeDescriber\\TypeDescriberInterface\.$#' count: 1 path: src/TypeDescriber/ArrayShapeDescriber.php From 392670ad4fb20ca8ef0ff4cbd5446498fdb499de Mon Sep 17 00:00:00 2001 From: lacatoire Date: Fri, 10 Apr 2026 20:12:05 +0200 Subject: [PATCH 09/11] test: add ArrayShapeController fixture for array shape types Auto-generated fixture validating the OpenAPI output of the ArrayShapeDescriber for sealed and unsealed array shapes. --- .../Fixtures/ArrayShapeController.json | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 tests/Functional/Fixtures/ArrayShapeController.json diff --git a/tests/Functional/Fixtures/ArrayShapeController.json b/tests/Functional/Fixtures/ArrayShapeController.json new file mode 100644 index 000000000..8e7f32b41 --- /dev/null +++ b/tests/Functional/Fixtures/ArrayShapeController.json @@ -0,0 +1,76 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "", + "version": "0.0.0" + }, + "paths": { + "/array-shapes": { + "get": { + "operationId": "get_nelmio_apidoc_tests_functional_arrayshape_arrayshapes", + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ArrayShapeTypes" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "ArrayShapeTypes": { + "required": [ + "sealed", + "unsealed" + ], + "properties": { + "sealed": { + "required": [ + "age", + "name" + ], + "properties": { + "age": { + "type": "integer" + }, + "email": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "type": "object", + "additionalProperties": false + }, + "unsealed": { + "required": [ + "id", + "label" + ], + "properties": { + "id": { + "type": "integer" + }, + "label": { + "type": "string" + } + }, + "type": "object", + "additionalProperties": { + "nullable": true + } + } + }, + "type": "object" + } + } + } +} \ No newline at end of file From 1310437fccff8508e7d40cee408f1b00a6876324 Mon Sep 17 00:00:00 2001 From: lacatoire Date: Fri, 10 Apr 2026 20:30:17 +0200 Subject: [PATCH 10/11] fix: remove unnecessary fallback and add edge case tests - 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 --- src/TypeDescriber/ArrayShapeDescriber.php | 2 +- tests/Functional/Entity/ArrayShapeTypes.php | 9 +++ .../Fixtures/ArrayShapeController.json | 68 ++++++++++++++++++- 3 files changed, 77 insertions(+), 2 deletions(-) diff --git a/src/TypeDescriber/ArrayShapeDescriber.php b/src/TypeDescriber/ArrayShapeDescriber.php index 38b8e7003..6f55feb75 100644 --- a/src/TypeDescriber/ArrayShapeDescriber.php +++ b/src/TypeDescriber/ArrayShapeDescriber.php @@ -35,7 +35,7 @@ public function describe(Type $type, Schema $schema, array $context = []): void $property = Util::getProperty($schema, (string) $key); $this->describer->describe($shapeValue['type'], $property, $context); - if (!($shapeValue['optional'] ?? false)) { + if (!$shapeValue['optional']) { $required[] = (string) $key; } } diff --git a/tests/Functional/Entity/ArrayShapeTypes.php b/tests/Functional/Entity/ArrayShapeTypes.php index cb3723a22..06eedc432 100644 --- a/tests/Functional/Entity/ArrayShapeTypes.php +++ b/tests/Functional/Entity/ArrayShapeTypes.php @@ -20,4 +20,13 @@ class ArrayShapeTypes /** @var array{id: int, label: string, ...} */ public array $unsealed; + + /** @var array{user: array{name: string, age: int}, active: bool} */ + public array $nested; + + /** @var array{0: string, 1: int} */ + public array $numericKeys; + + /** @var array{name: ?string, email: ?string} */ + public array $nullableValues; } diff --git a/tests/Functional/Fixtures/ArrayShapeController.json b/tests/Functional/Fixtures/ArrayShapeController.json index 8e7f32b41..c0fe63063 100644 --- a/tests/Functional/Fixtures/ArrayShapeController.json +++ b/tests/Functional/Fixtures/ArrayShapeController.json @@ -28,7 +28,10 @@ "ArrayShapeTypes": { "required": [ "sealed", - "unsealed" + "unsealed", + "nested", + "numericKeys", + "nullableValues" ], "properties": { "sealed": { @@ -67,6 +70,69 @@ "additionalProperties": { "nullable": true } + }, + "nested": { + "required": [ + "active", + "user" + ], + "properties": { + "active": { + "type": "boolean" + }, + "user": { + "required": [ + "age", + "name" + ], + "properties": { + "age": { + "type": "integer" + }, + "name": { + "type": "string" + } + }, + "type": "object", + "additionalProperties": false + } + }, + "type": "object", + "additionalProperties": false + }, + "numericKeys": { + "required": [ + "0", + "1" + ], + "properties": { + "0": { + "type": "string" + }, + "1": { + "type": "integer" + } + }, + "type": "object", + "additionalProperties": false + }, + "nullableValues": { + "required": [ + "email", + "name" + ], + "properties": { + "email": { + "type": "string", + "nullable": true + }, + "name": { + "type": "string", + "nullable": true + } + }, + "type": "object", + "additionalProperties": false } }, "type": "object" From 1f7a3e8ce8faffbb51295cb68b5eb63fc2d591e1 Mon Sep 17 00:00:00 2001 From: lacatoire Date: Fri, 10 Apr 2026 20:51:03 +0200 Subject: [PATCH 11/11] fix: version-specific fixture for Symfony 7.3 and remove stale baseline 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 --- phpstan-baseline.neon | 6 ------ tests/Functional/ControllerTest.php | 2 +- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index e91ffc7c4..a7141140a 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -42,12 +42,6 @@ parameters: count: 2 path: src/Render/Html/HtmlOpenApiRenderer.php - - - message: '#^Unable to resolve the template type T in call to static method Symfony\\Component\\TypeInfo\\Type\:\:collection\(\)$#' - identifier: argument.templateType - count: 1 - path: src/Util/LegacyTypeConverter.php - - message: '#^PHPDoc tag @implements has invalid type Symfony\\Component\\TypeInfo\\Type\\ArrayShapeType\.$#' count: 1 diff --git a/tests/Functional/ControllerTest.php b/tests/Functional/ControllerTest.php index 948a584b0..1fff1f78b 100644 --- a/tests/Functional/ControllerTest.php +++ b/tests/Functional/ControllerTest.php @@ -720,7 +720,7 @@ static function (RoutingConfigurator $routes) { if (version_compare(Kernel::VERSION, '7.3.0', '>=') && class_exists(TypeInfoType\ArrayShapeType::class)) { yield 'Array shape types' => [ 'ArrayShapeController', - null, + version_compare(Kernel::VERSION, '7.4.0', '>=') ? null : 'symfony-7.3', [], [ 'nelmio_api_doc' => [