diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index 41755c4cb525b..04070e45039dc 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -25,7 +25,6 @@ use Symfony\Bridge\Monolog\Processor\DebugProcessor; use Symfony\Bridge\Twig\Extension\CsrfExtension; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; -use Symfony\Bundle\FrameworkBundle\Routing\AnnotatedRouteControllerLoader; use Symfony\Bundle\FrameworkBundle\Routing\RouteLoaderInterface; use Symfony\Bundle\FullStack; use Symfony\Bundle\MercureBundle\MercureBundle; @@ -194,8 +193,7 @@ use Symfony\Component\RateLimiter\LimiterInterface; use Symfony\Component\RateLimiter\RateLimiterFactory; use Symfony\Component\RateLimiter\Storage\CacheStorage; -use Symfony\Component\Routing\Loader\AnnotationDirectoryLoader; -use Symfony\Component\Routing\Loader\AnnotationFileLoader; +use Symfony\Component\Routing\Loader\Psr4DirectoryLoader; use Symfony\Component\Security\Core\AuthenticationEvents; use Symfony\Component\Security\Core\Exception\AuthenticationException; use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface; @@ -1157,29 +1155,9 @@ private function registerRouterConfiguration(array $config, ContainerBuilder $co ->replaceArgument(0, $config['default_uri']); } - $container->register('routing.loader.annotation', AnnotatedRouteControllerLoader::class) - ->setPublic(false) - ->addTag('routing.loader', ['priority' => -10]) - ->setArguments([ - new Reference('annotation_reader', ContainerInterface::NULL_ON_INVALID_REFERENCE), - '%kernel.environment%', - ]); - - $container->register('routing.loader.annotation.directory', AnnotationDirectoryLoader::class) - ->setPublic(false) - ->addTag('routing.loader', ['priority' => -10]) - ->setArguments([ - new Reference('file_locator'), - new Reference('routing.loader.annotation'), - ]); - - $container->register('routing.loader.annotation.file', AnnotationFileLoader::class) - ->setPublic(false) - ->addTag('routing.loader', ['priority' => -10]) - ->setArguments([ - new Reference('file_locator'), - new Reference('routing.loader.annotation'), - ]); + if (!class_exists(Psr4DirectoryLoader::class)) { + $container->removeDefinition('routing.loader.psr4'); + } } private function registerSessionConfiguration(array $config, ContainerBuilder $container, PhpFileLoader $loader) diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/routing.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/routing.php index 8ecb477e18aeb..86a7cf874629c 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/routing.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/routing.php @@ -15,6 +15,7 @@ use Symfony\Bundle\FrameworkBundle\CacheWarmer\RouterCacheWarmer; use Symfony\Bundle\FrameworkBundle\Controller\RedirectController; use Symfony\Bundle\FrameworkBundle\Controller\TemplateController; +use Symfony\Bundle\FrameworkBundle\Routing\AnnotatedRouteControllerLoader; use Symfony\Bundle\FrameworkBundle\Routing\DelegatingLoader; use Symfony\Bundle\FrameworkBundle\Routing\RedirectableCompiledUrlMatcher; use Symfony\Bundle\FrameworkBundle\Routing\Router; @@ -23,10 +24,13 @@ use Symfony\Component\Routing\Generator\CompiledUrlGenerator; use Symfony\Component\Routing\Generator\Dumper\CompiledUrlGeneratorDumper; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; +use Symfony\Component\Routing\Loader\AnnotationDirectoryLoader; +use Symfony\Component\Routing\Loader\AnnotationFileLoader; use Symfony\Component\Routing\Loader\ContainerLoader; use Symfony\Component\Routing\Loader\DirectoryLoader; use Symfony\Component\Routing\Loader\GlobFileLoader; use Symfony\Component\Routing\Loader\PhpFileLoader; +use Symfony\Component\Routing\Loader\Psr4DirectoryLoader; use Symfony\Component\Routing\Loader\XmlFileLoader; use Symfony\Component\Routing\Loader\YamlFileLoader; use Symfony\Component\Routing\Matcher\Dumper\CompiledUrlMatcherDumper; @@ -88,6 +92,33 @@ ]) ->tag('routing.loader') + ->set('routing.loader.annotation', AnnotatedRouteControllerLoader::class) + ->args([ + service('annotation_reader')->nullOnInvalid(), + '%kernel.environment%', + ]) + ->tag('routing.loader', ['priority' => -10]) + + ->set('routing.loader.annotation.directory', AnnotationDirectoryLoader::class) + ->args([ + service('file_locator'), + service('routing.loader.annotation'), + ]) + ->tag('routing.loader', ['priority' => -10]) + + ->set('routing.loader.annotation.file', AnnotationFileLoader::class) + ->args([ + service('file_locator'), + service('routing.loader.annotation'), + ]) + ->tag('routing.loader', ['priority' => -10]) + + ->set('routing.loader.psr4', Psr4DirectoryLoader::class) + ->args([ + service('file_locator'), + ]) + ->tag('routing.loader', ['priority' => -10]) + ->set('routing.loader', DelegatingLoader::class) ->public() ->args([ diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/AbstractAttributeRoutingTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/AbstractAttributeRoutingTest.php new file mode 100644 index 0000000000000..cf971d0d60dc3 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/AbstractAttributeRoutingTest.php @@ -0,0 +1,45 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\Tests\Functional; + +abstract class AbstractAttributeRoutingTest extends AbstractWebTestCase +{ + /** + * @dataProvider getRoutes + */ + public function testAnnotatedController(string $path, string $expectedValue) + { + $client = $this->createClient(['test_case' => $this->getTestCaseApp(), 'root_config' => 'config.yml']); + $client->request('GET', '/annotated'.$path); + + $this->assertSame(200, $client->getResponse()->getStatusCode()); + $this->assertSame($expectedValue, $client->getResponse()->getContent()); + + $router = self::getContainer()->get('router'); + + $this->assertSame('/annotated/create-transaction', $router->generate('symfony_framework_tests_functional_test_annotated_createtransaction')); + } + + public function getRoutes(): array + { + return [ + ['/null_request', 'Symfony\Component\HttpFoundation\Request'], + ['/null_argument', ''], + ['/null_argument_with_route_param', ''], + ['/null_argument_with_route_param/value', 'value'], + ['/argument_with_route_param_and_default', 'value'], + ['/argument_with_route_param_and_default/custom', 'custom'], + ]; + } + + abstract protected function getTestCaseApp(): string; +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/AnnotatedControllerTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/AnnotatedControllerTest.php index a0be5fcef06a7..631d75bb50dff 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/AnnotatedControllerTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/AnnotatedControllerTest.php @@ -11,33 +11,10 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\Functional; -class AnnotatedControllerTest extends AbstractWebTestCase +class AnnotatedControllerTest extends AbstractAttributeRoutingTest { - /** - * @dataProvider getRoutes - */ - public function testAnnotatedController($path, $expectedValue) + protected function getTestCaseApp(): string { - $client = $this->createClient(['test_case' => 'AnnotatedController', 'root_config' => 'config.yml']); - $client->request('GET', '/annotated'.$path); - - $this->assertSame(200, $client->getResponse()->getStatusCode()); - $this->assertSame($expectedValue, $client->getResponse()->getContent()); - - $router = self::getContainer()->get('router'); - - $this->assertSame('/annotated/create-transaction', $router->generate('symfony_framework_tests_functional_test_annotated_createtransaction')); - } - - public function getRoutes() - { - return [ - ['/null_request', 'Symfony\Component\HttpFoundation\Request'], - ['/null_argument', ''], - ['/null_argument_with_route_param', ''], - ['/null_argument_with_route_param/value', 'value'], - ['/argument_with_route_param_and_default', 'value'], - ['/argument_with_route_param_and_default/custom', 'custom'], - ]; + return 'AnnotatedController'; } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/Controller/AnnotatedController.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/Controller/AnnotatedController.php index e6b917b63cc60..2bee3d0b833e1 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/Controller/AnnotatedController.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/Controller/AnnotatedController.php @@ -17,42 +17,32 @@ class AnnotatedController { - /** - * @Route("/null_request", name="null_request") - */ - public function requestDefaultNullAction(Request $request = null) + #[Route('/null_request', name: 'null_request')] + public function requestDefaultNullAction(Request $request = null): Response { return new Response($request ? $request::class : null); } - /** - * @Route("/null_argument", name="null_argument") - */ - public function argumentDefaultNullWithoutRouteParamAction($value = null) + #[Route('/null_argument', name: 'null_argument')] + public function argumentDefaultNullWithoutRouteParamAction($value = null): Response { return new Response($value); } - /** - * @Route("/null_argument_with_route_param/{value}", name="null_argument_with_route_param") - */ - public function argumentDefaultNullWithRouteParamAction($value = null) + #[Route('/null_argument_with_route_param/{value}', name: 'null_argument_with_route_param')] + public function argumentDefaultNullWithRouteParamAction($value = null): Response { return new Response($value); } - /** - * @Route("/argument_with_route_param_and_default/{value}", defaults={"value": "value"}, name="argument_with_route_param_and_default") - */ - public function argumentWithoutDefaultWithRouteParamAndDefaultAction($value) + #[Route('/argument_with_route_param_and_default/{value}', defaults: ['value' => 'value'], name: 'argument_with_route_param_and_default')] + public function argumentWithoutDefaultWithRouteParamAndDefaultAction($value): Response { return new Response($value); } - /** - * @Route("/create-transaction") - */ - public function createTransaction() + #[Route('/create-transaction')] + public function createTransaction(): Response { return new Response(); } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Psr4RoutingTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Psr4RoutingTest.php new file mode 100644 index 0000000000000..0d0d715c9d0b4 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Psr4RoutingTest.php @@ -0,0 +1,23 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\Tests\Functional; + +/** + * @requires function Symfony\Component\Routing\Loader\Psr4DirectoryLoader::__construct + */ +final class Psr4RoutingTest extends AbstractAttributeRoutingTest +{ + protected function getTestCaseApp(): string + { + return 'Psr4Routing'; + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/Psr4Routing/bundles.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/Psr4Routing/bundles.php new file mode 100644 index 0000000000000..15ff182c6fed5 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/Psr4Routing/bundles.php @@ -0,0 +1,18 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Symfony\Bundle\FrameworkBundle\FrameworkBundle; +use Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\TestBundle\TestBundle; + +return [ + new FrameworkBundle(), + new TestBundle(), +]; diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/Psr4Routing/config.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/Psr4Routing/config.yml new file mode 100644 index 0000000000000..377d3e7852064 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/Psr4Routing/config.yml @@ -0,0 +1,2 @@ +imports: + - { resource: ../config/default.yml } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/Psr4Routing/routing.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/Psr4Routing/routing.yml new file mode 100644 index 0000000000000..94c328341c969 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/Psr4Routing/routing.yml @@ -0,0 +1,4 @@ +test_bundle: + prefix: /annotated + resource: "@TestBundle/Controller" + type: attribute@Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\TestBundle\Controller diff --git a/src/Symfony/Component/Routing/Loader/Psr4DirectoryLoader.php b/src/Symfony/Component/Routing/Loader/Psr4DirectoryLoader.php new file mode 100644 index 0000000000000..701b79b01098f --- /dev/null +++ b/src/Symfony/Component/Routing/Loader/Psr4DirectoryLoader.php @@ -0,0 +1,66 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing\Loader; + +use Symfony\Component\Config\FileLocatorInterface; +use Symfony\Component\Config\Loader\FileLoader; +use Symfony\Component\Routing\RouteCollection; + +/** + * A loader that discovers controller classes in a directory that follows PSR-4. + * + * @author Alexander M. Turek + */ +final class Psr4DirectoryLoader extends FileLoader +{ + public function __construct(FileLocatorInterface $locator) + { + // PSR-4 directory loader has no env-aware logic, so we drop the $env constructor parameter. + parent::__construct($locator); + } + + public function load(mixed $resource, string $type = null): ?RouteCollection + { + $path = $this->locator->locate($resource); + if (!is_dir($path)) { + return new RouteCollection(); + } + + return $this->loadFromDirectory($path, trim(substr($type, 10), ' \\')); + } + + public function supports(mixed $resource, string $type = null): bool + { + return \is_string($resource) && null !== $type && str_starts_with($type, 'attribute@'); + } + + private function loadFromDirectory(string $directory, string $psr4Prefix): RouteCollection + { + $collection = new RouteCollection(); + + /** @var \SplFileInfo $file */ + foreach (new \FilesystemIterator($directory) as $file) { + if ($file->isDir()) { + $collection->addCollection($this->loadFromDirectory($file->getPathname(), $psr4Prefix.'\\'.$file->getFilename())); + + continue; + } + if ('php' !== $file->getExtension() || !class_exists($className = $psr4Prefix.'\\'.$file->getBasename('.php'))) { + continue; + } + + $collection->addCollection($this->import($className, 'attribute')); + } + + return $collection; + } +} diff --git a/src/Symfony/Component/Routing/Tests/Fixtures/Psr4Controllers/MyController.php b/src/Symfony/Component/Routing/Tests/Fixtures/Psr4Controllers/MyController.php new file mode 100644 index 0000000000000..faa72826fac0a --- /dev/null +++ b/src/Symfony/Component/Routing/Tests/Fixtures/Psr4Controllers/MyController.php @@ -0,0 +1,24 @@ + + * + * 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\Psr4Controllers; + +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Routing\Annotation\Route; + +#[Route('/my/route', name: 'my_route')] +final class MyController +{ + public function __invoke(): Response + { + return new Response(status: Response::HTTP_NO_CONTENT); + } +} diff --git a/src/Symfony/Component/Routing/Tests/Fixtures/Psr4Controllers/MyUnannotatedController.php b/src/Symfony/Component/Routing/Tests/Fixtures/Psr4Controllers/MyUnannotatedController.php new file mode 100644 index 0000000000000..45f6989369a4a --- /dev/null +++ b/src/Symfony/Component/Routing/Tests/Fixtures/Psr4Controllers/MyUnannotatedController.php @@ -0,0 +1,22 @@ + + * + * 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\Psr4Controllers; + +use Symfony\Component\HttpFoundation\Response; + +final class MyUnannotatedController +{ + public function myAction(): Response + { + return new Response(status: Response::HTTP_NO_CONTENT); + } +} diff --git a/src/Symfony/Component/Routing/Tests/Fixtures/Psr4Controllers/SubNamespace/EvenDeeperNamespace/MyOtherController.php b/src/Symfony/Component/Routing/Tests/Fixtures/Psr4Controllers/SubNamespace/EvenDeeperNamespace/MyOtherController.php new file mode 100644 index 0000000000000..a5e43d8f22f55 --- /dev/null +++ b/src/Symfony/Component/Routing/Tests/Fixtures/Psr4Controllers/SubNamespace/EvenDeeperNamespace/MyOtherController.php @@ -0,0 +1,31 @@ + + * + * 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\Psr4Controllers\SubNamespace\EvenDeeperNamespace; + +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Routing\Annotation\Route; + +#[Route('/my/other/route', name: 'my_other_controller_', methods: ['PUT'])] +final class MyOtherController +{ + #[Route('/first', name: 'one')] + public function firstAction(): Response + { + return new Response(status: Response::HTTP_NO_CONTENT); + } + + #[Route('/second', name: 'two')] + public function secondAction(): Response + { + return new Response(status: Response::HTTP_NO_CONTENT); + } +} diff --git a/src/Symfony/Component/Routing/Tests/Fixtures/Psr4Controllers/SubNamespace/IrrelevantInterface.php b/src/Symfony/Component/Routing/Tests/Fixtures/Psr4Controllers/SubNamespace/IrrelevantInterface.php new file mode 100644 index 0000000000000..eb7dac654eebf --- /dev/null +++ b/src/Symfony/Component/Routing/Tests/Fixtures/Psr4Controllers/SubNamespace/IrrelevantInterface.php @@ -0,0 +1,7 @@ + + * + * 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\Psr4Controllers\SubNamespace; + +use Symfony\Component\Routing\Annotation\Route; + +#[Route('/my/controller/with/a/trait', name: 'my_controller_')] +final class MyControllerWithATrait implements IrrelevantInterface +{ + use SomeSharedImplementation; +} diff --git a/src/Symfony/Component/Routing/Tests/Fixtures/Psr4Controllers/SubNamespace/SomeSharedImplementation.php b/src/Symfony/Component/Routing/Tests/Fixtures/Psr4Controllers/SubNamespace/SomeSharedImplementation.php new file mode 100644 index 0000000000000..736ae6db197db --- /dev/null +++ b/src/Symfony/Component/Routing/Tests/Fixtures/Psr4Controllers/SubNamespace/SomeSharedImplementation.php @@ -0,0 +1,24 @@ + + * + * 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\Psr4Controllers\SubNamespace; + +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Routing\Annotation\Route; + +trait SomeSharedImplementation +{ + #[Route('/a/route/from/a/trait', name: 'with_a_trait')] + public function someAction(): Response + { + return new Response(status: Response::HTTP_NO_CONTENT); + } +} diff --git a/src/Symfony/Component/Routing/Tests/Fixtures/class-attributes.xml b/src/Symfony/Component/Routing/Tests/Fixtures/class-attributes.xml new file mode 100644 index 0000000000000..55d1a92f2708a --- /dev/null +++ b/src/Symfony/Component/Routing/Tests/Fixtures/class-attributes.xml @@ -0,0 +1,9 @@ + + + + + diff --git a/src/Symfony/Component/Routing/Tests/Fixtures/class-attributes.yaml b/src/Symfony/Component/Routing/Tests/Fixtures/class-attributes.yaml new file mode 100644 index 0000000000000..545fa8df07fe3 --- /dev/null +++ b/src/Symfony/Component/Routing/Tests/Fixtures/class-attributes.yaml @@ -0,0 +1,4 @@ +my_controllers: + resource: Symfony\Component\Routing\Tests\Fixtures\Psr4Controllers\MyController + type: attribute + prefix: /my-prefix diff --git a/src/Symfony/Component/Routing/Tests/Fixtures/psr4-attributes.xml b/src/Symfony/Component/Routing/Tests/Fixtures/psr4-attributes.xml new file mode 100644 index 0000000000000..7c01826044436 --- /dev/null +++ b/src/Symfony/Component/Routing/Tests/Fixtures/psr4-attributes.xml @@ -0,0 +1,9 @@ + + + + + diff --git a/src/Symfony/Component/Routing/Tests/Fixtures/psr4-attributes.yaml b/src/Symfony/Component/Routing/Tests/Fixtures/psr4-attributes.yaml new file mode 100644 index 0000000000000..d9902860a321b --- /dev/null +++ b/src/Symfony/Component/Routing/Tests/Fixtures/psr4-attributes.yaml @@ -0,0 +1,4 @@ +my_controllers: + resource: ./Psr4Controllers + type: attribute@Symfony\Component\Routing\Tests\Fixtures\Psr4Controllers + prefix: /my-prefix diff --git a/src/Symfony/Component/Routing/Tests/Loader/Psr4DirectoryLoaderTest.php b/src/Symfony/Component/Routing/Tests/Loader/Psr4DirectoryLoaderTest.php new file mode 100644 index 0000000000000..6ad14da22590e --- /dev/null +++ b/src/Symfony/Component/Routing/Tests/Loader/Psr4DirectoryLoaderTest.php @@ -0,0 +1,107 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing\Tests\Loader; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Config\FileLocator; +use Symfony\Component\Config\Loader\DelegatingLoader; +use Symfony\Component\Config\Loader\LoaderResolver; +use Symfony\Component\Routing\Loader\AnnotationClassLoader; +use Symfony\Component\Routing\Loader\Psr4DirectoryLoader; +use Symfony\Component\Routing\Route; +use Symfony\Component\Routing\RouteCollection; +use Symfony\Component\Routing\Tests\Fixtures\Psr4Controllers\MyController; +use Symfony\Component\Routing\Tests\Fixtures\Psr4Controllers\SubNamespace\EvenDeeperNamespace\MyOtherController; +use Symfony\Component\Routing\Tests\Fixtures\Psr4Controllers\SubNamespace\MyControllerWithATrait; + +class Psr4DirectoryLoaderTest extends TestCase +{ + public function testTopLevelController() + { + $route = $this->loadPsr4Controllers()->get('my_route'); + + $this->assertSame('/my/route', $route->getPath()); + $this->assertSame(MyController::class.'::__invoke', $route->getDefault('_controller')); + } + + public function testNestedController() + { + $collection = $this->loadPsr4Controllers(); + + $route = $collection->get('my_other_controller_one'); + $this->assertSame('/my/other/route/first', $route->getPath()); + $this->assertSame(['PUT'], $route->getMethods()); + $this->assertSame(MyOtherController::class.'::firstAction', $route->getDefault('_controller')); + + $route = $collection->get('my_other_controller_two'); + $this->assertSame('/my/other/route/second', $route->getPath()); + $this->assertSame(['PUT'], $route->getMethods()); + $this->assertSame(MyOtherController::class.'::secondAction', $route->getDefault('_controller')); + } + + public function testTraitController() + { + $route = $this->loadPsr4Controllers()->get('my_controller_with_a_trait'); + + $this->assertSame('/my/controller/with/a/trait/a/route/from/a/trait', $route->getPath()); + $this->assertSame(MyControllerWithATrait::class.'::someAction', $route->getDefault('_controller')); + } + + /** + * @dataProvider provideResourceTypesThatNeedTrimming + */ + public function testPsr4NamespaceTrim(string $resourceType) + { + $route = $this->getLoader() + ->load('Psr4Controllers', $resourceType) + ->get('my_route'); + + $this->assertSame('/my/route', $route->getPath()); + $this->assertSame(MyController::class.'::__invoke', $route->getDefault('_controller')); + } + + public function provideResourceTypesThatNeedTrimming(): array + { + return [ + ['attribute@ Symfony\Component\Routing\Tests\Fixtures\Psr4Controllers'], + ['attribute@\\Symfony\Component\Routing\Tests\Fixtures\Psr4Controllers'], + ['attribute@Symfony\Component\Routing\Tests\Fixtures\Psr4Controllers\\'], + ['attribute@\\Symfony\Component\Routing\Tests\Fixtures\Psr4Controllers\\'], + ['attribute@ \\Symfony\Component\Routing\Tests\Fixtures\Psr4Controllers'], + ]; + } + + private function loadPsr4Controllers(): RouteCollection + { + return $this->getLoader()->load( + 'Psr4Controllers', + 'attribute@Symfony\Component\Routing\Tests\Fixtures\Psr4Controllers' + ); + } + + private function getLoader(): DelegatingLoader + { + $locator = new FileLocator(\dirname(__DIR__).'/Fixtures'); + + return new DelegatingLoader( + new LoaderResolver([ + new Psr4DirectoryLoader($locator), + new class() extends AnnotationClassLoader { + protected function configureRoute(Route $route, \ReflectionClass $class, \ReflectionMethod $method, object $annot) + { + $route->setDefault('_controller', $class->getName().'::'.$method->getName()); + } + }, + ]) + ); + } +} diff --git a/src/Symfony/Component/Routing/Tests/Loader/XmlFileLoaderTest.php b/src/Symfony/Component/Routing/Tests/Loader/XmlFileLoaderTest.php index 7637fd600aa92..e52e96673acd3 100644 --- a/src/Symfony/Component/Routing/Tests/Loader/XmlFileLoaderTest.php +++ b/src/Symfony/Component/Routing/Tests/Loader/XmlFileLoaderTest.php @@ -13,11 +13,15 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\Config\FileLocator; +use Symfony\Component\Config\Loader\LoaderResolver; use Symfony\Component\Config\Resource\FileResource; +use Symfony\Component\Routing\Loader\AnnotationClassLoader; +use Symfony\Component\Routing\Loader\Psr4DirectoryLoader; use Symfony\Component\Routing\Loader\XmlFileLoader; use Symfony\Component\Routing\Route; use Symfony\Component\Routing\RouteCollection; use Symfony\Component\Routing\Tests\Fixtures\CustomXmlFileLoader; +use Symfony\Component\Routing\Tests\Fixtures\Psr4Controllers\MyController; class XmlFileLoaderTest extends TestCase { @@ -592,4 +596,40 @@ public function testImportingAliases() $this->assertEquals($expectedRoutes('xml'), $routes); } + + public function testImportAttributesWithPsr4Prefix() + { + $locator = new FileLocator(\dirname(__DIR__).'/Fixtures'); + new LoaderResolver([ + $loader = new XmlFileLoader($locator), + new Psr4DirectoryLoader($locator), + new class() extends AnnotationClassLoader { + protected function configureRoute(Route $route, \ReflectionClass $class, \ReflectionMethod $method, object $annot) + { + $route->setDefault('_controller', $class->getName().'::'.$method->getName()); + } + }, + ]); + + $route = $loader->load('psr4-attributes.xml')->get('my_route'); + $this->assertSame('/my-prefix/my/route', $route->getPath()); + $this->assertSame(MyController::class.'::__invoke', $route->getDefault('_controller')); + } + + public function testImportAttributesFromClass() + { + new LoaderResolver([ + $loader = new XmlFileLoader(new FileLocator(\dirname(__DIR__).'/Fixtures')), + new class() extends AnnotationClassLoader { + protected function configureRoute(Route $route, \ReflectionClass $class, \ReflectionMethod $method, object $annot) + { + $route->setDefault('_controller', $class->getName().'::'.$method->getName()); + } + }, + ]); + + $route = $loader->load('class-attributes.xml')->get('my_route'); + $this->assertSame('/my-prefix/my/route', $route->getPath()); + $this->assertSame(MyController::class.'::__invoke', $route->getDefault('_controller')); + } } diff --git a/src/Symfony/Component/Routing/Tests/Loader/YamlFileLoaderTest.php b/src/Symfony/Component/Routing/Tests/Loader/YamlFileLoaderTest.php index b509ce36237c8..a9944946ccde2 100644 --- a/src/Symfony/Component/Routing/Tests/Loader/YamlFileLoaderTest.php +++ b/src/Symfony/Component/Routing/Tests/Loader/YamlFileLoaderTest.php @@ -13,10 +13,14 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\Config\FileLocator; +use Symfony\Component\Config\Loader\LoaderResolver; use Symfony\Component\Config\Resource\FileResource; +use Symfony\Component\Routing\Loader\AnnotationClassLoader; +use Symfony\Component\Routing\Loader\Psr4DirectoryLoader; use Symfony\Component\Routing\Loader\YamlFileLoader; use Symfony\Component\Routing\Route; use Symfony\Component\Routing\RouteCollection; +use Symfony\Component\Routing\Tests\Fixtures\Psr4Controllers\MyController; class YamlFileLoaderTest extends TestCase { @@ -458,4 +462,40 @@ public function testImportingAliases() $this->assertEquals($expectedRoutes('yaml'), $routes); } + + public function testImportAttributesWithPsr4Prefix() + { + $locator = new FileLocator(\dirname(__DIR__).'/Fixtures'); + new LoaderResolver([ + $loader = new YamlFileLoader($locator), + new Psr4DirectoryLoader($locator), + new class() extends AnnotationClassLoader { + protected function configureRoute(Route $route, \ReflectionClass $class, \ReflectionMethod $method, object $annot) + { + $route->setDefault('_controller', $class->getName().'::'.$method->getName()); + } + }, + ]); + + $route = $loader->load('psr4-attributes.yaml')->get('my_route'); + $this->assertSame('/my-prefix/my/route', $route->getPath()); + $this->assertSame(MyController::class.'::__invoke', $route->getDefault('_controller')); + } + + public function testImportAttributesFromClass() + { + new LoaderResolver([ + $loader = new YamlFileLoader(new FileLocator(\dirname(__DIR__).'/Fixtures')), + new class() extends AnnotationClassLoader { + protected function configureRoute(Route $route, \ReflectionClass $class, \ReflectionMethod $method, object $annot) + { + $route->setDefault('_controller', $class->getName().'::'.$method->getName()); + } + }, + ]); + + $route = $loader->load('class-attributes.yaml')->get('my_route'); + $this->assertSame('/my-prefix/my/route', $route->getPath()); + $this->assertSame(MyController::class.'::__invoke', $route->getDefault('_controller')); + } }