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