[Routing] Added the Route attribute.

This commit is contained in:
Alexander M. Turek 2020-09-03 09:55:01 +02:00
parent 9150590818
commit f0978de493
29 changed files with 588 additions and 142 deletions

View File

@ -31,6 +31,7 @@ foreach ($loader->getClassMap() as $class => $file) {
case false !== strpos($file, '/src/Symfony/Component/PropertyInfo/Tests/Fixtures/Dummy.php'): case false !== strpos($file, '/src/Symfony/Component/PropertyInfo/Tests/Fixtures/Dummy.php'):
case false !== strpos($file, '/src/Symfony/Component/PropertyInfo/Tests/Fixtures/ParentDummy.php'): case false !== strpos($file, '/src/Symfony/Component/PropertyInfo/Tests/Fixtures/ParentDummy.php'):
case false !== strpos($file, '/src/Symfony/Component/PropertyInfo/Tests/Fixtures/Php80Dummy.php'): case false !== strpos($file, '/src/Symfony/Component/PropertyInfo/Tests/Fixtures/Php80Dummy.php'):
case false !== strpos($file, '/src/Symfony/Component/Routing/Tests/Fixtures/AttributeFixtures'):
case false !== strpos($file, '/src/Symfony/Component/Serializer/Tests/Normalizer/Features/ObjectOuter.php'): case false !== strpos($file, '/src/Symfony/Component/Serializer/Tests/Normalizer/Features/ObjectOuter.php'):
case false !== strpos($file, '/src/Symfony/Component/VarDumper/Tests/Fixtures/NotLoadableClass.php'): case false !== strpos($file, '/src/Symfony/Component/VarDumper/Tests/Fixtures/NotLoadableClass.php'):
case false !== strpos($file, '/src/Symfony/Component/VarDumper/Tests/Fixtures/Php74.php') && \PHP_VERSION_ID < 70400: case false !== strpos($file, '/src/Symfony/Component/VarDumper/Tests/Fixtures/Php74.php') && \PHP_VERSION_ID < 70400:

View File

@ -11,6 +11,8 @@
namespace Symfony\Component\Routing\Annotation; namespace Symfony\Component\Routing\Annotation;
use Attribute;
/** /**
* Annotation class for @Route(). * Annotation class for @Route().
* *
@ -18,7 +20,9 @@ namespace Symfony\Component\Routing\Annotation;
* @Target({"CLASS", "METHOD"}) * @Target({"CLASS", "METHOD"})
* *
* @author Fabien Potencier <fabien@symfony.com> * @author Fabien Potencier <fabien@symfony.com>
* @author Alexander M. Turek <me@derrabus.de>
*/ */
#[Attribute(Attribute::IS_REPEATABLE | Attribute::TARGET_CLASS | Attribute::TARGET_METHOD)]
class Route class Route
{ {
private $path; private $path;
@ -34,12 +38,59 @@ class Route
private $priority; private $priority;
/** /**
* @param array $data An array of key/value parameters * @param array|string $data data array managed by the Doctrine Annotations library or the path
* @param array|string|null $path
* @param string[] $requirements
* @param string[] $methods
* @param string[] $schemes
* *
* @throws \BadMethodCallException * @throws \BadMethodCallException
*/ */
public function __construct(array $data) public function __construct(
{ $data = [],
$path = null,
string $name = null,
array $requirements = [],
array $options = [],
array $defaults = [],
string $host = null,
array $methods = [],
array $schemes = [],
string $condition = null,
int $priority = null,
string $locale = null,
string $format = null,
bool $utf8 = null,
bool $stateless = null
) {
if (\is_string($data)) {
$data = ['path' => $data];
} elseif (!\is_array($data)) {
throw new \TypeError(sprintf('"%s": Argument $data is expected to be a string or array, got "%s".', __METHOD__, get_debug_type($data)));
}
if (null !== $path && !\is_string($path) && !\is_array($path)) {
throw new \TypeError(sprintf('"%s": Argument $path is expected to be a string, array or null, got "%s".', __METHOD__, get_debug_type($path)));
}
$data['path'] = $data['path'] ?? $path;
$data['name'] = $data['name'] ?? $name;
$data['requirements'] = $data['requirements'] ?? $requirements;
$data['options'] = $data['options'] ?? $options;
$data['defaults'] = $data['defaults'] ?? $defaults;
$data['host'] = $data['host'] ?? $host;
$data['methods'] = $data['methods'] ?? $methods;
$data['schemes'] = $data['schemes'] ?? $schemes;
$data['condition'] = $data['condition'] ?? $condition;
$data['priority'] = $data['priority'] ?? $priority;
$data['locale'] = $data['locale'] ?? $locale;
$data['format'] = $data['format'] ?? $format;
$data['utf8'] = $data['utf8'] ?? $utf8;
$data['stateless'] = $data['stateless'] ?? $stateless;
$data = array_filter($data, static function ($value): bool {
return null !== $value;
});
if (isset($data['localized_paths'])) { if (isset($data['localized_paths'])) {
throw new \BadMethodCallException(sprintf('Unknown property "localized_paths" on annotation "%s".', static::class)); throw new \BadMethodCallException(sprintf('Unknown property "localized_paths" on annotation "%s".', static::class));
} }

View File

@ -51,8 +51,24 @@ use Symfony\Component\Routing\RouteCollection;
* { * {
* } * }
* } * }
*
* On PHP 8, the annotation class can be used as an attribute as well:
* #[Route('/Blog')]
* class Blog
* {
* #[Route('/', name: 'blog_index')]
* public function index()
* {
* }
* #[Route('/{id}', name: 'blog_post', requirements: ["id" => '\d+'])]
* public function show()
* {
* }
* }
* *
* @author Fabien Potencier <fabien@symfony.com> * @author Fabien Potencier <fabien@symfony.com>
* @author Alexander M. Turek <me@derrabus.de>
*/ */
abstract class AnnotationClassLoader implements LoaderInterface abstract class AnnotationClassLoader implements LoaderInterface
{ {
@ -61,14 +77,14 @@ abstract class AnnotationClassLoader implements LoaderInterface
/** /**
* @var string * @var string
*/ */
protected $routeAnnotationClass = 'Symfony\\Component\\Routing\\Annotation\\Route'; protected $routeAnnotationClass = RouteAnnotation::class;
/** /**
* @var int * @var int
*/ */
protected $defaultRouteIndex = 0; protected $defaultRouteIndex = 0;
public function __construct(Reader $reader) public function __construct(Reader $reader = null)
{ {
$this->reader = $reader; $this->reader = $reader;
} }
@ -108,21 +124,17 @@ abstract class AnnotationClassLoader implements LoaderInterface
foreach ($class->getMethods() as $method) { foreach ($class->getMethods() as $method) {
$this->defaultRouteIndex = 0; $this->defaultRouteIndex = 0;
foreach ($this->reader->getMethodAnnotations($method) as $annot) { foreach ($this->getAnnotations($method) as $annot) {
if ($annot instanceof $this->routeAnnotationClass) {
$this->addRoute($collection, $annot, $globals, $class, $method); $this->addRoute($collection, $annot, $globals, $class, $method);
} }
} }
}
if (0 === $collection->count() && $class->hasMethod('__invoke')) { if (0 === $collection->count() && $class->hasMethod('__invoke')) {
$globals = $this->resetGlobals(); $globals = $this->resetGlobals();
foreach ($this->reader->getClassAnnotations($class) as $annot) { foreach ($this->getAnnotations($class) as $annot) {
if ($annot instanceof $this->routeAnnotationClass) {
$this->addRoute($collection, $annot, $globals, $class, $class->getMethod('__invoke')); $this->addRoute($collection, $annot, $globals, $class, $class->getMethod('__invoke'));
} }
} }
}
return $collection; return $collection;
} }
@ -130,7 +142,7 @@ abstract class AnnotationClassLoader implements LoaderInterface
/** /**
* @param RouteAnnotation $annot or an object that exposes a similar interface * @param RouteAnnotation $annot or an object that exposes a similar interface
*/ */
protected function addRoute(RouteCollection $collection, $annot, array $globals, \ReflectionClass $class, \ReflectionMethod $method) protected function addRoute(RouteCollection $collection, object $annot, array $globals, \ReflectionClass $class, \ReflectionMethod $method)
{ {
$name = $annot->getName(); $name = $annot->getName();
if (null === $name) { if (null === $name) {
@ -257,7 +269,15 @@ abstract class AnnotationClassLoader implements LoaderInterface
{ {
$globals = $this->resetGlobals(); $globals = $this->resetGlobals();
if ($annot = $this->reader->getClassAnnotation($class, $this->routeAnnotationClass)) { $annot = null;
if (\PHP_VERSION_ID >= 80000 && ($attribute = $class->getAttributes($this->routeAnnotationClass)[0] ?? null)) {
$annot = $attribute->newInstance();
}
if (!$annot && $this->reader) {
$annot = $this->reader->getClassAnnotation($class, $this->routeAnnotationClass);
}
if ($annot) {
if (null !== $annot->getName()) { if (null !== $annot->getName()) {
$globals['name'] = $annot->getName(); $globals['name'] = $annot->getName();
} }
@ -330,5 +350,33 @@ abstract class AnnotationClassLoader implements LoaderInterface
return new Route($path, $defaults, $requirements, $options, $host, $schemes, $methods, $condition); return new Route($path, $defaults, $requirements, $options, $host, $schemes, $methods, $condition);
} }
abstract protected function configureRoute(Route $route, \ReflectionClass $class, \ReflectionMethod $method, $annot); abstract protected function configureRoute(Route $route, \ReflectionClass $class, \ReflectionMethod $method, object $annot);
/**
* @param \ReflectionClass|\ReflectionMethod $reflection
*
* @return iterable|RouteAnnotation[]
*/
private function getAnnotations(object $reflection): iterable
{
if (\PHP_VERSION_ID >= 80000) {
foreach ($reflection->getAttributes($this->routeAnnotationClass) as $attribute) {
yield $attribute->newInstance();
}
}
if (!$this->reader) {
return;
}
$anntotations = $reflection instanceof \ReflectionClass
? $this->reader->getClassAnnotations($reflection)
: $this->reader->getMethodAnnotations($reflection);
foreach ($anntotations as $annotation) {
if ($annotation instanceof $this->routeAnnotationClass) {
yield $annotation;
}
}
}
} }

View File

@ -0,0 +1,25 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Routing\Tests\Fixtures\AnnotationFixtures;
use Symfony\Component\Routing\Annotation\Route;
/**
* @Route("/1", name="route1", schemes={"https"}, methods={"GET"})
* @Route("/2", name="route2", schemes={"https"}, methods={"GET"})
*/
class BazClass
{
public function __invoke()
{
}
}

View File

@ -0,0 +1,15 @@
<?php
namespace Symfony\Component\Routing\Tests\Fixtures\AnnotationFixtures;
use Symfony\Component\Routing\Annotation\Route;
class EncodingClass
{
/**
* @Route
*/
public function routeÀction()
{
}
}

View File

@ -0,0 +1,13 @@
<?php
namespace Symfony\Component\Routing\Tests\Fixtures\AttributeFixtures;
use Symfony\Component\Routing\Annotation\Route;
class ActionPathController
{
#[Route('/path', name: 'action')]
public function action()
{
}
}

View File

@ -0,0 +1,25 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Routing\Tests\Fixtures\AttributeFixtures;
use Symfony\Component\Routing\Annotation\Route;
#[
Route(path: '/1', name: 'route1', schemes: ['https'], methods: ['GET']),
Route(path: '/2', name: 'route2', schemes: ['https'], methods: ['GET']),
]
class BazClass
{
public function __invoke()
{
}
}

View File

@ -0,0 +1,21 @@
<?php
namespace Symfony\Component\Routing\Tests\Fixtures\AttributeFixtures;
use Symfony\Component\Routing\Annotation\Route;
class DefaultValueController
{
#[Route(path: '/{default}/path', name: 'action')]
public function action($default = 'value')
{
}
#[
Route(path: '/hello/{name<\w+>}', name: 'hello_without_default'),
Route(path: 'hello/{name<\w+>?Symfony}', name: 'hello_with_default'),
]
public function hello(string $name = 'World')
{
}
}

View File

@ -0,0 +1,13 @@
<?php
namespace Symfony\Component\Routing\Tests\Fixtures\AttributeFixtures;
use Symfony\Component\Routing\Annotation\Route;
class EncodingClass
{
#[Route]
public function routeÀction()
{
}
}

View File

@ -0,0 +1,13 @@
<?php
namespace Symfony\Component\Routing\Tests\Fixtures\AttributeFixtures;
use Symfony\Component\Routing\Annotation\Route;
class ExplicitLocalizedActionPathController
{
#[Route(path: ['en' => '/path', 'nl' => '/pad'], name: 'action')]
public function action()
{
}
}

View File

@ -0,0 +1,28 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Routing\Tests\Fixtures\AttributeFixtures;
use Symfony\Component\Routing\Annotation\Route;
#[Route(path: '/defaults', locale: 'g_locale', format: 'g_format')]
class GlobalDefaultsClass
{
#[Route(path: '/specific-locale', name: 'specific_locale', locale: 's_locale')]
public function locale()
{
}
#[Route(path: '/specific-format', name: 'specific_format', format: 's_format')]
public function format()
{
}
}

View File

@ -0,0 +1,13 @@
<?php
namespace Symfony\Component\Routing\Tests\Fixtures\AttributeFixtures;
use Symfony\Component\Routing\Annotation\Route;
#[Route(path: '/here', name: 'lol', methods: ["GET", "POST"], schemes: ['https'])]
class InvokableController
{
public function __invoke()
{
}
}

View File

@ -0,0 +1,13 @@
<?php
namespace Symfony\Component\Routing\Tests\Fixtures\AttributeFixtures;
use Symfony\Component\Routing\Annotation\Route;
#[Route(path: ["nl" => "/hier", "en" => "/here"], name: 'action')]
class InvokableLocalizedController
{
public function __invoke()
{
}
}

View File

@ -0,0 +1,13 @@
<?php
namespace Symfony\Component\Routing\Tests\Fixtures\AttributeFixtures;
use Symfony\Component\Routing\Annotation\Route;
class LocalizedActionPathController
{
#[Route(path: ['en' => '/path', 'nl' => '/pad'], name: 'action')]
public function action()
{
}
}

View File

@ -0,0 +1,19 @@
<?php
namespace Symfony\Component\Routing\Tests\Fixtures\AttributeFixtures;
use Symfony\Component\Routing\Annotation\Route;
#[Route(path: ['en' => '/the/path', 'nl' => '/het/pad'])]
class LocalizedMethodActionControllers
{
#[Route(name: 'post', methods: ['POST'])]
public function post()
{
}
#[Route(name: 'put', methods: ['PUT'])]
public function put()
{
}
}

View File

@ -0,0 +1,14 @@
<?php
namespace Symfony\Component\Routing\Tests\Fixtures\AttributeFixtures;
use Symfony\Component\Routing\Annotation\Route;
#[Route(path: ['nl' => '/nl', 'en' => '/en'])]
class LocalizedPrefixLocalizedActionController
{
#[Route(path: ['nl' => '/actie', 'en' => '/action'], name: 'action')]
public function action()
{
}
}

View File

@ -0,0 +1,14 @@
<?php
namespace Symfony\Component\Routing\Tests\Fixtures\AttributeFixtures;
use Symfony\Component\Routing\Annotation\Route;
#[Route(path: ['en' => '/en', 'nl' => '/nl'])]
class LocalizedPrefixWithRouteWithoutLocale
{
#[Route(path: '/suffix', name: 'action')]
public function action()
{
}
}

View File

@ -0,0 +1,19 @@
<?php
namespace Symfony\Component\Routing\Tests\Fixtures\AttributeFixtures;
use Symfony\Component\Routing\Annotation\Route;
#[Route('/the/path')]
class MethodActionControllers
{
#[Route(name: 'post', methods: ['POST'])]
public function post()
{
}
#[Route(name: 'put', methods: ['PUT'], priority: 10)]
public function put()
{
}
}

View File

@ -0,0 +1,13 @@
<?php
namespace Symfony\Component\Routing\Tests\Fixtures\AttributeFixtures;
use Symfony\Component\Routing\Annotation\Route;
class MissingRouteNameController
{
#[Route('/path')]
public function action()
{
}
}

View File

@ -0,0 +1,13 @@
<?php
namespace Symfony\Component\Routing\Tests\Fixtures\AttributeFixtures;
use Symfony\Component\Routing\Annotation\Route;
class NothingButNameController
{
#[Route(name: 'action')]
public function action()
{
}
}

View File

@ -0,0 +1,14 @@
<?php
namespace Symfony\Component\Routing\Tests\Fixtures\AttributeFixtures;
use Symfony\Component\Routing\Annotation\Route;
#[Route('/prefix')]
class PrefixedActionLocalizedRouteController
{
#[Route(path: ['en' => '/path', 'nl' => '/pad'], name: 'action')]
public function action()
{
}
}

View File

@ -0,0 +1,14 @@
<?php
namespace Symfony\Component\Routing\Tests\Fixtures\AttributeFixtures;
use Symfony\Component\Routing\Annotation\Route;
#[Route(path: '/prefix', host: 'frankdejonge.nl', condition: 'lol=fun')]
class PrefixedActionPathController
{
#[Route(path: '/path', name: 'action')]
public function action()
{
}
}

View File

@ -0,0 +1,23 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Routing\Tests\Fixtures\AttributeFixtures;
use Symfony\Component\Routing\Annotation\Route;
#[Route(path: '/', requirements: ['foo', '\d+'])]
class RequirementsWithoutPlaceholderNameController
{
#[Route(path: '/{foo}', name: 'foo', requirements: ['foo', '\d+'])]
public function foo()
{
}
}

View File

@ -0,0 +1,14 @@
<?php
namespace Symfony\Component\Routing\Tests\Fixtures\AttributeFixtures;
use Symfony\Component\Routing\Annotation\Route;
#[Route('/prefix')]
class RouteWithPrefixController
{
#[Route(path: '/path', name: 'action')]
public function action()
{
}
}

View File

@ -0,0 +1,19 @@
<?php
namespace Symfony\Component\Routing\Tests\Fixtures\AttributeFixtures;
use Symfony\Component\Routing\Annotation\Route;
#[Route('/test', utf8: true)]
class Utf8ActionControllers
{
#[Route(name: 'one')]
public function one()
{
}
#[Route(name: 'two', utf8: false)]
public function two()
{
}
}

View File

@ -11,50 +11,16 @@
namespace Symfony\Component\Routing\Tests\Loader; namespace Symfony\Component\Routing\Tests\Loader;
use Doctrine\Common\Annotations\AnnotationReader; use PHPUnit\Framework\TestCase;
use Doctrine\Common\Annotations\AnnotationRegistry;
use Symfony\Component\Routing\Annotation\Route as RouteAnnotation;
use Symfony\Component\Routing\Loader\AnnotationClassLoader; use Symfony\Component\Routing\Loader\AnnotationClassLoader;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\Tests\Fixtures\AnnotationFixtures\AbstractClassController; use Symfony\Component\Routing\Tests\Fixtures\AnnotationFixtures\AbstractClassController;
use Symfony\Component\Routing\Tests\Fixtures\AnnotationFixtures\ActionPathController;
use Symfony\Component\Routing\Tests\Fixtures\AnnotationFixtures\DefaultValueController;
use Symfony\Component\Routing\Tests\Fixtures\AnnotationFixtures\ExplicitLocalizedActionPathController;
use Symfony\Component\Routing\Tests\Fixtures\AnnotationFixtures\GlobalDefaultsClass;
use Symfony\Component\Routing\Tests\Fixtures\AnnotationFixtures\InvokableController;
use Symfony\Component\Routing\Tests\Fixtures\AnnotationFixtures\InvokableLocalizedController;
use Symfony\Component\Routing\Tests\Fixtures\AnnotationFixtures\LocalizedActionPathController;
use Symfony\Component\Routing\Tests\Fixtures\AnnotationFixtures\LocalizedMethodActionControllers;
use Symfony\Component\Routing\Tests\Fixtures\AnnotationFixtures\LocalizedPrefixLocalizedActionController;
use Symfony\Component\Routing\Tests\Fixtures\AnnotationFixtures\LocalizedPrefixMissingLocaleActionController;
use Symfony\Component\Routing\Tests\Fixtures\AnnotationFixtures\LocalizedPrefixMissingRouteLocaleActionController;
use Symfony\Component\Routing\Tests\Fixtures\AnnotationFixtures\LocalizedPrefixWithRouteWithoutLocale;
use Symfony\Component\Routing\Tests\Fixtures\AnnotationFixtures\MethodActionControllers;
use Symfony\Component\Routing\Tests\Fixtures\AnnotationFixtures\MissingRouteNameController;
use Symfony\Component\Routing\Tests\Fixtures\AnnotationFixtures\NothingButNameController;
use Symfony\Component\Routing\Tests\Fixtures\AnnotationFixtures\PrefixedActionLocalizedRouteController;
use Symfony\Component\Routing\Tests\Fixtures\AnnotationFixtures\PrefixedActionPathController;
use Symfony\Component\Routing\Tests\Fixtures\AnnotationFixtures\RequirementsWithoutPlaceholderNameController;
use Symfony\Component\Routing\Tests\Fixtures\AnnotationFixtures\RouteWithPrefixController;
use Symfony\Component\Routing\Tests\Fixtures\AnnotationFixtures\Utf8ActionControllers;
class AnnotationClassLoaderTest extends AbstractAnnotationLoaderTest abstract class AnnotationClassLoaderTest extends TestCase
{ {
/** /**
* @var AnnotationClassLoader * @var AnnotationClassLoader
*/ */
private $loader; protected $loader;
protected function setUp(): void
{
$reader = new AnnotationReader();
$this->loader = new class($reader) extends AnnotationClassLoader {
protected function configureRoute(Route $route, \ReflectionClass $class, \ReflectionMethod $method, $annot): void
{
}
};
AnnotationRegistry::registerLoader('class_exists');
}
/** /**
* @dataProvider provideTestSupportsChecksResource * @dataProvider provideTestSupportsChecksResource
@ -85,7 +51,7 @@ class AnnotationClassLoaderTest extends AbstractAnnotationLoaderTest
public function testSimplePathRoute() public function testSimplePathRoute()
{ {
$routes = $this->loader->load(ActionPathController::class); $routes = $this->loader->load($this->getNamespace().'\ActionPathController');
$this->assertCount(1, $routes); $this->assertCount(1, $routes);
$this->assertEquals('/path', $routes->get('action')->getPath()); $this->assertEquals('/path', $routes->get('action')->getPath());
} }
@ -95,12 +61,12 @@ class AnnotationClassLoaderTest extends AbstractAnnotationLoaderTest
$this->expectException(\InvalidArgumentException::class); $this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('A placeholder name must be a string (0 given). Did you forget to specify the placeholder key for the requirement "foo"'); $this->expectExceptionMessage('A placeholder name must be a string (0 given). Did you forget to specify the placeholder key for the requirement "foo"');
$this->loader->load(RequirementsWithoutPlaceholderNameController::class); $this->loader->load($this->getNamespace().'\RequirementsWithoutPlaceholderNameController');
} }
public function testInvokableControllerLoader() public function testInvokableControllerLoader()
{ {
$routes = $this->loader->load(InvokableController::class); $routes = $this->loader->load($this->getNamespace().'\InvokableController');
$this->assertCount(1, $routes); $this->assertCount(1, $routes);
$this->assertEquals('/here', $routes->get('lol')->getPath()); $this->assertEquals('/here', $routes->get('lol')->getPath());
$this->assertEquals(['GET', 'POST'], $routes->get('lol')->getMethods()); $this->assertEquals(['GET', 'POST'], $routes->get('lol')->getMethods());
@ -109,7 +75,7 @@ class AnnotationClassLoaderTest extends AbstractAnnotationLoaderTest
public function testInvokableLocalizedControllerLoading() public function testInvokableLocalizedControllerLoading()
{ {
$routes = $this->loader->load(InvokableLocalizedController::class); $routes = $this->loader->load($this->getNamespace().'\InvokableLocalizedController');
$this->assertCount(2, $routes); $this->assertCount(2, $routes);
$this->assertEquals('/here', $routes->get('action.en')->getPath()); $this->assertEquals('/here', $routes->get('action.en')->getPath());
$this->assertEquals('/hier', $routes->get('action.nl')->getPath()); $this->assertEquals('/hier', $routes->get('action.nl')->getPath());
@ -117,7 +83,7 @@ class AnnotationClassLoaderTest extends AbstractAnnotationLoaderTest
public function testLocalizedPathRoutes() public function testLocalizedPathRoutes()
{ {
$routes = $this->loader->load(LocalizedActionPathController::class); $routes = $this->loader->load($this->getNamespace().'\LocalizedActionPathController');
$this->assertCount(2, $routes); $this->assertCount(2, $routes);
$this->assertEquals('/path', $routes->get('action.en')->getPath()); $this->assertEquals('/path', $routes->get('action.en')->getPath());
$this->assertEquals('/pad', $routes->get('action.nl')->getPath()); $this->assertEquals('/pad', $routes->get('action.nl')->getPath());
@ -128,7 +94,7 @@ class AnnotationClassLoaderTest extends AbstractAnnotationLoaderTest
public function testLocalizedPathRoutesWithExplicitPathPropety() public function testLocalizedPathRoutesWithExplicitPathPropety()
{ {
$routes = $this->loader->load(ExplicitLocalizedActionPathController::class); $routes = $this->loader->load($this->getNamespace().'\ExplicitLocalizedActionPathController');
$this->assertCount(2, $routes); $this->assertCount(2, $routes);
$this->assertEquals('/path', $routes->get('action.en')->getPath()); $this->assertEquals('/path', $routes->get('action.en')->getPath());
$this->assertEquals('/pad', $routes->get('action.nl')->getPath()); $this->assertEquals('/pad', $routes->get('action.nl')->getPath());
@ -136,7 +102,7 @@ class AnnotationClassLoaderTest extends AbstractAnnotationLoaderTest
public function testDefaultValuesForMethods() public function testDefaultValuesForMethods()
{ {
$routes = $this->loader->load(DefaultValueController::class); $routes = $this->loader->load($this->getNamespace().'\DefaultValueController');
$this->assertCount(3, $routes); $this->assertCount(3, $routes);
$this->assertEquals('/{default}/path', $routes->get('action')->getPath()); $this->assertEquals('/{default}/path', $routes->get('action')->getPath());
$this->assertEquals('value', $routes->get('action')->getDefault('default')); $this->assertEquals('value', $routes->get('action')->getDefault('default'));
@ -146,7 +112,7 @@ class AnnotationClassLoaderTest extends AbstractAnnotationLoaderTest
public function testMethodActionControllers() public function testMethodActionControllers()
{ {
$routes = $this->loader->load(MethodActionControllers::class); $routes = $this->loader->load($this->getNamespace().'\MethodActionControllers');
$this->assertSame(['put', 'post'], array_keys($routes->all())); $this->assertSame(['put', 'post'], array_keys($routes->all()));
$this->assertEquals('/the/path', $routes->get('put')->getPath()); $this->assertEquals('/the/path', $routes->get('put')->getPath());
$this->assertEquals('/the/path', $routes->get('post')->getPath()); $this->assertEquals('/the/path', $routes->get('post')->getPath());
@ -154,7 +120,7 @@ class AnnotationClassLoaderTest extends AbstractAnnotationLoaderTest
public function testInvokableClassRouteLoadWithMethodAnnotation() public function testInvokableClassRouteLoadWithMethodAnnotation()
{ {
$routes = $this->loader->load(LocalizedMethodActionControllers::class); $routes = $this->loader->load($this->getNamespace().'\LocalizedMethodActionControllers');
$this->assertCount(4, $routes); $this->assertCount(4, $routes);
$this->assertEquals('/the/path', $routes->get('put.en')->getPath()); $this->assertEquals('/the/path', $routes->get('put.en')->getPath());
$this->assertEquals('/the/path', $routes->get('post.en')->getPath()); $this->assertEquals('/the/path', $routes->get('post.en')->getPath());
@ -162,7 +128,7 @@ class AnnotationClassLoaderTest extends AbstractAnnotationLoaderTest
public function testGlobalDefaultsRoutesLoadWithAnnotation() public function testGlobalDefaultsRoutesLoadWithAnnotation()
{ {
$routes = $this->loader->load(GlobalDefaultsClass::class); $routes = $this->loader->load($this->getNamespace().'\GlobalDefaultsClass');
$this->assertCount(2, $routes); $this->assertCount(2, $routes);
$specificLocaleRoute = $routes->get('specific_locale'); $specificLocaleRoute = $routes->get('specific_locale');
@ -180,7 +146,7 @@ class AnnotationClassLoaderTest extends AbstractAnnotationLoaderTest
public function testUtf8RoutesLoadWithAnnotation() public function testUtf8RoutesLoadWithAnnotation()
{ {
$routes = $this->loader->load(Utf8ActionControllers::class); $routes = $this->loader->load($this->getNamespace().'\Utf8ActionControllers');
$this->assertSame(['one', 'two'], array_keys($routes->all())); $this->assertSame(['one', 'two'], array_keys($routes->all()));
$this->assertTrue($routes->get('one')->getOption('utf8'), 'The route must accept utf8'); $this->assertTrue($routes->get('one')->getOption('utf8'), 'The route must accept utf8');
$this->assertFalse($routes->get('two')->getOption('utf8'), 'The route must not accept utf8'); $this->assertFalse($routes->get('two')->getOption('utf8'), 'The route must not accept utf8');
@ -188,7 +154,7 @@ class AnnotationClassLoaderTest extends AbstractAnnotationLoaderTest
public function testRouteWithPathWithPrefix() public function testRouteWithPathWithPrefix()
{ {
$routes = $this->loader->load(PrefixedActionPathController::class); $routes = $this->loader->load($this->getNamespace().'\PrefixedActionPathController');
$this->assertCount(1, $routes); $this->assertCount(1, $routes);
$route = $routes->get('action'); $route = $routes->get('action');
$this->assertEquals('/prefix/path', $route->getPath()); $this->assertEquals('/prefix/path', $route->getPath());
@ -198,7 +164,7 @@ class AnnotationClassLoaderTest extends AbstractAnnotationLoaderTest
public function testLocalizedRouteWithPathWithPrefix() public function testLocalizedRouteWithPathWithPrefix()
{ {
$routes = $this->loader->load(PrefixedActionLocalizedRouteController::class); $routes = $this->loader->load($this->getNamespace().'\PrefixedActionLocalizedRouteController');
$this->assertCount(2, $routes); $this->assertCount(2, $routes);
$this->assertEquals('/prefix/path', $routes->get('action.en')->getPath()); $this->assertEquals('/prefix/path', $routes->get('action.en')->getPath());
$this->assertEquals('/prefix/pad', $routes->get('action.nl')->getPath()); $this->assertEquals('/prefix/pad', $routes->get('action.nl')->getPath());
@ -206,7 +172,7 @@ class AnnotationClassLoaderTest extends AbstractAnnotationLoaderTest
public function testLocalizedPrefixLocalizedRoute() public function testLocalizedPrefixLocalizedRoute()
{ {
$routes = $this->loader->load(LocalizedPrefixLocalizedActionController::class); $routes = $this->loader->load($this->getNamespace().'\LocalizedPrefixLocalizedActionController');
$this->assertCount(2, $routes); $this->assertCount(2, $routes);
$this->assertEquals('/nl/actie', $routes->get('action.nl')->getPath()); $this->assertEquals('/nl/actie', $routes->get('action.nl')->getPath());
$this->assertEquals('/en/action', $routes->get('action.en')->getPath()); $this->assertEquals('/en/action', $routes->get('action.en')->getPath());
@ -214,73 +180,42 @@ class AnnotationClassLoaderTest extends AbstractAnnotationLoaderTest
public function testInvokableClassMultipleRouteLoad() public function testInvokableClassMultipleRouteLoad()
{ {
$classRouteData1 = [ $routeCollection = $this->loader->load($this->getNamespace().'\BazClass');
'name' => 'route1', $route = $routeCollection->get('route1');
'path' => '/1',
'schemes' => ['https'],
'methods' => ['GET'],
];
$classRouteData2 = [ $this->assertSame('/1', $route->getPath(), '->load preserves class route path');
'name' => 'route2', $this->assertSame(['https'], $route->getSchemes(), '->load preserves class route schemes');
'path' => '/2', $this->assertSame(['GET'], $route->getMethods(), '->load preserves class route methods');
'schemes' => ['https'],
'methods' => ['GET'],
];
$reader = $this->getReader(); $route = $routeCollection->get('route2');
$reader
->expects($this->exactly(1))
->method('getClassAnnotations')
->willReturn([new RouteAnnotation($classRouteData1), new RouteAnnotation($classRouteData2)])
;
$reader
->expects($this->once())
->method('getMethodAnnotations')
->willReturn([])
;
$loader = new class($reader) extends AnnotationClassLoader {
protected function configureRoute(Route $route, \ReflectionClass $class, \ReflectionMethod $method, $annot): void
{
}
};
$routeCollection = $loader->load('Symfony\Component\Routing\Tests\Fixtures\AnnotatedClasses\BazClass'); $this->assertSame('/2', $route->getPath(), '->load preserves class route path');
$route = $routeCollection->get($classRouteData1['name']); $this->assertEquals(['https'], $route->getSchemes(), '->load preserves class route schemes');
$this->assertEquals(['GET'], $route->getMethods(), '->load preserves class route methods');
$this->assertSame($classRouteData1['path'], $route->getPath(), '->load preserves class route path');
$this->assertEquals($classRouteData1['schemes'], $route->getSchemes(), '->load preserves class route schemes');
$this->assertEquals($classRouteData1['methods'], $route->getMethods(), '->load preserves class route methods');
$route = $routeCollection->get($classRouteData2['name']);
$this->assertSame($classRouteData2['path'], $route->getPath(), '->load preserves class route path');
$this->assertEquals($classRouteData2['schemes'], $route->getSchemes(), '->load preserves class route schemes');
$this->assertEquals($classRouteData2['methods'], $route->getMethods(), '->load preserves class route methods');
} }
public function testMissingPrefixLocale() public function testMissingPrefixLocale()
{ {
$this->expectException(\LogicException::class); $this->expectException(\LogicException::class);
$this->loader->load(LocalizedPrefixMissingLocaleActionController::class); $this->loader->load($this->getNamespace().'\LocalizedPrefixMissingLocaleActionController');
} }
public function testMissingRouteLocale() public function testMissingRouteLocale()
{ {
$this->expectException(\LogicException::class); $this->expectException(\LogicException::class);
$this->loader->load(LocalizedPrefixMissingRouteLocaleActionController::class); $this->loader->load($this->getNamespace().'\LocalizedPrefixMissingRouteLocaleActionController');
} }
public function testRouteWithoutName() public function testRouteWithoutName()
{ {
$routes = $this->loader->load(MissingRouteNameController::class)->all(); $routes = $this->loader->load($this->getNamespace().'\MissingRouteNameController')->all();
$this->assertCount(1, $routes); $this->assertCount(1, $routes);
$this->assertEquals('/path', reset($routes)->getPath()); $this->assertEquals('/path', reset($routes)->getPath());
} }
public function testNothingButName() public function testNothingButName()
{ {
$routes = $this->loader->load(NothingButNameController::class)->all(); $routes = $this->loader->load($this->getNamespace().'\NothingButNameController')->all();
$this->assertCount(1, $routes); $this->assertCount(1, $routes);
$this->assertEquals('/', reset($routes)->getPath()); $this->assertEquals('/', reset($routes)->getPath());
} }
@ -299,44 +234,18 @@ class AnnotationClassLoaderTest extends AbstractAnnotationLoaderTest
public function testLocalizedPrefixWithoutRouteLocale() public function testLocalizedPrefixWithoutRouteLocale()
{ {
$routes = $this->loader->load(LocalizedPrefixWithRouteWithoutLocale::class); $routes = $this->loader->load($this->getNamespace().'\LocalizedPrefixWithRouteWithoutLocale');
$this->assertCount(2, $routes); $this->assertCount(2, $routes);
$this->assertEquals('/en/suffix', $routes->get('action.en')->getPath()); $this->assertEquals('/en/suffix', $routes->get('action.en')->getPath());
$this->assertEquals('/nl/suffix', $routes->get('action.nl')->getPath()); $this->assertEquals('/nl/suffix', $routes->get('action.nl')->getPath());
} }
/**
* @requires function mb_strtolower
*/
public function testDefaultRouteName()
{
$methodRouteData = [
'name' => null,
];
$reader = $this->getReader();
$reader
->expects($this->once())
->method('getMethodAnnotations')
->willReturn([new RouteAnnotation($methodRouteData)])
;
$loader = new class($reader) extends AnnotationClassLoader {
protected function configureRoute(Route $route, \ReflectionClass $class, \ReflectionMethod $method, $annot): void
{
}
};
$routeCollection = $loader->load('Symfony\Component\Routing\Tests\Fixtures\AnnotatedClasses\EncodingClass');
$defaultName = array_keys($routeCollection->all())[0];
$this->assertSame($defaultName, 'symfony_component_routing_tests_fixtures_annotatedclasses_encodingclass_routeàction');
}
public function testLoadingRouteWithPrefix() public function testLoadingRouteWithPrefix()
{ {
$routes = $this->loader->load(RouteWithPrefixController::class); $routes = $this->loader->load($this->getNamespace().'\RouteWithPrefixController');
$this->assertCount(1, $routes); $this->assertCount(1, $routes);
$this->assertEquals('/prefix/path', $routes->get('action')->getPath()); $this->assertEquals('/prefix/path', $routes->get('action')->getPath());
} }
abstract protected function getNamespace(): string;
} }

View File

@ -0,0 +1,35 @@
<?php
namespace Symfony\Component\Routing\Tests\Loader;
use Doctrine\Common\Annotations\AnnotationReader;
use Doctrine\Common\Annotations\AnnotationRegistry;
use Symfony\Component\Routing\Loader\AnnotationClassLoader;
use Symfony\Component\Routing\Route;
class AnnotationClassLoaderWithAnnotationsTest extends AnnotationClassLoaderTest
{
protected function setUp(): void
{
$reader = new AnnotationReader();
$this->loader = new class($reader) extends AnnotationClassLoader {
protected function configureRoute(Route $route, \ReflectionClass $class, \ReflectionMethod $method, object $annot): void
{
}
};
AnnotationRegistry::registerLoader('class_exists');
}
public function testDefaultRouteName()
{
$routeCollection = $this->loader->load($this->getNamespace().'\EncodingClass');
$defaultName = array_keys($routeCollection->all())[0];
$this->assertSame('symfony_component_routing_tests_fixtures_annotationfixtures_encodingclass_routeàction', $defaultName);
}
protected function getNamespace(): string
{
return 'Symfony\Component\Routing\Tests\Fixtures\AnnotationFixtures';
}
}

View File

@ -0,0 +1,34 @@
<?php
namespace Symfony\Component\Routing\Tests\Loader;
use Symfony\Component\Routing\Loader\AnnotationClassLoader;
use Symfony\Component\Routing\Route;
/**
* @requires PHP 8
*/
class AnnotationClassLoaderWithAttributesTest extends AnnotationClassLoaderTest
{
protected function setUp(): void
{
$this->loader = new class() extends AnnotationClassLoader {
protected function configureRoute(Route $route, \ReflectionClass $class, \ReflectionMethod $method, object $annot): void
{
}
};
}
public function testDefaultRouteName()
{
$routeCollection = $this->loader->load($this->getNamespace().'\EncodingClass');
$defaultName = array_keys($routeCollection->all())[0];
$this->assertSame('symfony_component_routing_tests_fixtures_attributefixtures_encodingclass_routeàction', $defaultName);
}
protected function getNamespace(): string
{
return 'Symfony\Component\Routing\Tests\Fixtures\AttributeFixtures';
}
}

View File

@ -26,7 +26,7 @@
"symfony/yaml": "^4.4|^5.0", "symfony/yaml": "^4.4|^5.0",
"symfony/expression-language": "^4.4|^5.0", "symfony/expression-language": "^4.4|^5.0",
"symfony/dependency-injection": "^4.4|^5.0", "symfony/dependency-injection": "^4.4|^5.0",
"doctrine/annotations": "~1.2", "doctrine/annotations": "^1.7",
"psr/log": "~1.0" "psr/log": "~1.0"
}, },
"conflict": { "conflict": {