8000 [Routing] Deprecate annotations in favor of attributes · symfony/symfony@6ce15f2 · GitHub
[go: up one dir, main page]

Skip to content

Commit 6ce15f2

Browse files
committed
[Routing] Deprecate annotations in favor of attributes
1 parent 503a7b3 commit 6ce15f2

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;
@@ -1179,6 +1180,13 @@ private function registerRouterConfiguration(array $config, ContainerBuilder $co
11791180
if (!class_exists(Psr4DirectoryLoader::class)) {
11801181
$container->removeDefinition('routing.loader.psr4');
11811182
}
1183+
1184+
if ($this->isInitializedConfigEnabled('annotations') && (new \ReflectionClass(AnnotationClassLoader::class))->hasProperty('reader')) {
1185+
$container->getDefinition('routing.loader.annotation')->setArguments([
1186+
new Reference('annotation_reader'),
1187+
'%kernel.environment%',
1188+
]);
1189+
}
11821190
}
11831191

11841192
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< F438 code class="diff-text syntax-highlighted-line addition">+
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 <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)
0