Skip to content
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions config/services.php
Original file line number Diff line number Diff line change
Expand Up @@ -258,4 +258,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])
;
}
};
64 changes: 64 additions & 0 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,67 @@ parameters:
identifier: class.notFound
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
path: src/TypeDescriber/ArrayShapeDescriber.php

-
message: '#^Type Symfony\\Component\\TypeInfo\\Type\\ArrayShapeType in generic type Nelmio\\ApiDocBundle\\TypeDescriber\\TypeDescriberInterface\<Symfony\\Component\\TypeInfo\\Type\\ArrayShapeType\> 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

-
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
Comment on lines +90 to +101
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
-
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

Copilot uses AI. Check for mistakes.
59 changes: 59 additions & 0 deletions src/TypeDescriber/ArrayShapeDescriber.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?php

/*
* This file is part of the NelmioApiDocBundle package.
*
* (c) Nelmio
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Nelmio\ApiDocBundle\TypeDescriber;

use Nelmio\ApiDocBundle\OpenApiPhp\Util;
use OpenApi\Annotations as OA;
use OpenApi\Annotations\Schema;
use Symfony\Component\TypeInfo\Type;
use Symfony\Component\TypeInfo\Type\ArrayShapeType;

/**
* @implements TypeDescriberInterface<ArrayShapeType>
*
* @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;
}
}
1 change: 1 addition & 0 deletions src/TypeDescriber/DictionaryDescriber.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
&& !$type instanceof Type\ArrayShapeType
&& (
!class_exists(Type\ArrayShapeType::class)
|| !is_a($type, Type\ArrayShapeType::class)
)

Copilot uses AI. Check for mistakes.
&& $type->getCollectionKeyType() instanceof Type\BuiltinType
&& TypeIdentifier::STRING === $type->getCollectionKeyType()->getTypeIdentifier();
}
Expand Down
1 change: 1 addition & 0 deletions src/TypeDescriber/ListDescriber.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
&& !$type instanceof Type\ArrayShapeType
&& !(class_exists(Type\ArrayShapeType::class)
&& is_a($type, Type\ArrayShapeType::class))

Copilot uses AI. Check for mistakes.
&& $type->getCollectionKeyType() instanceof Type\BuiltinType
&& TypeIdentifier::INT === $type->getCollectionKeyType()->getTypeIdentifier();
}
Expand Down
32 changes: 32 additions & 0 deletions tests/Functional/Controller/ArrayShapeController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

declare(strict_types=1);

/*
* This file is part of the NelmioApiDocBundle package.
*
* (c) Nelmio
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Nelmio\ApiDocBundle\Tests\Functional\Controller;

use Nelmio\ApiDocBundle\Attribute\Model;
use Nelmio\ApiDocBundle\Tests\Functional\Entity\ArrayShapeTypes;
use OpenApi\Attributes as OA;
use Symfony\Component\Routing\Attribute\Route;

class ArrayShapeController
{
#[OA\Response(
response: '200',
description: 'Success',
content: new Model(type: ArrayShapeTypes::class),
)]
#[Route('/array-shapes', methods: ['GET'])]
public function arrayShapesAction(): void
{
}
}
13 changes: 13 additions & 0 deletions tests/Functional/ControllerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -717,6 +717,19 @@ static function (RoutingConfigurator $routes) {
];
}

if (version_compare(Kernel::VERSION, '7.3.0', '>=') && class_exists(TypeInfoType\ArrayShapeType::class)) {
yield 'Array shape types' => [
'ArrayShapeController',
null,
[],
[
'nelmio_api_doc' => [
'type_info' => true,
],
],
];
}

yield 'Custom model names' => [
'CustomModelNameController',
];
Expand Down
23 changes: 23 additions & 0 deletions tests/Functional/Entity/ArrayShapeTypes.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

declare(strict_types=1);

/*
* This file is part of the NelmioApiDocBundle package.
*
* (c) Nelmio
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Nelmio\ApiDocBundle\Tests\Functional\Entity;

class ArrayShapeTypes
{
/** @var array{name: string, age: int, email?: string} */
public array $sealed;

/** @var array{id: int, label: string, ...} */
public array $unsealed;
}
Loading