Skip to content

Commit f362aa4

Browse files
Merge branch '6.4' into 7.0
* 6.4: [FrameworkBundle][Routing] Deprecate annotations [FrameworkBundle][Serializer] Deprecate annotations
2 parents ecd1222 + 9884b24 commit f362aa4

20 files changed

+322
-170
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ CHANGELOG
1515
* Add native return type to `AnnotationClassLoader::setResolver()`
1616
* Deprecate Doctrine annotations support in favor of native attributes
1717
* Change the constructor signature of `AnnotationClassLoader` to `__construct(?string $env = null)`, passing an annotation reader as first argument is deprecated
18+
* Deprecate `AnnotationClassLoader`, use `AttributeClassLoader` instead
19+
* Deprecate `AnnotationDirectoryLoader`, use `AttributeDirectoryLoader` instead
20+
* Deprecate `AnnotationFileLoader`, use `AttributeFileLoader` instead
1821

1922
6.2
2023
---

Loader/AnnotationDirectoryLoader.php

Lines changed: 6 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -11,73 +11,15 @@
1111

1212
namespace Symfony\Component\Routing\Loader;
1313

14-
use Symfony\Component\Config\Resource\DirectoryResource;
15-
use Symfony\Component\Routing\RouteCollection;
14+
trigger_deprecation('symfony/routing', '6.4', 'The "%s" class is deprecated, use "%s" instead.', AnnotationDirectoryLoader::class, AttributeDirectoryLoader::class);
1615

17-
/**
18-
* AnnotationDirectoryLoader loads routing information from annotations set
19-
* on PHP classes and methods.
20-
*
21-
* @author Fabien Potencier <fabien@symfony.com>
22-
*/
23-
class AnnotationDirectoryLoader extends AnnotationFileLoader
24-
{
16+
class_exists(AttributeDirectoryLoader::class);
17+
18+
if (false) {
2519
/**
26-
* @throws \InvalidArgumentException When the directory does not exist or its routes cannot be parsed
20+
* @deprecated since Symfony 6.4, to be removed in 7.0, use {@link AttributeDirectoryLoader} instead
2721
*/
28-
public function load(mixed $path, string $type = null): ?RouteCollection
29-
{
30-
if (!is_dir($dir = $this->locator->locate($path))) {
31-
return parent::supports($path, $type) ? parent::load($path, $type) : new RouteCollection();
32-
}
33-
34-
$collection = new RouteCollection();
35-
$collection->addResource(new DirectoryResource($dir, '/\.php$/'));
36-
$files = iterator_to_array(new \RecursiveIteratorIterator(
37-
new \RecursiveCallbackFilterIterator(
38-
new \RecursiveDirectoryIterator($dir, \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::FOLLOW_SYMLINKS),
39-
fn (\SplFileInfo $current) => !str_starts_with($current->getBasename(), '.')
40-
),
41-
\RecursiveIteratorIterator::LEAVES_ONLY
42-
));
43-
usort($files, fn (\SplFileInfo $a, \SplFileInfo $b) => (string) $a > (string) $b ? 1 : -1);
44-
45-
foreach ($files as $file) {
46-
if (!$file->isFile() || !str_ends_with($file->getFilename(), '.php')) {
47-
continue;
48-
}
49-
50-
if ($class = $this->findClass($file)) {
51-
$refl = new \ReflectionClass($class);
52-
if ($refl->isAbstract()) {
53-
continue;
54-
}
55-
56-
$collection->addCollection($this->loader->load($class, $type));
57-
}
58-
}
59-
60-
return $collection;
61-
}
62-
63-
public function supports(mixed $resource, string $type = null): bool
22+
class AnnotationDirectoryLoader
6423
{
65-
if (!\is_string($resource)) {
66-
return false;
67-
}
68-
69-
if (\in_array($type, ['annotation', 'attribute'], true)) {
70-
return true;
71-
}
72-
73-
if ($type) {
74-
return false;
75-
}
76-
77-
try {
78-
return is_dir($this->locator->locate($resource));
79-
} catch (\Exception) {
80-
return false;
81-
}
8224
}
8325
}

Loader/AnnotationClassLoader.php renamed to Loader/AttributeClassLoader.php

Lines changed: 127 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
namespace Symfony\Component\Routing\Loader;
1313

14+
use Doctrine\Common\Annotations\Reader;
1415
use Symfony\Component\Config\Loader\LoaderInterface;
1516
use Symfony\Component\Config\Loader\LoaderResolverInterface;
1617
use Symfony\Component\Config\Resource\FileResource;
@@ -19,7 +20,7 @@
1920
use Symfony\Component\Routing\RouteCollection;
2021

2122
/**
22-
* AnnotationClassLoader loads routing information from a PHP class and its methods.
23+
* AttributeClassLoader loads routing information from a PHP class and its methods.
2324
*
2425
* You need to define an implementation for the configureRoute() method. Most of the
2526
* time, this method should define some PHP callable to be called for the route
@@ -48,21 +49,61 @@
4849
*
4950
* @author Fabien Potencier <fabien@symfony.com>
5051
* @author Alexander M. Turek <me@derrabus.de>
52+
* @author Alexandre Daubois <alex.daubois@gmail.com>
5153
*/
52-
abstract class AnnotationClassLoader implements LoaderInterface
54+
abstract class AttributeClassLoader implements LoaderInterface
5355
{
54-
protected string $routeAnnotationClass = RouteAnnotation::class;
55-
protected int $defaultRouteIndex = 0;
56+
/**
57+
* @var Reader|null
58+
*
59+
* @deprecated in Symfony 6.4, this property will be removed in Symfony 7.
60+
*/
61+
protected $reader;
62+
63+
/**
64+
* @var string|null
65+
*/
66+
protected $env;
67+
68+
/**
69+
* @var string
70+
*/
71+
protected $routeAnnotationClass = RouteAnnotation::class;
5672

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

62101
/**
63102
* Sets the annotation class to read route properties from.
103+
*
104+
* @return void
64105
*/
65-
public function setRouteAnnotationClass(string $class): void
106+
public function setRouteAnnotationClass(string $class)
66107
{
67108
$this->routeAnnotationClass = $class;
68109
}
@@ -83,47 +124,59 @@ public function load(mixed $class, string $type = null): RouteCollection
83124
throw new \InvalidArgumentException(sprintf('Annotations from class "%s" cannot be read as it is abstract.', $class->getName()));
84125
}
85126

86-
$globals = $this->getGlobals($class);
87-
$collection = new RouteCollection();
88-
$collection->addResource(new FileResource($class->getFileName()));
89-
if ($globals['env'] && $this->env !== $globals['env']) {
90-
return $collection;
91-
}
92-
$fqcnAlias = false;
93-
foreach ($class->getMethods() as $method) {
94-
$this->defaultRouteIndex = 0;
95-
$routeNamesBefore = array_keys($collection->all());
96-
foreach ($this->getAnnotations($method) as $annot) {
97-
$this->addRoute($collection, $annot, $globals, $class, $method);
98-
if ('__invoke' === $method->name) {
127+
$this->hasDeprecatedAnnotations = false;
128+
129+
try {
130+
$globals = $this->getGlobals($class);
131+
$collection = new RouteCollection();
132+
$collection->addResource(new FileResource($class->getFileName()));
133+
if ($globals['env'] && $this->env !== $globals['env']) {
134+
return $collection;
135+
}
136+
$fqcnAlias = false;
137+
foreach ($class->getMethods() as $method) {
138+
$this->defaultRouteIndex = 0;
139+
$routeNamesBefore = array_keys($collection->all());
140+
foreach ($this->getAnnotations($method) as $annot) {
141+
$this->addRoute($collection, $annot, $globals, $class, $method);
142+
if ('__invoke' === $method->name) {
143+
$fqcnAlias = true;
144+
}
145+
}
146+
147+
if (1 === $collection->count() - \count($routeNamesBefore)) {
148+
$newRouteName = current(array_diff(array_keys($collection->all()), $routeNamesBefore));
149+
$collection->addAlias(sprintf('%s::%s', $class->name, $method->name), $newRouteName);
150+
}
151+
}
152+
if (0 === $collection->count() && $class->hasMethod('__invoke')) {
153+
$globals = $this->resetGlobals();
154+
foreach ($this->getAnnotations($class) as $annot) {
155+
$this->addRoute($collection, $annot, $globals, $class, $class->getMethod('__invoke'));
99156
$fqcnAlias = true;
100157
}
101158
}
102-
103-
if (1 === $collection->count() - \count($routeNamesBefore)) {
104-
$newRouteName = current(array_diff(array_keys($collection->all()), $routeNamesBefore));
105-
$collection->addAlias(sprintf('%s::%s', $class->name, $method->name), $newRouteName);
159+
if ($fqcnAlias && 1 === $collection->count()) {
160+
$collection->addAlias($class->name, $invokeRouteName = key($collection->all()));
161+
$collection->addAlias(sprintf('%s::__invoke', $class->name), $invokeRouteName);
106162
}
107-
}
108-
if (0 === $collection->count() && $class->hasMethod('__invoke')) {
109-
$globals = $this->resetGlobals();
110-
foreach ($this->getAnnotations($class) as $annot) {
111-
$this->addRoute($collection, $annot, $globals, $class, $class->getMethod('__invoke'));
112-
$fqcnAlias = true;
163+
164+
if ($this->hasDeprecatedAnnotations) {
165+
trigger_deprecation('symfony/routing', '6.4', 'Class "%s" uses Doctrine Annotations to configure routes, which is deprecated. Use PHP attributes instead.', $class->getName());
113166
}
114-
}
115-
if ($fqcnAlias && 1 === $collection->count()) {
116-
$collection->addAlias($class->name, $invokeRouteName = key($collection->all()));
117-
$collection->addAlias(sprintf('%s::__invoke', $class->name), $invokeRouteName);
167+
} finally {
168+
$this->hasDeprecatedAnnotations = false;
118169
}
119170

120171
return $collection;
121172
}
122173

123174
/**
124175
* @param RouteAnnotation $annot or an object that exposes a similar interface
176+
*
177+
* @return void
125178
*/
126-
protected function addRoute(RouteCollection $collection, object $annot, array $globals, \ReflectionClass $class, \ReflectionMethod $method): void
179+
protected function addRoute(RouteCollection $collection, object $annot, array $globals, \ReflectionClass $class, \ReflectionMethod $method)
127180
{
128181
if ($annot->getEnv() && $annot->getEnv() !== $this->env) {
129182
return;
@@ -210,6 +263,10 @@ protected function addRoute(RouteCollection $collection, object $annot, array $g
210263

211264
public function supports(mixed $resource, string $type = null): bool
212265
{
266+
if ('annotation' === $type) {
267+
trigger_deprecation('symfony/routing', '6.4', 'The "annotation" route type is deprecated, use the "attribute" route type instead.');
268+
}
269+
213270
return \is_string($resource) && preg_match('/^(?:\\\\?[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)+$/', $resource) && (!$type || \in_array($type, ['annotation', 'attribute'], true));
214271
}
215272

@@ -223,8 +280,10 @@ public function getResolver(): LoaderResolverInterface
223280

224281
/**
225282
* Gets the default route name for a class method.
283+
*
284+
* @return string
226285
*/
227-
protected function getDefaultRouteName(\ReflectionClass $class, \ReflectionMethod $method): string
286+
protected function getDefaultRouteName(\ReflectionClass $class, \ReflectionMethod $method)
228287
{
229288
$name = str_replace('\\', '_', $class->name).'_'.$method->name;
230289
$name = \function_exists('mb_strtolower') && preg_match('//u', $name) ? mb_strtolower($name, 'UTF-8') : strtolower($name);
@@ -239,13 +298,19 @@ protected function getDefaultRouteName(\ReflectionClass $class, \ReflectionMetho
239298
/**
240299
* @return array<string, mixed>
241300
*/
242-
protected function getGlobals(\ReflectionClass $class): array
301+
protected function getGlobals(\ReflectionClass $class)
243302
{
244303
$globals = $this->resetGlobals();
245304

305+
$annot = null;
246306
if ($attribute = $class->getAttributes($this->routeAnnotationClass, \ReflectionAttribute::IS_INSTANCEOF)[0] ?? null) {
247307
$annot = $attribute->newInstance();
308+
}
309+
if (!$annot && $annot = $this->reader?->getClassAnnotation($class, $this->routeAnnotationClass)) {
310+
$this->hasDeprecatedAnnotations = true;
311+
}
248312

313+
if ($annot) {
249314
if (null !== $annot->getName()) {
250315
$globals['name'] = $annot->getName();
251316
}
@@ -315,7 +380,10 @@ private function resetGlobals(): array
315380
];
316381
}
317382

318-
protected function createRoute(string $path, array $defaults, array $requirements, array $options, ?string $host, array $schemes, array $methods, ?string $condition): Route
383+
/**
384+
* @return Route
385+
*/
386+
protected function createRoute(string $path, array $defaults, array $requirements, array $options, ?string $host, array $schemes, array $methods, ?string $condition)
319387
{
320388
return new Route($path, $defaults, $requirements, $options, $host, $schemes, $methods, $condition);
321389
}
@@ -333,5 +401,25 @@ private function getAnnotations(\ReflectionClass|\ReflectionMethod $reflection):
333401
foreach ($reflection->getAttributes($this->routeAnnotationClass, \ReflectionAttribute::IS_INSTANCEOF) as $attribute) {
334402
yield $attribute->newInstance();
335403
}
404+
405+
if (!$this->reader) {
406+
return;
407+
}
408+
409+
$annotations = $reflection instanceof \ReflectionClass
410+
? $this->reader->getClassAnnotations($reflection)
411+
: $this->reader->getMethodAnnotations($reflection);
412+
413+
foreach ($annotations as $annotation) {
414+
if ($annotation instanceof $this->routeAnnotationClass) {
415+
$this->hasDeprecatedAnnotations = true;
416+
417+
yield $annotation;
418+
}
419+
}
336420
}
337421
}
422+
423+
if (!class_exists(AnnotationClassLoader::class, false)) {
424+
class_alias(AttributeClassLoader::class, AnnotationClassLoader::class);
425+
}

0 commit comments

Comments
 (0)