Skip to content

Commit

Permalink
Tests and centralize logic in LiveComponentMetadataFactory
Browse files Browse the repository at this point in the history
  • Loading branch information
matheo committed Sep 12, 2023
1 parent bd7e719 commit ca5dafb
Show file tree
Hide file tree
Showing 7 changed files with 165 additions and 62 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ function (ChildDefinition $definition, AsLiveComponent $attribute) {
->setArguments([
tagged_iterator(LiveComponentBundle::HYDRATION_EXTENSION_TAG),
new Reference('property_accessor'),
new Reference('property_info'),
new Reference('ux.live_component.metadata_factory'),
new Reference('serializer'),
'%kernel.secret%',
])
Expand Down
51 changes: 11 additions & 40 deletions src/LiveComponent/src/LiveComponentHydrator.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
use Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException;
use Symfony\Component\PropertyAccess\Exception\UninitializedPropertyException;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface;
use Symfony\Component\PropertyInfo\Type;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
Expand All @@ -25,6 +24,7 @@
use Symfony\UX\LiveComponent\Exception\HydrationException;
use Symfony\UX\LiveComponent\Hydration\HydrationExtensionInterface;
use Symfony\UX\LiveComponent\Metadata\LiveComponentMetadata;
use Symfony\UX\LiveComponent\Metadata\LiveComponentMetadataFactory;
use Symfony\UX\LiveComponent\Metadata\LivePropMetadata;
use Symfony\UX\LiveComponent\Util\DehydratedProps;
use Symfony\UX\TwigComponent\ComponentAttributes;
Expand All @@ -47,7 +47,7 @@ final class LiveComponentHydrator
public function __construct(
private iterable $hydrationExtensions,
private PropertyAccessorInterface $propertyAccessor,
private PropertyTypeExtractorInterface $propertyTypeExtractor,
private LiveComponentMetadataFactory $liveComponentMetadataFactory,
private NormalizerInterface|DenormalizerInterface $normalizer,
private string $secret
) {
Expand Down Expand Up @@ -388,14 +388,14 @@ private function dehydrateObjectValue(object $value, string $classType, ?string
}
}

$propertiesValues = [];
$hydratedObject = [];
foreach ((new \ReflectionClass($classType))->getProperties() as $property) {
$propertyValue = $this->propertyAccessor->getValue($value, $property->getName());
$propMetadata = $this->generateLivePropMetadata($classType, $property->getName());
$propertiesValues[$property->getName()] = $this->dehydrateValue($propertyValue, $propMetadata, $component);
$propMetadata = $this->liveComponentMetadataFactory->createLivePropMetadata($classType, $property->getName(), $property);
$hydratedObject[$property->getName()] = $this->dehydrateValue($propertyValue, $propMetadata, $component);
}

return $propertiesValues;
return $hydratedObject;
}

private function hydrateValue(mixed $value, LivePropMetadata $propMetadata, object $component): mixed
Expand Down Expand Up @@ -472,9 +472,11 @@ private function hydrateObjectValue(mixed $value, string $className, bool $allow

if (\is_array($value)) {
$object = new $className();
foreach ($value as $property => $propertyValue) {
$propMetadata = $this->generateLivePropMetadata($className, $property);
$this->propertyAccessor->setValue($object, $property, $this->hydrateValue($propertyValue, $propMetadata, $component));
foreach ($value as $propertyName => $propertyValue) {
$reflexionClass = new \ReflectionClass($className);
$property = $reflexionClass->getProperty($propertyName);
$propMetadata = $this->liveComponentMetadataFactory->createLivePropMetadata($className, $propertyName, $property);
$this->propertyAccessor->setValue($object, $propertyName, $this->hydrateValue($propertyValue, $propMetadata, $component));
}

return $object;
Expand Down Expand Up @@ -572,35 +574,4 @@ private function recursiveKeySort(array &$data): void
}
ksort($data);
}

private function generateLivePropMetadata(string $className, string $propertyName): LivePropMetadata
{
$reflexionClass = new \ReflectionClass($className);
$property = $reflexionClass->getProperty($propertyName);

$collectionValueType = null;
$infoTypes = $this->propertyTypeExtractor->getTypes($className, $propertyName) ?? [];
foreach ($infoTypes as $infoType) {
if ($infoType->isCollection()) {
foreach ($infoType->getCollectionValueTypes() as $valueType) {
$collectionValueType = $valueType;
break;
}
}
}

$type = $property->getType();
if ($type instanceof \ReflectionUnionType || $type instanceof \ReflectionIntersectionType) {
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()));
}

return new LivePropMetadata(
$property->getName(),
new LiveProp(true),
$type ? $type->getName() : null,
$type ? $type->isBuiltin() : false,
$type ? $type->allowsNull() : true,
$collectionValueType,
);
}
}
48 changes: 27 additions & 21 deletions src/LiveComponent/src/Metadata/LiveComponentMetadataFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -59,32 +59,38 @@ public function createPropMetadatas(\ReflectionClass $class): array
continue;
}

$collectionValueType = null;
$infoTypes = $this->propertyTypeExtractor->getTypes($class->getName(), $property->getName()) ?? [];
foreach ($infoTypes as $infoType) {
if ($infoType->isCollection()) {
foreach ($infoType->getCollectionValueTypes() as $valueType) {
$collectionValueType = $valueType;
break;
}
$metadatas[$property->getName()] = $this->createLivePropMetadata($class->getName(), $property->getName(), $property, $attribute);
}

return array_values($metadatas);
}

public function createLivePropMetadata(string $className, string $propertyName, \ReflectionProperty $property, \ReflectionAttribute $attribute = null): LivePropMetadata
{
$collectionValueType = null;
$infoTypes = $this->propertyTypeExtractor->getTypes($className, $propertyName) ?? [];
foreach ($infoTypes as $infoType) {
if ($infoType->isCollection()) {
foreach ($infoType->getCollectionValueTypes() as $valueType) {
$collectionValueType = $valueType;
break;
}
}
}

$type = $property->getType();
if ($type instanceof \ReflectionUnionType || $type instanceof \ReflectionIntersectionType) {
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()));
}
$metadatas[$property->getName()] = new LivePropMetadata(
$property->getName(),
$attribute->newInstance(),
$type ? $type->getName() : null,
$type ? $type->isBuiltin() : false,
$type ? $type->allowsNull() : true,
$collectionValueType,
);
$type = $property->getType();
if ($type instanceof \ReflectionUnionType || $type instanceof \ReflectionIntersectionType) {
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()));
}

return array_values($metadatas);
return new LivePropMetadata(
$property->getName(),
null !== $attribute ? $attribute->newInstance() : new LiveProp(true),
$type?->getName(),
$type && $type->isBuiltin(),
!$type || $type->allowsNull(),
$collectionValueType,
);
}

/**
Expand Down
9 changes: 9 additions & 0 deletions src/LiveComponent/tests/Fixtures/Dto/Address.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

namespace Symfony\UX\LiveComponent\Tests\Fixtures\Dto;

class Address
{
public string $address;
public string $city;
}
11 changes: 11 additions & 0 deletions src/LiveComponent/tests/Fixtures/Dto/CustomerDetails.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

namespace Symfony\UX\LiveComponent\Tests\Fixtures\Dto;

class CustomerDetails
{
public string $firstName;
public string $lastName;

public Address $address;
}
1 change: 1 addition & 0 deletions src/LiveComponent/tests/Fixtures/Kernel.php
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ protected function configureContainer(ContainerConfigurator $c): void
'secrets' => false,
'session' => ['storage_factory_id' => 'session.storage.factory.mock_file'],
'http_method_override' => false,
'property_info' => ['enabled' => true],
]);

$c->extension('twig', [
Expand Down
105 changes: 105 additions & 0 deletions src/LiveComponent/tests/Integration/LiveComponentHydratorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@
use Symfony\UX\LiveComponent\Metadata\LiveComponentMetadataFactory;
use Symfony\UX\LiveComponent\Tests\Fixtures\Component\Component2;
use Symfony\UX\LiveComponent\Tests\Fixtures\Component\Component3;
use Symfony\UX\LiveComponent\Tests\Fixtures\Dto\Address;
use Symfony\UX\LiveComponent\Tests\Fixtures\Dto\BlogPostWithSerializationContext;
use Symfony\UX\LiveComponent\Tests\Fixtures\Dto\CustomerDetails;
use Symfony\UX\LiveComponent\Tests\Fixtures\Dto\Embeddable2;
use Symfony\UX\LiveComponent\Tests\Fixtures\Dto\Money;
use Symfony\UX\LiveComponent\Tests\Fixtures\Dto\Temperature;
Expand Down Expand Up @@ -783,6 +785,109 @@ public function provideDehydrationHydrationTests(): iterable
;
}, 80100];

yield 'Object: (de)hydrates DTO correctly' => [function () {
return HydrationTest::create(new class() {
#[LiveProp(writable: true)]
public ?Address $address = null;

public function mount()
{
$this->address = new Address();
$this->address->address = '1 rue du Bac';
$this->address->city = 'Paris';
}
})
->mountWith([])
->assertDehydratesTo([
'address' => [
'address' => '1 rue du Bac',
'city' => 'Paris',
],
'@checksum' => '2P/KES0BbGw1KNZsOn4CSEpN1HJ1s...+bQPY=',
])
->userUpdatesProps(['address' => ['address' => '4 rue des lilas', 'city' => 'Asnieres']])
->assertObjectAfterHydration(function (object $object) {
$this->assertSame($object->address->address, '4 rue des lilas');
$this->assertSame($object->address->city, 'Asnieres');
})
;
}];

yield 'Object: (de)hydrates correctly multidementional DTO' => [function () {
return HydrationTest::create(new class() {
#[LiveProp(writable: true)]
public ?CustomerDetails $customerDetails = null;

public function mount()
{
$this->customerDetails = new CustomerDetails();
$this->customerDetails->lastName = 'Matheo';
$this->customerDetails->firstName = 'Daninos';
$this->customerDetails->address = new Address();
$this->customerDetails->address->address = '1 rue du Bac';
$this->customerDetails->address->city = 'Paris';
}
})
->mountWith([])
->assertDehydratesTo([
'customerDetails' => [
'lastName' => 'Matheo',
'firstName' => 'Daninos',
'address' => [
'address' => '1 rue du Bac',
'city' => 'Paris',
],
],
'@checksum' => 'tjeTtPH8xCyM2TIxP+FOnRakGHNBE...qQiVA=',
])
->userUpdatesProps(['customerDetails' => ['lastName' => 'Matheo', 'firstName' => 'Daninos', 'address' => ['address' => '3 rue du Bac', 'city' => 'Paris']]])
->assertObjectAfterHydration(function (object $object) {
$this->assertSame($object->customerDetails->address->address, '3 rue du Bac');
$this->assertSame($object->customerDetails->address->city, 'Paris');
});
}];

yield 'Object: (de)hydrates correctly array of DTO' => [function () {
return HydrationTest::create(new class() {
/**
* @var Symfony\UX\LiveComponent\Tests\Fixtures\Dto\CustomerDetails[] $customerDetailsCollection
*/
#[LiveProp(writable: true)]
public array $customerDetailsCollection = [];

public function mount()
{
$customerDetails = new CustomerDetails();
$customerDetails->lastName = 'Matheo';
$customerDetails->firstName = 'Daninos';
$customerDetails->address = new Address();
$customerDetails->address->address = '1 rue du Bac';
$customerDetails->address->city = 'Paris';

$this->customerDetailsCollection[] = $customerDetails;
}
})
->mountWith([])
->assertDehydratesTo([
'customerDetailsCollection' => [
[
'lastName' => 'Matheo',
'firstName' => 'Daninos',
'address' => [
'address' => '1 rue du Bac',
'city' => 'Paris',
],
],
],
'@checksum' => 'tjeTtPH8xCyM2TIxP+FOnRakGHNBE...qQiVA=',
])
->userUpdatesProps(['customerDetailsCollection' => [['lastName' => 'Matheo', 'firstName' => 'Daninos', 'address' => ['address' => '3 rue du Bac', 'city' => 'Paris']]]])
->assertObjectAfterHydration(function (object $object) {
$this->assertSame($object->customerDetailsCollection[0]->address->address, '3 rue du Bac');
$this->assertSame($object->customerDetailsCollection[0]->address->city, 'Paris');
});
}];

yield 'Object: using custom normalizer (de)hydrates correctly' => [function () {
return HydrationTest::create(new class() {
#[LiveProp(useSerializerForHydration: true)]
Expand Down

0 comments on commit ca5dafb

Please sign in to comment.