Skip to content

Commit 1d579d0

Browse files
soyukamtarld
andauthoredApr 17, 2025
feat(hal): use TypeInfo type (#7097)
Co-authored-by: Mathias Arlaud <mathias.arlaud@gmail.com>
1 parent a1952e3 commit 1d579d0

File tree

3 files changed

+53
-19
lines changed

3 files changed

+53
-19
lines changed
 

‎src/Hal/Serializer/ItemNormalizer.php

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,20 @@
2626
use ApiPlatform\Serializer\ContextTrait;
2727
use ApiPlatform\Serializer\TagCollectorInterface;
2828
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
29+
use Symfony\Component\PropertyInfo\PropertyInfoExtractor;
30+
use Symfony\Component\PropertyInfo\Type as LegacyType;
2931
use Symfony\Component\Serializer\Exception\CircularReferenceException;
3032
use Symfony\Component\Serializer\Exception\LogicException;
3133
use Symfony\Component\Serializer\Exception\UnexpectedValueException;
3234
use Symfony\Component\Serializer\Mapping\AttributeMetadataInterface;
3335
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface;
3436
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
3537
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
38+
use Symfony\Component\TypeInfo\Type;
39+
use Symfony\Component\TypeInfo\Type\CollectionType;
40+
use Symfony\Component\TypeInfo\Type\CompositeTypeInterface;
41+
use Symfony\Component\TypeInfo\Type\ObjectType;
42+
use Symfony\Component\TypeInfo\Type\WrappingTypeInterface;
3643

3744
/**
3845
* Converts between objects and array including HAL metadata.
@@ -175,22 +182,54 @@ private function getComponents(object $object, ?string $format, array $context):
175182
foreach ($attributes as $attribute) {
176183
$propertyMetadata = $this->propertyMetadataFactory->create($context['resource_class'], $attribute, $options);
177184

178-
$types = $propertyMetadata->getBuiltinTypes() ?? [];
185+
if (method_exists(PropertyInfoExtractor::class, 'getType')) {
186+
$type = $propertyMetadata->getNativeType();
187+
$types = $type instanceof CompositeTypeInterface ? $type->getTypes() : (null === $type ? [] : [$type]);
188+
/** @var class-string|null $className */
189+
$className = null;
190+
} else {
191+
$types = $propertyMetadata->getBuiltinTypes() ?? [];
192+
}
179193

180194
// prevent declaring $attribute as attribute if it's already declared as relationship
181195
$isRelationship = false;
196+
$typeIsResourceClass = function (Type $type) use (&$typeIsResourceClass, &$className): bool {
197+
return match (true) {
198+
$type instanceof WrappingTypeInterface => $type->wrappedTypeIsSatisfiedBy($typeIsResourceClass),
199+
$type instanceof CompositeTypeInterface => $type->composedTypesAreSatisfiedBy($typeIsResourceClass),
200+
default => $type instanceof ObjectType && $this->resourceClassResolver->isResourceClass($className = $type->getClassName()),
201+
};
202+
};
182203

183204
foreach ($types as $type) {
184205
$isOne = $isMany = false;
185206

186-
if (null !== $type) {
207+
/** @var Type|LegacyType|null $valueType */
208+
$valueType = null;
209+
210+
if ($type instanceof LegacyType) {
187211
if ($type->isCollection()) {
188212
$valueType = $type->getCollectionValueTypes()[0] ?? null;
189213
$isMany = null !== $valueType && ($className = $valueType->getClassName()) && $this->resourceClassResolver->isResourceClass($className);
190214
} else {
191215
$className = $type->getClassName();
192216
$isOne = $className && $this->resourceClassResolver->isResourceClass($className);
193217
}
218+
} elseif ($type instanceof Type) {
219+
$typeIsCollection = function (Type $type) use (&$typeIsCollection, &$valueType): bool {
220+
return match (true) {
221+
$type instanceof CollectionType => null !== $valueType = $type->getCollectionValueType(),
222+
$type instanceof WrappingTypeInterface => $type->wrappedTypeIsSatisfiedBy($typeIsCollection),
223+
$type instanceof CompositeTypeInterface => $type->composedTypesAreSatisfiedBy($typeIsCollection),
224+
default => false,
225+
};
226+
};
227+
228+
if ($type->isSatisfiedBy($typeIsCollection)) {
229+
$isMany = $valueType->isSatisfiedBy($typeIsResourceClass);
230+
} else {
231+
$isOne = $type->isSatisfiedBy($typeIsResourceClass);
232+
}
194233
}
195234

196235
if (!$isOne && !$isMany) {

‎src/Hal/composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@
2424
"php": ">=8.2",
2525
"api-platform/state": "^4.1",
2626
"api-platform/metadata": "^4.1",
27-
"api-platform/serializer": "^4.1"
27+
"api-platform/serializer": "^3.4 || ^4.0",
28+
"symfony/type-info": "^7.2"
2829
},
2930
"autoload": {
3031
"psr-4": {

‎tests/Hal/Serializer/ItemNormalizerTest.php

Lines changed: 10 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@
3131
use PHPUnit\Framework\TestCase;
3232
use Prophecy\Argument;
3333
use Prophecy\PhpUnit\ProphecyTrait;
34-
use Symfony\Component\PropertyInfo\Type;
3534
use Symfony\Component\Serializer\Exception\LogicException;
3635
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory;
3736
use Symfony\Component\Serializer\Mapping\Loader\AnnotationLoader;
@@ -41,6 +40,7 @@
4140
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
4241
use Symfony\Component\Serializer\Serializer;
4342
use Symfony\Component\Serializer\SerializerInterface;
43+
use Symfony\Component\TypeInfo\Type;
4444

4545
/**
4646
* @author Kévin Dunglas <dunglas@gmail.com>
@@ -119,10 +119,10 @@ public function testNormalize(): void
119119

120120
$propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class);
121121
$propertyMetadataFactoryProphecy->create(Dummy::class, 'name', [])->willReturn(
122-
(new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)])->withDescription('')->withReadable(true)
122+
(new ApiProperty())->withNativeType(Type::string())->withDescription('')->withReadable(true)
123123
);
124124
$propertyMetadataFactoryProphecy->create(Dummy::class, 'relatedDummy', [])->willReturn(
125-
(new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_OBJECT, false, RelatedDummy::class)])->withDescription('')->withReadable(true)->withWritable(false)->withWritableLink(false)
125+
(new ApiProperty())->withNativeType(Type::object(RelatedDummy::class))->withDescription('')->withReadable(true)->withWritable(false)->withWritableLink(false)
126126
);
127127

128128
$iriConverterProphecy = $this->prophesize(IriConverterInterface::class);
@@ -183,16 +183,10 @@ public function testNormalizeWithUnionIntersectTypes(): void
183183

184184
$propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class);
185185
$propertyMetadataFactoryProphecy->create(Book::class, 'author', [])->willReturn(
186-
(new ApiProperty())->withBuiltinTypes([
187-
new Type(Type::BUILTIN_TYPE_OBJECT, false, ActivableInterface::class),
188-
new Type(Type::BUILTIN_TYPE_OBJECT, false, TimestampableInterface::class),
189-
])->withReadable(true)
186+
(new ApiProperty())->withNativeType(Type::intersection(Type::object(ActivableInterface::class), Type::object(TimestampableInterface::class)))->withReadable(true)
190187
);
191188
$propertyMetadataFactoryProphecy->create(Book::class, 'library', [])->willReturn(
192-
(new ApiProperty())->withBuiltinTypes([
193-
new Type(Type::BUILTIN_TYPE_OBJECT, false, ActivableInterface::class),
194-
new Type(Type::BUILTIN_TYPE_OBJECT, false, TimestampableInterface::class),
195-
])->withReadable(true)
189+
(new ApiProperty())->withNativeType(Type::intersection(Type::object(ActivableInterface::class), Type::object(TimestampableInterface::class)))->withReadable(true)
196190
);
197191

198192
$iriConverterProphecy = $this->prophesize(IriConverterInterface::class);
@@ -247,10 +241,10 @@ public function testNormalizeWithoutCache(): void
247241

248242
$propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class);
249243
$propertyMetadataFactoryProphecy->create(Dummy::class, 'name', [])->willReturn(
250-
(new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)])->withDescription('')->withReadable(true)
244+
(new ApiProperty())->withNativeType(Type::string())->withDescription('')->withReadable(true)
251245
);
252246
$propertyMetadataFactoryProphecy->create(Dummy::class, 'relatedDummy', [])->willReturn(
253-
(new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_OBJECT, false, RelatedDummy::class)])->withDescription('')->withReadable(true)->withWritable(false)->withReadableLink(false)
247+
(new ApiProperty())->withNativeType(Type::object(RelatedDummy::class))->withDescription('')->withReadable(true)->withWritable(false)->withReadableLink(false)
254248
);
255249

256250
$iriConverterProphecy = $this->prophesize(IriConverterInterface::class);
@@ -325,13 +319,13 @@ public function testMaxDepth(): void
325319

326320
$propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class);
327321
$propertyMetadataFactoryProphecy->create(MaxDepthDummy::class, 'id', [])->willReturn(
328-
(new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_INT)])->withDescription('')->withReadable(true)
322+
(new ApiProperty())->withNativeType(Type::int())->withDescription('')->withReadable(true)
329323
);
330324
$propertyMetadataFactoryProphecy->create(MaxDepthDummy::class, 'name', [])->willReturn(
331-
(new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)])->withDescription('')->withReadable(true)
325+
(new ApiProperty())->withNativeType(Type::string())->withDescription('')->withReadable(true)
332326
);
333327
$propertyMetadataFactoryProphecy->create(MaxDepthDummy::class, 'child', [])->willReturn(
334-
(new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_OBJECT, false, MaxDepthDummy::class)])->withDescription('')->withReadable(true)->withWritable(false)->withReadableLink(true)
328+
(new ApiProperty())->withNativeType(Type::object(MaxDepthDummy::class))->withDescription('')->withReadable(true)->withWritable(false)->withReadableLink(true)
335329
);
336330

337331
$iriConverterProphecy = $this->prophesize(IriConverterInterface::class);

0 commit comments

Comments
 (0)
Failed to load comments.