Skip to content

Commit ef14213

Browse files
authored
feat(#2502): refactor Model::$type from property-info to type-info Type (#2526)
## Description Replaces deprecated property-info type with type-info Closes #2502 ## What type of PR is this? (check all applicable) - [ ] Bug Fix - [x] Feature - [x] Refactor - [x] Deprecation - [ ] Breaking Change - [ ] Documentation Update - [x] CI ## Checklist - [ ] I have made corresponding changes to the documentation (`docs/`) - [ ] I have made corresponding changes to the changelog (`CHANGELOG.md`) ``` BEGIN_COMMIT_OVERRIDE feat(#2502): refactor `Model::$type` from property-info to type-info `Type` chore(symfony): bumped minimum version for Symfony 7 from `^7.1` to `^7.2` END_COMMIT_OVERRIDE
1 parent b978e17 commit ef14213

26 files changed

+644
-190
lines changed

.github/workflows/continuous-integration.yml

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,6 @@ jobs:
3636
symfony-require: "6.4"
3737
- php-version: 8.4
3838
symfony-require: "6.4"
39-
- php-version: 8.2
40-
symfony-require: "7.1"
41-
- php-version: 8.3
42-
symfony-require: "7.1"
43-
- php-version: 8.4
44-
symfony-require: "7.1"
4539
- php-version: 8.2
4640
symfony-require: "7.2"
4741
- php-version: 8.3

composer.json

Lines changed: 31 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -22,16 +22,16 @@
2222
"psr/cache": "^1.0 || ^2.0 || ^3.0",
2323
"psr/container": "^1.0 || ^2.0",
2424
"psr/log": "^1.0 || ^2.0 || ^3.0",
25-
"symfony/config": "^6.4 || ^7.1",
26-
"symfony/console": "^6.4 || ^7.1",
27-
"symfony/dependency-injection": "^6.4 || ^7.1",
25+
"symfony/config": "^6.4 || ^7.2",
26+
"symfony/console": "^6.4 || ^7.2",
27+
"symfony/dependency-injection": "^6.4 || ^7.2",
2828
"symfony/deprecation-contracts": "^2.1 || ^3",
29-
"symfony/framework-bundle": "^6.4 || ^7.1",
30-
"symfony/http-foundation": "^6.4 || ^7.1",
31-
"symfony/http-kernel": "^6.4 || ^7.1",
32-
"symfony/options-resolver": "^6.4 || ^7.1",
33-
"symfony/property-info": "^6.4 || ^7.1",
34-
"symfony/routing": "^6.4 || ^7.1",
29+
"symfony/framework-bundle": "^6.4 || ^7.2",
30+
"symfony/http-foundation": "^6.4 || ^7.2",
31+
"symfony/http-kernel": "^6.4 || ^7.2",
32+
"symfony/options-resolver": "^6.4 || ^7.2",
33+
"symfony/property-info": "^6.4 || ^7.2",
34+
"symfony/routing": "^6.4 || ^7.2",
3535
"zircote/swagger-php": "^4.11.1 || ^5.0"
3636
},
3737
"require-dev": {
@@ -45,24 +45,24 @@
4545
"phpstan/phpstan-strict-rules": "^2.0",
4646
"phpstan/phpstan-symfony": "^2.0",
4747
"phpunit/phpunit": "^10.5",
48-
"symfony/asset": "^6.4 || ^7.1",
49-
"symfony/browser-kit": "^6.4 || ^7.1",
50-
"symfony/cache": "^6.4 || ^7.1",
51-
"symfony/dom-crawler": "^6.4 || ^7.1",
52-
"symfony/expression-language": "^6.4 || ^7.1",
53-
"symfony/finder": "^6.4 || ^7.1",
54-
"symfony/form": "^6.4 || ^7.1",
55-
"symfony/phpunit-bridge": "^6.4 || ^7.1",
56-
"symfony/property-access": "^6.4 || ^7.1",
57-
"symfony/security-csrf": "^6.4 || ^7.1",
58-
"symfony/security-http": "^6.4 || ^7.1",
59-
"symfony/serializer": "^6.4 || ^7.1",
60-
"symfony/stopwatch": "^6.4 || ^7.1",
61-
"symfony/templating": "^6.4 || ^7.1",
62-
"symfony/translation": "^6.4 || ^7.1",
63-
"symfony/twig-bundle": "^6.4 || ^7.1",
64-
"symfony/uid": "^6.4 || ^7.1",
65-
"symfony/validator": "^6.4 || ^7.1",
48+
"symfony/asset": "^6.4 || ^7.2",
49+
"symfony/browser-kit": "^6.4 || ^7.2",
50+
"symfony/cache": "^6.4 || ^7.2",
51+
"symfony/dom-crawler": "^6.4 || ^7.2",
52+
"symfony/expression-language": "^6.4 || ^7.2",
53+
"symfony/finder": "^6.4 || ^7.2",
54+
"symfony/form": "^6.4 || ^7.2",
55+
"symfony/phpunit-bridge": "^6.4 || ^7.2",
56+
"symfony/property-access": "^6.4 || ^7.2",
57+
"symfony/security-csrf": "^6.4 || ^7.2",
58+
"symfony/security-http": "^6.4 || ^7.2",
59+
"symfony/serializer": "^6.4 || ^7.2",
60+
"symfony/stopwatch": "^6.4 || ^7.2",
61+
"symfony/templating": "^6.4 || ^7.2",
62+
"symfony/translation": "^6.4 || ^7.2",
63+
"symfony/twig-bundle": "^6.4 || ^7.2",
64+
"symfony/uid": "^6.4 || ^7.2",
65+
"symfony/validator": "^6.4 || ^7.2",
6666
"willdurand/hateoas-bundle": "^2.7",
6767
"willdurand/negotiation": "^3.0"
6868
},
@@ -80,6 +80,7 @@
8080
"symfony/security-csrf": "For using csrf protection tokens in forms.",
8181
"symfony/serializer": "For describing your models.",
8282
"symfony/twig-bundle": "For using the Swagger UI.",
83+
"symfony/type-info": "For extracting type information from PHP elements like properties, arguments and return types.",
8384
"symfony/validator": "For describing the validation constraints in your models.",
8485
"willdurand/hateoas-bundle": "For extracting HATEOAS metadata."
8586
},
@@ -94,6 +95,7 @@
9495
}
9596
},
9697
"config": {
98+
"lock": false,
9799
"sort-packages": true,
98100
"audit": {
99101
"ignore": [
@@ -117,8 +119,8 @@
117119
"scripts": {
118120
"phpcs-check": "vendor/bin/php-cs-fixer check -v --diff",
119121
"phpcs-fix": "vendor/bin/php-cs-fixer fix -v",
120-
"phpstan": "phpstan",
121-
"phpstan-baseline": "phpstan --generate-baseline",
122+
"phpstan": "phpstan --memory-limit=2G",
123+
"phpstan-baseline": "phpstan --memory-limit=2G --generate-baseline",
122124
"phpunit": "phpunit"
123125
}
124126
}

phpstan-baseline.neon

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,9 @@ parameters:
2323
identifier: class.notFound
2424
count: 2
2525
path: src/Render/Html/HtmlOpenApiRenderer.php
26+
27+
-
28+
message: '#^Unable to resolve the template type T in call to static method Symfony\\Component\\TypeInfo\\Type\:\:collection\(\)$#'
29+
identifier: argument.templateType
30+
count: 1
31+
path: src/Util/LegacyTypeConverter.php

phpunit-ignore.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Ignoring deprecations from Nelmio\ApiDocBundle 5.6.0, Deprecation of Symfony PropertyInfo\Type
2+
/^Since nelmio\/api-doc-bundle 5\.7\.0\: Using Symfony\\Component\\PropertyInfo\\Type as type in Nelmio\\ApiDocBundle\\Model\\Model\:\:\_\_construct is deprecated, use Symfony\\Component\\TypeInfo\\Type instead\./

src/Model/Model.php

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@
1111

1212
namespace Nelmio\ApiDocBundle\Model;
1313

14-
use Symfony\Component\PropertyInfo\Type;
14+
use Nelmio\ApiDocBundle\Util\LegacyTypeConverter;
15+
use Symfony\Component\PropertyInfo\Type as LegacyType;
16+
use Symfony\Component\TypeInfo\Type;
1517

1618
final class Model
1719
{
@@ -22,22 +24,53 @@ final class Model
2224
* @param non-empty-string|null $name An optional custom name for the generated schema
2325
*/
2426
public function __construct(
25-
private Type $type,
27+
private LegacyType|Type $type,
2628
?array $groups = null,
2729
private array $options = [],
2830
private array $serializationContext = [],
2931
public readonly ?string $name = null,
3032
) {
33+
if ($type instanceof LegacyType) {
34+
trigger_deprecation(
35+
'nelmio/api-doc-bundle',
36+
'5.7.0',
37+
'Using Symfony\Component\PropertyInfo\Type as type in %s is deprecated, use Symfony\Component\TypeInfo\Type instead.',
38+
__METHOD__
39+
);
40+
}
41+
3142
if (null !== $groups) {
3243
$this->serializationContext['groups'] = $groups;
3344
}
3445
}
3546

36-
public function getType(): Type
47+
/**
48+
* @deprecated use {@see getTypeInfo()} instead
49+
*/
50+
public function getType(): LegacyType
3751
{
52+
if ($this->type instanceof Type) {
53+
return LegacyTypeConverter::toLegacyType($this->type);
54+
}
55+
3856
return $this->type;
3957
}
4058

59+
public function getTypeInfo(): Type
60+
{
61+
if ($this->type instanceof Type) {
62+
return $this->type;
63+
}
64+
65+
$converted = LegacyTypeConverter::toTypeInfoType([$this->type]);
66+
67+
if (null === $converted) {
68+
throw new \LogicException('Could not convert legacy type to TypeInfo type.');
69+
}
70+
71+
return $converted;
72+
}
73+
4174
/**
4275
* @return string[]|null
4376
*/
@@ -56,7 +89,11 @@ public function getSerializationContext(): array
5689

5790
public function getHash(): string
5891
{
59-
return md5(serialize([$this->type, $this->getSerializationContext(), $this->name]));
92+
$type = class_exists(Type::class)
93+
? $this->getTypeInfo()->__toString()
94+
: $this->getType();
95+
96+
return md5(serialize([$type, $this->getSerializationContext(), $this->name]));
6097
}
6198

6299
/**

src/Model/ModelRegistry.php

Lines changed: 52 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,12 @@
1414
use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareInterface;
1515
use Nelmio\ApiDocBundle\ModelDescriber\ModelDescriberInterface;
1616
use Nelmio\ApiDocBundle\OpenApiPhp\Util;
17+
use Nelmio\ApiDocBundle\Util\LegacyTypeConverter;
1718
use OpenApi\Annotations as OA;
1819
use Psr\Log\LoggerAwareTrait;
1920
use Psr\Log\NullLogger;
20-
use Symfony\Component\PropertyInfo\Type;
21+
use Symfony\Component\PropertyInfo\Type as LegacyType;
22+
use Symfony\Component\TypeInfo\Type;
2123

2224
final class ModelRegistry
2325
{
@@ -74,7 +76,7 @@ public function __construct($modelDescribers, OA\OpenApi $api, array $alternativ
7476

7577
foreach ($alternativeNames as $alternativeName => $criteria) {
7678
$model = new Model(
77-
new Type('object', false, $criteria['type']),
79+
LegacyTypeConverter::createType($criteria['type']),
7880
$criteria['groups'],
7981
$criteria['options'] ?? [],
8082
$criteria['serializationContext'] ?? [],
@@ -170,9 +172,13 @@ public function registerSchemas(): void
170172
$schema = $this->describeSchema($model, $name);
171173

172174
if (null === $schema) {
173-
$errorMessage = \sprintf('Schema of type "%s" can\'t be generated, no describer supports it.', $this->typeToString($model->getType()));
174-
if (Type::BUILTIN_TYPE_OBJECT === $model->getType()->getBuiltinType() && !class_exists($className = $model->getType()->getClassName())) {
175-
$errorMessage .= \sprintf(' Class "\\%s" does not exist, did you forget a use statement, or typed it wrong?', $className);
175+
$type = class_exists(Type::class)
176+
? $model->getTypeInfo()
177+
: $model->getType();
178+
179+
$errorMessage = \sprintf('Schema of type "%s" can\'t be generated, no describer supports it.', $this->typeToString($type));
180+
if (method_exists($type, 'getClassName') && null !== $type->getClassName() && !class_exists($className = $type->getClassName())) {
181+
$errorMessage .= \sprintf(' Class "%s" does not exist, did you forget a use statement, or typed it wrong?', $className);
176182
}
177183
throw new \LogicException($errorMessage);
178184
}
@@ -194,8 +200,12 @@ private function determineModelName(Model $model): string
194200
return $model->name;
195201
}
196202

203+
$type = class_exists(Type::class)
204+
? $model->getTypeInfo()
205+
: $model->getType();
206+
197207
// 3. Generate from the type
198-
return $this->getTypeShortName($model->getType());
208+
return $this->getTypeShortName($type);
199209
}
200210

201211
private function generateUniqueModelName(Model $model): string
@@ -225,32 +235,45 @@ private function generateUniqueModelName(Model $model): string
225235
*/
226236
private function modelToArray(Model $model): array
227237
{
228-
$getType = function (Type $type) use (&$getType): array {
229-
return [
230-
'class' => $type->getClassName(),
231-
'built_in_type' => $type->getBuiltinType(),
232-
'nullable' => $type->isNullable(),
233-
'collection' => $type->isCollection(),
234-
'collection_key_types' => $type->isCollection() ? array_map($getType, $type->getCollectionKeyTypes()) : null,
235-
'collection_value_types' => $type->isCollection() ? array_map($getType, $type->getCollectionValueTypes()) : null,
236-
];
237-
};
238+
$type = class_exists(Type::class)
239+
? $model->getTypeInfo()
240+
: $model->getType();
241+
242+
$dataType = $this->typeToString($type);
238243

239244
return [
240-
'type' => $getType($model->getType()),
245+
'type' => $dataType,
241246
'options' => $model->getOptions(),
242247
'groups' => $model->getGroups(),
243248
'serialization_context' => $model->getSerializationContext(),
244249
];
245250
}
246251

247-
private function getTypeShortName(Type $type): string
252+
private function getTypeShortName(LegacyType|Type $type): string
248253
{
249-
if (null !== $collectionType = $this->getCollectionValueType($type)) {
254+
if ($type instanceof Type) {
255+
if ($type instanceof Type\CollectionType) {
256+
return $this->getTypeShortName($type->getCollectionValueType()).'[]';
257+
}
258+
259+
if ($type instanceof Type\ObjectType) {
260+
$parts = explode('\\', $type->getClassName());
261+
262+
return end($parts);
263+
}
264+
265+
if ($type instanceof Type\NullableType) {
266+
return $this->getTypeShortName($type->getWrappedType());
267+
}
268+
269+
return $type->__toString();
270+
}
271+
272+
if (null !== $collectionType = ($type->getCollectionValueTypes()[0] ?? null)) {
250273
return $this->getTypeShortName($collectionType).'[]';
251274
}
252275

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

256279
return end($parts);
@@ -259,23 +282,22 @@ private function getTypeShortName(Type $type): string
259282
return $type->getBuiltinType();
260283
}
261284

262-
private function typeToString(Type $type): string
285+
private function typeToString(LegacyType|Type $type): string
263286
{
264-
if (Type::BUILTIN_TYPE_OBJECT === $type->getBuiltinType()) {
265-
return '\\'.$type->getClassName();
287+
if ($type instanceof Type) {
288+
return $type->__toString();
289+
}
290+
291+
if (LegacyType::BUILTIN_TYPE_OBJECT === $type->getBuiltinType()) {
292+
return $type->getClassName();
266293
} elseif ($type->isCollection()) {
267-
if (null !== $collectionType = $this->getCollectionValueType($type)) {
294+
if (null !== $collectionType = ($type->getCollectionValueTypes()[0] ?? null)) {
268295
return $this->typeToString($collectionType).'[]';
269296
} else {
270297
return 'mixed[]';
271298
}
272-
} else {
273-
return $type->getBuiltinType();
274299
}
275-
}
276300

277-
private function getCollectionValueType(Type $type): ?Type
278-
{
279-
return $type->getCollectionValueTypes()[0] ?? null;
301+
return $type->getBuiltinType();
280302
}
281303
}

src/ModelDescriber/ApplyOpenApiDiscriminatorTrait.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@
1414
use Nelmio\ApiDocBundle\Model\Model;
1515
use Nelmio\ApiDocBundle\Model\ModelRegistry;
1616
use Nelmio\ApiDocBundle\OpenApiPhp\Util;
17+
use Nelmio\ApiDocBundle\Util\LegacyTypeConverter;
1718
use OpenApi\Annotations as OA;
18-
use Symfony\Component\PropertyInfo\Type;
1919

2020
/**
2121
* Contains helper methods that add `discriminator` and `oneOf` values to
@@ -52,7 +52,7 @@ protected function applyOpenApiDiscriminator(
5252
foreach ($typeMap as $propertyValue => $className) {
5353
$oneOfSchema = new OA\Schema(['_context' => $weakContext]);
5454
$oneOfSchema->ref = $modelRegistry->register(new Model(
55-
new Type(Type::BUILTIN_TYPE_OBJECT, false, $className),
55+
LegacyTypeConverter::createType($className),
5656
$model->getGroups(),
5757
$model->getOptions(),
5858
$model->getSerializationContext()

src/ModelDescriber/BazingaHateoasModelDescriber.php

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
use Nelmio\ApiDocBundle\Model\ModelRegistry;
2222
use Nelmio\ApiDocBundle\OpenApiPhp\Util;
2323
use OpenApi\Annotations as OA;
24+
use Symfony\Component\PropertyInfo\Type as LegacyType;
25+
use Symfony\Component\TypeInfo\Type\ObjectType;
2426

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

8890
private function getHateoasMetadata(Model $model): ?object
8991
{
92+
/** @var ObjectType|LegacyType $type */
93+
$type = class_exists(\Symfony\Component\TypeInfo\Type::class)
94+
? $model->getTypeInfo()
95+
: $model->getType();
96+
9097
try {
91-
return $this->factory->getMetadataForClass($model->getType()->getClassName());
98+
return $this->factory->getMetadataForClass($type->getClassName());
9299
} catch (\ReflectionException $e) {
93100
}
94101

0 commit comments

Comments
 (0)