Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
85d21d2
bump symfony minimum to 7.2
DjordyKoert Jul 10, 2025
1d8b1db
add symfony/type-info
DjordyKoert Jul 10, 2025
ee1fdfd
remove unused ModelRegistryAware
DjordyKoert Jul 11, 2025
d876a36
convert legacy type
DjordyKoert Jul 11, 2025
4d78785
hash from type info
DjordyKoert Jul 11, 2025
15e1bee
remove null
DjordyKoert Jul 11, 2025
52fa17c
register models with type-info
DjordyKoert Jul 11, 2025
ef34f6c
temp
DjordyKoert Aug 4, 2025
cb3fe5a
partial revert
DjordyKoert Aug 4, 2025
214e005
Merge branch '5.x' into fix-2486-type
DjordyKoert Aug 4, 2025
2a05c23
Migrate to `getTypeInfo` method
DjordyKoert Aug 29, 2025
04a5b85
migrate more to type info
DjordyKoert Aug 29, 2025
b4c25d9
fix registering as nullable
DjordyKoert Aug 29, 2025
db6c071
Bump from Symfony 7.1 to 7.2
DjordyKoert Aug 29, 2025
0b2f746
Add Symfony 7.3 to CI
DjordyKoert Aug 29, 2025
e8d9480
PHP minimum version bump
DjordyKoert Aug 29, 2025
eb7ab33
remove property check on MapRequestPayload
DjordyKoert Aug 29, 2025
0bce6c1
Revert "remove property check on MapRequestPayload"
DjordyKoert Aug 29, 2025
a8ebf91
partial-revert: Bump from Symfony 7.1 to 7.2
DjordyKoert Aug 29, 2025
caf6889
phpunit-ignore new deprecation
DjordyKoert Aug 29, 2025
c8b768b
fix phpunit-ignore
DjordyKoert Aug 29, 2025
5be30a3
fix type-info test for 7.3.3
DjordyKoert Aug 29, 2025
f38cb30
Merge branch '5.x' into fix-2486-type
DjordyKoert Aug 29, 2025
ce8e97b
phpstan
DjordyKoert Aug 29, 2025
f2c7894
add legacy support to baseline
DjordyKoert Aug 29, 2025
dcbf485
revert php version bump
DjordyKoert Sep 1, 2025
c5095e2
suggest symfony/type-info
DjordyKoert Sep 1, 2025
0c8de06
Revert "Bump from Symfony 7.1 to 7.2"
DjordyKoert Sep 1, 2025
9df539f
Revert "bump symfony minimum to 7.2"
DjordyKoert Sep 1, 2025
64a0abc
Merge branch '5.x' into fix-2486-type
DjordyKoert Sep 12, 2025
e72ba84
conflict symfony/type-info <7.2
DjordyKoert Sep 12, 2025
a7224bd
BC compatibility
DjordyKoert Sep 12, 2025
b5dc6a8
Revert "conflict symfony/type-info <7.2"
DjordyKoert Sep 12, 2025
2d58561
use typeToString during logging
DjordyKoert Sep 19, 2025
2eb0252
expand ModelRegistryTest tests
DjordyKoert Sep 19, 2025
6cb7d51
phpstan
DjordyKoert Sep 19, 2025
ec52d06
remove slash prefix for className
DjordyKoert Sep 19, 2025
3844303
revert test update
DjordyKoert Sep 19, 2025
2b9ac4f
fix symfony 6.4 calling getTypeInfo()
DjordyKoert Sep 19, 2025
4f80d72
remove `|null` suffix
DjordyKoert Sep 19, 2025
2b3dba8
Revert "remove `|null` suffix"
DjordyKoert Sep 19, 2025
e01f314
properly check generated schema name
DjordyKoert Sep 19, 2025
9569371
prevent passing `null` to `class_exists`
DjordyKoert Sep 19, 2025
6dd0dab
require symfony/type-info
DjordyKoert Sep 19, 2025
0f1eba0
Revert "require symfony/type-info"
DjordyKoert Sep 19, 2025
dc72924
Reapply "conflict symfony/type-info <7.2"
DjordyKoert Sep 19, 2025
0b7d543
Revert "Reapply "conflict symfony/type-info <7.2""
DjordyKoert Sep 19, 2025
e09dd9f
handle type-info 7.1 nullable
DjordyKoert Sep 19, 2025
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
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@
"symfony/security-csrf": "For using csrf protection tokens in forms.",
"symfony/serializer": "For describing your models.",
"symfony/twig-bundle": "For using the Swagger UI.",
"symfony/type-info": "For extracting type information from PHP elements like properties, arguments and return types.",
"symfony/validator": "For describing the validation constraints in your models.",
"willdurand/hateoas-bundle": "For extracting HATEOAS metadata."
},
Expand Down
5 changes: 5 additions & 0 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,8 @@ parameters:
message: "#^Call to function is_callable\\(\\) with array\\{string, mixed\\} will always evaluate to false\\.$#"
count: 1
path: src/RouteDescriber/FosRestDescriber.php

-
message: "#^Unable to resolve the template type T in call to method static method Symfony\\\\Component\\\\TypeInfo\\\\Type\\:\\:collection\\(\\)$#"
count: 1
path: src/Util/LegacyTypeConverter.php
2 changes: 2 additions & 0 deletions phpunit-ignore.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Ignoring deprecations from Nelmio\ApiDocBundle 5.6.0, Deprecation of Symfony PropertyInfo\Type
/^Since nelmio\/api-doc-bundle 5\.X\: Using Symfony\\Component\\PropertyInfo\\Type as type in Nelmio\\ApiDocBundle\\Model\\Model\:\:\_\_construct is deprecated, use Symfony\\Component\\TypeInfo\\Type instead\./
45 changes: 41 additions & 4 deletions src/Model/Model.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@

namespace Nelmio\ApiDocBundle\Model;

use Symfony\Component\PropertyInfo\Type;
use Nelmio\ApiDocBundle\Util\LegacyTypeConverter;
use Symfony\Component\PropertyInfo\Type as LegacyType;
use Symfony\Component\TypeInfo\Type;

final class Model
{
Expand All @@ -22,22 +24,53 @@ final class Model
* @param non-empty-string|null $name An optional custom name for the generated schema
*/
public function __construct(
private Type $type,
private LegacyType|Type $type,
?array $groups = null,
private array $options = [],
private array $serializationContext = [],
public readonly ?string $name = null,
) {
if ($type instanceof LegacyType) {
trigger_deprecation(
'nelmio/api-doc-bundle',
'5.X', // TODO
'Using Symfony\Component\PropertyInfo\Type as type in %s is deprecated, use Symfony\Component\TypeInfo\Type instead.',
__METHOD__
);
}

if (null !== $groups) {
$this->serializationContext['groups'] = $groups;
}
}

public function getType(): Type
/**
* @deprecated use {@see getTypeInfo()} instead
*/
public function getType(): LegacyType
{
if ($this->type instanceof Type) {
return LegacyTypeConverter::toLegacyType($this->type);
}

return $this->type;
}

public function getTypeInfo(): Type
{
if ($this->type instanceof Type) {
return $this->type;
}

$converted = LegacyTypeConverter::toTypeInfoType([$this->type]);

if (null === $converted) {
throw new \LogicException('Could not convert legacy type to TypeInfo type.');
}

return $converted;
}

/**
* @return string[]|null
*/
Expand All @@ -56,7 +89,11 @@ public function getSerializationContext(): array

public function getHash(): string
{
return md5(serialize([$this->type, $this->getSerializationContext(), $this->name]));
$type = class_exists(Type::class)
? $this->getTypeInfo()->__toString()
: $this->getType();

return md5(serialize([$type, $this->getSerializationContext(), $this->name]));
}

/**
Expand Down
86 changes: 56 additions & 30 deletions src/Model/ModelRegistry.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@
use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareInterface;
use Nelmio\ApiDocBundle\ModelDescriber\ModelDescriberInterface;
use Nelmio\ApiDocBundle\OpenApiPhp\Util;
use Nelmio\ApiDocBundle\Util\LegacyTypeConverter;
use OpenApi\Annotations as OA;
use Psr\Log\LoggerAwareTrait;
use Psr\Log\NullLogger;
use Symfony\Component\PropertyInfo\Type;
use Symfony\Component\PropertyInfo\Type as LegacyType;
use Symfony\Component\TypeInfo\Type;

final class ModelRegistry
{
Expand Down Expand Up @@ -74,7 +76,7 @@ public function __construct($modelDescribers, OA\OpenApi $api, array $alternativ

foreach ($alternativeNames as $alternativeName => $criteria) {
$model = new Model(
new Type('object', false, $criteria['type']),
LegacyTypeConverter::createType($criteria['type']),
$criteria['groups'],
$criteria['options'] ?? [],
$criteria['serializationContext'] ?? [],
Expand Down Expand Up @@ -170,9 +172,13 @@ public function registerSchemas(): void
$schema = $this->describeSchema($model, $name);

if (null === $schema) {
$errorMessage = \sprintf('Schema of type "%s" can\'t be generated, no describer supports it.', $this->typeToString($model->getType()));
if (Type::BUILTIN_TYPE_OBJECT === $model->getType()->getBuiltinType() && !class_exists($className = $model->getType()->getClassName())) {
$errorMessage .= \sprintf(' Class "\\%s" does not exist, did you forget a use statement, or typed it wrong?', $className);
$type = class_exists(Type::class)
? $model->getTypeInfo()
: $model->getType();

$errorMessage = \sprintf('Schema of type "%s" can\'t be generated, no describer supports it.', $this->typeToString($type));
if (method_exists($type, 'getClassName') && null !== $type->getClassName() && !class_exists($className = $type->getClassName())) {
$errorMessage .= \sprintf(' Class "%s" does not exist, did you forget a use statement, or typed it wrong?', $className);
}
throw new \LogicException($errorMessage);
}
Expand All @@ -194,8 +200,12 @@ private function determineModelName(Model $model): string
return $model->name;
}

$type = class_exists(Type::class)
? $model->getTypeInfo()
: $model->getType();

// 3. Generate from the type
return $this->getTypeShortName($model->getType());
return $this->getTypeShortName($type);
}

private function generateUniqueModelName(Model $model): string
Expand Down Expand Up @@ -225,32 +235,49 @@ private function generateUniqueModelName(Model $model): string
*/
private function modelToArray(Model $model): array
{
$getType = function (Type $type) use (&$getType): array {
return [
'class' => $type->getClassName(),
'built_in_type' => $type->getBuiltinType(),
'nullable' => $type->isNullable(),
'collection' => $type->isCollection(),
'collection_key_types' => $type->isCollection() ? array_map($getType, $type->getCollectionKeyTypes()) : null,
'collection_value_types' => $type->isCollection() ? array_map($getType, $type->getCollectionValueTypes()) : null,
];
};
$type = class_exists(Type::class)
? $model->getTypeInfo()
: $model->getType();

$dataType = $this->typeToString($type);

return [
'type' => $getType($model->getType()),
'type' => $dataType,
'options' => $model->getOptions(),
'groups' => $model->getGroups(),
'serialization_context' => $model->getSerializationContext(),
];
}

private function getTypeShortName(Type $type): string
private function getTypeShortName(LegacyType|Type $type): string
{
if (null !== $collectionType = $this->getCollectionValueType($type)) {
if ($type instanceof Type) {
if ($type instanceof Type\CollectionType) {
return $this->getTypeShortName($type->getCollectionValueType()).'[]';
}

if ($type instanceof Type\ObjectType) {
$parts = explode('\\', $type->getClassName());

return end($parts);
}

if ($type instanceof Type\NullableType) {
return $this->getTypeShortName($type->getWrappedType());
}

if ($type instanceof Type\UnionType && method_exists($type, 'asNonNullable')) {
return $this->getTypeShortName($type->asNonNullable());
}

return $type->__toString();
}

if (null !== $collectionType = ($type->getCollectionValueTypes()[0] ?? null)) {
return $this->getTypeShortName($collectionType).'[]';
}

if (Type::BUILTIN_TYPE_OBJECT === $type->getBuiltinType()) {
if (LegacyType::BUILTIN_TYPE_OBJECT === $type->getBuiltinType()) {
$parts = explode('\\', $type->getClassName());

return end($parts);
Expand All @@ -259,23 +286,22 @@ private function getTypeShortName(Type $type): string
return $type->getBuiltinType();
}

private function typeToString(Type $type): string
private function typeToString(LegacyType|Type $type): string
{
if (Type::BUILTIN_TYPE_OBJECT === $type->getBuiltinType()) {
return '\\'.$type->getClassName();
if ($type instanceof Type) {
return $type->__toString();
}

if (LegacyType::BUILTIN_TYPE_OBJECT === $type->getBuiltinType()) {
return $type->getClassName();
} elseif ($type->isCollection()) {
if (null !== $collectionType = $this->getCollectionValueType($type)) {
if (null !== $collectionType = ($type->getCollectionValueTypes()[0] ?? null)) {
return $this->typeToString($collectionType).'[]';
} else {
return 'mixed[]';
}
} else {
return $type->getBuiltinType();
}
}

private function getCollectionValueType(Type $type): ?Type
{
return $type->getCollectionValueTypes()[0] ?? null;
return $type->getBuiltinType();
}
}
4 changes: 2 additions & 2 deletions src/ModelDescriber/ApplyOpenApiDiscriminatorTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@
use Nelmio\ApiDocBundle\Model\Model;
use Nelmio\ApiDocBundle\Model\ModelRegistry;
use Nelmio\ApiDocBundle\OpenApiPhp\Util;
use Nelmio\ApiDocBundle\Util\LegacyTypeConverter;
use OpenApi\Annotations as OA;
use Symfony\Component\PropertyInfo\Type;

/**
* Contains helper methods that add `discriminator` and `oneOf` values to
Expand Down Expand Up @@ -52,7 +52,7 @@ protected function applyOpenApiDiscriminator(
foreach ($typeMap as $propertyValue => $className) {
$oneOfSchema = new OA\Schema(['_context' => $weakContext]);
$oneOfSchema->ref = $modelRegistry->register(new Model(
new Type(Type::BUILTIN_TYPE_OBJECT, false, $className),
LegacyTypeConverter::createType($className),
$model->getGroups(),
$model->getOptions(),
$model->getSerializationContext()
Expand Down
9 changes: 8 additions & 1 deletion src/ModelDescriber/BazingaHateoasModelDescriber.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
use Nelmio\ApiDocBundle\Model\ModelRegistry;
use Nelmio\ApiDocBundle\OpenApiPhp\Util;
use OpenApi\Annotations as OA;
use Symfony\Component\PropertyInfo\Type as LegacyType;
use Symfony\Component\TypeInfo\Type\ObjectType;

class BazingaHateoasModelDescriber implements ModelDescriberInterface, ModelRegistryAwareInterface
{
Expand Down Expand Up @@ -87,8 +89,13 @@ public function describe(Model $model, OA\Schema $schema): void

private function getHateoasMetadata(Model $model): ?object
{
/** @var ObjectType|LegacyType $type */
$type = class_exists(\Symfony\Component\TypeInfo\Type::class)
? $model->getTypeInfo()
: $model->getType();

try {
return $this->factory->getMetadataForClass($model->getType()->getClassName());
return $this->factory->getMetadataForClass($type->getClassName());
} catch (\ReflectionException $e) {
}

Expand Down
17 changes: 14 additions & 3 deletions src/ModelDescriber/EnumModelDescriber.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,20 @@

use Nelmio\ApiDocBundle\Model\Model;
use OpenApi\Annotations\Schema;
use Symfony\Component\PropertyInfo\Type;
use Symfony\Component\PropertyInfo\Type as LegacyType;
use Symfony\Component\TypeInfo\Type\ObjectType;

class EnumModelDescriber implements ModelDescriberInterface
{
public const FORCE_NAMES = '_nelmio_enum_force_names';

public function describe(Model $model, Schema $schema): void
{
$enumClass = $model->getType()->getClassName();
/** @var ObjectType|LegacyType $type */
$type = class_exists(\Symfony\Component\TypeInfo\Type::class)
? $model->getTypeInfo()
: $model->getType();
$enumClass = $type->getClassName();
$forceName = isset($model->getSerializationContext()[self::FORCE_NAMES]) && true === $model->getSerializationContext()[self::FORCE_NAMES];

$enums = [];
Expand All @@ -40,7 +45,13 @@ public function describe(Model $model, Schema $schema): void

public function supports(Model $model): bool
{
return Type::BUILTIN_TYPE_OBJECT === $model->getType()->getBuiltinType()
if (class_exists(\Symfony\Component\TypeInfo\Type::class)) {
return $model->getTypeInfo() instanceof ObjectType
&& enum_exists($model->getTypeInfo()->getClassName())
&& is_subclass_of($model->getTypeInfo()->getClassName(), \BackedEnum::class);
}

return LegacyType::BUILTIN_TYPE_OBJECT === $model->getType()->getBuiltinType()
&& enum_exists($model->getType()->getClassName())
&& is_subclass_of($model->getType()->getClassName(), \BackedEnum::class);
}
Expand Down
9 changes: 7 additions & 2 deletions src/ModelDescriber/FallbackObjectModelDescriber.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@

use Nelmio\ApiDocBundle\Model\Model;
use OpenApi\Annotations as OA;
use Symfony\Component\PropertyInfo\Type;
use Symfony\Component\PropertyInfo\Type as LegacyType;
use Symfony\Component\TypeInfo\Type\ObjectType;

class FallbackObjectModelDescriber implements ModelDescriberInterface
{
Expand All @@ -23,6 +24,10 @@ public function describe(Model $model, OA\Schema $schema): void

public function supports(Model $model): bool
{
return Type::BUILTIN_TYPE_OBJECT === $model->getType()->getBuiltinType();
if (class_exists(\Symfony\Component\TypeInfo\Type::class)) {
return $model->getTypeInfo() instanceof ObjectType;
}

return LegacyType::BUILTIN_TYPE_OBJECT === $model->getType()->getBuiltinType();
}
}
Loading