Skip to content

Commit c15fd16

Browse files
Lctrsnicolas-grekas
authored andcommitted
[Routing] Add support for aliasing routes
1 parent aedf2a8 commit c15fd16

32 files changed

+859
-13
lines changed

Alias.php

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
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\Component\Routing;
13+
14+
use Symfony\Component\Routing\Exception\InvalidArgumentException;
15+
16+
class Alias
17+
{
18+
private $id;
19+
private $deprecation = [];
20+
21+
public function __construct(string $id)
22+
{
23+
$this->id = $id;
24+
}
25+
26+
/**
27+
* @return static
28+
*/
29+
public function withId(string $id): self
30+
{
31+
$new = clone $this;
32+
33+
$new->id = $id;
34+
35+
return $new;
36+
}
37+
38+
/**
39+
* Returns the target name of this alias.
40+
*
41+
* @return string The target name
42+
*/
43+
public function getId(): string
44+
{
45+
return $this->id;
46+
}
47+
48+
/**
49+
* Whether this alias is deprecated, that means it should not be referenced anymore.
50+
*
51+
* @param string $package The name of the composer package that is triggering the deprecation
52+
* @param string $version The version of the package that introduced the deprecation
53+
* @param string $message The deprecation message to use
54+
*
55+
* @return $this
56+
*
57+
* @throws InvalidArgumentException when the message template is invalid
58+
*/
59+
public function setDeprecated(string $package, string $version, string $message): self
60+
{
61+
if ('' !== $message) {
62+
if (preg_match('#[\r\n]|\*/#', $message)) {
63+
throw new InvalidArgumentException('Invalid characters found in deprecation template.');
64+
}
65+
66+
if (!str_contains($message, '%alias_id%')) {
67+
throw new InvalidArgumentException('The deprecation template must contain the "%alias_id%" placeholder.');
68+
}
69+
}
70+
71+
$this->deprecation = [
72+
'package' => $package,
73+
'version' => $version,
74+
'message' => $message ?: 'The "%alias_id%" route alias is deprecated. You should stop using it, as it will be removed in the future.',
75+
];
76+
77+
return $this;
78+
}
79+
80+
public function isDeprecated(): bool
81+
{
82+
return (bool) $this->deprecation;
83+
}
84+
85+
/**
86+
* @param string $name Route name relying on this alias
87+
*/
88+
public function getDeprecation(string $name): array
89+
{
90+
return [
91+
'package' => $this->deprecation['package'],
92+
'version' => $this->deprecation['version'],
93+
'message' => str_replace('%alias_id%', $name, $this->deprecation['message']),
94+
];
95+
}
96+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
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\Component\Routing\Exception;
13+
14+
class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface
15+
{
16+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
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\Component\Routing\Exception;
13+
14+
class RouteCircularReferenceException extends RuntimeException
15+
{
16+
public function __construct(string $routeId, array $path)
17+
{
18+
parent::__construct(sprintf('Circular reference detected for route "%s", path: "%s".', $routeId, implode(' -> ', $path)));
19+
}
20+
}

Exception/RuntimeException.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
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\Component\Routing\Exception;
13+
14+
class RuntimeException extends \RuntimeException implements ExceptionInterface
15+
{
16+
}

Generator/CompiledUrlGenerator.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,11 @@ public function generate(string $name, array $parameters = [], int $referenceTyp
5050
throw new RouteNotFoundException(sprintf('Unable to generate a URL for the named route "%s" as such route does not exist.', $name));
5151
}
5252

53-
[$variables, $defaults, $requirements, $tokens, $hostTokens, $requiredSchemes] = $this->compiledRoutes[$name];
53+
[$variables, $defaults, $requirements, $tokens, $hostTokens, $requiredSchemes, $deprecations] = $this->compiledRoutes[$name] + [6 => []];
54+
55+
foreach ($deprecations as $deprecation) {
56+
trigger_deprecation($deprecation['package'], $deprecation['version'], $deprecation['message']);
57+
}
5458

5559
if (isset($defaults['_canonical_route']) && isset($defaults['_locale'])) {
5660
if (!\in_array('_locale', $variables, true)) {

Generator/Dumper/CompiledUrlGeneratorDumper.php

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111

1212
namespace Symfony\Component\Routing\Generator\Dumper;
1313

14+
use Symfony\Component\Routing\Exception\RouteCircularReferenceException;
15+
use Symfony\Component\Routing\Exception\RouteNotFoundException;
1416
use Symfony\Component\Routing\Matcher\Dumper\CompiledUrlMatcherDumper;
1517

1618
/**
@@ -35,12 +37,57 @@ public function getCompiledRoutes(): array
3537
$compiledRoute->getTokens(),
3638
$compiledRoute->getHostTokens(),
3739
$route->getSchemes(),
40+
[],
3841
];
3942
}
4043

4144
return $compiledRoutes;
4245
}
4346

47+
public function getCompiledAliases(): array
48+
{
49+
$routes = $this->getRoutes();
50+
$compiledAliases = [];
51+
foreach ($routes->getAliases() as $name => $alias) {
52+
$deprecations = $alias->isDeprecated() ? [$alias->getDeprecation($name)] : [];
53+
$currentId = $alias->getId();
54+
$visited = [];
55+
while (null !== $alias = $routes->getAlias($currentId) ?? null) {
56+
if (false !== $searchKey = array_search($currentId, $visited)) {
57+
$visited[] = $currentId;
58+
59+
throw new RouteCircularReferenceException($currentId, \array_slice($visited, $searchKey));
60+
}
61+
62+
if ($alias->isDeprecated()) {
63+
$deprecations[] = $deprecation = $alias->getDeprecation($currentId);
64+
trigger_deprecation($deprecation['package'], $deprecation['version'], $deprecation['message']);
65+
}
66+
67+
$visited[] = $currentId;
68+
$currentId = $alias->getId();
69+
}
70+
71+
if (null === $target = $routes->get($currentId)) {
72+
throw new RouteNotFoundException(sprintf('Target route "%s" for alias "%s" does not exist.', $currentId, $name));
73+
}
74+
75+
$compiledTarget = $target->compile();
76+
77+
$compiledAliases[$name] = [
78+
$compiledTarget->getVariables(),
79+
$target->getDefaults(),
80+
$target->getRequirements(),
81+
$compiledTarget->getTokens(),
82+
$compiledTarget->getHostTokens(),
83+
$target->getSchemes(),
84+
$deprecations,
85+
];
86+
}
87+
88+
return $compiledAliases;
89+
}
90+
4491
/**
4592
* {@inheritdoc}
4693
*/
@@ -68,6 +115,10 @@ private function generateDeclaredRoutes(): string
68115
$routes .= sprintf("\n '%s' => %s,", $name, CompiledUrlMatcherDumper::export($properties));
69116
}
70117

118+
foreach ($this->getCompiledAliases() as $alias => $properties) {
119+
$routes .= sprintf("\n '%s' => %s,", $alias, CompiledUrlMatcherDumper::export($properties));
120+
}
121+
71122
return $routes;
72123
}
73124
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
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\Component\Routing\Loader\Configurator;
13+
14+
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
15+
use Symfony\Component\Routing\Alias;
16+
17+
class AliasConfigurator
18+
{
19+
private $alias;
20+
21+
public function __construct(Alias $alias)
22+
{
23+
$this->alias = $alias;
24+
}
25+
26+
/**
27+
* Whether this alias is deprecated, that means it should not be called anymore.
28+
*
29+
* @param string $package The name of the composer package that is triggering the deprecation
30+
* @param string $version The version of the package that introduced the deprecation
31+
* @param string $message The deprecation message to use
32+
*
33+
* @return $this
34+
*
35+
* @throws InvalidArgumentException when the message template is invalid
36+
*/
37+
public function deprecate(string $package, string $version, string $message): self
38+
{
39+
$this->alias->setDeprecated($package, $version, $message);
40+
41+
return $this;
42+
}
43+
}

Loader/Configurator/Traits/AddTrait.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
namespace Symfony\Component\Routing\Loader\Configurator\Traits;
1313

14+
use Symfony\Component\Routing\Loader\Configurator\AliasConfigurator;
1415
use Symfony\Component\Routing\Loader\Configurator\CollectionConfigurator;
1516
use Symfony\Component\Routing\Loader\Configurator\RouteConfigurator;
1617
use Symfony\Component\Routing\RouteCollection;
@@ -42,6 +43,11 @@ public function add(string $name, $path): RouteConfigurator
4243
return new RouteConfigurator($this->collection, $route, $this->name, $parentConfigurator, $this->prefixes);
4344
}
4445

46+
public function alias(string $name, string $alias): AliasConfigurator
47+
{
48+
return new AliasConfigurator($this->collection->addAlias($name, $alias));
49+
}
50+
4551
/**
4652
* Adds a route.
4753
*

Loader/XmlFileLoader.php

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,16 @@ protected function parseRoute(RouteCollection $collection, \DOMElement $node, st
118118
throw new \InvalidArgumentException(sprintf('The <route> element in file "%s" must have an "id" attribute.', $path));
119119
}
120120

121+
if ('' !== $alias = $node->getAttribute('alias')) {
122+
$alias = $collection->addAlias($id, $alias);
123+
124+
if ($deprecationInfo = $this->parseDeprecation($node, $path)) {
125+
$alias->setDeprecated($deprecationInfo['package'], $deprecationInfo['version'], $deprecationInfo['message']);
126+
}
127+
128+
return;
129+
}
130+
121131
$schemes = preg_split('/[\s,\|]++/', $node->getAttribute('schemes'), -1, \PREG_SPLIT_NO_EMPTY);
122132
$methods = preg_split('/[\s,\|]++/', $node->getAttribute('methods'), -1, \PREG_SPLIT_NO_EMPTY);
123133

@@ -419,4 +429,41 @@ private function isElementValueNull(\DOMElement $element): bool
419429

420430
return 'true' === $element->getAttributeNS($namespaceUri, 'nil') || '1' === $element->getAttributeNS($namespaceUri, 'nil');
421431
}
432+
433+
/**
434+
* Parses the deprecation elements.
435+
*
436+
* @throws \InvalidArgumentException When the XML is invalid
437+
*/
438+
private function parseDeprecation(\DOMElement $node, string $path): array
439+
{
440+
$deprecatedNode = null;
441+
foreach ($node->childNodes as $child) {
442+
if (!$child instanceof \DOMElement || self::NAMESPACE_URI !== $child->namespaceURI) {
443+
continue;
444+
}
445+
if ('deprecated' !== $child->localName) {
446+
throw new \InvalidArgumentException(sprintf('Invalid child element "%s" defined for alias "%s" in "%s".', $child->localName, $node->getAttribute('id'), $path));
447+
}
448+
449+
$deprecatedNode = $child;
450+
}
451+
452+
if (null === $deprecatedNode) {
453+
return [];
454+
}
455+
456+
if (!$deprecatedNode->hasAttribute('package')) {
457+
throw new \InvalidArgumentException(sprintf('The <deprecated> element in file "%s" must have a "package" attribute.', $path));
458+
}
459+
if (!$deprecatedNode->hasAttribute('version')) {
460+
throw new \InvalidArgumentException(sprintf('The <deprecated> element in file "%s" must have a "version" attribute.', $path));
461+
}
462+
463+
return [
464+
'package' => $deprecatedNode->getAttribute('package'),
465+
'version' => $deprecatedNode->getAttribute('version'),
466+
'message' => trim($deprecatedNode->nodeValue),
467+
];
468+
}
422469
}

0 commit comments

Comments
 (0)