Skip to content

Commit a1952e3

Browse files
soyukamtarld
andauthored
feat(openapi): use TypeInfo's Type (#7096)
Co-authored-by: Mathias Arlaud <mathias.arlaud@gmail.com>
1 parent 55461a8 commit a1952e3

File tree

5 files changed

+145
-39
lines changed

5 files changed

+145
-39
lines changed

src/OpenApi/Factory/OpenApiFactory.php

+47-6
Original file line numberDiff line numberDiff line change
@@ -57,9 +57,12 @@
5757
use ApiPlatform\State\Pagination\PaginationOptions;
5858
use ApiPlatform\Validator\Exception\ValidationException;
5959
use Psr\Container\ContainerInterface;
60-
use Symfony\Component\PropertyInfo\Type;
60+
use Symfony\Component\PropertyInfo\PropertyInfoExtractor;
61+
use Symfony\Component\PropertyInfo\Type as LegacyType;
6162
use Symfony\Component\Routing\RouteCollection;
6263
use Symfony\Component\Routing\RouterInterface;
64+
use Symfony\Component\TypeInfo\Type;
65+
use Symfony\Component\TypeInfo\TypeIdentifier;
6366

6467
/**
6568
* Generates an Open API v3 specification.
@@ -691,17 +694,32 @@ private function getFilterParameter(string $name, array $description, string $sh
691694
if (!isset($description['openapi']) || $description['openapi'] instanceof Parameter) {
692695
$schema = $description['schema'] ?? [];
693696

694-
if (isset($description['type']) && \in_array($description['type'], Type::$builtinTypes, true) && !isset($schema['type'])) {
695-
$schema += $this->getType(new Type($description['type'], false, null, $description['is_collection'] ?? false));
697+
if (method_exists(PropertyInfoExtractor::class, 'getType')) {
698+
if (isset($description['type']) && \in_array($description['type'], TypeIdentifier::values(), true) && !isset($schema['type'])) {
699+
$type = Type::builtin($description['type']);
700+
if ($description['is_collection'] ?? false) {
701+
$type = Type::array($type, Type::int());
702+
}
703+
704+
$schema += $this->getType($type);
705+
}
706+
// TODO: remove in 5.x
707+
} else {
708+
if (isset($description['type']) && \in_array($description['type'], LegacyType::$builtinTypes, true) && !isset($schema['type'])) {
709+
$schema += $this->getType(new LegacyType($description['type'], false, null, $description['is_collection'] ?? false));
710+
}
696711
}
697712

698713
if (!isset($schema['type'])) {
699714
$schema['type'] = 'string';
700715
}
701716

717+
$arrayValueType = method_exists(PropertyInfoExtractor::class, 'getType') ? TypeIdentifier::ARRAY->value : LegacyType::BUILTIN_TYPE_ARRAY;
718+
$objectValueType = method_exists(PropertyInfoExtractor::class, 'getType') ? TypeIdentifier::OBJECT->value : LegacyType::BUILTIN_TYPE_OBJECT;
719+
702720
$style = 'array' === ($schema['type'] ?? null) && \in_array(
703721
$description['type'],
704-
[Type::BUILTIN_TYPE_ARRAY, Type::BUILTIN_TYPE_OBJECT],
722+
[$arrayValueType, $objectValueType],
705723
true
706724
) ? 'deepObject' : 'form';
707725

@@ -719,7 +737,30 @@ private function getFilterParameter(string $name, array $description, string $sh
719737
}
720738

721739
trigger_deprecation('api-platform/core', '4.0', \sprintf('Not using "%s" on the "openapi" field of the %s::getDescription() (%s) is deprecated.', Parameter::class, $filter, $shortName));
722-
$schema = $description['schema'] ?? (\in_array($description['type'], Type::$builtinTypes, true) ? $this->getType(new Type($description['type'], false, null, $description['is_collection'] ?? false)) : ['type' => 'string']);
740+
741+
$schema = $description['schema'] ?? null;
742+
743+
if (!$schema) {
744+
if (method_exists(PropertyInfoExtractor::class, 'getType')) {
745+
if (isset($description['type']) && \in_array($description['type'], TypeIdentifier::values(), true)) {
746+
$type = Type::builtin($description['type']);
747+
if ($description['is_collection'] ?? false) {
748+
$type = Type::array($type, key: Type::int());
749+
}
750+
$schema = $this->getType($type);
751+
} else {
752+
$schema = ['type' => 'string'];
753+
}
754+
// TODO: remove in 5.x
755+
} else {
756+
$schema = isset($description['type']) && \in_array($description['type'], LegacyType::$builtinTypes, true)
757+
? $this->getType(new LegacyType($description['type'], false, null, $description['is_collection'] ?? false))
758+
: ['type' => 'string'];
759+
}
760+
}
761+
762+
$arrayValueType = method_exists(PropertyInfoExtractor::class, 'getType') ? TypeIdentifier::ARRAY->value : LegacyType::BUILTIN_TYPE_ARRAY;
763+
$objectValueType = method_exists(PropertyInfoExtractor::class, 'getType') ? TypeIdentifier::OBJECT->value : LegacyType::BUILTIN_TYPE_OBJECT;
723764

724765
return new Parameter(
725766
$name,
@@ -731,7 +772,7 @@ private function getFilterParameter(string $name, array $description, string $sh
731772
$schema,
732773
'array' === $schema['type'] && \in_array(
733774
$description['type'],
734-
[Type::BUILTIN_TYPE_ARRAY, Type::BUILTIN_TYPE_OBJECT],
775+
[$arrayValueType, $objectValueType],
735776
true
736777
) ? 'deepObject' : 'form',
737778
$description['openapi']['explode'] ?? ('array' === $schema['type']),

src/OpenApi/Factory/TypeFactoryTrait.php

+74-10
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,11 @@
1414
namespace ApiPlatform\OpenApi\Factory;
1515

1616
use Ramsey\Uuid\UuidInterface;
17-
use Symfony\Component\PropertyInfo\Type;
17+
use Symfony\Component\PropertyInfo\Type as LegacyType;
18+
use Symfony\Component\TypeInfo\Type as NativeType;
19+
use Symfony\Component\TypeInfo\Type\CollectionType;
20+
use Symfony\Component\TypeInfo\Type\ObjectType;
21+
use Symfony\Component\TypeInfo\TypeIdentifier;
1822
use Symfony\Component\Uid\Ulid;
1923
use Symfony\Component\Uid\Uuid;
2024

@@ -23,13 +27,20 @@
2327
*/
2428
trait TypeFactoryTrait
2529
{
26-
private function getType(Type $type): array
30+
/**
31+
* @return array<string, mixed>
32+
*/
33+
private function getType(LegacyType|NativeType $type): array
2734
{
35+
if ($type instanceof NativeType) {
36+
return $this->getNativeType($type);
37+
}
38+
2839
if ($type->isCollection()) {
2940
$keyType = $type->getCollectionKeyTypes()[0] ?? null;
30-
$subType = ($type->getCollectionValueTypes()[0] ?? null) ?? new Type($type->getBuiltinType(), false, $type->getClassName(), false);
41+
$subType = ($type->getCollectionValueTypes()[0] ?? null) ?? new LegacyType($type->getBuiltinType(), false, $type->getClassName(), false);
3142

32-
if (null !== $keyType && Type::BUILTIN_TYPE_STRING === $keyType->getBuiltinType()) {
43+
if (null !== $keyType && LegacyType::BUILTIN_TYPE_STRING === $keyType->getBuiltinType()) {
3344
return $this->addNullabilityToTypeDefinition([
3445
'type' => 'object',
3546
'additionalProperties' => $this->getType($subType),
@@ -42,22 +53,74 @@ private function getType(Type $type): array
4253
], $type);
4354
}
4455

56+
return $this->addNullabilityToTypeDefinition($this->makeLegacyBasicType($type), $type);
57+
}
58+
59+
/**
60+
* @return array<string, mixed>
61+
*/
62+
private function getNativeType(NativeType $type): array
63+
{
64+
if ($type instanceof CollectionType) {
65+
$keyType = $type->getCollectionKeyType();
66+
$subType = $type->getCollectionValueType();
67+
68+
if ($keyType->isIdentifiedBy(TypeIdentifier::STRING)) {
69+
return $this->addNullabilityToTypeDefinition([
70+
'type' => 'object',
71+
'additionalProperties' => $this->getNativeType($subType),
72+
], $type);
73+
}
74+
75+
return $this->addNullabilityToTypeDefinition([
76+
'type' => 'array',
77+
'items' => $this->getNativeType($subType),
78+
], $type);
79+
}
80+
4581
return $this->addNullabilityToTypeDefinition($this->makeBasicType($type), $type);
4682
}
4783

48-
private function makeBasicType(Type $type): array
84+
/**
85+
* @return array<string, mixed>
86+
*/
87+
private function makeLegacyBasicType(LegacyType $type): array
4988
{
5089
return match ($type->getBuiltinType()) {
51-
Type::BUILTIN_TYPE_INT => ['type' => 'integer'],
52-
Type::BUILTIN_TYPE_FLOAT => ['type' => 'number'],
53-
Type::BUILTIN_TYPE_BOOL => ['type' => 'boolean'],
54-
Type::BUILTIN_TYPE_OBJECT => $this->getClassType($type->getClassName(), $type->isNullable()),
90+
LegacyType::BUILTIN_TYPE_INT => ['type' => 'integer'],
91+
LegacyType::BUILTIN_TYPE_FLOAT => ['type' => 'number'],
92+
LegacyType::BUILTIN_TYPE_BOOL => ['type' => 'boolean'],
93+
LegacyType::BUILTIN_TYPE_OBJECT => $this->getClassType($type->getClassName(), $type->isNullable()),
5594
default => ['type' => 'string'],
5695
};
5796
}
5897

98+
/**
99+
* @return array<string, mixed>
100+
*/
101+
private function makeBasicType(NativeType $type): array
102+
{
103+
if ($type->isIdentifiedBy(TypeIdentifier::INT)) {
104+
return ['type' => 'integer'];
105+
}
106+
if ($type->isIdentifiedBy(TypeIdentifier::FLOAT)) {
107+
return ['type' => 'number'];
108+
}
109+
if ($type->isIdentifiedBy(TypeIdentifier::BOOL)) {
110+
return ['type' => 'boolean'];
111+
}
112+
if ($type instanceof ObjectType) {
113+
return $this->getClassType($type->getClassName(), $type->isNullable());
114+
}
115+
116+
// Default for other built-in types like string, resource, mixed, etc.
117+
return ['type' => 'string'];
118+
}
119+
59120
/**
60121
* Gets the JSON Schema document which specifies the data type corresponding to the given PHP class, and recursively adds needed new schema to the current schema if provided.
122+
*
123+
* @return array<string, mixed>
61124
*/
62125
private function getClassType(?string $className, bool $nullable): array
63126
{
@@ -95,6 +158,7 @@ private function getClassType(?string $className, bool $nullable): array
95158
'format' => 'binary',
96159
];
97160
}
161+
98162
if (is_a($className, \BackedEnum::class, true)) {
99163
$enumCases = array_map(static fn (\BackedEnum $enum): string|int => $enum->value, $className::cases());
100164

@@ -118,7 +182,7 @@ private function getClassType(?string $className, bool $nullable): array
118182
*
119183
* @return array<string, mixed>
120184
*/
121-
private function addNullabilityToTypeDefinition(array $jsonSchema, Type $type): array
185+
private function addNullabilityToTypeDefinition(array $jsonSchema, LegacyType|NativeType $type): array
122186
{
123187
if (!$type->isNullable()) {
124188
return $jsonSchema;

src/OpenApi/Tests/Factory/OpenApiFactoryTest.php

+16-16
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,8 @@
6969
use Prophecy\Argument;
7070
use Prophecy\PhpUnit\ProphecyTrait;
7171
use Psr\Container\ContainerInterface;
72-
use Symfony\Component\PropertyInfo\Type;
7372
use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter;
73+
use Symfony\Component\TypeInfo\Type;
7474

7575
class OpenApiFactoryTest extends TestCase
7676
{
@@ -303,7 +303,7 @@ public function testInvoke(): void
303303
$propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class);
304304
$propertyMetadataFactoryProphecy->create(Dummy::class, 'id', Argument::any())->shouldBeCalled()->willReturn(
305305
(new ApiProperty())
306-
->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_INT)])
306+
->withNativeType(Type::int())
307307
->withDescription('This is an id.')
308308
->withReadable(true)
309309
->withWritable(false)
@@ -312,7 +312,7 @@ public function testInvoke(): void
312312
);
313313
$propertyMetadataFactoryProphecy->create(Dummy::class, 'name', Argument::any())->shouldBeCalled()->willReturn(
314314
(new ApiProperty())
315-
->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)])
315+
->withNativeType(Type::string())
316316
->withDescription('This is a name.')
317317
->withReadable(true)
318318
->withWritable(true)
@@ -324,7 +324,7 @@ public function testInvoke(): void
324324
);
325325
$propertyMetadataFactoryProphecy->create(Dummy::class, 'description', Argument::any())->shouldBeCalled()->willReturn(
326326
(new ApiProperty())
327-
->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)])
327+
->withNativeType(Type::string())
328328
->withDescription('This is an initializable but not writable property.')
329329
->withReadable(true)
330330
->withWritable(false)
@@ -337,7 +337,7 @@ public function testInvoke(): void
337337
);
338338
$propertyMetadataFactoryProphecy->create(Dummy::class, 'dummyDate', Argument::any())->shouldBeCalled()->willReturn(
339339
(new ApiProperty())
340-
->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_OBJECT, true, \DateTime::class)])
340+
->withNativeType(Type::nullable(Type::object(\DateTime::class)))
341341
->withDescription('This is a \DateTimeInterface object.')
342342
->withReadable(true)
343343
->withWritable(true)
@@ -349,7 +349,7 @@ public function testInvoke(): void
349349
);
350350
$propertyMetadataFactoryProphecy->create(Dummy::class, 'enum', Argument::any())->shouldBeCalled()->willReturn(
351351
(new ApiProperty())
352-
->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)])
352+
->withNativeType(Type::string())
353353
->withDescription('This is an enum.')
354354
->withReadable(true)
355355
->withWritable(true)
@@ -362,7 +362,7 @@ public function testInvoke(): void
362362
);
363363
$propertyMetadataFactoryProphecy->create(OutputDto::class, 'id', Argument::any())->shouldBeCalled()->willReturn(
364364
(new ApiProperty())
365-
->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_INT)])
365+
->withNativeType(Type::int())
366366
->withDescription('This is an id.')
367367
->withReadable(true)
368368
->withWritable(false)
@@ -371,7 +371,7 @@ public function testInvoke(): void
371371
);
372372
$propertyMetadataFactoryProphecy->create(OutputDto::class, 'name', Argument::any())->shouldBeCalled()->willReturn(
373373
(new ApiProperty())
374-
->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)])
374+
->withNativeType(Type::string())
375375
->withDescription('This is a name.')
376376
->withReadable(true)
377377
->withWritable(true)
@@ -383,7 +383,7 @@ public function testInvoke(): void
383383
);
384384
$propertyMetadataFactoryProphecy->create(OutputDto::class, 'description', Argument::any())->shouldBeCalled()->willReturn(
385385
(new ApiProperty())
386-
->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)])
386+
->withNativeType(Type::string())
387387
->withDescription('This is an initializable but not writable property.')
388388
->withReadable(true)
389389
->withWritable(false)
@@ -394,7 +394,7 @@ public function testInvoke(): void
394394
);
395395
$propertyMetadataFactoryProphecy->create(OutputDto::class, 'dummyDate', Argument::any())->shouldBeCalled()->willReturn(
396396
(new ApiProperty())
397-
->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_OBJECT, true, \DateTime::class)])
397+
->withNativeType(Type::nullable(Type::object(\DateTime::class)))
398398
->withDescription('This is a \DateTimeInterface object.')
399399
->withReadable(true)
400400
->withWritable(true)
@@ -404,7 +404,7 @@ public function testInvoke(): void
404404
);
405405
$propertyMetadataFactoryProphecy->create(OutputDto::class, 'enum', Argument::any())->shouldBeCalled()->willReturn(
406406
(new ApiProperty())
407-
->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)])
407+
->withNativeType(Type::string())
408408
->withDescription('This is an enum.')
409409
->withReadable(true)
410410
->withWritable(true)
@@ -417,7 +417,7 @@ public function testInvoke(): void
417417
foreach ([DummyErrorResource::class, Error::class] as $cl) {
418418
$propertyMetadataFactoryProphecy->create($cl, 'type', Argument::any())->shouldBeCalled()->willReturn(
419419
(new ApiProperty())
420-
->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)])
420+
->withNativeType(Type::string())
421421
->withDescription('This is an error type.')
422422
->withReadable(true)
423423
->withWritable(false)
@@ -428,7 +428,7 @@ public function testInvoke(): void
428428
);
429429
$propertyMetadataFactoryProphecy->create($cl, 'title', Argument::any())->shouldBeCalled()->willReturn(
430430
(new ApiProperty())
431-
->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)])
431+
->withNativeType(Type::string())
432432
->withDescription('This is an error title.')
433433
->withReadable(true)
434434
->withWritable(false)
@@ -439,7 +439,7 @@ public function testInvoke(): void
439439
);
440440
$propertyMetadataFactoryProphecy->create($cl, 'status', Argument::any())->shouldBeCalled()->willReturn(
441441
(new ApiProperty())
442-
->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_INT)])
442+
->withNativeType(Type::int())
443443
->withDescription('This is an error status.')
444444
->withReadable(true)
445445
->withWritable(false)
@@ -448,7 +448,7 @@ public function testInvoke(): void
448448
);
449449
$propertyMetadataFactoryProphecy->create($cl, 'detail', Argument::any())->shouldBeCalled()->willReturn(
450450
(new ApiProperty())
451-
->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)])
451+
->withNativeType(Type::string())
452452
->withDescription('This is an error detail.')
453453
->withReadable(true)
454454
->withWritable(false)
@@ -459,7 +459,7 @@ public function testInvoke(): void
459459
);
460460
$propertyMetadataFactoryProphecy->create($cl, 'instance', Argument::any())->shouldBeCalled()->willReturn(
461461
(new ApiProperty())
462-
->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)])
462+
->withNativeType(Type::string())
463463
->withDescription('This is an error instance.')
464464
->withReadable(true)
465465
->withWritable(false)

0 commit comments

Comments
 (0)