diff --git a/src/LiveComponent/src/DependencyInjection/LiveComponentExtension.php b/src/LiveComponent/src/DependencyInjection/LiveComponentExtension.php index 6934f0ff76e..6415526c528 100644 --- a/src/LiveComponent/src/DependencyInjection/LiveComponentExtension.php +++ b/src/LiveComponent/src/DependencyInjection/LiveComponentExtension.php @@ -53,7 +53,6 @@ use Symfony\UX\LiveComponent\Util\LiveControllerAttributesCreator; use Symfony\UX\LiveComponent\Util\RequestPropsExtractor; use Symfony\UX\LiveComponent\Util\TwigAttributeHelperFactory; -use Symfony\UX\LiveComponent\Util\UrlFactory; use Symfony\UX\TwigComponent\ComponentFactory; use Symfony\UX\TwigComponent\ComponentRenderer; @@ -142,7 +141,7 @@ function (ChildDefinition $definition, AsLiveComponent $attribute) { ->setArguments([ new Reference('ux.live_component.metadata_factory'), new Reference('ux.live_component.component_hydrator'), - new Reference('ux.live_component.url_factory'), + new Reference('router'), ]) ->addTag('kernel.event_subscriber') ; @@ -213,9 +212,6 @@ function (ChildDefinition $definition, AsLiveComponent $attribute) { $container->register('ux.live_component.attribute_helper_factory', TwigAttributeHelperFactory::class); - $container->register('ux.live_component.url_factory', UrlFactory::class) - ->setArguments([new Reference('router')]); - $container->register('ux.live_component.live_controller_attributes_creator', LiveControllerAttributesCreator::class) ->setArguments([ new Reference('ux.live_component.metadata_factory'), diff --git a/src/LiveComponent/src/EventListener/LiveUrlSubscriber.php b/src/LiveComponent/src/EventListener/LiveUrlSubscriber.php index c7193a59bb9..1bcb79a9cf7 100644 --- a/src/LiveComponent/src/EventListener/LiveUrlSubscriber.php +++ b/src/LiveComponent/src/EventListener/LiveUrlSubscriber.php @@ -14,9 +14,10 @@ use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\HttpKernel\Event\ResponseEvent; use Symfony\Component\HttpKernel\KernelEvents; +use Symfony\Component\Routing\Exception\ResourceNotFoundException; +use Symfony\Component\Routing\RouterInterface; use Symfony\UX\LiveComponent\LiveComponentHydrator; use Symfony\UX\LiveComponent\Metadata\LiveComponentMetadataFactory; -use Symfony\UX\LiveComponent\Util\UrlFactory; use Symfony\UX\TwigComponent\MountedComponent; /** @@ -29,35 +30,28 @@ class LiveUrlSubscriber implements EventSubscriberInterface public function __construct( private LiveComponentMetadataFactory $metadataFactory, private LiveComponentHydrator $liveComponentHydrator, - private UrlFactory $urlFactory, + private RouterInterface $router, ) { } public function onKernelResponse(ResponseEvent $event): void { - if (!$event->isMainRequest()) { - return; - } - $request = $event->getRequest(); - if (!$request->attributes->has('_live_component')) { + if (!$event->isMainRequest() + || !$event->getResponse()->isSuccessful() + || !$request->attributes->has('_live_component') + || !$request->attributes->has('_mounted_component') + || !($previousLiveUrl = $request->headers->get(self::URL_HEADER)) + ) { return; } - if (!$request->attributes->has('_mounted_component')) { - return; - } + /** @var MountedComponent $mounted */ + $mounted = $request->attributes->get('_mounted_component'); - $newLiveUrl = null; - if ($previousLiveUrl = $request->headers->get(self::URL_HEADER)) { - $mounted = $request->attributes->get('_mounted_component'); - $liveProps = $this->getLiveProps($mounted); - $newLiveUrl = $this->urlFactory->createFromPreviousAndProps($previousLiveUrl, $liveProps['path'], $liveProps['query']); - } + [$pathProps, $queryProps] = $this->extractUrlLiveProps($mounted); - if ($newLiveUrl) { - $event->getResponse()->headers->set(self::URL_HEADER, $newLiveUrl); - } + $event->getResponse()->headers->set(self::URL_HEADER, $this->generateNewLiveUrl($previousLiveUrl, $pathProps, $queryProps)); } public static function getSubscribedEvents(): array @@ -68,34 +62,73 @@ public static function getSubscribedEvents(): array } /** - * @return array{ - * path: array, - * query: array - * } + * @return array{ array, array } */ - private function getLiveProps(MountedComponent $mounted): array + private function extractUrlLiveProps(MountedComponent $mounted): array { - $metadata = $this->metadataFactory->getMetadata($mounted->getName()); + $pathProps = $queryProps = []; + + $mountedMetadata = $this->metadataFactory->getMetadata($mounted->getName()); + + if ([] !== $urlMappings = $mountedMetadata->getAllUrlMappings($mounted->getComponent())) { + $dehydratedProps = $this->liveComponentHydrator->dehydrate($mounted->getComponent(), $mounted->getAttributes(), $mountedMetadata); + $props = $dehydratedProps->getProps(); + + foreach ($urlMappings as $name => $urlMapping) { + if (\array_key_exists($name, $props)) { + if ($urlMapping->mapPath) { + $pathProps[$urlMapping->as ?? $name] = $props[$name]; + } else { + $queryProps[$urlMapping->as ?? $name] = $props[$name]; + } + } + } + } - $dehydratedProps = $this->liveComponentHydrator->dehydrate( - $mounted->getComponent(), - $mounted->getAttributes(), - $metadata - ); + return [$pathProps, $queryProps]; + } - $values = $dehydratedProps->getProps(); + private function generateNewLiveUrl(string $previousUrl, array $pathProps, array $queryProps): string + { + $previousUrlParsed = parse_url($previousUrl); + $newUrl = $previousUrlParsed['path']; + $newQueryString = $previousUrlParsed['query'] ?? ''; + + if ([] !== $pathProps) { + $context = $this->router->getContext(); + try { + // Re-create a context for the URL rendering the current LiveComponent + $tmpContext = clone $context; + $tmpContext->setMethod('GET'); + $this->router->setContext($tmpContext); + + $routeMatched = $this->router->match($previousUrlParsed['path']); + $routeParams = []; + foreach ($routeMatched as $k => $v) { + if ('_route' === $k || '_controller' === $k) { + continue; + } + $routeParams[$k] = \array_key_exists($k, $pathProps) ? $pathProps[$k] : $v; + } + + $newUrl = $this->router->generate($routeMatched['_route'], $routeParams); + } catch (ResourceNotFoundException) { + // reuse the previous URL path + } finally { + $this->router->setContext($context); + } + } - $urlLiveProps = [ - 'path' => [], - 'query' => [], - ]; + if ([] !== $queryProps) { + $previousQueryString = []; - foreach ($metadata->getAllUrlMappings($mounted->getComponent()) as $name => $urlMapping) { - if (isset($values[$name]) && $urlMapping) { - $urlLiveProps[$urlMapping->mapPath ? 'path' : 'query'][$urlMapping->as ?? $name] = $values[$name]; + if (isset($previousUrlParsed['query'])) { + parse_str($previousUrlParsed['query'], $previousQueryString); } + + $newQueryString = http_build_query([...$previousQueryString, ...$queryProps]); } - return $urlLiveProps; + return $newUrl.($newQueryString ? '?'.$newQueryString : ''); } } diff --git a/src/LiveComponent/src/Metadata/LiveComponentMetadata.php b/src/LiveComponent/src/Metadata/LiveComponentMetadata.php index 73dc4a7a2a1..91b895afe5f 100644 --- a/src/LiveComponent/src/Metadata/LiveComponentMetadata.php +++ b/src/LiveComponent/src/Metadata/LiveComponentMetadata.php @@ -69,9 +69,10 @@ public function getOnlyPropsThatAcceptUpdatesFromParent(array $inputProps): arra /** * @return UrlMapping[] */ - public function getAllUrlMappings(object $component): iterable + public function getAllUrlMappings(object $component): array { $urlMappings = []; + foreach ($this->getAllLivePropsMetadata($component) as $livePropMetadata) { if ($livePropMetadata->urlMapping()) { $urlMappings[$livePropMetadata->getName()] = $livePropMetadata->urlMapping(); diff --git a/src/LiveComponent/src/Util/UrlFactory.php b/src/LiveComponent/src/Util/UrlFactory.php deleted file mode 100644 index b0d92436730..00000000000 --- a/src/LiveComponent/src/Util/UrlFactory.php +++ /dev/null @@ -1,108 +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\Util; - -use Symfony\Component\Routing\Exception\MethodNotAllowedException; -use Symfony\Component\Routing\Exception\MissingMandatoryParametersException; -use Symfony\Component\Routing\Exception\ResourceNotFoundException; -use Symfony\Component\Routing\RouterInterface; - -/** - * @internal - */ -class UrlFactory -{ - public function __construct( - private RouterInterface $router, - ) { - } - - public function createFromPreviousAndProps( - string $previousUrl, - array $pathMappedProps, - array $queryMappedProps, - ): ?string { - $parsed = parse_url($previousUrl); - if (false === $parsed) { - return null; - } - - try { - $newUrl = $this->createPath($parsed['path'] ?? '', $pathMappedProps); - } catch (ResourceNotFoundException|MethodNotAllowedException|MissingMandatoryParametersException) { - return null; - } - - return $this->replaceQueryString( - $newUrl, - array_merge( - $this->getPreviousQueryParameters($parsed['query'] ?? ''), - $this->getRemnantProps($newUrl), - $queryMappedProps, - ) - ); - } - - private function createPath(string $previousUrl, array $props): string - { - $newPath = $this->router->generate( - $this->matchRoute($previousUrl), - $props - ); - - return $newPath; - } - - private function matchRoute(string $previousUrl): string - { - $context = $this->router->getContext(); - $tmpContext = clone $context; - $tmpContext->setMethod('GET'); - $this->router->setContext($tmpContext); - try { - $match = $this->router->match($previousUrl); - } finally { - $this->router->setContext($context); - } - - return $match['_route'] ?? ''; - } - - private function replaceQueryString($url, array $props): string - { - $queryString = http_build_query($props); - - return preg_replace('/[?#].*/', '', $url). - ('' !== $queryString ? '?' : ''). - $queryString; - } - - /** - * Keep the query parameters of the previous request. - */ - private function getPreviousQueryParameters(string $query): array - { - parse_str($query, $previousQueryParams); - - return $previousQueryParams; - } - - /** - * Symfony router will set props in query if they do not match route parameter. - */ - private function getRemnantProps(string $newUrl): array - { - parse_str(parse_url($newUrl)['query'] ?? '', $remnantQueryParams); - - return $remnantQueryParams; - } -} diff --git a/src/LiveComponent/tests/Fixtures/Component/ComponentWithUrlBoundProps.php b/src/LiveComponent/tests/Fixtures/Component/ComponentWithUrlBoundProps.php index 9bb965d40be..1ee8b268326 100644 --- a/src/LiveComponent/tests/Fixtures/Component/ComponentWithUrlBoundProps.php +++ b/src/LiveComponent/tests/Fixtures/Component/ComponentWithUrlBoundProps.php @@ -92,6 +92,9 @@ public function modifyMaybeBoundProp(LiveProp $prop): LiveProp #[LiveProp(writable: true, url: new UrlMapping(as: 'pathAlias', mapPath: true))] public ?string $pathPropWithAlias = null; + #[LiveProp(writable: true, url: new UrlMapping(mapPath: true))] + public ?string $pathPropForAnotherController = 'foo'; + public function modifyBoundPropWithCustomAlias(LiveProp $liveProp): LiveProp { if ($this->customAlias) { diff --git a/src/LiveComponent/tests/Fixtures/Kernel.php b/src/LiveComponent/tests/Fixtures/Kernel.php index 1d45f78dc0f..cd344e7388a 100644 --- a/src/LiveComponent/tests/Fixtures/Kernel.php +++ b/src/LiveComponent/tests/Fixtures/Kernel.php @@ -217,7 +217,9 @@ protected function configureRoutes(RoutingConfigurator $routes): void $routes->add('homepage', '/')->controller('kernel::index'); $routes->add('alternate_live_route', '/alt/{_live_component}/{_live_action}')->defaults(['_live_action' => 'get']); $routes->add('localized_route', '/locale/{_locale}/{_live_component}/{_live_action}')->defaults(['_live_action' => 'get']); - $routes->add('route_with_prop', '/route_with_prop/{pathProp}')->methods(['GET']); - $routes->add('route_with_alias_prop', '/route_with_alias_prop/{pathAlias}'); + $routes->add('route_with_prop', '/route_with_prop/{pathProp}')->methods(['GET'])->requirements(['pathProp' => '\w+']); + $routes->add('route_with_alias_prop', '/route_with_alias_prop/{pathAlias}')->requirements(['pathAlias' => '\w+']); + $routes->add('route_with_two_props', '/route_with_two_props/{pathProp}/{pathAlias}')->methods(['GET'])->requirements(['pathProp' => '\w+', 'pathAlias' => '\w+']); + $routes->add('route_with_two_path_params_but_one_prop', '/route_with_two_path_params_but_one_prop/{pathProp}/{id}')->methods(['GET'])->requirements(['pathProp' => '\w+', 'id' => '\d+']); } } diff --git a/src/LiveComponent/tests/Functional/EventListener/AddLiveAttributesSubscriberTest.php b/src/LiveComponent/tests/Functional/EventListener/AddLiveAttributesSubscriberTest.php index f76cc2e697e..1ded114e149 100644 --- a/src/LiveComponent/tests/Functional/EventListener/AddLiveAttributesSubscriberTest.php +++ b/src/LiveComponent/tests/Functional/EventListener/AddLiveAttributesSubscriberTest.php @@ -143,6 +143,7 @@ public function testQueryStringMappingAttribute() 'pathPropWithAlias' => ['name' => 'pathAlias'], 'objectPropWithSerializerForHydration' => ['name' => 'objectPropWithSerializerForHydration'], 'propertyWithModifierAndAlias' => ['name' => 'alias_p'], + 'pathPropForAnotherController' => ['name' => 'pathPropForAnotherController'], ]; $this->assertEquals($expected, $queryMapping); diff --git a/src/LiveComponent/tests/Functional/EventListener/LiveUrlSubscriberTest.php b/src/LiveComponent/tests/Functional/EventListener/LiveUrlSubscriberTest.php index c64b0b5a0c1..97d85be7532 100644 --- a/src/LiveComponent/tests/Functional/EventListener/LiveUrlSubscriberTest.php +++ b/src/LiveComponent/tests/Functional/EventListener/LiveUrlSubscriberTest.php @@ -23,39 +23,33 @@ class LiveUrlSubscriberTest extends KernelTestCase public function getTestData(): iterable { - yield 'missing_header' => [ + yield 'Missing header' => [ 'previousLocation' => null, 'expectedLocation' => null, - 'props' => [], - 'args' => [], - ]; - yield 'unknown_previous_location' => [ - 'previousLocation' => 'foo/bar', - 'expectedLocation' => null, - 'props' => [], + 'initialComponentData' => [], 'args' => [], ]; - yield 'no_prop' => [ - 'previousLocation' => '/route_with_prop/foo', - 'expectedLocation' => null, - 'props' => [], + yield 'Unknown previous location' => [ + 'previousLocation' => 'foo/bar', + 'expectedLocation' => 'foo/bar', + 'initialComponentData' => [], 'args' => [], ]; - yield 'no_change' => [ + yield 'No props change' => [ 'previousLocation' => '/route_with_prop/foo', 'expectedLocation' => '/route_with_prop/foo', - 'props' => [ + 'initialComponentData' => [ 'pathProp' => 'foo', ], 'args' => [], ]; - yield 'path_prop_changed' => [ + yield 'Changes in prop' => [ 'previousLocation' => '/route_with_prop/foo', 'expectedLocation' => '/route_with_prop/bar', - 'props' => [ + 'initialComponentData' => [ 'pathProp' => 'foo', ], 'args' => [ @@ -64,10 +58,46 @@ public function getTestData(): iterable ], ]; - yield 'path_alias_prop_changed' => [ + yield 'Change in query' => [ + 'previousLocation' => '/route_with_prop/foo', + 'expectedLocation' => '/route_with_prop/foo?stringProp=hello', + 'initialComponentData' => [ + 'pathProp' => 'foo', + ], + 'args' => [ + 'propName' => 'stringProp', + 'propValue' => 'hello', + ], + ]; + + yield 'Change in query (with an existing query that is not a LiveProp)' => [ + 'previousLocation' => '/route_with_prop/foo?not-a-prop=value', + 'expectedLocation' => '/route_with_prop/foo?not-a-prop=value&stringProp=hello', + 'initialComponentData' => [ + 'pathProp' => 'foo', + ], + 'args' => [ + 'propName' => 'stringProp', + 'propValue' => 'hello', + ], + ]; + yield 'Change in query (with two existing query, one LiveProp, one non-LiveProp)' => [ + 'previousLocation' => '/route_with_prop/foo?not-a-prop=value&stringProp=hello', + 'expectedLocation' => '/route_with_prop/foo?not-a-prop=value&stringProp=bye', + 'initialComponentData' => [ + 'pathProp' => 'foo', + 'stringProp' => 'hello', + ], + 'args' => [ + 'propName' => 'stringProp', + 'propValue' => 'bye', + ], + ]; + + yield 'Changes in prop (alias)' => [ 'previousLocation' => '/route_with_alias_prop/foo', 'expectedLocation' => '/route_with_alias_prop/bar', - 'props' => [ + 'initialComponentData' => [ 'pathPropWithAlias' => 'foo', ], 'args' => [ @@ -76,20 +106,44 @@ public function getTestData(): iterable ], ]; - yield 'query_alias_prop_changed' => [ + yield 'Changes in prop, with two props' => [ + 'previousLocation' => '/route_with_two_props/foo/alias', + 'expectedLocation' => '/route_with_two_props/bar/alias', + 'initialComponentData' => [ + 'pathProp' => 'foo', + 'pathPropWithAlias' => 'alias', + ], + 'args' => [ + 'propName' => 'pathProp', + 'propValue' => 'bar', + ], + ]; + yield 'Changes in prop, with two path params but only one prop' => [ + 'previousLocation' => '/route_with_two_path_params_but_one_prop/foo/30', + 'expectedLocation' => '/route_with_two_path_params_but_one_prop/bar/30', + 'initialComponentData' => [ + 'pathProp' => 'foo', + ], + 'args' => [ + 'propName' => 'pathProp', + 'propValue' => 'bar', + ], + ]; + + yield 'Changes in query (alias)' => [ 'previousLocation' => '/', - 'expectedLocation' => '/?q=search%2Bterm', - 'props' => [], + 'expectedLocation' => '/?q=search+term', + 'initialComponentData' => [], 'args' => [ 'propName' => 'boundPropWithAlias', - 'propValue' => 'search+term', + 'propValue' => 'search term', ], ]; - yield 'path_and_query_alias_prop_changed' => [ + yield 'Changes in props and query' => [ 'previousLocation' => '/route_with_prop/foo', 'expectedLocation' => '/route_with_prop/baz?q=foo+bar', - 'props' => [ + 'initialComponentData' => [ 'pathProp' => 'baz', ], 'args' => [ @@ -104,7 +158,7 @@ public function getTestData(): iterable 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' => [ + 'initialComponentData' => [ 'objectProp' => $address, ], 'args' => [ @@ -119,7 +173,7 @@ public function getTestData(): iterable 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' => [ + 'initialComponentData' => [ 'objectPropWithSerializerForHydration' => $address, ], 'args' => [ @@ -131,7 +185,7 @@ public function getTestData(): iterable yield 'query with alias ("p") and modifier (prefix by "alias_")' => [ 'previousLocation' => '/', 'expectedLocation' => '/?alias_p=test', - 'props' => [ + 'initialComponentData' => [ 'propertyWithModifierAndAlias' => 'test', ], 'args' => [], @@ -144,10 +198,10 @@ public function getTestData(): iterable public function testNewLiveUrlAfterLiveAction( ?string $previousLocation, ?string $expectedLocation, - array $props, + array $initalComponentData, array $args, ): void { - $component = $this->mountComponent('component_with_url_bound_props', $props); + $component = $this->mountComponent('component_with_url_bound_props', $initalComponentData); $dehydrated = $this->dehydrateComponent($component); $this->browser() diff --git a/src/LiveComponent/tests/Unit/Util/UrlFactoryTest.php b/src/LiveComponent/tests/Unit/Util/UrlFactoryTest.php deleted file mode 100644 index 76d46a710b7..00000000000 --- a/src/LiveComponent/tests/Unit/Util/UrlFactoryTest.php +++ /dev/null @@ -1,204 +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\Util; - -use PHPUnit\Framework\TestCase; -use Symfony\Component\Routing\Exception\MethodNotAllowedException; -use Symfony\Component\Routing\Exception\MissingMandatoryParametersException; -use Symfony\Component\Routing\Exception\ResourceNotFoundException; -use Symfony\Component\Routing\RequestContext; -use Symfony\Component\Routing\RouterInterface; -use Symfony\UX\LiveComponent\Util\UrlFactory; - -class UrlFactoryTest extends TestCase -{ - public static function provideTestCreate(): \Generator - { - yield 'keep_default_url' => []; - - yield 'keep_relative_url' => [ - 'input' => ['previousUrl' => '/foo/bar'], - 'expectedUrl' => '/foo/bar', - ]; - - yield 'keep_absolute_url' => [ - 'input' => ['previousUrl' => 'https://symfony.com/foo/bar'], - 'expectedUrl' => '/foo/bar', - 'routerStubData' => [ - 'previousUrl' => '/foo/bar', - 'newUrl' => '/foo/bar', - ], - ]; - - yield 'keep_url_with_query_parameters' => [ - 'input' => ['previousUrl' => 'https://symfony.com/foo/bar?prop1=val1&prop2=val2'], - 'expectedUrl' => '/foo/bar?prop1=val1&prop2=val2', - 'routerStubData' => [ - 'previousUrl' => '/foo/bar', - 'newUrl' => '/foo/bar', - ], - ]; - - yield 'add_query_parameters' => [ - 'input' => [ - 'previousUrl' => '/foo/bar', - 'queryMappedProps' => ['prop1' => 'val1', 'prop2' => 'val2'], - ], - 'expectedUrl' => '/foo/bar?prop1=val1&prop2=val2', - ]; - - yield 'override_previous_matching_query_parameters' => [ - 'input' => [ - 'previousUrl' => '/foo/bar?prop1=oldValue&prop3=oldValue', - 'queryMappedProps' => ['prop1' => 'val1', 'prop2' => 'val2'], - ], - 'expectedUrl' => '/foo/bar?prop1=val1&prop3=oldValue&prop2=val2', - 'routerStubData' => [ - 'previousUrl' => '/foo/bar', - 'newUrl' => '/foo/bar', - ], - ]; - - yield 'add_path_parameters' => [ - 'input' => [ - 'previousUrl' => '/foo/bar', - 'pathMappedProps' => ['value' => 'baz'], - ], - 'expectedUrl' => '/foo/baz', - 'routerStubData' => [ - 'previousUrl' => '/foo/bar', - 'newUrl' => '/foo/baz', - 'props' => ['value' => 'baz'], - ], - ]; - - yield 'add_both_parameters' => [ - 'input' => [ - 'previousUrl' => '/foo/bar', - 'pathMappedProps' => ['value' => 'baz'], - 'queryMappedProps' => ['filter' => 'all'], - ], - 'expectedUrl' => '/foo/baz?filter=all', - 'routerStubData' => [ - 'previousUrl' => '/foo/bar', - 'newUrl' => '/foo/baz', - 'props' => ['value' => 'baz'], - ], - ]; - - yield 'handle_path_parameter_not_recognized' => [ - 'input' => [ - 'previousUrl' => '/foo/bar', - 'pathMappedProps' => ['value' => 'baz'], - ], - 'expectedUrl' => '/foo/bar?value=baz', - 'routerStubData' => [ - 'previousUrl' => '/foo/bar', - 'newUrl' => '/foo/bar?value=baz', - 'props' => ['value' => 'baz'], - ], - ]; - } - - /** - * @dataProvider provideTestCreate - */ - public function testCreate( - array $input = [], - string $expectedUrl = '', - array $routerStubData = [], - ): void { - $previousUrl = $input['previousUrl'] ?? ''; - $router = $this->createRouterStub( - $routerStubData['previousUrl'] ?? $previousUrl, - $routerStubData['newUrl'] ?? $previousUrl, - $routerStubData['props'] ?? [], - ); - $factory = new UrlFactory($router); - $newUrl = $factory->createFromPreviousAndProps( - $previousUrl, - $input['pathMappedProps'] ?? [], - $input['queryMappedProps'] ?? [] - ); - - $this->assertEquals($expectedUrl, $newUrl); - } - - public function testResourceNotFoundException() - { - $previousUrl = '/foo/bar'; - $router = $this->createMock(RouterInterface::class); - $router->expects(self::once()) - ->method('match') - ->with($previousUrl) - ->willThrowException(new ResourceNotFoundException()); - $factory = new UrlFactory($router); - - $this->assertNull($factory->createFromPreviousAndProps($previousUrl, [], [])); - } - - public function testMethodNotAllowedException() - { - $previousUrl = '/foo/bar'; - $router = $this->createMock(RouterInterface::class); - $router->expects(self::once()) - ->method('match') - ->with($previousUrl) - ->willThrowException(new MethodNotAllowedException(['GET'])); - $factory = new UrlFactory($router); - - $this->assertNull($factory->createFromPreviousAndProps($previousUrl, [], [])); - } - - public function testMissingMandatoryParametersException() - { - $previousUrl = '/foo/bar'; - $matchedRouteName = 'foo_bar'; - $router = $this->createMock(RouterInterface::class); - $router->expects(self::once()) - ->method('match') - ->with($previousUrl) - ->willReturn(['_route' => $matchedRouteName]); - $router->expects(self::once()) - ->method('generate') - ->with($matchedRouteName, []) - ->willThrowException(new MissingMandatoryParametersException($matchedRouteName, ['baz'])); - $factory = new UrlFactory($router); - - $this->assertNull($factory->createFromPreviousAndProps($previousUrl, [], [])); - } - - private function createRouterStub( - string $previousUrl, - string $newUrl, - array $props = [], - ): RouterInterface { - $matchedRoute = 'default'; - $context = $this->createMock(RequestContext::class); - $router = $this->createMock(RouterInterface::class); - $router->expects(self::once()) - ->method('getContext') - ->willReturn($context); - $router->expects(self::exactly(2)) - ->method('setContext'); - $router->expects(self::once()) - ->method('match') - ->with($previousUrl) - ->willReturn(['_route' => $matchedRoute]); - $router->expects(self::once()) - ->method('generate') - ->with($matchedRoute, $props) - ->willReturn($newUrl); - - return $router; - } -}