diff --git a/src/LiveComponent/src/LiveComponentHydrator.php b/src/LiveComponent/src/LiveComponentHydrator.php index df7baa2cfde..7fe27e3ca7d 100644 --- a/src/LiveComponent/src/LiveComponentHydrator.php +++ b/src/LiveComponent/src/LiveComponentHydrator.php @@ -222,6 +222,18 @@ public function hydrate(object $component, array $props, array $updatedProps, Li $dehydratedUpdatedProps, ); + if (0 < \count($updatedWritablePaths)) { + try { + $propertyValue = $this->hydrateValue( + $propertyValue, + $propMetadata, + $component, + ); + } catch (HydrationException $e) { + // swallow this: it's bad data from the user + } + } + try { $this->propertyAccessor->setValue($component, $propMetadata->getName(), $propertyValue); } catch (PropertyAccessExceptionInterface $exception) { diff --git a/src/LiveComponent/tests/Fixtures/Component/ComponentWithHydrateWithProps.php b/src/LiveComponent/tests/Fixtures/Component/ComponentWithHydrateWithProps.php new file mode 100644 index 00000000000..49ac2546280 --- /dev/null +++ b/src/LiveComponent/tests/Fixtures/Component/ComponentWithHydrateWithProps.php @@ -0,0 +1,41 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\UX\LiveComponent\Tests\Fixtures\Component; + +use Symfony\UX\LiveComponent\Attribute\AsLiveComponent; +use Symfony\UX\LiveComponent\Attribute\LiveProp; + +#[AsLiveComponent] +final class ComponentWithHydrateWithProps +{ + /** + * @var array + */ + #[LiveProp(writable: true, hydrateWith: 'hydrateIntegers')] + public array $integers = [ + 'one' => 1, + 'two' => 2, + 'three' => 3, + ]; + + /** + * @param array $data + * + * @return array + */ + public function hydrateIntegers(array $data): array + { + return array_map(intval(...), $data); + } +} diff --git a/src/LiveComponent/tests/Unit/LiveComponentHydratorTest.php b/src/LiveComponent/tests/Unit/LiveComponentHydratorTest.php index 618f26d34b5..1f296a1c422 100644 --- a/src/LiveComponent/tests/Unit/LiveComponentHydratorTest.php +++ b/src/LiveComponent/tests/Unit/LiveComponentHydratorTest.php @@ -12,6 +12,7 @@ namespace Symfony\UX\LiveComponent\Tests\Unit; use PHPUnit\Framework\TestCase; +use Symfony\Component\PropertyAccess\PropertyAccess; use Symfony\Component\PropertyAccess\PropertyAccessorInterface; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; @@ -20,9 +21,13 @@ use Symfony\UX\LiveComponent\Attribute\LiveProp; use Symfony\UX\LiveComponent\LiveComponentHydrator; use Symfony\UX\LiveComponent\Metadata\LegacyLivePropMetadata; +use Symfony\UX\LiveComponent\Metadata\LiveComponentMetadata; use Symfony\UX\LiveComponent\Metadata\LiveComponentMetadataFactory; use Symfony\UX\LiveComponent\Metadata\LivePropMetadata; +use Symfony\UX\LiveComponent\Tests\Fixtures\Component\ComponentWithHydrateWithProps; +use Symfony\UX\TwigComponent\ComponentMetadata; use Twig\Environment; +use Twig\Runtime\EscaperRuntime; final class LiveComponentHydratorTest extends TestCase { @@ -80,6 +85,76 @@ public function testItCanHydrateWithNullValues() self::assertNull($hydratedValue); } } + + public function testHydrateWithIsCalledWithNestedArrayAsProps() + { + $twigMock = $this->createMock(Environment::class); + $twigMock->method('getRuntime')->willReturn(new EscaperRuntime()); + $hydrator = new LiveComponentHydrator( + [], + PropertyAccess::createPropertyAccessor(), + $this->createMock(LiveComponentMetadataFactory::class), + new Serializer(normalizers: [new ObjectNormalizer()]), + 'foo', + $twigMock, + ); + $component = new ComponentWithHydrateWithProps(); + + // BC layer when "symfony/type-info" is not available + if (!class_exists(Type::class)) { + $hydrator->hydrate( + $component, + [ + '@checksum' => 'SNKH1tHUgwgWT8V+Z/A7z126JQ2dWGiG6xVmjx7FbeA=', + 'integers' => [ + 'one' => 1, + 'two' => 2, + 'three' => 3, + ], + ], + [ + 'integers.one' => '4', + 'integers.two' => '5', + 'integers.three' => '6', + ], + new LiveComponentMetadata( + new ComponentMetadata([]), + [ + new LegacyLivePropMetadata('integers', new LiveProp(writable: true, hydrateWith: 'hydrateIntegers'), typeName: 'array', isBuiltIn: true, allowsNull: false, collectionValueType: new \Symfony\Component\PropertyInfo\Type('int')), + ], + ), + ); + } else { + $hydrator->hydrate( + $component, + [ + '@checksum' => 'SNKH1tHUgwgWT8V+Z/A7z126JQ2dWGiG6xVmjx7FbeA=', + 'integers' => [ + 'one' => 1, + 'two' => 2, + 'three' => 3, + ], + ], + [ + 'integers.one' => '4', + 'integers.two' => '5', + 'integers.three' => '6', + ], + new LiveComponentMetadata( + new ComponentMetadata([]), + [ + new LivePropMetadata('integers', new LiveProp(writable: true, hydrateWith: 'hydrateIntegers'), Type::array(Type::int(), Type::string())), + ], + ), + ); + } + + self::assertSame([ + 'one' => 4, + 'two' => 5, + 'three' => 6, + ], $component->integers); + } } class Foo