Skip to content

Commit 32d4dfa

Browse files
feature symfony#49358 [Routing] Deprecate annotations in favor of attributes (derrabus)
This PR was merged into the 6.4 branch. Discussion ---------- [Routing] Deprecate annotations in favor of attributes | Q | A | ------------- | --- | Branch? | 6.3 | Bug fix? | no | New feature? | yes | Deprecations? | yes | Tickets | Follows symfony#50888 | License | MIT | Doc PR | TODO, see symfony/symfony-docs#18589 This PR deprecates the integration of Doctrine Annotations for the Routing component. Attributes are the way to go now if we want to annotate controllers with routing information. Existing applications can be migrated easily using [Rector](https://getrector.com/blog/how-to-upgrade-annotations-to-attributes). Thus I believe that keeping support for oldschool Doctrine Annotations is not necessary anymore. If this PR is accepted, I would work on a follow-up that renames all `Annotation*Loader` classes because the names of those classes are probably misleading as soon as they support attributes only. Commits ------- 6ce15f2 [Routing] Deprecate annotations in favor of attributes
2 parents 8e36ac6 + 6ce15f2 commit 32d4dfa

16 files changed

+211
-230
lines changed

UPGRADE-6.4.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@ Routing
6464
-------
6565

6666
* Add native return type to `AnnotationClassLoader::setResolver()`
67+
* Deprecate Doctrine annotations support in favor of native attributes
68+
* Change the constructor signature of `AnnotationClassLoader` to `__construct(?string $env = null)`, passing an annotation reader as first argument is deprecated
6769

6870
Security
6971
--------

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,7 @@
144144
use Symfony\Component\RateLimiter\Storage\CacheStorage;
145145
use Symfony\Component\RemoteEvent\Attribute\AsRemoteEventConsumer;
146146
use Symfony\Component\RemoteEvent\RemoteEvent;
147+
use Symfony\Component\Routing\Loader\AnnotationClassLoader;
147148
use Symfony\Component\Routing\Loader\Psr4DirectoryLoader;
148149
use Symfony\Component\Scheduler\Attribute\AsSchedule;
149150
use Symfony\Component\Scheduler\Messenger\SchedulerTransportFactory;
@@ -1180,6 +1181,13 @@ private function registerRouterConfiguration(array $config, ContainerBuilder $co
11801181
if (!class_exists(Psr4DirectoryLoader::class)) {
11811182
$container->removeDefinition('routing.loader.psr4');
11821183
}
1184+
1185+
if ($this->isInitializedConfigEnabled('annotations') && (new \ReflectionClass(AnnotationClassLoader::class))->hasProperty('reader')) {
1186+
$container->getDefinition('routing.loader.annotation')->setArguments([
1187+
new Reference('annotation_reader'),
1188+
'%kernel.environment%',
1189+
]);
1190+
}
11831191
}
11841192

11851193
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/Bundle/FrameworkBundle/composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
"symfony/polyfill-mbstring": "~1.0",
3131
"symfony/filesystem": "^5.4|^6.0|^7.0",
3232
"symfony/finder": "^5.4|^6.0|^7.0",
33-
"symfony/routing": "^6.1|^7.0"
33+
"symfony/routing": "^6.4|^7.0"
3434
},
3535
"require-dev": {
3636
"doctrine/annotations": "^1.13.1|^2",

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+
* Change the constructor signature of `AnnotationClassLoader` 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: 71 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,7 +52,16 @@
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+
*/
7565
protected $env;
7666

7767
/**
@@ -84,10 +74,27 @@ abstract class AnnotationClassLoader implements LoaderInterface
8474
*/
8575
protected $defaultRouteIndex = 0;
8676

87-
public function __construct(Reader $reader = null, string $env = null)
77+
private bool $hasDeprecatedAnnotations = false;
78+
79+
/**
80+
* @param string|null $env
81+
*/
82+
public function __construct($env = null)
8883
{
89-
$this->reader = $reader;
90-
$this->env = $env;
84+
if ($env instanceof Reader || null === $env && \func_num_args() > 1 && null !== func_get_arg(1)) {
85+
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__);
86+
87+
$this->reader = $env;
88+
$env = \func_num_args() > 1 ? func_get_arg(1) : null;
89+
}
90+
91+
if (\is_string($env) || null === $env) {
92+
$this->env = $env;
93+
} elseif ($env instanceof \Stringable || \is_scalar($env)) {
94+
$this->env = (string) $env;
95+
} else {
96+
throw new \TypeError(__METHOD__.sprintf(': Parameter $env was expected to be a string or null, "%s" given.', get_debug_type($env)));
97+
}
9198
}
9299

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

119-
$globals = $this->getGlobals($class);
126+
$this->hasDeprecatedAnnotations = false;
120127

121-
$collection = new RouteCollection();
122-
$collection->addResource(new FileResource($class->getFileName()));
123-
124-
if ($globals['env'] && $this->env !== $globals['env']) {
125-
return $collection;
126-
}
128+
try {
129+
$globals = $this->getGlobals($class);
130+
$collection = new RouteCollection();
131+
$collection->addResource(new FileResource($class->getFileName()));
132+
if ($globals['env'] && $this->env !== $globals['env']) {
133+
return $collection;
134+
}
135+
$fqcnAlias = false;
136+
foreach ($class->getMethods() as $method) {
137+
$this->defaultRouteIndex = 0;
138+
$routeNamesBefore = array_keys($collection->all());
139+
foreach ($this->getAnnotations($method) as $annot) {
140+
$this->addRoute($collection, $annot, $globals, $class, $method);
141+
if ('__invoke' === $method->name) {
142+
$fqcnAlias = true;
143+
}
144+
}
127145

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) {
146+
if (1 === $collection->count() - \count($routeNamesBefore)) {
147+
$newRouteName = current(array_diff(array_keys($collection->all()), $routeNamesBefore));
148+
$collection->addAlias(sprintf('%s::%s', $class->name, $method->name), $newRouteName);
149+
}
150+
}
151+
if (0 === $collection->count() && $class->hasMethod('__invoke')) {
152+
$globals = $this->resetGlobals();
153+
foreach ($this->getAnnotations($class) as $annot) {
154+
$this->addRoute($collection, $annot, $globals, $class, $class->getMethod('__invoke'));
135155
$fqcnAlias = true;
136156
}
137157
}
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);
158+
if ($fqcnAlias && 1 === $collection->count()) {
159+
$collection->addAlias($class->name, $invokeRouteName = key($collection->all()));
160+
$collection->addAlias(sprintf('%s::__invoke', $class->name), $invokeRouteName);
142161
}
143-
}
144162

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;
163+
if ($this->hasDeprecatedAnnotations) {
164+
trigger_deprecation('symfony/routing', '6.4', 'Class "%s" uses Doctrine Annotations to configure routes, which is deprecated. Use PHP attributes instead.', $class->getName());
150165
}
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);
166+
} finally {
167+
$this->hasDeprecatedAnnotations = false;
156168
}
157169

158170
return $collection;
@@ -279,7 +291,7 @@ protected function getDefaultRouteName(\ReflectionClass $class, \ReflectionMetho
279291
}
280292

281293
/**
282-
* @return array
294+
* @return array<string, mixed>
283295
*/
284296
protected function getGlobals(\ReflectionClass $class)
285297
{
@@ -289,8 +301,8 @@ protected function getGlobals(\ReflectionClass $class)
289301
if ($attribute = $class->getAttributes($this->routeAnnotationClass, \ReflectionAttribute::IS_INSTANCEOF)[0] ?? null) {
290302
$annot = $attribute->newInstance();
291303
}
292-
if (!$annot && $this->reader) {
293-
$annot = $this->reader->getClassAnnotation($class, $this->routeAnnotationClass);
304+
if (!$annot && $annot = $this->reader?->getClassAnnotation($class, $this->routeAnnotationClass)) {
305+
$this->hasDeprecatedAnnotations = true;
294306
}
295307

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

379391
/**
380-
* @param \ReflectionClass|\ReflectionMethod $reflection
381-
*
382392
* @return iterable<int, RouteAnnotation>
383393
*/
384-
private function getAnnotations(object $reflection): iterable
394+
private function getAnnotations(\ReflectionClass|\ReflectionMethod $reflection): iterable
385395
{
386396
foreach ($reflection->getAttributes($this->routeAnnotationClass, \ReflectionAttribute::IS_INSTANCEOF) as $attribute) {
387397
yield $attribute->newInstance();
@@ -397,6 +407,8 @@ private function getAnnotations(object $reflection): iterable
397407

398408
foreach ($annotations as $annotation) {
399409
if ($annotation instanceof $this->routeAnnotationClass) {
410+
$this->hasDeprecatedAnnotations = true;
411+
400412
yield $annotation;
401413
}
402414
}

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/{arg1}')]
1821
public function routeAction($arg1, $arg2 = 'defaultValue2', $arg3 = 'defaultValue3')
1922
{
2023
}

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 <[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\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)