Skip to content

[Routing] PSR-4 directory loader #47916

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Oct 20, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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)
Expand Down
31 changes: 31 additions & 0 deletions src/Symfony/Bundle/FrameworkBundle/Resources/config/routing.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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([
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?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\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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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';
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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\Bundle\FrameworkBundle\Tests\Functional;

/**
* @requires function Symfony\Component\Routing\Loader\Psr4DirectoryLoader::__construct
*/
final class Psr4RoutingTest extends AbstractAttributeRoutingTest
{
protected function getTestCaseApp(): string
{
return 'Psr4Routing';
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?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.
*/

use Symfony\Bundle\FrameworkBundle\FrameworkBundle;
use Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\TestBundle\TestBundle;

return [
new FrameworkBundle(),
new TestBundle(),
];
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
imports:
- { resource: ../config/default.yml }
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
test_bundle:
prefix: /annotated
resource: "@TestBundle/Controller"
type: attribute@Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\TestBundle\Controller
66 changes: 66 additions & 0 deletions src/Symfony/Component/Routing/Loader/Psr4DirectoryLoader.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<?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\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 <me@derrabus.de>
*/
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;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?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\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);
}
}
Loading