Skip to content

Commit 350d6d7

Browse files
soyukamtarld
andauthored
feat(hydra): use TypeInfo's Type (#7099)
Co-authored-by: Mathias Arlaud <mathias.arlaud@gmail.com>
1 parent 827e1f7 commit 350d6d7

File tree

3 files changed

+165
-60
lines changed

3 files changed

+165
-60
lines changed

src/Hydra/Serializer/DocumentationNormalizer.php

+148-43
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,17 @@
2929
use ApiPlatform\Metadata\Resource\ResourceMetadataCollection;
3030
use ApiPlatform\Metadata\ResourceClassResolverInterface;
3131
use ApiPlatform\Metadata\UrlGeneratorInterface;
32-
use Symfony\Component\PropertyInfo\Type;
32+
use Symfony\Component\PropertyInfo\PropertyInfoExtractor;
33+
use Symfony\Component\PropertyInfo\Type as LegacyType;
3334
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
3435
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
3536
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
37+
use Symfony\Component\TypeInfo\Type;
38+
use Symfony\Component\TypeInfo\Type\CollectionType;
39+
use Symfony\Component\TypeInfo\Type\CompositeTypeInterface;
40+
use Symfony\Component\TypeInfo\Type\ObjectType;
41+
use Symfony\Component\TypeInfo\Type\WrappingTypeInterface;
42+
use Symfony\Component\TypeInfo\TypeIdentifier;
3643

3744
use const ApiPlatform\JsonLd\HYDRA_CONTEXT;
3845

@@ -356,73 +363,171 @@ private function getRange(ApiProperty $propertyMetadata): array|string|null
356363
return $jsonldContext['@type'];
357364
}
358365

359-
$builtInTypes = $propertyMetadata->getBuiltinTypes() ?? [];
360366
$types = [];
361367

362-
foreach ($builtInTypes as $type) {
363-
if ($type->isCollection() && null !== $collectionType = $type->getCollectionValueTypes()[0] ?? null) {
364-
$type = $collectionType;
368+
if (method_exists(PropertyInfoExtractor::class, 'getType')) {
369+
$nativeType = $propertyMetadata->getNativeType();
370+
if (null === $nativeType) {
371+
return null;
365372
}
366373

367-
switch ($type->getBuiltinType()) {
368-
case Type::BUILTIN_TYPE_STRING:
369-
if (!\in_array('xmls:string', $types, true)) {
370-
$types[] = 'xmls:string';
371-
}
372-
break;
373-
case Type::BUILTIN_TYPE_INT:
374-
if (!\in_array('xmls:integer', $types, true)) {
375-
$types[] = 'xmls:integer';
376-
}
377-
break;
378-
case Type::BUILTIN_TYPE_FLOAT:
379-
if (!\in_array('xmls:decimal', $types, true)) {
380-
$types[] = 'xmls:decimal';
381-
}
382-
break;
383-
case Type::BUILTIN_TYPE_BOOL:
384-
if (!\in_array('xmls:boolean', $types, true)) {
385-
$types[] = 'xmls:boolean';
386-
}
387-
break;
388-
case Type::BUILTIN_TYPE_OBJECT:
389-
if (null === $className = $type->getClassName()) {
390-
continue 2;
374+
/** @var Type|null $collectionValueType */
375+
$collectionValueType = null;
376+
$typeIsCollection = static function (Type $type) use (&$typeIsCollection, &$collectionValueType): bool {
377+
return match (true) {
378+
$type instanceof CollectionType => null !== $collectionValueType = $type->getCollectionValueType(),
379+
$type instanceof WrappingTypeInterface => $type->wrappedTypeIsSatisfiedBy($typeIsCollection),
380+
$type instanceof CompositeTypeInterface => $type->composedTypesAreSatisfiedBy($typeIsCollection),
381+
default => false,
382+
};
383+
};
384+
385+
if ($nativeType->isSatisfiedBy($typeIsCollection)) {
386+
$nativeType = $collectionValueType;
387+
}
388+
389+
// Check for specific types after potentially unwrapping the collection
390+
if (null === $nativeType) {
391+
return null; // Should not happen if collection had a value type, but safety check
392+
}
393+
394+
if ($nativeType->isIdentifiedBy(TypeIdentifier::STRING)) {
395+
$types[] = 'xmls:string';
396+
}
397+
398+
if ($nativeType->isIdentifiedBy(TypeIdentifier::INT)) {
399+
$types[] = 'xmls:integer';
400+
}
401+
402+
if ($nativeType->isIdentifiedBy(TypeIdentifier::FLOAT)) {
403+
$types[] = 'xmls:decimal';
404+
}
405+
406+
if ($nativeType->isIdentifiedBy(TypeIdentifier::BOOL)) {
407+
$types[] = 'xmls:boolean';
408+
}
409+
410+
if ($nativeType->isIdentifiedBy(\DateTimeInterface::class)) {
411+
$types[] = 'xmls:dateTime';
412+
}
413+
414+
/** @var class-string|null $className */
415+
$className = null;
416+
417+
$typeIsResourceClass = function (Type $type) use (&$typeIsResourceClass, &$className): bool {
418+
return match (true) {
419+
$type instanceof WrappingTypeInterface => $type->wrappedTypeIsSatisfiedBy($typeIsResourceClass),
420+
$type instanceof CompositeTypeInterface => $type->composedTypesAreSatisfiedBy($typeIsResourceClass),
421+
default => $type instanceof ObjectType && $this->resourceClassResolver->isResourceClass($className = $type->getClassName()),
422+
};
423+
};
424+
425+
if ($nativeType->isSatisfiedBy($typeIsResourceClass) && $className) {
426+
$resourceMetadata = $this->resourceMetadataFactory->create($className);
427+
$operation = $resourceMetadata->getOperation();
428+
429+
if (!$operation instanceof HttpOperation || !$operation->getTypes()) {
430+
if (!\in_array("#{$operation->getShortName()}", $types, true)) {
431+
$types[] = "#{$operation->getShortName()}";
391432
}
433+
} else {
434+
$types = array_unique(array_merge($types, $operation->getTypes()));
435+
}
436+
}
437+
// TODO: remove in 5.x
438+
} else {
439+
$builtInTypes = $propertyMetadata->getBuiltinTypes() ?? [];
440+
441+
foreach ($builtInTypes as $type) {
442+
if ($type->isCollection() && null !== $collectionType = $type->getCollectionValueTypes()[0] ?? null) {
443+
$type = $collectionType;
444+
}
392445

393-
if (is_a($className, \DateTimeInterface::class, true)) {
394-
if (!\in_array('xmls:dateTime', $types, true)) {
395-
$types[] = 'xmls:dateTime';
446+
switch ($type->getBuiltinType()) {
447+
case LegacyType::BUILTIN_TYPE_STRING:
448+
if (!\in_array('xmls:string', $types, true)) {
449+
$types[] = 'xmls:string';
396450
}
397451
break;
398-
}
399-
400-
if ($this->resourceClassResolver->isResourceClass($className)) {
401-
$resourceMetadata = $this->resourceMetadataFactory->create($className);
402-
$operation = $resourceMetadata->getOperation();
452+
case LegacyType::BUILTIN_TYPE_INT:
453+
if (!\in_array('xmls:integer', $types, true)) {
454+
$types[] = 'xmls:integer';
455+
}
456+
break;
457+
case LegacyType::BUILTIN_TYPE_FLOAT:
458+
if (!\in_array('xmls:decimal', $types, true)) {
459+
$types[] = 'xmls:decimal';
460+
}
461+
break;
462+
case LegacyType::BUILTIN_TYPE_BOOL:
463+
if (!\in_array('xmls:boolean', $types, true)) {
464+
$types[] = 'xmls:boolean';
465+
}
466+
break;
467+
case LegacyType::BUILTIN_TYPE_OBJECT:
468+
if (null === $className = $type->getClassName()) {
469+
continue 2;
470+
}
403471

404-
if (!$operation instanceof HttpOperation || !$operation->getTypes()) {
405-
if (!\in_array("#{$operation->getShortName()}", $types, true)) {
406-
$types[] = "#{$operation->getShortName()}";
472+
if (is_a($className, \DateTimeInterface::class, true)) {
473+
if (!\in_array('xmls:dateTime', $types, true)) {
474+
$types[] = 'xmls:dateTime';
407475
}
408476
break;
409477
}
410478

411-
$types = array_unique(array_merge($types, $operation->getTypes()));
412-
break;
413-
}
479+
if ($this->resourceClassResolver->isResourceClass($className)) {
480+
$resourceMetadata = $this->resourceMetadataFactory->create($className);
481+
$operation = $resourceMetadata->getOperation();
482+
483+
if (!$operation instanceof HttpOperation || !$operation->getTypes()) {
484+
if (!\in_array("#{$operation->getShortName()}", $types, true)) {
485+
$types[] = "#{$operation->getShortName()}";
486+
}
487+
break;
488+
}
489+
490+
$types = array_unique(array_merge($types, $operation->getTypes()));
491+
break;
492+
}
493+
}
414494
}
415495
}
416496

417497
if ([] === $types) {
418498
return null;
419499
}
420500

501+
$types = array_unique($types);
502+
421503
return 1 === \count($types) ? $types[0] : $types;
422504
}
423505

424506
private function isSingleRelation(ApiProperty $propertyMetadata): bool
425507
{
508+
if (method_exists(PropertyInfoExtractor::class, 'getType')) {
509+
$nativeType = $propertyMetadata->getNativeType();
510+
if (null === $nativeType) {
511+
return false;
512+
}
513+
514+
if ($nativeType instanceof CollectionType) {
515+
return false;
516+
}
517+
518+
$typeIsResourceClass = function (Type $type) use (&$typeIsResourceClass): bool {
519+
return match (true) {
520+
$type instanceof CollectionType => false,
521+
$type instanceof WrappingTypeInterface => $type->wrappedTypeIsSatisfiedBy($typeIsResourceClass),
522+
$type instanceof CompositeTypeInterface => $type->composedTypesAreSatisfiedBy($typeIsResourceClass),
523+
default => $type instanceof ObjectType && $this->resourceClassResolver->isResourceClass($type->getClassName()),
524+
};
525+
};
526+
527+
return $nativeType->isSatisfiedBy($typeIsResourceClass);
528+
}
529+
530+
// TODO: remove in 5.x
426531
$builtInTypes = $propertyMetadata->getBuiltinTypes() ?? [];
427532

428533
foreach ($builtInTypes as $type) {

src/Hydra/Tests/Serializer/DocumentationNormalizerTest.php

+14-14
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
use PHPUnit\Framework\TestCase;
3636
use Prophecy\Argument;
3737
use Prophecy\PhpUnit\ProphecyTrait;
38-
use Symfony\Component\PropertyInfo\Type;
38+
use Symfony\Component\TypeInfo\Type;
3939

4040
use const ApiPlatform\JsonLd\HYDRA_CONTEXT;
4141

@@ -77,15 +77,15 @@ private function doTestNormalize($resourceMetadataFactory = null): void
7777

7878
$propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class);
7979
$propertyMetadataFactoryProphecy->create('dummy', 'name', Argument::type('array'))->shouldBeCalled()->willReturn(
80-
(new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)])->withDescription('name')->withReadable(true)->withWritable(true)->withReadableLink(true)->withWritableLink(true)
80+
(new ApiProperty())->withNativeType(Type::string())->withDescription('name')->withReadable(true)->withWritable(true)->withReadableLink(true)->withWritableLink(true)
8181
);
8282
$propertyMetadataFactoryProphecy->create('dummy', 'description', Argument::type('array'))->shouldBeCalled()->willReturn(
83-
(new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)])->withDescription('description')->withReadable(true)->withWritable(true)->withReadableLink(true)->withWritableLink(true)->withJsonldContext(['@type' => '@id'])
83+
(new ApiProperty())->withNativeType(Type::string())->withDescription('description')->withReadable(true)->withWritable(true)->withReadableLink(true)->withWritableLink(true)->withJsonldContext(['@type' => '@id'])
8484
);
8585
$propertyMetadataFactoryProphecy->create('dummy', 'nameConverted', Argument::type('array'))->shouldBeCalled()->willReturn(
86-
(new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)])->withDescription('name converted')->withReadable(true)->withWritable(true)->withReadableLink(true)->withWritableLink(true)
86+
(new ApiProperty())->withNativeType(Type::string())->withDescription('name converted')->withReadable(true)->withWritable(true)->withReadableLink(true)->withWritableLink(true)
8787
);
88-
$propertyMetadataFactoryProphecy->create('dummy', 'relatedDummy', Argument::type('array'))->shouldBeCalled()->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_OBJECT, false, 'dummy', true, null, new Type(Type::BUILTIN_TYPE_OBJECT, false, 'relatedDummy'))])->withDescription('This is a name.')->withReadable(true)->withWritable(true)->withReadableLink(true)->withWritableLink(true));
88+
$propertyMetadataFactoryProphecy->create('dummy', 'relatedDummy', Argument::type('array'))->shouldBeCalled()->willReturn((new ApiProperty())->withNativeType(Type::collection(Type::object('dummy'), Type::object('relatedDummy')))->withDescription('This is a name.')->withReadable(true)->withWritable(true)->withReadableLink(true)->withWritableLink(true)); // @phpstan-ignore-line
8989
$propertyMetadataFactoryProphecy->create('dummy', 'iri', Argument::type('array'))->shouldBeCalled()->willReturn((new ApiProperty())->withIris(['https://schema.org/Dummy']));
9090

9191
$resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class);
@@ -365,10 +365,10 @@ public function testNormalizeInputOutputClass(): void
365365
]));
366366

367367
$propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class);
368-
$propertyMetadataFactoryProphecy->create('inputClass', 'a', Argument::type('array'))->shouldBeCalled()->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)])->withDescription('a')->withReadable(true)->withWritable(true)->withReadableLink(true)->withWritableLink(true));
369-
$propertyMetadataFactoryProphecy->create('inputClass', 'b', Argument::type('array'))->shouldBeCalled()->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)])->withDescription('b')->withReadable(true)->withWritable(true)->withReadableLink(true)->withWritableLink(true));
370-
$propertyMetadataFactoryProphecy->create('outputClass', 'c', Argument::type('array'))->shouldBeCalled()->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)])->withDescription('c')->withReadable(true)->withWritable(true)->withReadableLink(true)->withWritableLink(true));
371-
$propertyMetadataFactoryProphecy->create('outputClass', 'd', Argument::type('array'))->shouldBeCalled()->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)])->withDescription('d')->withReadable(true)->withWritable(true)->withReadableLink(true)->withWritableLink(true));
368+
$propertyMetadataFactoryProphecy->create('inputClass', 'a', Argument::type('array'))->shouldBeCalled()->willReturn((new ApiProperty())->withNativeType(Type::string())->withDescription('a')->withReadable(true)->withWritable(true)->withReadableLink(true)->withWritableLink(true));
369+
$propertyMetadataFactoryProphecy->create('inputClass', 'b', Argument::type('array'))->shouldBeCalled()->willReturn((new ApiProperty())->withNativeType(Type::string())->withDescription('b')->withReadable(true)->withWritable(true)->withReadableLink(true)->withWritableLink(true));
370+
$propertyMetadataFactoryProphecy->create('outputClass', 'c', Argument::type('array'))->shouldBeCalled()->willReturn((new ApiProperty())->withNativeType(Type::string())->withDescription('c')->withReadable(true)->withWritable(true)->withReadableLink(true)->withWritableLink(true));
371+
$propertyMetadataFactoryProphecy->create('outputClass', 'd', Argument::type('array'))->shouldBeCalled()->willReturn((new ApiProperty())->withNativeType(Type::string())->withDescription('d')->withReadable(true)->withWritable(true)->withReadableLink(true)->withWritableLink(true));
372372

373373
$resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class);
374374
$resourceClassResolverProphecy->isResourceClass(Argument::type('string'))->willReturn(true);
@@ -502,7 +502,7 @@ public function testHasHydraContext(): void
502502
$propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class);
503503
$propertyMetadataFactoryProphecy->create('dummy', 'name', Argument::type('array'))->shouldBeCalled()->willReturn(
504504
(new ApiProperty())
505-
->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)])
505+
->withNativeType(Type::string())
506506
->withDescription('b')
507507
->withReadable(true)
508508
->withWritable(true)
@@ -564,15 +564,15 @@ public function testNormalizeWithoutPrefix(): void
564564

565565
$propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class);
566566
$propertyMetadataFactoryProphecy->create('dummy', 'name', Argument::type('array'))->shouldBeCalled()->willReturn(
567-
(new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)])->withDescription('name')->withReadable(true)->withWritable(true)->withReadableLink(true)->withWritableLink(true)
567+
(new ApiProperty())->withNativeType(Type::string())->withDescription('name')->withReadable(true)->withWritable(true)->withReadableLink(true)->withWritableLink(true)
568568
);
569569
$propertyMetadataFactoryProphecy->create('dummy', 'description', Argument::type('array'))->shouldBeCalled()->willReturn(
570-
(new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)])->withDescription('description')->withReadable(true)->withWritable(true)->withReadableLink(true)->withWritableLink(true)->withJsonldContext(['@type' => '@id'])
570+
(new ApiProperty())->withNativeType(Type::string())->withDescription('description')->withReadable(true)->withWritable(true)->withReadableLink(true)->withWritableLink(true)->withJsonldContext(['@type' => '@id'])
571571
);
572572
$propertyMetadataFactoryProphecy->create('dummy', 'nameConverted', Argument::type('array'))->shouldBeCalled()->willReturn(
573-
(new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)])->withDescription('name converted')->withReadable(true)->withWritable(true)->withReadableLink(true)->withWritableLink(true)
573+
(new ApiProperty())->withNativeType(Type::string())->withDescription('name converted')->withReadable(true)->withWritable(true)->withReadableLink(true)->withWritableLink(true)
574574
);
575-
$propertyMetadataFactoryProphecy->create('dummy', 'relatedDummy', Argument::type('array'))->shouldBeCalled()->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_OBJECT, false, 'dummy', true, null, new Type(Type::BUILTIN_TYPE_OBJECT, false, 'relatedDummy'))])->withDescription('This is a name.')->withReadable(true)->withWritable(true)->withReadableLink(true)->withWritableLink(true));
575+
$propertyMetadataFactoryProphecy->create('dummy', 'relatedDummy', Argument::type('array'))->shouldBeCalled()->willReturn((new ApiProperty())->withNativeType(Type::collection(Type::object('dummy'), Type::object('relatedDummy')))->withDescription('This is a name.')->withReadable(true)->withWritable(true)->withReadableLink(true)->withWritableLink(true)); // @phpstan-ignore-line
576576
$propertyMetadataFactoryProphecy->create('dummy', 'iri', Argument::type('array'))->shouldBeCalled()->willReturn((new ApiProperty())->withIris(['https://schema.org/Dummy']));
577577

578578
$resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class);

0 commit comments

Comments
 (0)