Skip to content

Commit 93aaa37

Browse files
committed
[WIP][Live] migrate Hydration system to standard Symfony normalizers
1 parent b355eb9 commit 93aaa37

10 files changed

+121
-265
lines changed

src/LiveComponent/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@
2626
+ <div {{ attributes }}>
2727
```
2828

29+
- [BC BREAK] Replace property hydration system with `symfony/serializer` normalizers. This
30+
is a BC break if you've created custom hydrators. They'll need to be converted to
31+
normalizers.
32+
2933
## 2.0.0
3034

3135
- Support for `stimulus` version 2 was removed and support for `@hotwired/stimulus`

src/LiveComponent/composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
},
2828
"require": {
2929
"php": ">=8.0",
30+
"symfony/serializer": "^5.4|^6.0",
3031
"symfony/ux-twig-component": "^2.1"
3132
},
3233
"require-dev": {
@@ -38,7 +39,6 @@
3839
"symfony/framework-bundle": "^5.4|^6.0",
3940
"symfony/phpunit-bridge": "^6.0",
4041
"symfony/security-csrf": "^5.4|^6.0",
41-
"symfony/serializer": "^5.4|^6.0",
4242
"symfony/twig-bundle": "^5.4|^6.0",
4343
"symfony/validator": "^5.4|^6.0",
4444
"zenstruck/browser": "^0.9.1",

src/LiveComponent/src/DependencyInjection/Compiler/OptionalDependencyPass.php

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@
22

33
namespace Symfony\UX\LiveComponent\DependencyInjection\Compiler;
44

5+
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
56
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
67
use Symfony\Component\DependencyInjection\ContainerBuilder;
78
use Symfony\Component\DependencyInjection\Reference;
8-
use Symfony\UX\LiveComponent\Hydrator\DoctrineEntityPropertyHydrator;
9-
use Symfony\UX\LiveComponent\Hydrator\NormalizerBridgePropertyHydrator;
9+
use Symfony\UX\LiveComponent\Normalizer\DoctrineObjectNormalizer;
1010

1111
/**
1212
* @author Kevin Bond <[email protected]>
@@ -16,16 +16,9 @@ final class OptionalDependencyPass implements CompilerPassInterface
1616
public function process(ContainerBuilder $container): void
1717
{
1818
if ($container->hasDefinition('doctrine')) {
19-
$container->register('ux.live_component.doctrine_entity_property_hydrator', DoctrineEntityPropertyHydrator::class)
20-
->setArguments([[new Reference('doctrine')]])
21-
->addTag('twig.component.property_hydrator', ['priority' => -100])
22-
;
23-
}
24-
25-
if ($container->hasDefinition('serializer')) {
26-
$container->register('ux.live_component.serializer_property_hydrator', NormalizerBridgePropertyHydrator::class)
27-
->setArguments([new Reference('serializer')])
28-
->addTag('twig.component.property_hydrator', ['priority' => -200])
19+
$container->register('ux.live_component.doctrine_object_normalizer', DoctrineObjectNormalizer::class)
20+
->setArguments([new IteratorArgument([new Reference('doctrine')])]) // todo add other object managers (mongo)
21+
->addTag('serializer.normalizer', ['priority' => 100])
2922
;
3023
}
3124
}

src/LiveComponent/src/DependencyInjection/LiveComponentExtension.php

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111

1212
namespace Symfony\UX\LiveComponent\DependencyInjection;
1313

14-
use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument;
1514
use Symfony\Component\DependencyInjection\ChildDefinition;
1615
use Symfony\Component\DependencyInjection\ContainerBuilder;
1716
use Symfony\Component\DependencyInjection\Extension\Extension;
@@ -22,7 +21,6 @@
2221
use Symfony\UX\LiveComponent\EventListener\AddLiveAttributesSubscriber;
2322
use Symfony\UX\LiveComponent\EventListener\LiveComponentSubscriber;
2423
use Symfony\UX\LiveComponent\LiveComponentHydrator;
25-
use Symfony\UX\LiveComponent\PropertyHydratorInterface;
2624
use Symfony\UX\LiveComponent\Twig\LiveComponentExtension as LiveComponentTwigExtension;
2725
use Symfony\UX\LiveComponent\Twig\LiveComponentRuntime;
2826
use Symfony\UX\TwigComponent\ComponentFactory;
@@ -52,13 +50,9 @@ function (ChildDefinition $definition, AsLiveComponent $attribute) {
5250
}
5351
);
5452

55-
$container->registerForAutoconfiguration(PropertyHydratorInterface::class)
56-
->addTag('twig.component.property_hydrator')
57-
;
58-
5953
$container->register('ux.live_component.component_hydrator', LiveComponentHydrator::class)
6054
->setArguments([
61-
new TaggedIteratorArgument('twig.component.property_hydrator'),
55+
new Reference('serializer'),
6256
new Reference('property_accessor'),
6357
'%kernel.secret%',
6458
])

src/LiveComponent/src/Hydrator/DoctrineEntityPropertyHydrator.php

Lines changed: 0 additions & 75 deletions
This file was deleted.

src/LiveComponent/src/Hydrator/NormalizerBridgePropertyHydrator.php

Lines changed: 0 additions & 58 deletions
This file was deleted.

src/LiveComponent/src/LiveComponentBundle.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
namespace Symfony\UX\LiveComponent;
1313

14+
use Symfony\Component\DependencyInjection\Compiler\PassConfig;
1415
use Symfony\Component\DependencyInjection\ContainerBuilder;
1516
use Symfony\Component\HttpKernel\Bundle\Bundle;
1617
use Symfony\UX\LiveComponent\DependencyInjection\Compiler\OptionalDependencyPass;
@@ -24,6 +25,7 @@ final class LiveComponentBundle extends Bundle
2425
{
2526
public function build(ContainerBuilder $container): void
2627
{
27-
$container->addCompilerPass(new OptionalDependencyPass());
28+
// must run before Symfony\Component\Serializer\DependencyInjection\SerializerPass
29+
$container->addCompilerPass(new OptionalDependencyPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, 100);
2830
}
2931
}

src/LiveComponent/src/LiveComponentHydrator.php

Lines changed: 17 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,10 @@
1414
use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;
1515
use Symfony\Component\PropertyAccess\Exception\UnexpectedTypeException;
1616
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
17+
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
18+
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
1719
use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
1820
use Symfony\UX\LiveComponent\Attribute\LivePropContext;
19-
use Symfony\UX\LiveComponent\Exception\UnsupportedHydrationException;
2021
use Symfony\UX\TwigComponent\ComponentAttributes;
2122
use Symfony\UX\TwigComponent\MountedComponent;
2223

@@ -29,23 +30,16 @@
2930
*/
3031
final class LiveComponentHydrator
3132
{
33+
public const LIVE_CONTEXT = 'live-component';
3234
private const CHECKSUM_KEY = '_checksum';
3335
private const EXPOSED_PROP_KEY = '_id';
3436
private const ATTRIBUTES_KEY = '_attributes';
3537

36-
/** @var PropertyHydratorInterface[] */
37-
private iterable $propertyHydrators;
38-
private PropertyAccessorInterface $propertyAccessor;
39-
private string $secret;
40-
41-
/**
42-
* @param PropertyHydratorInterface[] $propertyHydrators
43-
*/
44-
public function __construct(iterable $propertyHydrators, PropertyAccessorInterface $propertyAccessor, string $secret)
45-
{
46-
$this->propertyHydrators = $propertyHydrators;
47-
$this->propertyAccessor = $propertyAccessor;
48-
$this->secret = $secret;
38+
public function __construct(
39+
private NormalizerInterface|DenormalizerInterface $normalizer,
40+
private PropertyAccessorInterface $propertyAccessor,
41+
private string $secret
42+
) {
4943
}
5044

5145
public function dehydrate(MountedComponent $mounted): array
@@ -75,6 +69,7 @@ public function dehydrate(MountedComponent $mounted): array
7569

7670
throw new \LogicException($message);
7771
}
72+
7873
$frontendPropertyNames[$frontendName] = $name;
7974

8075
if ($liveProp->isReadonly()) {
@@ -83,13 +78,13 @@ public function dehydrate(MountedComponent $mounted): array
8378

8479
// TODO: improve error message if not readable
8580
$value = $this->propertyAccessor->getValue($component, $name);
86-
8781
$dehydratedValue = null;
82+
8883
if ($method = $liveProp->dehydrateMethod()) {
8984
// TODO: Error checking
9085
$dehydratedValue = $component->$method($value);
91-
} else {
92-
$dehydratedValue = $this->dehydrateProperty($value, $name, $component);
86+
} elseif (\is_object($dehydratedValue = $value)) {
87+
$dehydratedValue = $this->normalizer->normalize($dehydratedValue, 'json', [self::LIVE_CONTEXT => true]);
9388
}
9489

9590
if (\count($liveProp->exposed()) > 0) {
@@ -98,7 +93,7 @@ public function dehydrate(MountedComponent $mounted): array
9893
];
9994
foreach ($liveProp->exposed() as $propertyPath) {
10095
$value = $this->propertyAccessor->getValue($component, sprintf('%s.%s', $name, $propertyPath));
101-
$data[$frontendName][$propertyPath] = $this->dehydrateProperty($value, $propertyPath, $component);
96+
$data[$frontendName][$propertyPath] = \is_object($value) ? $this->normalizer->normalize($value, 'json', [self::LIVE_CONTEXT => true]) : $value;
10297
}
10398
} else {
10499
$data[$frontendName] = $dehydratedValue;
@@ -168,11 +163,13 @@ public function hydrate(object $component, array $data, string $componentName):
168163
unset($data[$frontendName][self::EXPOSED_PROP_KEY]);
169164
}
170165

166+
$value = $dehydratedValue;
167+
171168
if ($method = $liveProp->hydrateMethod()) {
172169
// TODO: Error checking
173170
$value = $component->$method($dehydratedValue);
174-
} else {
175-
$value = $this->hydrateProperty($property, $dehydratedValue);
171+
} elseif ($property->getType() instanceof \ReflectionNamedType && !$property->getType()->isBuiltin()) {
172+
$value = $this->normalizer->denormalize($value, $property->getType()->getName(), 'json', [self::LIVE_CONTEXT => true]);
176173
}
177174

178175
foreach ($liveProp->exposed() as $exposedProperty) {
@@ -237,57 +234,6 @@ private function verifyChecksum(array $data, array $readonlyProperties): void
237234
}
238235
}
239236

240-
/**
241-
* @param scalar|array|null $value
242-
*
243-
* @return mixed
244-
*/
245-
private function hydrateProperty(\ReflectionProperty $property, $value)
246-
{
247-
if (!$property->getType() || !$property->getType() instanceof \ReflectionNamedType || $property->getType()->isBuiltin()) {
248-
return $value;
249-
}
250-
251-
foreach ($this->propertyHydrators as $hydrator) {
252-
try {
253-
return $hydrator->hydrate($property->getType()->getName(), $value);
254-
} catch (UnsupportedHydrationException $e) {
255-
continue;
256-
}
257-
}
258-
259-
return $value;
260-
}
261-
262-
/**
263-
* @param mixed $value
264-
*
265-
* @return scalar|array|null
266-
*/
267-
private function dehydrateProperty($value, string $name, object $component)
268-
{
269-
if (is_scalar($value) || \is_array($value) || null === $value) {
270-
// nothing to dehydrate...
271-
return $value;
272-
}
273-
274-
foreach ($this->propertyHydrators as $hydrator) {
275-
try {
276-
$value = $hydrator->dehydrate($value);
277-
278-
break;
279-
} catch (UnsupportedHydrationException $e) {
280-
continue;
281-
}
282-
}
283-
284-
if (!is_scalar($value) && !\is_array($value) && null !== $value) {
285-
throw new \LogicException(sprintf('Cannot dehydrate property "%s" of "%s". The value "%s" does not have a dehydrator.', $name, \get_class($component), get_debug_type($value)));
286-
}
287-
288-
return $value;
289-
}
290-
291237
/**
292238
* Transforms a path like `post.name` into `[post][name]`.
293239
*

0 commit comments

Comments
 (0)