diff --git a/src/Symfony/Component/Routing/CHANGELOG.md b/src/Symfony/Component/Routing/CHANGELOG.md index 4ef96d53232fe..47a1ec50b3455 100644 --- a/src/Symfony/Component/Routing/CHANGELOG.md +++ b/src/Symfony/Component/Routing/CHANGELOG.md @@ -4,6 +4,7 @@ CHANGELOG 7.4 --- + * Allow objects to provide route parameters by implementing `RoutableInterface` * Allow query-specific parameters in `UrlGenerator` using `_query` 7.3 diff --git a/src/Symfony/Component/Routing/Generator/RoutableInterface.php b/src/Symfony/Component/Routing/Generator/RoutableInterface.php new file mode 100644 index 0000000000000..523c382c421f7 --- /dev/null +++ b/src/Symfony/Component/Routing/Generator/RoutableInterface.php @@ -0,0 +1,17 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing\Generator; + +interface RoutableInterface +{ + public function getRouterParameters(): RouterParameters; +} diff --git a/src/Symfony/Component/Routing/Generator/RouterParameters.php b/src/Symfony/Component/Routing/Generator/RouterParameters.php new file mode 100644 index 0000000000000..ee8755e5e02cb --- /dev/null +++ b/src/Symfony/Component/Routing/Generator/RouterParameters.php @@ -0,0 +1,71 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing\Generator; + +use Symfony\Component\DependencyInjection\Attribute\Exclude; + +#[Exclude] +final class RouterParameters +{ + /** @var array> */ + private array $parameters = []; + + /** + * The constructors take the default parameters which are used to fulfill the requirements for all routes which + * are not specified using the `add` method. + * + * @param array $defaultParameters + */ + public function __construct( + private readonly array $defaultParameters, + ) {} + + /** + * Using this method, one can define specific routing parameters for one or more route names. This is useful when + * this `RoutableInterface` instance is used to build the parameters for a parent object. When the many-to-one + * relation Match -> Pool exists, it would be possible to build parameters for the Pool detail page, by providing a + * Match entity like `->generate('pool_details', $match)`. + * + * @param string|array $routeNames + * @param array $parameters + */ + public function add(string|array $routeNames, array $parameters): self + { + if (is_array($routeNames)) { + foreach ($routeNames as $route) { + $this->parameters[$route] = $parameters; + } + } else { + $this->parameters[$routeNames] = $parameters; + } + + return $this; + } + + /** + * @return array + */ + public function getParameters(string $route): array + { + $result = []; + + foreach ($this->parameters[$route] ?? $this->defaultParameters as $key => $value) { + if ($value instanceof RoutableInterface) { + $result = array_replace($result, $value->getRouterParameters()->getParameters($route)); + } else { + $result[$key] = $value; + } + } + + return $result; + } +} diff --git a/src/Symfony/Component/Routing/Generator/UrlGenerator.php b/src/Symfony/Component/Routing/Generator/UrlGenerator.php index d82b91898194a..34ae56e33fba8 100644 --- a/src/Symfony/Component/Routing/Generator/UrlGenerator.php +++ b/src/Symfony/Component/Routing/Generator/UrlGenerator.php @@ -154,8 +154,19 @@ protected function doGenerate(array $variables, array $defaults, array $requirem } } + $resolvedParameters = []; + foreach ($parameters as $key => $value) { + if (!$value instanceof RoutableInterface) { + $resolvedParameters[$key] = $value; + + continue; + } + + $resolvedParameters = array_replace($resolvedParameters, $value->getRouterParameters()->getParameters($name)); + } + $variables = array_flip($variables); - $mergedParams = array_replace($defaults, $this->context->getParameters(), $parameters); + $mergedParams = array_replace($defaults, $this->context->getParameters(), $resolvedParameters); // all params must be given if ($diff = array_diff_key($variables, $mergedParams)) { @@ -271,7 +282,7 @@ protected function doGenerate(array $variables, array $defaults, array $requirem } // add a query string if needed - $extra = array_udiff_assoc(array_diff_key($parameters, $variables), $defaults, fn ($a, $b) => $a == $b ? 0 : 1); + $extra = array_udiff_assoc(array_diff_key($resolvedParameters, $variables), $defaults, fn ($a, $b) => $a == $b ? 0 : 1); $extra = array_merge($extra, $queryParameters); array_walk_recursive($extra, $caster = static function (&$v) use (&$caster) { diff --git a/src/Symfony/Component/Routing/Tests/Generator/UrlGeneratorTest.php b/src/Symfony/Component/Routing/Tests/Generator/UrlGeneratorTest.php index 75196bd214aa2..8177b4ce58209 100644 --- a/src/Symfony/Component/Routing/Tests/Generator/UrlGeneratorTest.php +++ b/src/Symfony/Component/Routing/Tests/Generator/UrlGeneratorTest.php @@ -18,6 +18,8 @@ use Symfony\Component\Routing\Exception\MissingMandatoryParametersException; use Symfony\Component\Routing\Exception\RouteCircularReferenceException; use Symfony\Component\Routing\Exception\RouteNotFoundException; +use Symfony\Component\Routing\Generator\RoutableInterface; +use Symfony\Component\Routing\Generator\RouterParameters; use Symfony\Component\Routing\Generator\UrlGenerator; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; use Symfony\Component\Routing\RequestContext; @@ -1109,6 +1111,39 @@ public function testQueryParameterCannotSubstituteRouteParameter() ]); } + public function testRoutableClass() + { + $routes = new RouteCollection(); + $routes->add('test', new Route('/testing/{param}')); + $routes->add('test2', new Route('/testing/{param}/{framework}')); + $routes->add('test3', new Route('/testing/{param}')); + $routes->add('test4', new Route('/testing/{base}/{param}')); + + // Regular + $url = $this->getGenerator($routes)->generate('test', [new RoutableObject()]); + $this->assertEquals('/app.php/testing/first', $url); + + // Override routable config with parameter + $url = $this->getGenerator($routes)->generate('test', [new RoutableObject(), 'param' => 'baz']); + $this->assertEquals('/app.php/testing/baz', $url); + + // Override parameter with routable config + $url = $this->getGenerator($routes)->generate('test', ['param' => 'baz', new RoutableObject()]); + $this->assertEquals('/app.php/testing/first', $url); + + // Routable config for a specific route + $url = $this->getGenerator($routes)->generate('test2', [new RoutableObject()]); + $this->assertEquals('/app.php/testing/first/symfony', $url); + + // Recursive use of Routable config + $url = $this->getGenerator($routes)->generate('test3', [new RoutableObject()]); + $this->assertEquals('/app.php/testing/second', $url); + + // Extending the config of a parent object + $url = $this->getGenerator($routes)->generate('test4', [new ThirdRoutableObject(new RoutableObject())]); + $this->assertEquals('/app.php/testing/third/first', $url); + } + /** * @group legacy */ @@ -1174,3 +1209,42 @@ class NonStringableObjectWithPublicProperty { public $foo = 'property'; } + +class RoutableObject implements RoutableInterface +{ + private string $param = 'first'; + + public function getRouterParameters(): RouterParameters + { + return (new RouterParameters(['param' => $this->param])) + ->add('test2', ['param' => $this->param, 'framework' => 'symfony']) + ->add('test3', [new SecondRoutableObject()]); + } +} + +class SecondRoutableObject implements RoutableInterface +{ + private string $param = 'second'; + + public function getRouterParameters(): RouterParameters + { + return (new RouterParameters(['param' => $this->param])); + } +} + +class ThirdRoutableObject implements RoutableInterface +{ + private RoutableObject $base; + + private string $param = 'third'; + + public function __construct(RoutableObject $base) + { + $this->base = $base; + } + + public function getRouterParameters(): RouterParameters + { + return (new RouterParameters([...$this->base->getRouterParameters()->getParameters('default'), 'base' => $this->param])); + } +} pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy