Skip to content

[Routing] Add RoutableInterface allowing objects to provide routing parameters #61038

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: 7.4
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/Symfony/Component/Routing/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
17 changes: 17 additions & 0 deletions src/Symfony/Component/Routing/Generator/RoutableInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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;
}
71 changes: 71 additions & 0 deletions src/Symfony/Component/Routing/Generator/RouterParameters.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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<string, array<int|string, RoutableInterface|string|int|null>> */
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<int|string, RoutableInterface|string|int|null> $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<string> $routeNames
* @param array<int|string, RoutableInterface|string|int|null> $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<int|string, string|int|null>
*/
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;
}
}
15 changes: 13 additions & 2 deletions src/Symfony/Component/Routing/Generator/UrlGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down Expand Up @@ -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) {
Expand Down
74 changes: 74 additions & 0 deletions src/Symfony/Component/Routing/Tests/Generator/UrlGeneratorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
*/
Expand Down Expand Up @@ -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