Skip to content

Commit d0c915c

Browse files
committed
feature #37474 [RFC][Routing] Added the Route attribute (derrabus)
This PR was merged into the 5.2-dev branch. Discussion ---------- [RFC][Routing] Added the Route attribute | Q | A | ------------- | --- | Branch? | 5.2 | Bug fix? | no | New feature? | yes | Deprecations? | no | Tickets | N/A | License | MIT | Doc PR | not yet I was wondering if we can make use of [attributes](https://wiki.php.net/rfc/attributes_v2) as a replacement for Doctrine Annotations for configuring routes. I have modified the existing `AnnotationClassLoader` so that it merges attributes and annotations. This way, an application could transparently switch from annotations to attributes. Moreover, the `AnnotationClassLoader` does not require an annotation reader anymore. Since the chosen syntax `#[…]` is treated as a comment by php 7, I was able to use the existing annotation class, which makes the migration even easier. #### Example with Doctrine Annotations ```php use Symfony\Component\Routing\Attribute\Route; class ActionPathController { /** * @route("/path", name: "action") */ public function action() { } } ``` #### Same example with the proposed Attribute ```php use Symfony\Component\Routing\Attribute\Route; class ActionPathController { #[Route('/path', name: 'action')] public function action() { } } ``` Commits ------- f0978de [Routing] Added the Route attribute.
2 parents 2cc1259 + f0978de commit d0c915c

29 files changed

+590
-144
lines changed

.github/patch-types.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
case false !== strpos($file, '/src/Symfony/Component/PropertyInfo/Tests/Fixtures/Dummy.php'):
3232
case false !== strpos($file, '/src/Symfony/Component/PropertyInfo/Tests/Fixtures/ParentDummy.php'):
3333
case false !== strpos($file, '/src/Symfony/Component/PropertyInfo/Tests/Fixtures/Php80Dummy.php'):
34+
case false !== strpos($file, '/src/Symfony/Component/Routing/Tests/Fixtures/AttributeFixtures'):
3435
case false !== strpos($file, '/src/Symfony/Component/Serializer/Tests/Normalizer/Features/ObjectOuter.php'):
3536
case false !== strpos($file, '/src/Symfony/Component/VarDumper/Tests/Fixtures/NotLoadableClass.php'):
3637
case false !== strpos($file, '/src/Symfony/Component/VarDumper/Tests/Fixtures/Php74.php') && \PHP_VERSION_ID < 70400:

src/Symfony/Component/Routing/Annotation/Route.php

Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,18 @@
1111

1212
namespace Symfony\Component\Routing\Annotation;
1313

14+
use Attribute;
15+
1416
/**
1517
* Annotation class for @Route().
1618
*
1719
* @Annotation
1820
* @Target({"CLASS", "METHOD"})
1921
*
2022
* @author Fabien Potencier <fabien@symfony.com>
23+
* @author Alexander M. Turek <me@derrabus.de>
2124
*/
25+
#[Attribute(Attribute::IS_REPEATABLE | Attribute::TARGET_CLASS | Attribute::TARGET_METHOD)]
2226
class Route
2327
{
2428
private $path;
@@ -34,12 +38,59 @@ class Route
3438
private $priority;
3539

3640
/**
37-
* @param array $data An array of key/value parameters
41+
* @param array|string $data data array managed by the Doctrine Annotations library or the path
42+
* @param array|string|null $path
43+
* @param string[] $requirements
44+
* @param string[] $methods
45+
* @param string[] $schemes
3846
*
3947
* @throws \BadMethodCallException
4048
*/
41-
public function __construct(array $data)
42-
{
49+
public function __construct(
50+
$data = [],
51+
$path = null,
52+
string $name = null,
53+
array $requirements = [],
54+
array $options = [],
55+
array $defaults = [],
56+
string $host = null,
57+
array $methods = [],
58+
array $schemes = [],
59+
string $condition = null,
60+
int $priority = null,
61+
string $locale = null,
62+
string $format = null,
63+
bool $utf8 = null,
64+
bool $stateless = null
65+
) {
66+
if (\is_string($data)) {
67+
$data = ['path' => $data];
68+
} elseif (!\is_array($data)) {
69+
throw new \TypeError(sprintf('"%s": Argument $data is expected to be a string or array, got "%s".', __METHOD__, get_debug_type($data)));
70+
}
71+
if (null !== $path && !\is_string($path) && !\is_array($path)) {
72+
throw new \TypeError(sprintf('"%s": Argument $path is expected to be a string, array or null, got "%s".', __METHOD__, get_debug_type($path)));
73+
}
74+
75+
$data['path'] = $data['path'] ?? $path;
76+
$data['name'] = $data['name'] ?? $name;
77+
$data['requirements'] = $data['requirements'] ?? $requirements;
78+
$data['options'] = $data['options'] ?? $options;
79+
$data['defaults'] = $data['defaults'] ?? $defaults;
80+
$data['host'] = $data['host'] ?? $host;
81+
$data['methods'] = $data['methods'] ?? $methods;
82+
$data['schemes'] = $data['schemes'] ?? $schemes;
83+
$data['condition'] = $data['condition'] ?? $condition;
84+
$data['priority'] = $data['priority'] ?? $priority;
85+
$data['locale'] = $data['locale'] ?? $locale;
86+
$data['format'] = $data['format'] ?? $format;
87+
$data['utf8'] = $data['utf8'] ?? $utf8;
88+
$data['stateless'] = $data['stateless'] ?? $stateless;
89+
90+
$data = array_filter($data, static function ($value): bool {
91+
return null !== $value;
92+
});
93+
4394
if (isset($data['localized_paths'])) {
4495
throw new \BadMethodCallException(sprintf('Unknown property "localized_paths" on annotation "%s".', static::class));
4596
}

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

Lines changed: 61 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,24 @@
5151
* {
5252
* }
5353
* }
54+
*
55+
* On PHP 8, the annotation class can be used as an attribute as well:
56+
* #[Route('/Blog')]
57+
* class Blog
58+
* {
59+
* #[Route('/', name: 'blog_index')]
60+
* public function index()
61+
* {
62+
* }
63+
* #[Route('/{id}', name: 'blog_post', requirements: ["id" => '\d+'])]
64+
* public function show()
65+
* {
66+
* }
67+
* }
68+
5469
*
5570
* @author Fabien Potencier <fabien@symfony.com>
71+
* @author Alexander M. Turek <me@derrabus.de>
5672
*/
5773
abstract class AnnotationClassLoader implements LoaderInterface
5874
{
@@ -61,14 +77,14 @@ abstract class AnnotationClassLoader implements LoaderInterface
6177
/**
6278
* @var string
6379
*/
64-
protected $routeAnnotationClass = 'Symfony\\Component\\Routing\\Annotation\\Route';
80+
protected $routeAnnotationClass = RouteAnnotation::class;
6581

6682
/**
6783
* @var int
6884
*/
6985
protected $defaultRouteIndex = 0;
7086

71-
public function __construct(Reader $reader)
87+
public function __construct(Reader $reader = null)
7288
{
7389
$this->reader = $reader;
7490
}
@@ -108,19 +124,15 @@ public function load($class, string $type = null)
108124

109125
foreach ($class->getMethods() as $method) {
110126
$this->defaultRouteIndex = 0;
111-
foreach ($this->reader->getMethodAnnotations($method) as $annot) {
112-
if ($annot instanceof $this->routeAnnotationClass) {
113-
$this->addRoute($collection, $annot, $globals, $class, $method);
114-
}
127+
foreach ($this->getAnnotations($method) as $annot) {
128+
$this->addRoute($collection, $annot, $globals, $class, $method);
115129
}
116130
}
117131

118132
if (0 === $collection->count() && $class->hasMethod('__invoke')) {
119133
$globals = $this->resetGlobals();
120-
foreach ($this->reader->getClassAnnotations($class) as $annot) {
121-
if ($annot instanceof $this->routeAnnotationClass) {
122-
$this->addRoute($collection, $annot, $globals, $class, $class->getMethod('__invoke'));
123-
}
134+
foreach ($this->getAnnotations($class) as $annot) {
135+
$this->addRoute($collection, $annot, $globals, $class, $class->getMethod('__invoke'));
124136
}
125137
}
126138

@@ -130,7 +142,7 @@ public function load($class, string $type = null)
130142
/**
131143
* @param RouteAnnotation $annot or an object that exposes a similar interface
132144
*/
133-
protected function addRoute(RouteCollection $collection, $annot, array $globals, \ReflectionClass $class, \ReflectionMethod $method)
145+
protected function addRoute(RouteCollection $collection, object $annot, array $globals, \ReflectionClass $class, \ReflectionMethod $method)
134146
{
135147
$name = $annot->getName();
136148
if (null === $name) {
@@ -257,7 +269,15 @@ protected function getGlobals(\ReflectionClass $class)
257269
{
258270
$globals = $this->resetGlobals();
259271

260-
if ($annot = $this->reader->getClassAnnotation($class, $this->routeAnnotationClass)) {
272+
$annot = null;
273+
if (\PHP_VERSION_ID >= 80000 && ($attribute = $class->getAttributes($this->routeAnnotationClass)[0] ?? null)) {
274+
$annot = $attribute->newInstance();
275+
}
276+
if (!$annot && $this->reader) {
277+
$annot = $this->reader->getClassAnnotation($class, $this->routeAnnotationClass);
278+
}
279+
280+
if ($annot) {
261281
if (null !== $annot->getName()) {
262282
$globals['name'] = $annot->getName();
263283
}
@@ -330,5 +350,33 @@ protected function createRoute(string $path, array $defaults, array $requirement
330350
return new Route($path, $defaults, $requirements, $options, $host, $schemes, $methods, $condition);
331351
}
332352

333-
abstract protected function configureRoute(Route $route, \ReflectionClass $class, \ReflectionMethod $method, $annot);
353+
abstract protected function configureRoute(Route $route, \ReflectionClass $class, \ReflectionMethod $method, object $annot);
354+
355+
/**
356+
* @param \ReflectionClass|\ReflectionMethod $reflection
357+
*
358+
* @return iterable|RouteAnnotation[]
359+
*/
360+
private function getAnnotations(object $reflection): iterable
361+
{
362+
if (\PHP_VERSION_ID >= 80000) {
363+
foreach ($reflection->getAttributes($this->routeAnnotationClass) as $attribute) {
364+
yield $attribute->newInstance();
365+
}
366+
}
367+
368+
if (!$this->reader) {
369+
return;
370+
}
371+
372+
$anntotations = $reflection instanceof \ReflectionClass
373+
? $this->reader->getClassAnnotations($reflection)
374+
: $this->reader->getMethodAnnotations($reflection);
375+
376+
foreach ($anntotations as $annotation) {
377+
if ($annotation instanceof $this->routeAnnotationClass) {
378+
yield $annotation;
379+
}
380+
}
381+
}
334382
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
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\AnnotationFixtures;
13+
14+
use Symfony\Component\Routing\Annotation\Route;
15+
16+
/**
17+
* @Route("/1", name="route1", schemes={"https"}, methods={"GET"})
18+
* @Route("/2", name="route2", schemes={"https"}, methods={"GET"})
19+
*/
20+
class BazClass
21+
{
22+
public function __invoke()
23+
{
24+
}
25+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php
2+
3+
namespace Symfony\Component\Routing\Tests\Fixtures\AnnotationFixtures;
4+
5+
use Symfony\Component\Routing\Annotation\Route;
6+
7+
class EncodingClass
8+
{
9+
/**
10+
* @Route
11+
*/
12+
public function routeÀction()
13+
{
14+
}
15+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
3+
namespace Symfony\Component\Routing\Tests\Fixtures\AttributeFixtures;
4+
5+
use Symfony\Component\Routing\Annotation\Route;
6+
7+
class ActionPathController
8+
{
9+
#[Route('/path', name: 'action')]
10+
public function action()
11+
{
12+
}
13+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
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\AttributeFixtures;
13+
14+
use Symfony\Component\Routing\Annotation\Route;
15+
16+
#[
17+
Route(path: '/1', name: 'route1', schemes: ['https'], methods: ['GET']),
18+
Route(path: '/2', name: 'route2', schemes: ['https'], methods: ['GET']),
19+
]
20+
class BazClass
21+
{
22+
public function __invoke()
23+
{
24+
}
25+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
namespace Symfony\Component\Routing\Tests\Fixtures\AttributeFixtures;
4+
5+
use Symfony\Component\Routing\Annotation\Route;
6+
7+
class DefaultValueController
8+
{
9+
#[Route(path: '/{default}/path', name: 'action')]
10+
public function action($default = 'value')
11+
{
12+
}
13+
14+
#[
15+
Route(path: '/hello/{name<\w+>}', name: 'hello_without_default'),
16+
Route(path: 'hello/{name<\w+>?Symfony}', name: 'hello_with_default'),
17+
]
18+
public function hello(string $name = 'World')
19+
{
20+
}
21+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
3+
namespace Symfony\Component\Routing\Tests\Fixtures\AttributeFixtures;
4+
5+
use Symfony\Component\Routing\Annotation\Route;
6+
7+
class EncodingClass
8+
{
9+
#[Route]
10+
public function routeÀction()
11+
{
12+
}
13+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
3+
namespace Symfony\Component\Routing\Tests\Fixtures\AttributeFixtures;
4+
5+
use Symfony\Component\Routing\Annotation\Route;
6+
7+
class ExplicitLocalizedActionPathController
8+
{
9+
#[Route(path: ['en' => '/path', 'nl' => '/pad'], name: 'action')]
10+
public function action()
11+
{
12+
}
13+
}

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