Skip to content

Commit ec7c82a

Browse files
committed
[Routing] Deprecate annotations in favor of attributes
1 parent 80f1096 commit ec7c82a

16 files changed

+212
-230
lines changed

UPGRADE-6.4.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ Routing
5555
-------
5656

5757
* Add native return type to `AnnotationClassLoader::setResolver()`
58+
* Deprecate Doctrine annotations support in favor of native attributes
59+
* The constructor signature of `AnnotationClassLoader` has changed to `__construct(?string $env = null)`, passing an annotation reader as first argument is deprecated
5860

5961
Security
6062
--------

src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1179,6 +1179,13 @@ private function registerRouterConfiguration(array $config, ContainerBuilder $co
11791179
if (!class_exists(Psr4DirectoryLoader::class)) {
11801180
$container->removeDefinition('routing.loader.psr4');
11811181
}
1182+
1183+
if ($this->isInitializedConfigEnabled('annotations')) {
1184+
$container->getDefinition('routing.loader.annotation')->setArguments([
1185+
new Reference('annotation_reader'),
1186+
'%kernel.environment%',
1187+
]);
1188+
}
11821189
}
11831190

11841191
private function registerSessionConfiguration(array $config, ContainerBuilder $container, PhpFileLoader $loader): void

src/Symfony/Bundle/FrameworkBundle/Resources/config/routing.php

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,6 @@
9494

9595
->set('routing.loader.annotation', AnnotatedRouteControllerLoader::class)
9696
->args([
97-
service('annotation_reader')->nullOnInvalid(),
9897
'%kernel.environment%',
9998
])
10099
->tag('routing.loader', ['priority' => -10])

src/Symfony/Component/Routing/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ CHANGELOG
66

77
* Add FQCN and FQCN::method aliases for routes loaded from attributes/annotations when applicable
88
* Add native return type to `AnnotationClassLoader::setResolver()`
9+
* Deprecate Doctrine annotations support in favor of native attributes
10+
* The constructor signature of `AnnotationClassLoader` has changed to `__construct(?string $env = null)`, passing an annotation reader as first argument is deprecated
911

1012
6.2
1113
---

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

Lines changed: 77 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -26,33 +26,14 @@
2626
* time, this method should define some PHP callable to be called for the route
2727
* (a controller in MVC speak).
2828
*
29-
* The @Route annotation can be set on the class (for global parameters),
29+
* The #[Route] attribute can be set on the class (for global parameters),
3030
* and on each method.
3131
*
32-
* The @Route annotation main value is the route path. The annotation also
32+
* The #[Route] attribute main value is the route path. The attribute also
3333
* recognizes several parameters: requirements, options, defaults, schemes,
3434
* methods, host, and name. The name parameter is mandatory.
3535
* Here is an example of how you should be able to use it:
36-
* /**
37-
* * @Route("/Blog")
38-
* * /
39-
* class Blog
40-
* {
41-
* /**
42-
* * @Route("/", name="blog_index")
43-
* * /
44-
* public function index()
45-
* {
46-
* }
47-
* /**
48-
* * @Route("/{id}", name="blog_post", requirements = {"id" = "\d+"})
49-
* * /
50-
* public function show()
51-
* {
52-
* }
53-
* }
5436
*
55-
* On PHP 8, the annotation class can be used as an attribute as well:
5637
* #[Route('/Blog')]
5738
* class Blog
5839
* {
@@ -71,23 +52,55 @@
7152
*/
7253
abstract class AnnotationClassLoader implements LoaderInterface
7354
{
55+
/**
56+
* @var Reader|null
57+
*
58+
* @deprecated in Symfony 6.4, this property will be removed in Symfony 7.
59+
*/
7460
protected $reader;
61+
62+
/**
63+
* @var string|null
64+
*
65+
* @internal since Symfony 6.4, this property will be private in Symfony 7.
66+
*/
7567
protected $env;
7668

7769
/**
7870
* @var string
71+
*
72+
* @internal since Symfony 6.4, this property will be private in Symfony 7.
7973
*/
8074
protected $routeAnnotationClass = RouteAnnotation::class;
8175

8276
/**
8377
* @var int
78+
*
79+
* @internal since Symfony 6.4, this property will be private in Symfony 7.
8480
*/
8581
protected $defaultRouteIndex = 0;
8682

87-
public function __construct(Reader $reader = null, string $env = null)
83+
private bool $hasDeprecatedAnnotations = false;
84+
85+
/**
86+
* @param string|null $env
87+
*/
88+
public function __construct($env = null)
8889
{
89-
$this->reader = $reader;
90-
$this->env = $env;
90+
if ($env instanceof Reader || null === $env && \func_num_args() > 1 && null !== func_get_arg(1)) {
91+
trigger_deprecation('symfony/routing', '6.4', 'Passing an instance of "%s" as first and the environment as second argument to "%s" is deprecated. Pass the environment as first argument instead.', Reader::class, __METHOD__);
92+
93+
$this->reader = $env;
94+
$env = \func_num_args() > 1 ? func_get_arg(1) : null;
95+
}
96+
97+
if (\is_string($env) || null === $env) {
98+
$this->env = $env;
99+
} elseif ($env instanceof \Stringable || \is_scalar($env)) {
100+
$this->env = (string) $env;
101+
} else {
102+
throw new \TypeError(__METHOD__.sprintf(': Parameter $env was expected to be a string or null, "%s" given.', get_debug_type($env)));
103+
}
91104
}
92105

93106
/**
@@ -116,43 +129,48 @@ public function load(mixed $class, string $type = null): RouteCollection
116129
throw new \InvalidArgumentException(sprintf('Annotations from class "%s" cannot be read as it is abstract.', $class->getName()));
117130
}
118131

119-
$globals = $this->getGlobals($class);
120-
121-
$collection = new RouteCollection();
122-
$collection->addResource(new FileResource($class->getFileName()));
132+
$this->hasDeprecatedAnnotations = false;
123133

124-
if ($globals['env'] && $this->env !== $globals['env']) {
125-
return $collection;
126-
}
134+
try {
135+
$globals = $this->getGlobals($class);
136+
$collection = new RouteCollection();
137+
$collection->addResource(new FileResource($class->getFileName()));
138+
if ($globals['env'] && $this->env !== $globals['env']) {
139+
return $collection;
140+
}
141+
$fqcnAlias = false;
142+
foreach ($class->getMethods() as $method) {
143+
$this->defaultRouteIndex = 0;
144+
$routeNamesBefore = array_keys($collection->all());
145+
foreach ($this->getAnnotations($method) as $annot) {
146+
$this->addRoute($collection, $annot, $globals, $class, $method);
147+
if ('__invoke' === $method->name) {
148+
$fqcnAlias = true;
149+
}
150+
}
127151

128-
$fqcnAlias = false;
129-
foreach ($class->getMethods() as $method) {
130-
$this->defaultRouteIndex = 0;
131-
$routeNamesBefore = array_keys($collection->all());
132-
foreach ($this->getAnnotations($method) as $annot) {
133-
$this->addRoute($collection, $annot, $globals, $class, $method);
134-
if ('__invoke' === $method->name) {
152+
if (1 === $collection->count() - \count($routeNamesBefore)) {
153+
$newRouteName = current(array_diff(array_keys($collection->all()), $routeNamesBefore));
154+
$collection->addAlias(sprintf('%s::%s', $class->name, $method->name), $newRouteName);
155+
}
156+
}
157+
if (0 === $collection->count() && $class->hasMethod('__invoke')) {
158+
$globals = $this->resetGlobals();
159+
foreach ($this->getAnnotations($class) as $annot) {
160+
$this->addRoute($collection, $annot, $globals, $class, $class->getMethod('__invoke'));
135161
$fqcnAlias = true;
136162
}
137163
}
138-
139-
if (1 === $collection->count() - \count($routeNamesBefore)) {
140-
$newRouteName = current(array_diff(array_keys($collection->all()), $routeNamesBefore));
141-
$collection->addAlias(sprintf('%s::%s', $class->name, $method->name), $newRouteName);
164+
if ($fqcnAlias && 1 === $collection->count()) {
165+
$collection->addAlias($class->name, $invokeRouteName = key($collection->all()));
166+
$collection->addAlias(sprintf('%s::__invoke', $class->name), $invokeRouteName);
142167
}
143-
}
144168

145-
if (0 === $collection->count() && $class->hasMethod('__invoke')) {
146-
$globals = $this->resetGlobals();
147-
foreach ($this->getAnnotations($class) as $annot) {
148-
$this->addRoute($collection, $annot, $globals, $class, $class->getMethod('__invoke'));
149-
$fqcnAlias = true;
169+
if ($this->hasDeprecatedAnnotations) {
170+
trigger_deprecation('symfony/routing', '6.4', 'Class "%s" uses Doctrine Annotations to configure routes, which is deprecated. Use PHP attributes instead.', $class->getName());
150171
}
151-
}
152-
153-
if ($fqcnAlias && 1 === $collection->count()) {
154-
$collection->addAlias($class->name, $invokeRouteName = key($collection->all()));
155-
$collection->addAlias(sprintf('%s::__invoke', $class->name), $invokeRouteName);
172+
} finally {
173+
$this->hasDeprecatedAnnotations = false;
156174
}
157175

158176
return $collection;
@@ -279,7 +297,7 @@ protected function getDefaultRouteName(\ReflectionClass $class, \ReflectionMetho
279297
}
280298

281299
/**
282-
* @return array
300+
* @return array<string, mixed>
283301
*/
284302
protected function getGlobals(\ReflectionClass $class)
285303
{
@@ -289,8 +307,8 @@ protected function getGlobals(\ReflectionClass $class)
289307
if ($attribute = $class->getAttributes($this->routeAnnotationClass, \ReflectionAttribute::IS_INSTANCEOF)[0] ?? null) {
290308
$annot = $attribute->newInstance();
291309
}
292-
if (!$annot && $this->reader) {
293-
$annot = $this->reader->getClassAnnotation($class, $this->routeAnnotationClass);
310+
if (!$annot && $annot = $this->reader?->getClassAnnotation($class, $this->routeAnnotationClass)) {
311+
$this->hasDeprecatedAnnotations = true;
294312
}
295313

296314
if ($annot) {
@@ -377,11 +395,9 @@ protected function createRoute(string $path, array $defaults, array $requirement
377395
abstract protected function configureRoute(Route $route, \ReflectionClass $class, \ReflectionMethod $method, object $annot);
378396

379397
/**
380-
* @param \ReflectionClass|\ReflectionMethod $reflection
381-
*
382398
* @return iterable<int, RouteAnnotation>
383399
*/
384-
private function getAnnotations(object $reflection): iterable
400+
private function getAnnotations(\ReflectionClass|\ReflectionMethod $reflection): iterable
385401
{
386402
foreach ($reflection->getAttributes($this->routeAnnotationClass, \ReflectionAttribute::IS_INSTANCEOF) as $attribute) {
387403
yield $attribute->newInstance();
@@ -397,6 +413,8 @@ private function getAnnotations(object $reflection): iterable
397413

398414
foreach ($annotations as $annotation) {
399415
if ($annotation instanceof $this->routeAnnotationClass) {
416+
$this->hasDeprecatedAnnotations = true;
417+
400418
yield $annotation;
401419
}
402420
}

src/Symfony/Component/Routing/Tests/Fixtures/AnnotatedClasses/AbstractClass.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,13 @@
1111

1212
namespace Symfony\Component\Routing\Tests\Fixtures\AnnotatedClasses;
1313

14+
use Symfony\Component\Routing\Annotation\Route;
15+
1416
abstract class AbstractClass
1517
{
1618
abstract public function abstractRouteAction();
1719

20+
#[Route('/path/to/route')]
1821
public function routeAction($arg1, $arg2 = 'defaultValue2', $arg3 = 'defaultValue3')
1922
{
2023
}

src/Symfony/Component/Routing/Tests/Fixtures/OtherAnnotatedClasses/AnonymousClassInTrait.php

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

1212
namespace Symfony\Component\Routing\Tests\Fixtures\OtherAnnotatedClasses;
1313

14+
use Symfony\Component\Routing\Annotation\Route;
15+
1416
trait AnonymousClassInTrait
1517
{
1618
public function test()
1719
{
1820
return new class() {
21+
#[Route('/path/to/route')]
1922
public function foo()
2023
{
2124
}

src/Symfony/Component/Routing/Tests/Fixtures/OtherAnnotatedClasses/VariadicClass.php

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

1212
namespace Symfony\Component\Routing\Tests\Fixtures\OtherAnnotatedClasses;
1313

14+
use Symfony\Component\Routing\Annotation\Route;
15+
1416
class VariadicClass
1517
{
18+
#[Route('/path/to/{id}')]
1619
public function routeAction(...$params)
1720
{
1821
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
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;
13+
14+
use Symfony\Component\Routing\Loader\AnnotationClassLoader;
15+
use Symfony\Component\Routing\Route;
16+
use Symfony\Component\Routing\RouteCollection;
17+
18+
final class TraceableAnnotationClassLoader extends AnnotationClassLoader
19+
{
20+
/** @var list<string> */
21+
public array $foundClasses = [];
22+
23+
public function load(mixed $class, string $type = null): RouteCollection
24+
{
25+
if (!is_string($class)) {
26+
throw new \InvalidArgumentException(sprintf('Expected string, got "%s"', get_debug_type($class)));
27+
}
28+
29+
$this->foundClasses[] = $class;
30+
31+
return parent::load($class, $type);
32+
}
33+
34+
protected function configureRoute(Route $route, \ReflectionClass $class, \ReflectionMethod $method, object $annot): void
35+
{
36+
}
37+
}

src/Symfony/Component/Routing/Tests/Loader/AbstractAnnotationLoaderTestCase.php

Lines changed: 0 additions & 34 deletions
This file was deleted.

src/Symfony/Component/Routing/Tests/Loader/AnnotationClassLoaderTestCase.php

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,7 @@
1818

1919
abstract class AnnotationClassLoaderTestCase extends TestCase
2020
{
21-
/**
22-
* @var AnnotationClassLoader
23-
*/
24-
protected $loader;
21+
protected AnnotationClassLoader $loader;
2522

2623
/**
2724
* @dataProvider provideTestSupportsChecksResource
@@ -31,7 +28,7 @@ public function testSupportsChecksResource($resource, $expectedSupports)
3128
$this->assertSame($expectedSupports, $this->loader->supports($resource), '->supports() returns true if the resource is loadable');
3229
}
3330

34-
public static function provideTestSupportsChecksResource()
31+
public static function provideTestSupportsChecksResource(): array
3532
{
3633
return [
3734
['class', true],

0 commit comments

Comments
 (0)