Skip to content

Commit ec4cef8

Browse files
committed
[Routing] Added the Route attribute.
1 parent 5f8b8b9 commit ec4cef8

28 files changed

+589
-144
lines changed

Annotation/Route.php

+54-3
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
}

Loader/AnnotationClassLoader.php

+61-13
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
}
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+
}
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+
}
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+
}
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+
}
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+
}
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+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
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+
#[Route(path: '/defaults', locale: 'g_locale', format: 'g_format')]
17+
class GlobalDefaultsClass
18+
{
19+
#[Route(path: '/specific-locale', name: 'specific_locale', locale: 's_locale')]
20+
public function locale()
21+
{
22+
}
23+
24+
#[Route(path: '/specific-format', name: 'specific_format', format: 's_format')]
25+
public function format()
26+
{
27+
}
28+
}
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+
#[Route(path: '/here', name: 'lol', methods: ["GET", "POST"], schemes: ['https'])]
8+
class InvokableController
9+
{
10+
public function __invoke()
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+
#[Route(path: ["nl" => "/hier", "en" => "/here"], name: 'action')]
8+
class InvokableLocalizedController
9+
{
10+
public function __invoke()
11+
{
12+
}
13+
}

0 commit comments

Comments
 (0)