Skip to content

Commit 81650fd

Browse files
committed
[LiveComponent] Use TypeInfo Type
1 parent 6d2daab commit 81650fd

File tree

6 files changed

+145
-108
lines changed

6 files changed

+145
-108
lines changed

src/LiveComponent/composer.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,9 @@
2929
"php": ">=8.1",
3030
"symfony/deprecation-contracts": "^2.5|^3.0",
3131
"symfony/property-access": "^5.4.5|^6.0|^7.0",
32+
"symfony/property-info": "^5.4|^6.0|^7.0",
3233
"symfony/stimulus-bundle": "^2.9",
34+
"symfony/type-info": "^7.2",
3335
"symfony/ux-twig-component": "^2.8",
3436
"twig/twig": "^3.8.0"
3537
},
@@ -46,7 +48,6 @@
4648
"symfony/framework-bundle": "^5.4|^6.0|^7.0",
4749
"symfony/options-resolver": "^5.4|^6.0|^7.0",
4850
"symfony/phpunit-bridge": "^6.1|^7.0",
49-
"symfony/property-info": "^5.4|^6.0|^7.0",
5051
"symfony/security-bundle": "^5.4|^6.0|^7.0",
5152
"symfony/serializer": "^5.4|^6.0|^7.0",
5253
"symfony/twig-bundle": "^5.4|^6.0|^7.0",

src/LiveComponent/src/DependencyInjection/LiveComponentExtension.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,7 @@ function (ChildDefinition $definition, AsLiveComponent $attribute) {
188188
$container->register('ux.live_component.metadata_factory', LiveComponentMetadataFactory::class)
189189
->setArguments([
190190
new Reference('ux.twig_component.component_factory'),
191-
new Reference('property_info'),
191+
new Reference('type_info.resolver'),
192192
])
193193
->addTag('kernel.reset', ['method' => 'reset'])
194194
;

src/LiveComponent/src/LiveComponentHydrator.php

+90-43
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,15 @@
1818
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
1919
use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor;
2020
use Symfony\Component\PropertyInfo\PropertyInfoExtractor;
21-
use Symfony\Component\PropertyInfo\Type;
2221
use Symfony\Component\Serializer\Exception\ExceptionInterface as SerializerExceptionInterface;
2322
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
2423
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
24+
use Symfony\Component\TypeInfo\Type;
25+
use Symfony\Component\TypeInfo\Type\CollectionType;
26+
use Symfony\Component\TypeInfo\Type\CompositeTypeInterface;
27+
use Symfony\Component\TypeInfo\Type\ObjectType;
28+
use Symfony\Component\TypeInfo\Type\WrappingTypeInterface;
29+
use Symfony\Component\TypeInfo\TypeIdentifier;
2530
use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
2631
use Symfony\UX\LiveComponent\Attribute\LiveProp;
2732
use Symfony\UX\LiveComponent\Exception\HydrationException;
@@ -266,50 +271,71 @@ public function hydrateValue(mixed $value, LivePropMetadata $propMetadata, objec
266271
throw new \LogicException(\sprintf('The LiveProp "%s" on component "%s" has "useSerializerForHydration: true", but the given serializer does not implement DenormalizerInterface.', $propMetadata->getName(), $parentObject::class));
267272
}
268273

269-
if ($propMetadata->collectionValueType()) {
270-
$builtInType = $propMetadata->collectionValueType()->getBuiltinType();
271-
if (Type::BUILTIN_TYPE_OBJECT === $builtInType) {
272-
$type = $propMetadata->collectionValueType()->getClassName().'[]';
273-
} else {
274-
$type = $builtInType.'[]';
275-
}
276-
} else {
277-
$type = $propMetadata->getType();
274+
if (null === $type = $propMetadata->getType()) {
275+
throw new \LogicException(\sprintf('The "%s::%s" object should be hydrated with the Serializer, but no type could be guessed.', $parentObject::class, $propMetadata->getName()));
278276
}
279277

280-
if (null === $type) {
281-
throw new \LogicException(\sprintf('The "%s::%s" object should be hydrated with the Serializer, but no type could be guessed.', $parentObject::class, $propMetadata->getName()));
278+
if ($type->isNullable()) {
279+
$type = $type->getWrappedType();
282280
}
283281

284-
return $this->serializer->denormalize($value, $type, 'json', $propMetadata->serializationContext());
285-
}
282+
if ($isCollection = $type instanceof CollectionType) {
283+
$type = $type->getCollectionValueType();
284+
}
286285

287-
if ($propMetadata->collectionValueType() && Type::BUILTIN_TYPE_OBJECT === $propMetadata->collectionValueType()->getBuiltinType()) {
288-
$collectionClass = $propMetadata->collectionValueType()->getClassName();
289-
foreach ($value as $key => $objectItem) {
290-
$value[$key] = $this->hydrateObjectValue($objectItem, $collectionClass, true, $propMetadata->getFormat(), $parentObject::class, \sprintf('%s.%s', $propMetadata->getName(), $key), $parentObject);
286+
while ($type instanceof WrappingTypeInterface) {
287+
$type = $type->getWrappedType();
291288
}
289+
290+
$typeString = $type.($isCollection ? '[]' : '');
291+
292+
return $this->serializer->denormalize($value, $typeString, 'json', $propMetadata->serializationContext());
292293
}
293294

294295
// no type? no hydration
295-
if (!$propMetadata->getType()) {
296+
if (null === $type = $propMetadata->getType()) {
296297
return $value;
297298
}
298299

299300
if (null === $value) {
300301
return null;
301302
}
302303

303-
if (\is_string($value) && $propMetadata->isBuiltIn() && \in_array($propMetadata->getType(), ['int', 'float', 'bool'], true)) {
304-
return self::coerceStringValue($value, $propMetadata->getType(), $propMetadata->allowsNull());
304+
if ($isNullable = $type->isNullable()) {
305+
$type = $type->getWrappedType();
305306
}
306307

307-
// for all other built-ins: int, boolean, array, return as is
308-
if ($propMetadata->isBuiltIn()) {
309-
return $value;
308+
if ($type instanceof CollectionType) {
309+
$collectionValueType = $type->getCollectionValueType();
310+
if ($collectionValueType instanceof CompositeTypeInterface) {
311+
$collectionValueType = $collectionValueType->getTypes()[0];
312+
}
313+
314+
while ($collectionValueType instanceof WrappingTypeInterface) {
315+
$collectionValueType = $collectionValueType->getWrappedType();
316+
}
317+
318+
if ($collectionValueType instanceof ObjectType) {
319+
foreach ($value as $key => $objectItem) {
320+
$value[$key] = $this->hydrateObjectValue($objectItem, $collectionValueType->getClassName(), true, $propMetadata->getFormat(), $parentObject::class, \sprintf('%s.%s', $propMetadata->getName(), $key), $parentObject);
321+
}
322+
}
323+
}
324+
325+
if (\is_string($value) && $type->isIdentifiedBy(TypeIdentifier::INT, TypeIdentifier::FLOAT, TypeIdentifier::BOOL)) {
326+
return self::coerceStringValue($value, $type, $isNullable);
327+
}
328+
329+
while ($type instanceof WrappingTypeInterface) {
330+
$type = $type->getWrappedType();
331+
}
332+
333+
if ($type instanceof ObjectType) {
334+
return $this->hydrateObjectValue($value, $type->getClassName(), $isNullable, $propMetadata->getFormat(), $parentObject::class, $propMetadata->getName(), $parentObject);
310335
}
311336

312-
return $this->hydrateObjectValue($value, $propMetadata->getType(), $propMetadata->allowsNull(), $propMetadata->getFormat(), $parentObject::class, $propMetadata->getName(), $parentObject);
337+
// for all other built-ins: int, boolean, array, return as is
338+
return $value;
313339
}
314340

315341
public function addChecksumToData(array $data): array
@@ -319,18 +345,18 @@ public function addChecksumToData(array $data): array
319345
return $data;
320346
}
321347

322-
private static function coerceStringValue(string $value, string $type, bool $allowsNull): int|float|bool|null
348+
private static function coerceStringValue(string $value, Type $type, bool $isNullable): int|float|bool|null
323349
{
324350
$value = trim($value);
325351

326-
if ('' === $value && $allowsNull) {
352+
if ('' === $value && $isNullable) {
327353
return null;
328354
}
329355

330-
return match ($type) {
331-
'int' => (int) $value,
332-
'float' => (float) $value,
333-
'bool' => self::coerceStringToBoolean($value),
356+
return match (true) {
357+
$type->isIdentifiedBy(TypeIdentifier::INT) => (int) $value,
358+
$type->isIdentifiedBy(TypeIdentifier::FLOAT) => (float) $value,
359+
$type->isIdentifiedBy(TypeIdentifier::BOOL) => self::coerceStringToBoolean($value),
334360
default => throw new \LogicException(\sprintf('Cannot coerce value "%s" to type "%s"', $value, $type)),
335361
};
336362
}
@@ -462,15 +488,35 @@ private function dehydrateValue(mixed $value, LivePropMetadata $propMetadata, ob
462488
return $value;
463489
}
464490

491+
if (!$type = $propMetadata->getType()) {
492+
throw new \LogicException(\sprintf('The "%s" property on component "%s" is missing its property-type. Add the "%s" type so the object can be hydrated later.', $propMetadata->getName(), $parentObject::class, $value::class));
493+
}
494+
465495
if (\is_array($value)) {
466-
if ($propMetadata->collectionValueType() && Type::BUILTIN_TYPE_OBJECT === $propMetadata->collectionValueType()->getBuiltinType()) {
467-
$collectionClass = $propMetadata->collectionValueType()->getClassName();
468-
foreach ($value as $key => $objectItem) {
469-
if (!$objectItem instanceof $collectionClass) {
470-
throw new \LogicException(\sprintf('The LiveProp "%s" on component "%s" is an array. We determined the array is full of %s objects, but at least one key had a different value of %s', $propMetadata->getName(), $parentObject::class, $collectionClass, get_debug_type($objectItem)));
471-
}
496+
if ($type->isNullable()) {
497+
$type = $type->getWrappedType();
498+
}
472499

473-
$value[$key] = $this->dehydrateObjectValue($objectItem, $collectionClass, $propMetadata->getFormat(), $parentObject);
500+
if ($type instanceof CollectionType) {
501+
$collectionValueType = $type->getCollectionValueType();
502+
if ($collectionValueType instanceof CompositeTypeInterface) {
503+
$collectionValueType = $collectionValueType->getTypes()[0];
504+
}
505+
506+
while ($collectionValueType instanceof WrappingTypeInterface) {
507+
$collectionValueType = $collectionValueType->getWrappedType();
508+
}
509+
510+
if ($collectionValueType instanceof ObjectType) {
511+
$collectionClass = $collectionValueType->getClassName();
512+
513+
foreach ($value as $key => $objectItem) {
514+
if (!$objectItem instanceof $collectionClass) {
515+
throw new \LogicException(\sprintf('The LiveProp "%s" on component "%s" is an array. We determined the array is full of %s objects, but at least one key had a different value of %s', $propMetadata->getName(), $parentObject::class, $collectionClass, get_debug_type($objectItem)));
516+
}
517+
518+
$value[$key] = $this->dehydrateObjectValue($objectItem, $collectionClass, $propMetadata->getFormat(), $parentObject);
519+
}
474520
}
475521
}
476522

@@ -485,14 +531,15 @@ private function dehydrateValue(mixed $value, LivePropMetadata $propMetadata, ob
485531
throw new \LogicException(\sprintf('Unable to dehydrate value of type "%s" for property "%s" on component "%s". Change this to a simpler type of an object that can be dehydrated. Or set the hydrateWith/dehydrateWith options in LiveProp or set "useSerializerForHydration: true" on the LiveProp to use the serializer.', get_debug_type($value), $propMetadata->getName(), $parentObject::class));
486532
}
487533

488-
if (!$propMetadata->getType() || $propMetadata->isBuiltIn()) {
489-
throw new \LogicException(\sprintf('The "%s" property on component "%s" is missing its property-type. Add the "%s" type so the object can be hydrated later.', $propMetadata->getName(), $parentObject::class, $value::class));
534+
while ($type instanceof WrappingTypeInterface) {
535+
$type = $type->getWrappedType();
490536
}
491537

492-
// at this point, we have an object and can assume $propMetadata->getType()
493-
// is set correctly (needed for hydration later)
538+
if ($type instanceof ObjectType) {
539+
return $this->dehydrateObjectValue($value, $type->getClassName(), $propMetadata->getFormat(), $parentObject);
540+
}
494541

495-
return $this->dehydrateObjectValue($value, $propMetadata->getType(), $propMetadata->getFormat(), $parentObject);
542+
return $value;
496543
}
497544

498545
private function dehydrateObjectValue(object $value, string $classType, ?string $dateFormat, object $parentObject): mixed

src/LiveComponent/src/Metadata/LiveComponentMetadataFactory.php

+13-34
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,11 @@
1111

1212
namespace Symfony\UX\LiveComponent\Metadata;
1313

14-
use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface;
15-
use Symfony\Component\PropertyInfo\Type;
14+
use Symfony\Component\TypeInfo\Exception\UnsupportedException;
15+
use Symfony\Component\TypeInfo\Type\IntersectionType;
16+
use Symfony\Component\TypeInfo\Type\NullableType;
17+
use Symfony\Component\TypeInfo\Type\UnionType;
18+
use Symfony\Component\TypeInfo\TypeResolver\TypeResolverInterface;
1619
use Symfony\Contracts\Service\ResetInterface;
1720
use Symfony\UX\LiveComponent\Attribute\LiveProp;
1821
use Symfony\UX\TwigComponent\ComponentFactory;
@@ -29,7 +32,7 @@ class LiveComponentMetadataFactory implements ResetInterface
2932

3033
public function __construct(
3134
private ComponentFactory $componentFactory,
32-
private PropertyTypeExtractorInterface $propertyTypeExtractor,
35+
private TypeResolverInterface $typeResolver,
3336
) {
3437
}
3538

@@ -74,41 +77,17 @@ public function createPropMetadatas(\ReflectionClass $class): array
7477

7578
public function createLivePropMetadata(string $className, string $propertyName, \ReflectionProperty $property, LiveProp $liveProp): LivePropMetadata
7679
{
77-
$type = $property->getType();
78-
if ($type instanceof \ReflectionUnionType || $type instanceof \ReflectionIntersectionType) {
79-
throw new \LogicException(\sprintf('Union or intersection types are not supported for LiveProps. You may want to change the type of property %s in %s.', $property->getName(), $property->getDeclaringClass()->getName()));
80+
try {
81+
$type = $this->typeResolver->resolve($property);
82+
} catch (UnsupportedException) {
83+
$type = null;
8084
}
8185

82-
$infoTypes = $this->propertyTypeExtractor->getTypes($className, $propertyName) ?? [];
83-
84-
$collectionValueType = null;
85-
foreach ($infoTypes as $infoType) {
86-
if ($infoType->isCollection()) {
87-
foreach ($infoType->getCollectionValueTypes() as $valueType) {
88-
$collectionValueType = $valueType;
89-
break;
90-
}
91-
}
92-
}
93-
94-
if (null === $type && null === $collectionValueType && isset($infoTypes[0])) {
95-
$infoType = Type::BUILTIN_TYPE_OBJECT === $infoTypes[0]->getBuiltinType() ? $infoTypes[0]->getClassName() : $infoTypes[0]->getBuiltinType();
96-
$isTypeBuiltIn = null === $infoTypes[0]->getClassName();
97-
$isTypeNullable = $infoTypes[0]->isNullable();
98-
} else {
99-
$infoType = $type?->getName();
100-
$isTypeBuiltIn = $type?->isBuiltin() ?? false;
101-
$isTypeNullable = $type?->allowsNull() ?? true;
86+
if ($type instanceof UnionType && !$type instanceof NullableType || $type instanceof IntersectionType) {
87+
throw new \LogicException(\sprintf('Union or intersection types are not supported for LiveProps. You may want to change the type of property "%s" in "%s".', $propertyName, $className));
10288
}
10389

104-
return new LivePropMetadata(
105-
$property->getName(),
106-
$liveProp,
107-
$infoType,
108-
$isTypeBuiltIn,
109-
$isTypeNullable,
110-
$collectionValueType
111-
);
90+
return new LivePropMetadata($property->getName(), $liveProp, $type);
11291
}
11392

11493
/**

src/LiveComponent/src/Metadata/LivePropMetadata.php

+4-22
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
namespace Symfony\UX\LiveComponent\Metadata;
1313

14-
use Symfony\Component\PropertyInfo\Type;
14+
use Symfony\Component\TypeInfo\Type;
1515
use Symfony\UX\LiveComponent\Attribute\LiveProp;
1616

1717
/**
@@ -24,10 +24,7 @@ final class LivePropMetadata
2424
public function __construct(
2525
private string $name,
2626
private LiveProp $liveProp,
27-
private ?string $typeName,
28-
private bool $isBuiltIn,
29-
private bool $allowsNull,
30-
private ?Type $collectionValueType,
27+
private ?Type $type,
3128
) {
3229
}
3330

@@ -36,19 +33,9 @@ public function getName(): string
3633
return $this->name;
3734
}
3835

39-
public function getType(): ?string
36+
public function getType(): ?Type
4037
{
41-
return $this->typeName;
42-
}
43-
44-
public function isBuiltIn(): bool
45-
{
46-
return $this->isBuiltIn;
47-
}
48-
49-
public function allowsNull(): bool
50-
{
51-
return $this->allowsNull;
38+
return $this->type;
5239
}
5340

5441
public function urlMapping(): ?UrlMapping
@@ -99,11 +86,6 @@ public function serializationContext(): array
9986
return $this->liveProp->serializationContext();
10087
}
10188

102-
public function collectionValueType(): ?Type
103-
{
104-
return $this->collectionValueType;
105-
}
106-
10789
public function getFormat(): ?string
10890
{
10991
return $this->liveProp->format();

0 commit comments

Comments
 (0)