Skip to content

Commit d8b3cc6

Browse files
committed
feature #58819 [Routing] Allow aliases in #[Route] attribute (damienfern)
This PR was squashed before being merged into the 7.3 branch. Discussion ---------- [Routing] Allow aliases in `#[Route]` attribute | Q | A | ------------- | --- | Branch? | 7.3 | Bug fix? | no | New feature? | yes <!-- please update src/**/CHANGELOG.md files --> | Deprecations? | no <!-- please update UPGRADE-*.md and src/**/CHANGELOG.md files --> | License | MIT While scrolling the [Routing documentation](https://symfony.com/doc/current/routing.html#route-aliasing), I noticed that we can't configure aliases with the `#[Route]` attribute. With this PR, we can. ```php #[Route('/path', name: 'action_with_alias', alias: ['alias', 'completely_different_name'])] public function action() { } ``` <!-- Replace this notice by a description of your feature/bugfix. This will help reviewers and should be a good start for the documentation. Additionally (see https://symfony.com/releases): - Always add tests and ensure they pass. - Bug fixes must be submitted against the lowest maintained branch where they apply (lowest branches are regularly merged to upper ones so they get the fixes too). - Features and deprecations must be submitted against the latest branch. - For new features, provide some code snippets to help understand usage. - Changelog entry should follow https://symfony.com/doc/current/contributing/code/conventions.html#writing-a-changelog-entry - Never break backward compatibility (see https://symfony.com/bc). --> Commits ------- d2ae097 [Routing] Allow aliases in `#[Route]` attribute
2 parents 4fdf3f7 + d2ae097 commit d8b3cc6

13 files changed

+368
-16
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
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\Component\Routing\Attribute;
13+
14+
/**
15+
* This class is meant to be used in {@see Route} to define an alias for a route.
16+
*/
17+
class DeprecatedAlias
18+
{
19+
public function __construct(
20+
private string $aliasName,
21+
private string $package,
22+
private string $version,
23+
private string $message = '',
24+
) {
25+
}
26+
27+
public function getMessage(): string
28+
{
29+
return $this->message;
30+
}
31+
32+
public function getAliasName(): string
33+
{
34+
return $this->aliasName;
35+
}
36+
37+
public function getPackage(): string
38+
{
39+
return $this->package;
40+
}
41+
42+
public function getVersion(): string
43+
{
44+
return $this->version;
45+
}
46+
}

src/Symfony/Component/Routing/Attribute/Route.php

+38-15
Original file line numberDiff line numberDiff line change
@@ -22,23 +22,28 @@ class Route
2222
private array $localizedPaths = [];
2323
private array $methods;
2424
private array $schemes;
25+
/**
26+
* @var (string|DeprecatedAlias)[]
27+
*/
28+
private array $aliases = [];
2529

2630
/**
27-
* @param string|array<string,string>|null $path The route path (i.e. "/user/login")
28-
* @param string|null $name The route name (i.e. "app_user_login")
29-
* @param array<string|\Stringable> $requirements Requirements for the route attributes, @see https://symfony.com/doc/current/routing.html#parameters-validation
30-
* @param array<string, mixed> $options Options for the route (i.e. ['prefix' => '/api'])
31-
* @param array<string, mixed> $defaults Default values for the route attributes and query parameters
32-
* @param string|null $host The host for which this route should be active (i.e. "localhost")
33-
* @param string|string[] $methods The list of HTTP methods allowed by this route
34-
* @param string|string[] $schemes The list of schemes allowed by this route (i.e. "https")
35-
* @param string|null $condition An expression that must evaluate to true for the route to be matched, @see https://symfony.com/doc/current/routing.html#matching-expressions
36-
* @param int|null $priority The priority of the route if multiple ones are defined for the same path
37-
* @param string|null $locale The locale accepted by the route
38-
* @param string|null $format The format returned by the route (i.e. "json", "xml")
39-
* @param bool|null $utf8 Whether the route accepts UTF-8 in its parameters
40-
* @param bool|null $stateless Whether the route is defined as stateless or stateful, @see https://symfony.com/doc/current/routing.html#stateless-routes
41-
* @param string|null $env The env in which the route is defined (i.e. "dev", "test", "prod")
31+
* @param string|array<string,string>|null $path The route path (i.e. "/user/login")
32+
* @param string|null $name The route name (i.e. "app_user_login")
33+
* @param array<string|\Stringable> $requirements Requirements for the route attributes, @see https://symfony.com/doc/current/routing.html#parameters-validation
34+
* @param array<string, mixed> $options Options for the route (i.e. ['prefix' => '/api'])
35+
* @param array<string, mixed> $defaults Default values for the route attributes and query parameters
36+
* @param string|null $host The host for which this route should be active (i.e. "localhost")
37+
* @param string|string[] $methods The list of HTTP methods allowed by this route
38+
* @param string|string[] $schemes The list of schemes allowed by this route (i.e. "https")
39+
* @param string|null $condition An expression that must evaluate to true for the route to be matched, @see https://symfony.com/doc/current/routing.html#matching-expressions
40+
* @param int|null $priority The priority of the route if multiple ones are defined for the same path
41+
* @param string|null $locale The locale accepted by the route
42+
* @param string|null $format The format returned by the route (i.e. "json", "xml")
43+
* @param bool|null $utf8 Whether the route accepts UTF-8 in its parameters
44+
* @param bool|null $stateless Whether the route is defined as stateless or stateful, @see https://symfony.com/doc/current/routing.html#stateless-routes
45+
* @param string|null $env The env in which the route is defined (i.e. "dev", "test", "prod")
46+
* @param string|DeprecatedAlias|(string|DeprecatedAlias)[] $alias The list of aliases for this route
4247
*/
4348
public function __construct(
4449
string|array|null $path = null,
@@ -56,6 +61,7 @@ public function __construct(
5661
?bool $utf8 = null,
5762
?bool $stateless = null,
5863
private ?string $env = null,
64+
string|DeprecatedAlias|array $alias = [],
5965
) {
6066
if (\is_array($path)) {
6167
$this->localizedPaths = $path;
@@ -64,6 +70,7 @@ public function __construct(
6470
}
6571
$this->setMethods($methods);
6672
$this->setSchemes($schemes);
73+
$this->setAliases($alias);
6774

6875
if (null !== $locale) {
6976
$this->defaults['_locale'] = $locale;
@@ -201,6 +208,22 @@ public function getEnv(): ?string
201208
{
202209
return $this->env;
203210
}
211+
212+
/**
213+
* @return (string|DeprecatedAlias)[]
214+
*/
215+
public function getAliases(): array
216+
{
217+
return $this->aliases;
218+
}
219+
220+
/**
221+
* @param string|DeprecatedAlias|(string|DeprecatedAlias)[] $aliases
222+
*/
223+
public function setAliases(string|DeprecatedAlias|array $aliases): void
224+
{
225+
$this->aliases = \is_array($aliases) ? $aliases : [$aliases];
226+
}
204227
}
205228

206229
if (!class_exists(\Symfony\Component\Routing\Annotation\Route::class, false)) {

src/Symfony/Component/Routing/CHANGELOG.md

+5
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
CHANGELOG
22
=========
33

4+
7.3
5+
---
6+
7+
* Allow aliases and deprecations in `#[Route]` attribute
8+
49
7.2
510
---
611

src/Symfony/Component/Routing/Loader/AttributeClassLoader.php

+24
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@
1414
use Symfony\Component\Config\Loader\LoaderInterface;
1515
use Symfony\Component\Config\Loader\LoaderResolverInterface;
1616
use Symfony\Component\Config\Resource\FileResource;
17+
use Symfony\Component\Routing\Attribute\DeprecatedAlias;
1718
use Symfony\Component\Routing\Attribute\Route as RouteAttribute;
19+
use Symfony\Component\Routing\Exception\InvalidArgumentException;
1820
use Symfony\Component\Routing\Exception\LogicException;
1921
use Symfony\Component\Routing\Route;
2022
use Symfony\Component\Routing\RouteCollection;
@@ -107,6 +109,15 @@ public function load(mixed $class, ?string $type = null): RouteCollection
107109
return $collection;
108110
}
109111
$fqcnAlias = false;
112+
113+
if (!$class->hasMethod('__invoke')) {
114+
foreach ($this->getAttributes($class) as $attr) {
115+
if ($attr->getAliases()) {
116+
throw new InvalidArgumentException(\sprintf('Route aliases cannot be used on non-invokable class "%s".', $class->getName()));
117+
}
118+
}
119+
}
120+
110121
foreach ($class->getMethods() as $method) {
111122
$this->defaultRouteIndex = 0;
112123
$routeNamesBefore = array_keys($collection->all());
@@ -230,6 +241,19 @@ protected function addRoute(RouteCollection $collection, object $attr, array $gl
230241
} else {
231242
$collection->add($name, $route, $priority);
232243
}
244+
foreach ($attr->getAliases() as $aliasAttribute) {
245+
if ($aliasAttribute instanceof DeprecatedAlias) {
246+
$alias = $collection->addAlias($aliasAttribute->getAliasName(), $name);
247+
$alias->setDeprecated(
248+
$aliasAttribute->getPackage(),
249+
$aliasAttribute->getVersion(),
250+
$aliasAttribute->getMessage()
251+
);
252+
continue;
253+
}
254+
255+
$collection->addAlias($aliasAttribute, $name);
256+
}
233257
}
234258
}
235259

src/Symfony/Component/Routing/Tests/Attribute/RouteTest.php

+2-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
* file that was distributed with this source code.
1010
*/
1111

12-
namespace Symfony\Component\Routing\Tests\Annotation;
12+
namespace Symfony\Component\Routing\Tests\Attribute;
1313

1414
use PHPUnit\Framework\TestCase;
1515
use Symfony\Component\Routing\Attribute\Route;
@@ -40,6 +40,7 @@ public static function getValidParameters(): iterable
4040
['methods', 'getMethods', ['GET', 'POST']],
4141
['host', 'getHost', '{locale}.example.com'],
4242
['condition', 'getCondition', 'context.getMethod() == \'GET\''],
43+
['alias', 'getAliases', ['alias', 'completely_different_name']],
4344
];
4445
}
4546
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
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\Component\Routing\Tests\Fixtures\AttributeFixtures;
13+
14+
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
15+
use Symfony\Component\Routing\Attribute\Route;
16+
17+
#[Route('/hello', alias: ['alias', 'completely_different_name'])]
18+
class AliasClassController
19+
{
20+
#[Route('/world')]
21+
public function actionWorld()
22+
{
23+
}
24+
25+
#[Route('/symfony')]
26+
public function actionSymfony()
27+
{
28+
}
29+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
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\Component\Routing\Tests\Fixtures\AttributeFixtures;
13+
14+
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
15+
use Symfony\Component\Routing\Attribute\Route;
16+
17+
#[Route('/path', name:'invokable_path', alias: ['alias', 'completely_different_name'])]
18+
class AliasInvokableController
19+
{
20+
public function __invoke()
21+
{
22+
}
23+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
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\Component\Routing\Tests\Fixtures\AttributeFixtures;
13+
14+
use Symfony\Component\Routing\Attribute\Route;
15+
16+
class AliasRouteController
17+
{
18+
#[Route('/path', name: 'action_with_alias', alias: ['alias', 'completely_different_name'])]
19+
public function action()
20+
{
21+
}
22+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
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\Component\Routing\Tests\Fixtures\AttributeFixtures;
13+
14+
use Symfony\Component\Routing\Attribute\DeprecatedAlias;
15+
use Symfony\Component\Routing\Attribute\Route;
16+
17+
class DeprecatedAliasCustomMessageRouteController
18+
{
19+
20+
#[Route('/path', name: 'action_with_deprecated_alias', alias: new DeprecatedAlias('my_other_alias_deprecated', 'MyBundleFixture', '1.0', message: '%alias_id% alias is deprecated.'))]
21+
public function action()
22+
{
23+
}
24+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
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\Component\Routing\Tests\Fixtures\AttributeFixtures;
13+
14+
use Symfony\Component\Routing\Attribute\DeprecatedAlias;
15+
use Symfony\Component\Routing\Attribute\Route;
16+
17+
class DeprecatedAliasRouteController
18+
{
19+
#[Route('/path', name: 'action_with_deprecated_alias', alias: new DeprecatedAlias('my_other_alias_deprecated', 'MyBundleFixture', '1.0'))]
20+
public function action()
21+
{
22+
}
23+
}

src/Symfony/Component/Routing/Tests/Fixtures/AttributeFixtures/FooController.php

+5
Original file line numberDiff line numberDiff line change
@@ -55,4 +55,9 @@ public function host()
5555
public function condition()
5656
{
5757
}
58+
59+
#[Route(alias: ['alias', 'completely_different_name'])]
60+
public function alias()
61+
{
62+
}
5863
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
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\Component\Routing\Tests\Fixtures\AttributeFixtures;
13+
14+
use Symfony\Component\Routing\Attribute\DeprecatedAlias;
15+
use Symfony\Component\Routing\Attribute\Route;
16+
17+
class MultipleDeprecatedAliasRouteController
18+
{
19+
#[Route('/path', name: 'action_with_multiple_deprecated_alias', alias: [
20+
new DeprecatedAlias('my_first_alias_deprecated', 'MyFirstBundleFixture', '1.0'),
21+
new DeprecatedAlias('my_second_alias_deprecated', 'MySecondBundleFixture', '2.0'),
22+
new DeprecatedAlias('my_third_alias_deprecated', 'SurprisedThirdBundleFixture', '3.0'),
23+
])]
24+
public function action()
25+
{
26+
}
27+
}

0 commit comments

Comments
 (0)