Skip to content

Commit 488a653

Browse files
committed
UrlFactory
1 parent 2dc8b53 commit 488a653

File tree

3 files changed

+107
-53
lines changed

3 files changed

+107
-53
lines changed

src/LiveComponent/src/DependencyInjection/LiveComponentExtension.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
use Symfony\UX\LiveComponent\Util\LiveComponentStack;
5353
use Symfony\UX\LiveComponent\Util\LiveControllerAttributesCreator;
5454
use Symfony\UX\LiveComponent\Util\TwigAttributeHelperFactory;
55+
use Symfony\UX\LiveComponent\Util\UrlFactory;
5556
use Symfony\UX\LiveComponent\Util\RequestPropsExtractor;
5657
use Symfony\UX\TwigComponent\ComponentFactory;
5758
use Symfony\UX\TwigComponent\ComponentRenderer;
@@ -138,8 +139,8 @@ function (ChildDefinition $definition, AsLiveComponent $attribute) {
138139

139140
$container->register('ux.live_component.live_url_subscriber', LiveUrlSubscriber::class)
140141
->setArguments([
141-
new Reference('router'),
142142
new Reference('ux.live_component.metadata_factory'),
143+
new Reference('ux.live_component.url_factory'),
143144
])
144145
->addTag('kernel.event_subscriber')
145146
;
@@ -209,6 +210,9 @@ function (ChildDefinition $definition, AsLiveComponent $attribute) {
209210
$container->register('ux.live_component.attribute_helper_factory', TwigAttributeHelperFactory::class)
210211
->setArguments([new Reference('twig')]);
211212

213+
$container->register('ux.live_component.url_factory', UrlFactory::class)
214+
->setArguments([new Reference('router')]);
215+
212216
$container->register('ux.live_component.live_controller_attributes_creator', LiveControllerAttributesCreator::class)
213217
->setArguments([
214218
new Reference('ux.live_component.metadata_factory'),

src/LiveComponent/src/EventListener/LiveUrlSubscriber.php

Lines changed: 19 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -15,40 +15,47 @@
1515
use Symfony\Component\HttpFoundation\Request;
1616
use Symfony\Component\HttpKernel\Event\ResponseEvent;
1717
use Symfony\Component\HttpKernel\KernelEvents;
18-
use Symfony\Component\Routing\RouterInterface;
1918
use Symfony\UX\LiveComponent\Metadata\LiveComponentMetadataFactory;
19+
use Symfony\UX\LiveComponent\Util\UrlFactory;
2020

2121
class LiveUrlSubscriber implements EventSubscriberInterface
2222
{
2323
private const URL_HEADER = 'X-Live-Url';
2424

2525
public function __construct(
26-
private readonly RouterInterface $router,
27-
private readonly LiveComponentMetadataFactory $metadataFactory,
26+
private LiveComponentMetadataFactory $metadataFactory,
27+
private UrlFactory $urlFactory,
2828
) {
2929
}
3030

3131
public function onKernelResponse(ResponseEvent $event): void
3232
{
33-
if (!$this->isLiveComponentRequest($request = $event->getRequest())) {
33+
$request = $event->getRequest();
34+
if (!$request->attributes->has('_live_component')) {
3435
return;
3536
}
3637
if (!$event->isMainRequest()) {
3738
return;
3839
}
3940

41+
$newUrl = null;
4042
if ($previousLocation = $request->headers->get(self::URL_HEADER)) {
41-
$newUrl = $this->computeNewUrl(
42-
$previousLocation,
43-
$this->getLivePropsToMap($request)
44-
);
45-
if ($newUrl) {
46-
$event->getResponse()->headers->set(
47-
self::URL_HEADER,
48-
$newUrl
43+
$liveProps = $this->getLivePropsToMap($request);
44+
if (!empty($liveProps)) {
45+
$newUrl = $this->urlFactory->createFromPreviousAndProps(
46+
$previousLocation,
47+
$liveProps['path'],
48+
$liveProps['query']
4949
);
5050
}
5151
}
52+
53+
if ($newUrl) {
54+
$event->getResponse()->headers->set(
55+
self::URL_HEADER,
56+
$newUrl
57+
);
58+
}
5259
}
5360

5461
public static function getSubscribedEvents(): array
@@ -82,44 +89,4 @@ private function getLivePropsToMap(Request $request): array
8289

8390
return $urlLiveProps;
8491
}
85-
86-
private function computeNewUrl(string $previousUrl, array $livePropsToMap): string
87-
{
88-
$parsed = parse_url($previousUrl);
89-
90-
$url = $parsed['path'] ?? '';
91-
if (isset($parsed['query'])) {
92-
$url .= '?'.$parsed['query'];
93-
}
94-
parse_str($parsed['query'] ?? '', $previousQueryParams);
95-
96-
$newUrl = $this->router->generate(
97-
$this->router->match($url)['_route'],
98-
array_merge($previousQueryParams, $livePropsToMap['path'])
99-
);
100-
parse_str(parse_url($newUrl)['query'] ?? '', $queryParams);
101-
$queryString = http_build_query(array_merge($queryParams, $livePropsToMap['query']));
102-
103-
return preg_replace('/[?#].*/', '', $newUrl).
104-
('' !== $queryString ? '?' : '').
105-
$queryString;
106-
}
107-
108-
/**
109-
* copied from LiveComponentSubscriber.
110-
*/
111-
private function isLiveComponentRequest(Request $request): bool
112-
{
113-
if (!$request->attributes->has('_live_component')) {
114-
return false;
115-
}
116-
117-
// if ($this->testMode) {
118-
// return true;
119-
// }
120-
121-
// Except when testing, require the correct content-type in the Accept header.
122-
// This also acts as a CSRF protection since this can only be set in accordance with same-origin/CORS policies.
123-
return \in_array('application/vnd.live-component+html', $request->getAcceptableContentTypes(), true);
124-
}
12592
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\UX\LiveComponent\Util;
13+
14+
use Symfony\Component\Routing\RouterInterface;
15+
16+
/**
17+
* @internal
18+
*/
19+
class UrlFactory
20+
{
21+
public function __construct(
22+
private RouterInterface $router,
23+
) {
24+
}
25+
26+
public function createFromPreviousAndProps(
27+
string $previousUrl,
28+
array $pathMappedProps,
29+
array $queryMappedProps,
30+
): string {
31+
$parsed = parse_url($previousUrl);
32+
33+
// Make sure to handle only path and query
34+
$previousUrl = $parsed['path'] ?? '';
35+
if (isset($parsed['query'])) {
36+
$previousUrl .= '?'.$parsed['query'];
37+
}
38+
39+
$newUrl = $this->createPath($previousUrl, $pathMappedProps);
40+
41+
return $this->replaceQueryString(
42+
$newUrl,
43+
array_merge(
44+
$this->getPreviousQueryParameters($parsed['query'] ?? ''),
45+
$this->getRemnantProps($newUrl),
46+
$queryMappedProps,
47+
)
48+
);
49+
}
50+
51+
private function createPath(string $previousUrl, array $props): string
52+
{
53+
return $this->router->generate(
54+
$this->router->match($previousUrl)['_route'],
55+
$props
56+
);
57+
}
58+
59+
private function replaceQueryString($url, array $props): string
60+
{
61+
$queryString = http_build_query($props);
62+
63+
return preg_replace('/[?#].*/', '', $url).
64+
('' !== $queryString ? '?' : '').
65+
$queryString;
66+
}
67+
68+
// Keep the query parameters of the previous request
69+
private function getPreviousQueryParameters(string $query): array
70+
{
71+
parse_str($query, $previousQueryParams);
72+
73+
return $previousQueryParams;
74+
}
75+
76+
// Symfony router will set props in query if they do not match route parameter
77+
private function getRemnantProps(string $newUrl): array
78+
{
79+
parse_str(parse_url($newUrl)['query'] ?? '', $remnantQueryParams);
80+
81+
return $remnantQueryParams;
82+
}
83+
}

0 commit comments

Comments
 (0)