diff --git a/src/LiveComponent/src/DependencyInjection/LiveComponentExtension.php b/src/LiveComponent/src/DependencyInjection/LiveComponentExtension.php index ffe6c88b2fd..6934f0ff76e 100644 --- a/src/LiveComponent/src/DependencyInjection/LiveComponentExtension.php +++ b/src/LiveComponent/src/DependencyInjection/LiveComponentExtension.php @@ -141,6 +141,7 @@ function (ChildDefinition $definition, AsLiveComponent $attribute) { $container->register('ux.live_component.live_url_subscriber', LiveUrlSubscriber::class) ->setArguments([ new Reference('ux.live_component.metadata_factory'), + new Reference('ux.live_component.component_hydrator'), new Reference('ux.live_component.url_factory'), ]) ->addTag('kernel.event_subscriber') diff --git a/src/LiveComponent/src/EventListener/LiveUrlSubscriber.php b/src/LiveComponent/src/EventListener/LiveUrlSubscriber.php index df959f82d53..c7193a59bb9 100644 --- a/src/LiveComponent/src/EventListener/LiveUrlSubscriber.php +++ b/src/LiveComponent/src/EventListener/LiveUrlSubscriber.php @@ -12,11 +12,12 @@ namespace Symfony\UX\LiveComponent\EventListener; use Symfony\Component\EventDispatcher\EventSubscriberInterface; -use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Event\ResponseEvent; use Symfony\Component\HttpKernel\KernelEvents; +use Symfony\UX\LiveComponent\LiveComponentHydrator; use Symfony\UX\LiveComponent\Metadata\LiveComponentMetadataFactory; use Symfony\UX\LiveComponent\Util\UrlFactory; +use Symfony\UX\TwigComponent\MountedComponent; /** * @internal @@ -27,6 +28,7 @@ class LiveUrlSubscriber implements EventSubscriberInterface public function __construct( private LiveComponentMetadataFactory $metadataFactory, + private LiveComponentHydrator $liveComponentHydrator, private UrlFactory $urlFactory, ) { } @@ -42,9 +44,14 @@ public function onKernelResponse(ResponseEvent $event): void return; } + if (!$request->attributes->has('_mounted_component')) { + return; + } + $newLiveUrl = null; if ($previousLiveUrl = $request->headers->get(self::URL_HEADER)) { - $liveProps = $this->getLivePropsFromRequest($request); + $mounted = $request->attributes->get('_mounted_component'); + $liveProps = $this->getLiveProps($mounted); $newLiveUrl = $this->urlFactory->createFromPreviousAndProps($previousLiveUrl, $liveProps['path'], $liveProps['query']); } @@ -66,26 +73,26 @@ public static function getSubscribedEvents(): array * query: array * } */ - private function getLivePropsFromRequest(Request $request): array + private function getLiveProps(MountedComponent $mounted): array { - $componentName = $request->attributes->get('_live_component'); - $metadata = $this->metadataFactory->getMetadata($componentName); - - $liveRequestData = $request->attributes->get('_live_request_data') ?? []; - $values = array_merge( - $liveRequestData['props'] ?? [], - $liveRequestData['updated'] ?? [], - $liveRequestData['responseProps'] ?? [] + $metadata = $this->metadataFactory->getMetadata($mounted->getName()); + + $dehydratedProps = $this->liveComponentHydrator->dehydrate( + $mounted->getComponent(), + $mounted->getAttributes(), + $metadata ); + $values = $dehydratedProps->getProps(); + $urlLiveProps = [ 'path' => [], 'query' => [], ]; - foreach ($metadata->getAllUrlMappings() as $name => $urlMapping) { + + foreach ($metadata->getAllUrlMappings($mounted->getComponent()) as $name => $urlMapping) { if (isset($values[$name]) && $urlMapping) { - $urlLiveProps[$urlMapping->mapPath ? 'path' : 'query'][$urlMapping->as ?? $name] = - $values[$name]; + $urlLiveProps[$urlMapping->mapPath ? 'path' : 'query'][$urlMapping->as ?? $name] = $values[$name]; } } diff --git a/src/LiveComponent/src/Metadata/LiveComponentMetadata.php b/src/LiveComponent/src/Metadata/LiveComponentMetadata.php index fb1ccd07b48..73dc4a7a2a1 100644 --- a/src/LiveComponent/src/Metadata/LiveComponentMetadata.php +++ b/src/LiveComponent/src/Metadata/LiveComponentMetadata.php @@ -69,10 +69,10 @@ public function getOnlyPropsThatAcceptUpdatesFromParent(array $inputProps): arra /** * @return UrlMapping[] */ - public function getAllUrlMappings(): iterable + public function getAllUrlMappings(object $component): iterable { $urlMappings = []; - foreach ($this->livePropsMetadata as $livePropMetadata) { + foreach ($this->getAllLivePropsMetadata($component) as $livePropMetadata) { if ($livePropMetadata->urlMapping()) { $urlMappings[$livePropMetadata->getName()] = $livePropMetadata->urlMapping(); } @@ -81,7 +81,7 @@ public function getAllUrlMappings(): iterable return $urlMappings; } - public function hasQueryStringBindings($component): bool + public function hasQueryStringBindings(object $component): bool { foreach ($this->getAllLivePropsMetadata($component) as $livePropMetadata) { if ($livePropMetadata->urlMapping()) { diff --git a/src/LiveComponent/tests/Fixtures/Component/ComponentWithUrlBoundProps.php b/src/LiveComponent/tests/Fixtures/Component/ComponentWithUrlBoundProps.php index 29495d2ac7c..9bb965d40be 100644 --- a/src/LiveComponent/tests/Fixtures/Component/ComponentWithUrlBoundProps.php +++ b/src/LiveComponent/tests/Fixtures/Component/ComponentWithUrlBoundProps.php @@ -12,6 +12,8 @@ namespace Symfony\UX\LiveComponent\Tests\Fixtures\Component; use Symfony\UX\LiveComponent\Attribute\AsLiveComponent; +use Symfony\UX\LiveComponent\Attribute\LiveAction; +use Symfony\UX\LiveComponent\Attribute\LiveArg; use Symfony\UX\LiveComponent\Attribute\LiveProp; use Symfony\UX\LiveComponent\DefaultActionTrait; use Symfony\UX\LiveComponent\Metadata\UrlMapping; @@ -37,6 +39,9 @@ class ComponentWithUrlBoundProps #[LiveProp(url: true)] public ?Address $objectProp = null; + #[LiveProp(url: true, useSerializerForHydration: true)] + public ?Address $objectPropWithSerializerForHydration = null; + #[LiveProp(fieldName: 'field1', url: true)] public ?string $propWithField1 = null; @@ -49,6 +54,19 @@ class ComponentWithUrlBoundProps #[LiveProp] public ?bool $maybeBoundPropInUrl = false; + #[LiveProp(url: new UrlMapping(as: 'p'), modifier: 'modifyPropertyWithModifierAndAlias')] + public ?string $propertyWithModifierAndAlias = null; + + public function modifyPropertyWithModifierAndAlias(LiveProp $liveProp): LiveProp + { + $urlMapping = $liveProp->url(); + if (!$urlMapping instanceof UrlMapping) { + return $liveProp; + } + + return $liveProp->withUrl(new UrlMapping(as: 'alias_' . $urlMapping->as)); + } + public function getField2(): string { return 'field2'; @@ -83,5 +101,13 @@ public function modifyBoundPropWithCustomAlias(LiveProp $liveProp): LiveProp return $liveProp; } + #[LiveAction] + public function updateLiveProp(#[LiveArg] string $propName, #[LiveArg] mixed $propValue): void + { + if (!property_exists($this, $propName)) { + throw new \InvalidArgumentException(\sprintf('Property "%s" does not exist on component "%s".', $propName, static::class)); + } + $this->{$propName} = $propValue; + } } diff --git a/src/LiveComponent/tests/Fixtures/Dto/Address.php b/src/LiveComponent/tests/Fixtures/Dto/Address.php index 8947915344d..6e6c8e37df4 100644 --- a/src/LiveComponent/tests/Fixtures/Dto/Address.php +++ b/src/LiveComponent/tests/Fixtures/Dto/Address.php @@ -2,8 +2,12 @@ namespace Symfony\UX\LiveComponent\Tests\Fixtures\Dto; +use Symfony\Component\Serializer\Attribute\SerializedName; + class Address { public string $address; + + #[SerializedName(serializedName: 'c')] public string $city; -} \ No newline at end of file +} diff --git a/src/LiveComponent/tests/Functional/EventListener/AddLiveAttributesSubscriberTest.php b/src/LiveComponent/tests/Functional/EventListener/AddLiveAttributesSubscriberTest.php index 8672af6a63b..f76cc2e697e 100644 --- a/src/LiveComponent/tests/Functional/EventListener/AddLiveAttributesSubscriberTest.php +++ b/src/LiveComponent/tests/Functional/EventListener/AddLiveAttributesSubscriberTest.php @@ -141,6 +141,8 @@ public function testQueryStringMappingAttribute() 'boundPropWithCustomAlias' => ['name' => 'customAlias'], 'pathProp' => ['name' => 'pathProp'], 'pathPropWithAlias' => ['name' => 'pathAlias'], + 'objectPropWithSerializerForHydration' => ['name' => 'objectPropWithSerializerForHydration'], + 'propertyWithModifierAndAlias' => ['name' => 'alias_p'], ]; $this->assertEquals($expected, $queryMapping); diff --git a/src/LiveComponent/tests/Functional/EventListener/LiveUrlSubscriberTest.php b/src/LiveComponent/tests/Functional/EventListener/LiveUrlSubscriberTest.php index a705ffa6ab0..c64b0b5a0c1 100644 --- a/src/LiveComponent/tests/Functional/EventListener/LiveUrlSubscriberTest.php +++ b/src/LiveComponent/tests/Functional/EventListener/LiveUrlSubscriberTest.php @@ -12,6 +12,7 @@ namespace Symfony\UX\LiveComponent\Tests\Functional\EventListener; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; +use Symfony\UX\LiveComponent\Tests\Fixtures\Dto\Address; use Symfony\UX\LiveComponent\Tests\LiveComponentTestHelper; use Zenstruck\Browser\Test\HasBrowser; @@ -26,17 +27,20 @@ public function getTestData(): iterable 'previousLocation' => null, 'expectedLocation' => null, 'props' => [], + 'args' => [], ]; yield 'unknown_previous_location' => [ 'previousLocation' => 'foo/bar', 'expectedLocation' => null, 'props' => [], + 'args' => [], ]; yield 'no_prop' => [ 'previousLocation' => '/route_with_prop/foo', 'expectedLocation' => null, 'props' => [], + 'args' => [], ]; yield 'no_change' => [ @@ -45,39 +49,103 @@ public function getTestData(): iterable 'props' => [ 'pathProp' => 'foo', ], + 'args' => [], ]; - yield 'prop_changed' => [ + yield 'path_prop_changed' => [ 'previousLocation' => '/route_with_prop/foo', 'expectedLocation' => '/route_with_prop/bar', 'props' => [ 'pathProp' => 'foo', ], - 'updated' => [ - 'pathProp' => 'bar', + 'args' => [ + 'propName' => 'pathProp', + 'propValue' => 'bar', ], ]; - yield 'alias_prop_changed' => [ + yield 'path_alias_prop_changed' => [ 'previousLocation' => '/route_with_alias_prop/foo', 'expectedLocation' => '/route_with_alias_prop/bar', 'props' => [ 'pathPropWithAlias' => 'foo', ], - 'updated' => [ - 'pathPropWithAlias' => 'bar', + 'args' => [ + 'propName' => 'pathPropWithAlias', + 'propValue' => 'bar', ], ]; + + yield 'query_alias_prop_changed' => [ + 'previousLocation' => '/', + 'expectedLocation' => '/?q=search%2Bterm', + 'props' => [], + 'args' => [ + 'propName' => 'boundPropWithAlias', + 'propValue' => 'search+term', + ], + ]; + + yield 'path_and_query_alias_prop_changed' => [ + 'previousLocation' => '/route_with_prop/foo', + 'expectedLocation' => '/route_with_prop/baz?q=foo+bar', + 'props' => [ + 'pathProp' => 'baz', + ], + 'args' => [ + 'propName' => 'boundPropWithAlias', + 'propValue' => 'foo bar', + ], + ]; + + $address = new Address(); + $address->address = '123 Main St'; + $address->city = 'Anytown'; + yield 'with an object in query, keys "address" and "city" must be present' => [ + 'previousLocation' => '/', + 'expectedLocation' => '/?objectProp%5Baddress%5D=123+Main+St&objectProp%5Bcity%5D=Anytown&q=search', + 'props' => [ + 'objectProp' => $address, + ], + 'args' => [ + 'propName' => 'boundPropWithAlias', + 'propValue' => 'search', + ], + ]; + + $address = new Address(); + $address->address = '123 Main St'; + $address->city = 'Anytown'; + yield 'with an object in query, with "useSerializerForHydration: true", keys "address" and "c" must be present' => [ + 'previousLocation' => '/', + 'expectedLocation' => '/?intProp=3&objectPropWithSerializerForHydration%5Baddress%5D=123+Main+St&objectPropWithSerializerForHydration%5Bc%5D=Anytown', + 'props' => [ + 'objectPropWithSerializerForHydration' => $address, + ], + 'args' => [ + 'propName' => 'intProp', + 'propValue' => '3', + ], + ]; + + yield 'query with alias ("p") and modifier (prefix by "alias_")' => [ + 'previousLocation' => '/', + 'expectedLocation' => '/?alias_p=test', + 'props' => [ + 'propertyWithModifierAndAlias' => 'test', + ], + 'args' => [], + ]; } /** * @dataProvider getTestData */ - public function testNoHeader( + public function testNewLiveUrlAfterLiveAction( ?string $previousLocation, ?string $expectedLocation, array $props, - array $updated = [], + array $args, ): void { $component = $this->mountComponent('component_with_url_bound_props', $props); $dehydrated = $this->dehydrateComponent($component); @@ -85,12 +153,14 @@ public function testNoHeader( $this->browser() ->throwExceptions() ->post( - '/_components/component_with_url_bound_props', + [] === $args + ? '/_components/component_with_url_bound_props' + : '/_components/component_with_url_bound_props/updateLiveProp', [ 'body' => [ 'data' => json_encode([ 'props' => $dehydrated->getProps(), - 'updated' => $updated, + 'args' => $args, ]), ], 'headers' => [ diff --git a/src/LiveComponent/tests/Integration/LiveComponentHydratorTest.php b/src/LiveComponent/tests/Integration/LiveComponentHydratorTest.php index 41bc8241fc2..c1fb3a978a3 100644 --- a/src/LiveComponent/tests/Integration/LiveComponentHydratorTest.php +++ b/src/LiveComponent/tests/Integration/LiveComponentHydratorTest.php @@ -451,7 +451,7 @@ public function onEntireEntityUpdated($oldValue) ; }]; - yield 'Persisted entity: with custom_normalizer and embeddable (de)hydrates correctly' => [function () { + yield 'Persisted entity: using serializer, with custom_normalizer and embeddable (de)hydrates correctly' => [function () { $entity2 = persist(Entity2::class, ['embedded1' => new Embeddable1('bar'), 'embedded2' => new Embeddable2('baz')]); return HydrationTest::create(new class { @@ -1001,7 +1001,7 @@ public function mount() }); }]; - yield 'Array with DTOs: fully writable allows anything to change' => [function () { + yield 'Array with DTOs: using serializer, fully writable allows anything to change' => [function () { $address1 = object(Address::class, ['address' => '17 Arcadia Road', 'city' => 'London']); $address2 = object(Address::class, ['address' => '4 Privet Drive', 'city' => 'Little Whinging']); $address3 = object(Address::class, ['address' => '124 Conch St.', 'city' => 'Bikini Bottom']); @@ -1018,11 +1018,11 @@ public function mount() ->assertDehydratesTo(['addresses' => [ [ 'address' => '17 Arcadia Road', - 'city' => 'London', + 'c' => 'London', ], [ 'address' => '4 Privet Drive', - 'city' => 'Little Whinging', + 'c' => 'Little Whinging', ], ]]) ->userUpdatesProps(['addresses' => [$address3, $address4]]) @@ -1042,7 +1042,7 @@ public function mount() /** * @var \Symfony\UX\LiveComponent\Tests\Fixtures\Dto\Address[] */ - #[LiveProp(writable: true, useSerializerForHydration: true)] + #[LiveProp(writable: true)] public array $addresses = []; }) ->mountWith(['addresses' => [$address1, $address2]]) @@ -1065,12 +1065,43 @@ public function mount() }); }]; + yield 'Array with DTOs: using serializer, fully writable allows partial changes' => [function () { + $address1 = object(Address::class, ['address' => '1600 Pennsylvania Avenue', 'city' => 'Washington DC']); + $address2 = object(Address::class, ['address' => '221 B Baker St', 'city' => 'Birmingham']); + + return HydrationTest::create(new class { + /** + * @var \Symfony\UX\LiveComponent\Tests\Fixtures\Dto\Address[] + */ + #[LiveProp(writable: true, useSerializerForHydration: true)] + public array $addresses = []; + }) + ->mountWith(['addresses' => [$address1, $address2]]) + ->assertDehydratesTo(['addresses' => [ + [ + 'address' => '1600 Pennsylvania Avenue', + 'c' => 'Washington DC', + ], + [ + 'address' => '221 B Baker St', + 'c' => 'Birmingham', + ], + ]]) + ->userUpdatesProps(['addresses.1.city' => 'London']) + ->assertObjectAfterHydration(function (object $object) { + self::assertEquals([ + object(Address::class, ['address' => '1600 Pennsylvania Avenue', 'city' => 'Washington DC']), + object(Address::class, ['address' => '221 B Baker St', 'city' => 'London']), + ], $object->addresses); + }); + }]; + yield 'Array with DTOs: fully writable allows deep partial changes' => [function () { return HydrationTest::create(new class { /** * @var \Symfony\UX\LiveComponent\Tests\Fixtures\Dto\HoldsArrayOfDtos[] $dtos */ - #[LiveProp(writable: true, useSerializerForHydration: true)] + #[LiveProp(writable: true)] public array $dtos = []; }) ->mountWith(['dtos' => [ @@ -1122,6 +1153,63 @@ public function mount() }); }]; + yield 'Array with DTOs: using serializer, fully writable allows deep partial changes' => [function () { + return HydrationTest::create(new class { + /** + * @var \Symfony\UX\LiveComponent\Tests\Fixtures\Dto\HoldsArrayOfDtos[] $dtos + */ + #[LiveProp(writable: true, useSerializerForHydration: true)] + public array $dtos = []; + }) + ->mountWith(['dtos' => [ + object(HoldsArrayOfDtos::class, ['addresses' => [ + object(Address::class, ['address' => '742 Evergreen Terrace', 'city' => 'Boston']), + object(Address::class, ['address' => 'Apartment 5A, 129 West 81st Street', 'city' => 'New York']), + object(Address::class, ['address' => '52 Festive Road', 'city' => 'London']), + ]]), + object(HoldsArrayOfDtos::class, ['addresses' => [ + object(Address::class, ['address' => '698 Sycamore Road', 'city' => 'San Pueblo']), + object(Address::class, ['address' => 'Madison Square Garden', 'city' => 'Chicago']), + ]]), + ]]) + ->assertDehydratesTo(['dtos' => [ + [ + 'addresses' => [ + ['address' => '742 Evergreen Terrace', 'c' => 'Boston'], + ['address' => 'Apartment 5A, 129 West 81st Street', 'c' => 'New York'], + ['address' => '52 Festive Road', 'c' => 'London'], + ], + ], + [ + 'addresses' => [ + ['address' => '698 Sycamore Road', 'c' => 'San Pueblo'], + ['address' => 'Madison Square Garden', 'c' => 'Chicago'], + ], + ], + ]]) + ->userUpdatesProps([ + 'dtos.0.addresses.0.city' => 'Springfield', + 'dtos.1.addresses.1.address' => '1060 West Addison Street', + 'dtos.1.addresses.1' => object(Address::class, ['address' => '10 Downing Street', 'city' => 'London']), + ]) + ->assertObjectAfterHydration(function (object $object) { + self::assertEquals( + [ + object(HoldsArrayOfDtos::class, ['addresses' => [ + object(Address::class, ['address' => '742 Evergreen Terrace', 'city' => 'Springfield']), + object(Address::class, ['address' => 'Apartment 5A, 129 West 81st Street', 'city' => 'New York']), + object(Address::class, ['address' => '52 Festive Road', 'city' => 'London']), + ]]), + object(HoldsArrayOfDtos::class, ['addresses' => [ + object(Address::class, ['address' => '698 Sycamore Road', 'city' => 'San Pueblo']), + object(Address::class, ['address' => '10 Downing Street', 'city' => 'London']), + ]]), + ], + $object->dtos + ); + }); + }]; + yield 'Object: (de)hydrates nested objects with phpdoc typehints' => [function () { return HydrationTest::create(new class { #[LiveProp(writable: true)] @@ -1151,7 +1239,7 @@ public function mount() }); }]; - yield 'Object: using custom normalizer (de)hydrates correctly' => [function () { + yield 'Object: using serializer, using custom normalizer (de)hydrates correctly' => [function () { return HydrationTest::create(new class { #[LiveProp(useSerializerForHydration: true)] public Money $money; @@ -1167,7 +1255,7 @@ public function mount() ; }]; - yield 'Object: dehydrates to array works correctly' => [function () { + yield 'Object: using serializer dehydrates to array works correctly' => [function () { return HydrationTest::create(new class { #[LiveProp(useSerializerForHydration: true)] public Temperature $temperature; @@ -1252,7 +1340,7 @@ public function mount() ; }]; - yield 'Context: Pass (de)normalization context' => [function () { + yield 'Context: using serializer, pass (de)normalization context' => [function () { return HydrationTest::create(new class { #[LiveProp(serializationContext: ['groups' => 'foo'])] public string $name; diff --git a/src/LiveComponent/tests/Unit/EventListener/LiveUrlSubscriberTest.php b/src/LiveComponent/tests/Unit/EventListener/LiveUrlSubscriberTest.php deleted file mode 100644 index cf40ac37f57..00000000000 --- a/src/LiveComponent/tests/Unit/EventListener/LiveUrlSubscriberTest.php +++ /dev/null @@ -1,192 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\UX\LiveComponent\Tests\Unit\EventListener; - -use PHPUnit\Framework\TestCase; -use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\HttpKernel\Event\ResponseEvent; -use Symfony\Component\HttpKernel\HttpKernelInterface; -use Symfony\UX\LiveComponent\EventListener\LiveUrlSubscriber; -use Symfony\UX\LiveComponent\Metadata\LiveComponentMetadata; -use Symfony\UX\LiveComponent\Metadata\LiveComponentMetadataFactory; -use Symfony\UX\LiveComponent\Metadata\UrlMapping; -use Symfony\UX\LiveComponent\Util\UrlFactory; - -class LiveUrlSubscriberTest extends TestCase -{ - public function getIgnoreData(): iterable - { - yield 'not_a_live_component' => [ - 'attributes' => [], - 'requestType' => HttpKernelInterface::MAIN_REQUEST, - 'headers' => ['X-Live-Url' => '/foo/bar'], - ]; - yield 'not_main_request' => [ - 'attributes' => ['_live_component' => 'componentName'], - 'requestType' => HttpKernelInterface::SUB_REQUEST, - 'headers' => ['X-Live-Url' => '/foo/bar'], - ]; - yield 'no_previous_url' => [ - 'attributes' => ['_live_component' => 'componentName'], - 'requestType' => HttpKernelInterface::MAIN_REQUEST, - 'headers' => [], - ]; - } - - /** - * @dataProvider getIgnoreData - */ - public function testDoNothing( - array $attributes = ['_live_component' => 'componentName'], - int $requestType = HttpKernelInterface::MAIN_REQUEST, - array $headers = ['X-Live-Url' => '/foo/bar'], - ): void { - $request = new Request(); - $request->attributes->add($attributes); - $request->headers->add($headers); - $response = new Response(); - $event = new ResponseEvent( - $this->createMock(HttpKernelInterface::class), - $request, - $requestType, - $response - ); - - $metadataFactory = $this->createMock(LiveComponentMetadataFactory::class); - $metadataFactory->expects(self::never())->method('getMetadata'); - $urlFactory = $this->createMock(UrlFactory::class); - $urlFactory->expects(self::never())->method('createFromPreviousAndProps'); - $liveUrlSubscriber = new LiveUrlSubscriber($metadataFactory, $urlFactory); - - $liveUrlSubscriber->onKernelResponse($event); - $this->assertNull($response->headers->get('X-Live-Url')); - } - - public static function provideTestUrlFactoryReceivesPathAndQuertyPropsFromRequestData(): iterable - { - yield 'prop_without_matching_property' => [ - 'liveRequestData' => [ - 'props' => ['notMatchingProp' => 0], - ], - ]; - yield 'prop_matching_non_mapped_property' => [ - 'liveRequestData' => [ - 'props' => ['nonMappedProp' => 0], - ], - ]; - yield 'props_matching_query_mapped_properties' => [ - 'liveRequestData' => [ - 'props' => ['queryMappedProp1' => 1], - 'updated' => ['queryMappedProp2' => 2], - 'responseProps' => ['queryMappedProp3' => 3], - ], - 'expectedPathProps' => [], - 'expectedQueryProps' => [ - 'queryMappedProp1' => 1, - 'queryMappedProp2' => 2, - 'queryMappedProp3' => 3, - ], - ]; - yield 'props_matching_path_mapped_properties' => [ - 'liveRequestData' => [ - 'props' => ['pathMappedProp1' => 1], - 'updated' => ['pathMappedProp2' => 2], - 'responseProps' => ['pathMappedProp3' => 3], - ], - 'expectedPathProps' => [ - 'pathMappedProp1' => 1, - 'pathMappedProp2' => 2, - 'pathMappedProp3' => 3, - ], - 'expectedQueryProps' => [], - ]; - yield 'props_matching_properties_with_alias' => [ - 'liveRequestData' => [ - 'props' => ['pathMappedPropWithAlias' => 1, 'queryMappedPropWithAlias' => 2], - ], - 'expectedPathProps' => ['pathAlias' => 1], - 'expectedQueryProps' => ['queryAlias' => 2], - ]; - yield 'responseProps_have_highest_priority' => [ - 'liveRequestData' => [ - 'props' => ['queryMappedProp1' => 1], - 'updated' => ['queryMappedProp1' => 2], - 'responseProps' => ['queryMappedProp1' => 3], - ], - 'expectedPathProps' => [], - 'expectedQueryProps' => ['queryMappedProp1' => 3], - ]; - yield 'updated_have_second_priority' => [ - 'liveRequestData' => [ - 'props' => ['queryMappedProp1' => 1], - 'updated' => ['queryMappedProp1' => 2], - ], - 'expectedPathProps' => [], - 'expectedQueryProps' => ['queryMappedProp1' => 2], - ]; - } - - /** - * @dataProvider provideTestUrlFactoryReceivesPathAndQuertyPropsFromRequestData - */ - public function testUrlFactoryReceivesPathAndQuertyPropsFromRequestData( - array $liveRequestData, - array $expectedPathProps = [], - array $expectedQueryProps = [], - ): void { - $previousLocation = '/foo/bar'; - $newLocation = '/foo/baz'; - $componentName = 'componentName'; - $component = $this->createMock(\stdClass::class); - $metaData = $this->createMock(LiveComponentMetadata::class); - $metaData->expects(self::once()) - ->method('getAllUrlMappings') - ->willReturn([ - 'nonMappedProp' => false, - 'queryMappedProp1' => new UrlMapping(), - 'queryMappedProp2' => new UrlMapping(), - 'queryMappedProp3' => new UrlMapping(), - 'pathMappedProp1' => new UrlMapping(mapPath: true), - 'pathMappedProp2' => new UrlMapping(mapPath: true), - 'pathMappedProp3' => new UrlMapping(mapPath: true), - 'queryMappedPropWithAlias' => new UrlMapping(as: 'queryAlias'), - 'pathMappedPropWithAlias' => new UrlMapping(as: 'pathAlias', mapPath: true), - ]); - $request = new Request(); - $request->attributes->add([ - '_live_component' => $componentName, - '_mounted_component' => $component, - '_live_request_data' => $liveRequestData, - ]); - $request->headers->add(['X-Live-Url' => $previousLocation]); - $response = new Response(); - $event = new ResponseEvent( - $this->createMock(HttpKernelInterface::class), - $request, - HttpKernelInterface::MAIN_REQUEST, - $response - ); - - $metadataFactory = $this->createMock(LiveComponentMetadataFactory::class); - $metadataFactory->expects(self::once())->method('getMetadata')->with($componentName)->willReturn($metaData); - $urlFactory = $this->createMock(UrlFactory::class); - $liveUrlSubscriber = new LiveUrlSubscriber($metadataFactory, $urlFactory); - - $urlFactory->expects(self::once()) - ->method('createFromPreviousAndProps') - ->with($previousLocation, $expectedPathProps, $expectedQueryProps) - ->willReturn($newLocation); - $liveUrlSubscriber->onKernelResponse($event); - $this->assertEquals($newLocation, $response->headers->get('X-Live-Url')); - } -} diff --git a/src/LiveComponent/tests/Unit/Metadata/LiveComponentMetadataTest.php b/src/LiveComponent/tests/Unit/Metadata/LiveComponentMetadataTest.php index dd064e7a9be..a612bc1e337 100644 --- a/src/LiveComponent/tests/Unit/Metadata/LiveComponentMetadataTest.php +++ b/src/LiveComponent/tests/Unit/Metadata/LiveComponentMetadataTest.php @@ -48,7 +48,7 @@ public function testGetAllUrlMappings() new LivePropMetadata('aliasUrlMapping', new LiveProp(url: $aliasUrlMapping), null, false, false, null), ]; $liveComponentMetadata = new LiveComponentMetadata(new ComponentMetadata([]), $propMetadas); - $urlMappings = $liveComponentMetadata->getAllUrlMappings(); + $urlMappings = $liveComponentMetadata->getAllUrlMappings(new \stdClass()); $this->assertCount(2, $urlMappings); $this->assertInstanceOf(UrlMapping::class, $urlMappings['basicUrlMapping']); $this->assertEquals($aliasUrlMapping, $urlMappings['aliasUrlMapping']);