Skip to content

[Live] migrate Hydration system to standard Symfony normalizers #263

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Feb 14, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/LiveComponent/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,16 @@
- Ability to send live action arguments to backend

- [BC BREAK] Remove `init_live_component()` twig function, use `{{ attributes }}` instead:

```diff
- <div {{ init_live_component() }}>
+ <div {{ attributes }}>
```

- [BC BREAK] Replace property hydration system with `symfony/serializer` normalizers. This
is a BC break if you've created custom hydrators. They'll need to be converted to
normalizers.

## 2.0.0

- Support for `stimulus` version 2 was removed and support for `@hotwired/stimulus`
Expand Down
2 changes: 1 addition & 1 deletion src/LiveComponent/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
},
"require": {
"php": ">=8.0",
"symfony/serializer": "^5.4|^6.0",
"symfony/ux-twig-component": "^2.1"
},
"require-dev": {
Expand All @@ -38,7 +39,6 @@
"symfony/framework-bundle": "^5.4|^6.0",
"symfony/phpunit-bridge": "^6.0",
"symfony/security-csrf": "^5.4|^6.0",
"symfony/serializer": "^5.4|^6.0",
"symfony/twig-bundle": "^5.4|^6.0",
"symfony/validator": "^5.4|^6.0",
"zenstruck/browser": "^0.9.1",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@

namespace Symfony\UX\LiveComponent\DependencyInjection\Compiler;

use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\UX\LiveComponent\Hydrator\DoctrineEntityPropertyHydrator;
use Symfony\UX\LiveComponent\Hydrator\NormalizerBridgePropertyHydrator;
use Symfony\UX\LiveComponent\Normalizer\DoctrineObjectNormalizer;

/**
* @author Kevin Bond <[email protected]>
Expand All @@ -16,16 +16,9 @@ final class OptionalDependencyPass implements CompilerPassInterface
public function process(ContainerBuilder $container): void
{
if ($container->hasDefinition('doctrine')) {
$container->register('ux.live_component.doctrine_entity_property_hydrator', DoctrineEntityPropertyHydrator::class)
->setArguments([[new Reference('doctrine')]])
->addTag('twig.component.property_hydrator', ['priority' => -100])
;
}

if ($container->hasDefinition('serializer')) {
$container->register('ux.live_component.serializer_property_hydrator', NormalizerBridgePropertyHydrator::class)
->setArguments([new Reference('serializer')])
->addTag('twig.component.property_hydrator', ['priority' => -200])
$container->register('ux.live_component.doctrine_object_normalizer', DoctrineObjectNormalizer::class)
->setArguments([new IteratorArgument([new Reference('doctrine')])]) // todo add other object managers (mongo)
->addTag('serializer.normalizer', ['priority' => 100])
;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@

namespace Symfony\UX\LiveComponent\DependencyInjection;

use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument;
use Symfony\Component\DependencyInjection\ChildDefinition;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Extension\Extension;
Expand All @@ -22,7 +21,6 @@
use Symfony\UX\LiveComponent\EventListener\AddLiveAttributesSubscriber;
use Symfony\UX\LiveComponent\EventListener\LiveComponentSubscriber;
use Symfony\UX\LiveComponent\LiveComponentHydrator;
use Symfony\UX\LiveComponent\PropertyHydratorInterface;
use Symfony\UX\LiveComponent\Twig\LiveComponentExtension as LiveComponentTwigExtension;
use Symfony\UX\LiveComponent\Twig\LiveComponentRuntime;
use Symfony\UX\TwigComponent\ComponentFactory;
Expand Down Expand Up @@ -52,13 +50,9 @@ function (ChildDefinition $definition, AsLiveComponent $attribute) {
}
);

$container->registerForAutoconfiguration(PropertyHydratorInterface::class)
->addTag('twig.component.property_hydrator')
;

$container->register('ux.live_component.component_hydrator', LiveComponentHydrator::class)
->setArguments([
new TaggedIteratorArgument('twig.component.property_hydrator'),
new Reference('serializer'),
new Reference('property_accessor'),
'%kernel.secret%',
])
Expand Down
75 changes: 0 additions & 75 deletions src/LiveComponent/src/Hydrator/DoctrineEntityPropertyHydrator.php

This file was deleted.

This file was deleted.

4 changes: 3 additions & 1 deletion src/LiveComponent/src/LiveComponentBundle.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

namespace Symfony\UX\LiveComponent;

use Symfony\Component\DependencyInjection\Compiler\PassConfig;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\Bundle\Bundle;
use Symfony\UX\LiveComponent\DependencyInjection\Compiler\OptionalDependencyPass;
Expand All @@ -24,6 +25,7 @@ final class LiveComponentBundle extends Bundle
{
public function build(ContainerBuilder $container): void
{
$container->addCompilerPass(new OptionalDependencyPass());
// must run before Symfony\Component\Serializer\DependencyInjection\SerializerPass
$container->addCompilerPass(new OptionalDependencyPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, 100);
}
}
88 changes: 17 additions & 71 deletions src/LiveComponent/src/LiveComponentHydrator.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,10 @@
use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;
use Symfony\Component\PropertyAccess\Exception\UnexpectedTypeException;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
use Symfony\UX\LiveComponent\Attribute\LivePropContext;
use Symfony\UX\LiveComponent\Exception\UnsupportedHydrationException;
use Symfony\UX\TwigComponent\ComponentAttributes;
use Symfony\UX\TwigComponent\MountedComponent;

Expand All @@ -29,23 +30,16 @@
*/
final class LiveComponentHydrator
{
public const LIVE_CONTEXT = 'live-component';
private const CHECKSUM_KEY = '_checksum';
private const EXPOSED_PROP_KEY = '_id';
private const ATTRIBUTES_KEY = '_attributes';

/** @var PropertyHydratorInterface[] */
private iterable $propertyHydrators;
private PropertyAccessorInterface $propertyAccessor;
private string $secret;

/**
* @param PropertyHydratorInterface[] $propertyHydrators
*/
public function __construct(iterable $propertyHydrators, PropertyAccessorInterface $propertyAccessor, string $secret)
{
$this->propertyHydrators = $propertyHydrators;
$this->propertyAccessor = $propertyAccessor;
$this->secret = $secret;
public function __construct(
private NormalizerInterface|DenormalizerInterface $normalizer,
private PropertyAccessorInterface $propertyAccessor,
private string $secret
) {
}

public function dehydrate(MountedComponent $mounted): array
Expand Down Expand Up @@ -75,6 +69,7 @@ public function dehydrate(MountedComponent $mounted): array

throw new \LogicException($message);
}

$frontendPropertyNames[$frontendName] = $name;

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

// TODO: improve error message if not readable
$value = $this->propertyAccessor->getValue($component, $name);

$dehydratedValue = null;

if ($method = $liveProp->dehydrateMethod()) {
// TODO: Error checking
$dehydratedValue = $component->$method($value);
} else {
$dehydratedValue = $this->dehydrateProperty($value, $name, $component);
} elseif (\is_object($dehydratedValue = $value)) {
$dehydratedValue = $this->normalizer->normalize($dehydratedValue, 'json', [self::LIVE_CONTEXT => true]);
}

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

$value = $dehydratedValue;

if ($method = $liveProp->hydrateMethod()) {
// TODO: Error checking
$value = $component->$method($dehydratedValue);
} else {
$value = $this->hydrateProperty($property, $dehydratedValue);
} elseif ($property->getType() instanceof \ReflectionNamedType && !$property->getType()->isBuiltin()) {
$value = $this->normalizer->denormalize($value, $property->getType()->getName(), 'json', [self::LIVE_CONTEXT => true]);
}

foreach ($liveProp->exposed() as $exposedProperty) {
Expand Down Expand Up @@ -237,57 +234,6 @@ private function verifyChecksum(array $data, array $readonlyProperties): void
}
}

/**
* @param scalar|array|null $value
*
* @return mixed
*/
private function hydrateProperty(\ReflectionProperty $property, $value)
{
if (!$property->getType() || !$property->getType() instanceof \ReflectionNamedType || $property->getType()->isBuiltin()) {
return $value;
}

foreach ($this->propertyHydrators as $hydrator) {
try {
return $hydrator->hydrate($property->getType()->getName(), $value);
} catch (UnsupportedHydrationException $e) {
continue;
}
}

return $value;
}

/**
* @param mixed $value
*
* @return scalar|array|null
*/
private function dehydrateProperty($value, string $name, object $component)
{
if (is_scalar($value) || \is_array($value) || null === $value) {
// nothing to dehydrate...
return $value;
}

foreach ($this->propertyHydrators as $hydrator) {
try {
$value = $hydrator->dehydrate($value);

break;
} catch (UnsupportedHydrationException $e) {
continue;
}
}

if (!is_scalar($value) && !\is_array($value) && null !== $value) {
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)));
}

return $value;
}

/**
* Transforms a path like `post.name` into `[post][name]`.
*
Expand Down
Loading