Skip to content

Commit 07e73bf

Browse files
committed
[LiveComponent] Fix new URL calculation when having #[LiveProp] with useSerializerForHydration: true and #[SerializedName]
1 parent 484f7a6 commit 07e73bf

File tree

8 files changed

+207
-225
lines changed

8 files changed

+207
-225
lines changed

src/LiveComponent/src/DependencyInjection/LiveComponentExtension.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@ function (ChildDefinition $definition, AsLiveComponent $attribute) {
141141
$container->register('ux.live_component.live_url_subscriber', LiveUrlSubscriber::class)
142142
->setArguments([
143143
new Reference('ux.live_component.metadata_factory'),
144+
new Reference('ux.live_component.component_hydrator'),
144145
new Reference('ux.live_component.url_factory'),
145146
])
146147
->addTag('kernel.event_subscriber')

src/LiveComponent/src/EventListener/LiveUrlSubscriber.php

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,12 @@
1212
namespace Symfony\UX\LiveComponent\EventListener;
1313

1414
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
15-
use Symfony\Component\HttpFoundation\Request;
1615
use Symfony\Component\HttpKernel\Event\ResponseEvent;
1716
use Symfony\Component\HttpKernel\KernelEvents;
17+
use Symfony\UX\LiveComponent\LiveComponentHydrator;
1818
use Symfony\UX\LiveComponent\Metadata\LiveComponentMetadataFactory;
1919
use Symfony\UX\LiveComponent\Util\UrlFactory;
20+
use Symfony\UX\TwigComponent\MountedComponent;
2021

2122
/**
2223
* @internal
@@ -27,6 +28,7 @@ class LiveUrlSubscriber implements EventSubscriberInterface
2728

2829
public function __construct(
2930
private LiveComponentMetadataFactory $metadataFactory,
31+
private LiveComponentHydrator $liveComponentHydrator,
3032
private UrlFactory $urlFactory,
3133
) {
3234
}
@@ -42,9 +44,14 @@ public function onKernelResponse(ResponseEvent $event): void
4244
return;
4345
}
4446

47+
if (!$request->attributes->has('_mounted_component')) {
48+
return;
49+
}
50+
4551
$newLiveUrl = null;
4652
if ($previousLiveUrl = $request->headers->get(self::URL_HEADER)) {
47-
$liveProps = $this->getLivePropsFromRequest($request);
53+
$mounted = $request->attributes->get('_mounted_component');
54+
$liveProps = $this->getLiveProps($mounted);
4855
$newLiveUrl = $this->urlFactory->createFromPreviousAndProps($previousLiveUrl, $liveProps['path'], $liveProps['query']);
4956
}
5057

@@ -66,26 +73,26 @@ public static function getSubscribedEvents(): array
6673
* query: array<string, mixed>
6774
* }
6875
*/
69-
private function getLivePropsFromRequest(Request $request): array
76+
private function getLiveProps(MountedComponent $mounted): array
7077
{
71-
$componentName = $request->attributes->get('_live_component');
72-
$metadata = $this->metadataFactory->getMetadata($componentName);
73-
74-
$liveRequestData = $request->attributes->get('_live_request_data') ?? [];
75-
$values = array_merge(
76-
$liveRequestData['props'] ?? [],
77-
$liveRequestData['updated'] ?? [],
78-
$liveRequestData['responseProps'] ?? []
78+
$metadata = $this->metadataFactory->getMetadata($mounted->getName());
79+
80+
$dehydratedProps = $this->liveComponentHydrator->dehydrate(
81+
$mounted->getComponent(),
82+
$mounted->getAttributes(),
83+
$metadata
7984
);
8085

86+
$values = $dehydratedProps->getProps();
87+
8188
$urlLiveProps = [
8289
'path' => [],
8390
'query' => [],
8491
];
92+
8593
foreach ($metadata->getAllUrlMappings() as $name => $urlMapping) {
8694
if (isset($values[$name]) && $urlMapping) {
87-
$urlLiveProps[$urlMapping->mapPath ? 'path' : 'query'][$urlMapping->as ?? $name] =
88-
$values[$name];
95+
$urlLiveProps[$urlMapping->mapPath ? 'path' : 'query'][$urlMapping->as ?? $name] = $values[$name];
8996
}
9097
}
9198

src/LiveComponent/tests/Fixtures/Component/ComponentWithUrlBoundProps.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
namespace Symfony\UX\LiveComponent\Tests\Fixtures\Component;
1313

1414
use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
15+
use Symfony\UX\LiveComponent\Attribute\LiveAction;
16+
use Symfony\UX\LiveComponent\Attribute\LiveArg;
1517
use Symfony\UX\LiveComponent\Attribute\LiveProp;
1618
use Symfony\UX\LiveComponent\DefaultActionTrait;
1719
use Symfony\UX\LiveComponent\Metadata\UrlMapping;
@@ -37,6 +39,9 @@ class ComponentWithUrlBoundProps
3739
#[LiveProp(url: true)]
3840
public ?Address $objectProp = null;
3941

42+
#[LiveProp(url: true, useSerializerForHydration: true)]
43+
public ?Address $objectPropWithSerializerForHydration = null;
44+
4045
#[LiveProp(fieldName: 'field1', url: true)]
4146
public ?string $propWithField1 = null;
4247

@@ -83,5 +88,13 @@ public function modifyBoundPropWithCustomAlias(LiveProp $liveProp): LiveProp
8388
return $liveProp;
8489
}
8590

91+
#[LiveAction]
92+
public function updateLiveProp(#[LiveArg] string $propName, #[LiveArg] mixed $propValue): void
93+
{
94+
if (!property_exists($this, $propName)) {
95+
throw new \InvalidArgumentException(\sprintf('Property "%s" does not exist on component "%s".', $propName, static::class));
96+
}
8697

98+
$this->{$propName} = $propValue;
99+
}
87100
}

src/LiveComponent/tests/Fixtures/Dto/Address.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,12 @@
22

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

5+
use Symfony\Component\Serializer\Attribute\SerializedName;
6+
57
class Address
68
{
79
public string $address;
10+
11+
#[SerializedName(serializedName: 'c')]
812
public string $city;
9-
}
13+
}

src/LiveComponent/tests/Functional/EventListener/AddLiveAttributesSubscriberTest.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@ public function testQueryStringMappingAttribute()
141141
'boundPropWithCustomAlias' => ['name' => 'customAlias'],
142142
'pathProp' => ['name' => 'pathProp'],
143143
'pathPropWithAlias' => ['name' => 'pathAlias'],
144+
'objectPropWithSerializerForHydration' => ['name' => 'objectPropWithSerializerForHydration'],
144145
];
145146

146147
$this->assertEquals($expected, $queryMapping);

src/LiveComponent/tests/Functional/EventListener/LiveUrlSubscriberTest.php

Lines changed: 70 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
namespace Symfony\UX\LiveComponent\Tests\Functional\EventListener;
1313

1414
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
15+
use Symfony\UX\LiveComponent\Tests\Fixtures\Dto\Address;
1516
use Symfony\UX\LiveComponent\Tests\LiveComponentTestHelper;
1617
use Zenstruck\Browser\Test\HasBrowser;
1718

@@ -26,17 +27,20 @@ public function getTestData(): iterable
2627
'previousLocation' => null,
2728
'expectedLocation' => null,
2829
'props' => [],
30+
'args' => [],
2931
];
3032
yield 'unknown_previous_location' => [
3133
'previousLocation' => 'foo/bar',
3234
'expectedLocation' => null,
3335
'props' => [],
36+
'args' => [],
3437
];
3538

3639
yield 'no_prop' => [
3740
'previousLocation' => '/route_with_prop/foo',
3841
'expectedLocation' => null,
3942
'props' => [],
43+
'args' => [],
4044
];
4145

4246
yield 'no_change' => [
@@ -45,52 +49,108 @@ public function getTestData(): iterable
4549
'props' => [
4650
'pathProp' => 'foo',
4751
],
52+
'args' => [],
4853
];
4954

50-
yield 'prop_changed' => [
55+
yield 'path_prop_changed' => [
5156
'previousLocation' => '/route_with_prop/foo',
5257
'expectedLocation' => '/route_with_prop/bar',
5358
'props' => [
5459
'pathProp' => 'foo',
5560
],
56-
'updated' => [
57-
'pathProp' => 'bar',
61+
'args' => [
62+
'propName' => 'pathProp',
63+
'propValue' => 'bar',
5864
],
5965
];
6066

61-
yield 'alias_prop_changed' => [
67+
yield 'path_alias_prop_changed' => [
6268
'previousLocation' => '/route_with_alias_prop/foo',
6369
'expectedLocation' => '/route_with_alias_prop/bar',
6470
'props' => [
6571
'pathPropWithAlias' => 'foo',
6672
],
67-
'updated' => [
68-
'pathPropWithAlias' => 'bar',
73+
'args' => [
74+
'propName' => 'pathPropWithAlias',
75+
'propValue' => 'bar',
76+
],
77+
];
78+
79+
yield 'query_alias_prop_changed' => [
80+
'previousLocation' => '/',
81+
'expectedLocation' => '/?q=search%2Bterm',
82+
'props' => [],
83+
'args' => [
84+
'propName' => 'boundPropWithAlias',
85+
'propValue' => 'search+term',
86+
],
87+
];
88+
89+
yield 'path_and_query_alias_prop_changed' => [
90+
'previousLocation' => '/route_with_prop/foo',
91+
'expectedLocation' => '/route_with_prop/baz?q=foo+bar',
92+
'props' => [
93+
'pathProp' => 'baz',
94+
],
95+
'args' => [
96+
'propName' => 'boundPropWithAlias',
97+
'propValue' => 'foo bar',
98+
],
99+
];
100+
101+
$address = new Address();
102+
$address->address = '123 Main St';
103+
$address->city = 'Anytown';
104+
yield 'with an object in query, keys "address" and "city" must be present' => [
105+
'previousLocation' => '/',
106+
'expectedLocation' => '/?objectProp%5Baddress%5D=123+Main+St&objectProp%5Bcity%5D=Anytown&q=search',
107+
'props' => [
108+
'objectProp' => $address,
109+
],
110+
'args' => [
111+
'propName' => 'boundPropWithAlias',
112+
'propValue' => 'search',
113+
],
114+
];
115+
$address = new Address();
116+
$address->address = '123 Main St';
117+
$address->city = 'Anytown';
118+
yield 'with an object in query, with "useSerializerForHydration: true", keys "address" and "c" must be present' => [
119+
'previousLocation' => '/',
120+
'expectedLocation' => '/?intProp=3&objectPropWithSerializerForHydration%5Baddress%5D=123+Main+St&objectPropWithSerializerForHydration%5Bc%5D=Anytown',
121+
'props' => [
122+
'objectPropWithSerializerForHydration' => $address,
123+
],
124+
'args' => [
125+
'propName' => 'intProp',
126+
'propValue' => '3',
69127
],
70128
];
71129
}
72130

73131
/**
74132
* @dataProvider getTestData
75133
*/
76-
public function testNoHeader(
134+
public function testNewLiveUrlAfterLiveAction(
77135
?string $previousLocation,
78136
?string $expectedLocation,
79137
array $props,
80-
array $updated = [],
138+
array $args,
81139
): void {
82140
$component = $this->mountComponent('component_with_url_bound_props', $props);
83141
$dehydrated = $this->dehydrateComponent($component);
84142

85143
$this->browser()
86144
->throwExceptions()
87145
->post(
88-
'/_components/component_with_url_bound_props',
146+
[] === $args
147+
? '/_components/component_with_url_bound_props'
148+
: '/_components/component_with_url_bound_props/updateLiveProp',
89149
[
90150
'body' => [
91151
'data' => json_encode([
92152
'props' => $dehydrated->getProps(),
93-
'updated' => $updated,
153+
'args' => $args,
94154
]),
95155
],
96156
'headers' => [

0 commit comments

Comments
 (0)