Skip to content

Commit 44c121f

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

14 files changed

+205
-229
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/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 || \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],

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()

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy