Skip to content

Commit 38b3061

Browse files
committed
[Routing] Deprecate annotations in favor of attributes
1 parent e869eb6 commit 38b3061

12 files changed

+197
-229
lines changed

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

Lines changed: 73 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,51 @@
7152
*/
7253
abstract class AnnotationClassLoader implements LoaderInterface
7354
{
55+
/**
56+
* @var Reader|null
57+
* @deprecated in Symfony 6.4, this property will be removed in Symfony 7.
58+
*/
7459
protected $reader;
60+
61+
/**
62+
* @var string|null
63+
* @internal since Symfony 6.4, this property will be private in Symfony 7.
64+
*/
7565
protected $env;
7666

7767
/**
7868
* @var string
69+
* @internal since Symfony 6.4, this property will be private in Symfony 7.
7970
*/
8071
protected $routeAnnotationClass = RouteAnnotation::class;
8172

8273
/**
8374
* @var int
75+
* @internal since Symfony 6.4, this property will be private in Symfony 7.
8476
*/
8577
protected $defaultRouteIndex = 0;
8678

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

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

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

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

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) {
148+
if (1 === $collection->count() - \count($routeNamesBefore)) {
149+
$newRouteName = current(array_diff(array_keys($collection->all()), $routeNamesBefore));
150+
$collection->addAlias(sprintf('%s::%s', $class->name, $method->name), $newRouteName);
151+
}
152+
}
153+
if (0 === $collection->count() && $class->hasMethod('__invoke')) {
154+
$globals = $this->resetGlobals();
155+
foreach ($this->getAnnotations($class) as $annot) {
156+
$this->addRoute($collection, $annot, $globals, $class, $class->getMethod('__invoke'));
135157
$fqcnAlias = true;
136158
}
137159
}
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);
160+
if ($fqcnAlias && 1 === $collection->count()) {
161+
$collection->addAlias($class->name, $invokeRouteName = key($collection->all()));
162+
$collection->addAlias(sprintf('%s::__invoke', $class->name), $invokeRouteName);
142163
}
143-
}
144164

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

158172
return $collection;
@@ -282,7 +296,7 @@ protected function getDefaultRouteName(\ReflectionClass $class, \ReflectionMetho
282296
}
283297

284298
/**
285-
* @return array
299+
* @return array<string, mixed>
286300
*/
287301
protected function getGlobals(\ReflectionClass $class)
288302
{
@@ -292,8 +306,8 @@ protected function getGlobals(\ReflectionClass $class)
292306
if ($attribute = $class->getAttributes($this->routeAnnotationClass, \ReflectionAttribute::IS_INSTANCEOF)[0] ?? null) {
293307
$annot = $attribute->newInstance();
294308
}
295-
if (!$annot && $this->reader) {
296-
$annot = $this->reader->getClassAnnotation($class, $this->routeAnnotationClass);
309+
if (!$annot && $annot = $this->reader?->getClassAnnotation($class, $this->routeAnnotationClass)) {
310+
$this->hasDeprecatedAnnotations = true;
297311
}
298312

299313
if ($annot) {
@@ -380,11 +394,9 @@ protected function createRoute(string $path, array $defaults, array $requirement
380394
abstract protected function configureRoute(Route $route, \ReflectionClass $class, \ReflectionMethod $method, object $annot);
381395

382396
/**
383-
* @param \ReflectionClass|\ReflectionMethod $reflection
384-
*
385397
* @return iterable<int, RouteAnnotation>
386398
*/
387-
private function getAnnotations(object $reflection): iterable
399+
private function getAnnotations(\ReflectionClass|\ReflectionMethod $reflection): iterable
388400
{
389401
foreach ($reflection->getAttributes($this->routeAnnotationClass, \ReflectionAttribute::IS_INSTANCEOF) as $attribute) {
390402
yield $attribute->newInstance();
@@ -400,6 +412,8 @@ private function getAnnotations(object $reflection): iterable
400412

401413
foreach ($annotations as $annotation) {
402414
if ($annotation instanceof $this->routeAnnotationClass) {
415+
$this->hasDeprecatedAnnotations = true;
416+
403417
yield $annotation;
404418
}
405419
}

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],

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

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,19 +12,17 @@
1212
namespace Symfony\Component\Routing\Tests\Loader;
1313

1414
use Doctrine\Common\Annotations\AnnotationReader;
15-
use Symfony\Component\Routing\Loader\AnnotationClassLoader;
16-
use Symfony\Component\Routing\Route;
15+
use Symfony\Component\Routing\Tests\Fixtures\TraceableAnnotationClassLoader;
1716

17+
/**
18+
* @group legacy
19+
*/
1820
class AnnotationClassLoaderWithAnnotationsTest extends AnnotationClassLoaderTestCase
1921
{
2022
protected function setUp(string $env = null): void
2123
{
2224
$reader = new AnnotationReader();
23-
$this->loader = new class($reader, $env) extends AnnotationClassLoader {
24-
protected function configureRoute(Route $route, \ReflectionClass $class, \ReflectionMethod $method, object $annot): void
25-
{
26-
}
27-
};
25+
$this->loader = new TraceableAnnotationClassLoader($reader, $env);
2826
}
2927

3028
public function testDefaultRouteName()

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

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,18 +11,13 @@
1111

1212
namespace Symfony\Component\Routing\Tests\Loader;
1313

14-
use Symfony\Component\Routing\Loader\AnnotationClassLoader;
15-
use Symfony\Component\Routing\Route;
14+
use Symfony\Component\Routing\Tests\Fixtures\TraceableAnnotationClassLoader;
1615

1716
class AnnotationClassLoaderWithAttributesTest extends AnnotationClassLoaderTestCase
1817
{
1918
protected function setUp(string $env = null): void
2019
{
21-
$this->loader = new class(null, $env) extends AnnotationClassLoader {
22-
protected function configureRoute(Route $route, \ReflectionClass $class, \ReflectionMethod $method, object $annot): void
23-
{
24-
}
25-
};
20+
$this->loader = new TraceableAnnotationClassLoader($env);
2621
}
2722

2823
public function testDefaultRouteName()

0 commit comments

Comments
 (0)