Skip to content

Commit a0b7514

Browse files
committed
feature #47916 [Routing] PSR-4 directory loader (derrabus)
This PR was merged into the 6.2 branch. Discussion ---------- [Routing] PSR-4 directory loader | Q | A | ------------- | --- | Branch? | 6.2 | Bug fix? | no | New feature? | yes | Deprecations? | no (but we could…) | Tickets | Fix #47881 | License | MIT | Doc PR | symfony/symfony-docs#17373 This PR adds a PSR-4 directory loader to the Routing component. When we currently want to load routes from a directory with controllers, we configure it like this: ```YAML controllers: resource: ../src/Controller/ type: attribute ``` What happens now is that `AnnotationDirectoryLoader` searches that directory recursively for `*.php` files and uses PHP's tokenizer extension to discover all controller classes that are defined within that directory. This step feels unnecessarily expensive given that modern projects follow the PSR-4 standard to structure their code. And if we use the DI component's autodiscovery feature to register our controllers as services, our controller directory already has to follow PSR-4. A client I'm working with adds the additional challange that they deliver their code to their customers in encrypted form (using SourceGuardian). This means that the PHP files contain encrypted bytecode instead of plain PHP and thus cannot be parsed. We currently overcome this limitation by extending `AnnotationDirectoryLoader` and overriding the protected `findClass()` method. This PR proposes to extend the resource type `attribute` and allow to suffix it with a PSR-4 namespace root. ```YAML controllers: resource: ../src/Controller/ type: attribute@App\Controller ``` In order to use PSR-4 to discover controller classes, the directory path is not enough. We always need an additional piece of information which is the namespace root for the given directory. Without changing large parts of the Config component, I did not find a nice way to pass down that information to the PSR-4 route loader. Encoding that information into the `type` parameter seemed like the pragmatic approach, but I'm open to discussing alternatives. This approach should play nicely with projects that already use attributes for routing and PSR-4 autodiscovery to register controllers as services. In fact, most project should be able to swap the `type: annotation` or `type: attribute` config with `type: attribute@Your\Namespace\Here` and the only difference they notice is that router compilation becomes a bit faster. Commits ------- 158e30df9a [Routing] PSR-4 directory loader
2 parents 5b9e08d + d0f7ffe commit a0b7514

File tree

9 files changed

+140
-72
lines changed

9 files changed

+140
-72
lines changed

DependencyInjection/FrameworkExtension.php

Lines changed: 4 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@
2525
use Symfony\Bridge\Monolog\Processor\DebugProcessor;
2626
use Symfony\Bridge\Twig\Extension\CsrfExtension;
2727
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
28-
use Symfony\Bundle\FrameworkBundle\Routing\AnnotatedRouteControllerLoader;
2928
use Symfony\Bundle\FrameworkBundle\Routing\RouteLoaderInterface;
3029
use Symfony\Bundle\FullStack;
3130
use Symfony\Bundle\MercureBundle\MercureBundle;
@@ -194,8 +193,7 @@
194193
use Symfony\Component\RateLimiter\LimiterInterface;
195194
use Symfony\Component\RateLimiter\RateLimiterFactory;
196195
use Symfony\Component\RateLimiter\Storage\CacheStorage;
197-
use Symfony\Component\Routing\Loader\AnnotationDirectoryLoader;
198-
use Symfony\Component\Routing\Loader\AnnotationFileLoader;
196+
use Symfony\Component\Routing\Loader\Psr4DirectoryLoader;
199197
use Symfony\Component\Security\Core\AuthenticationEvents;
200198
use Symfony\Component\Security\Core\Exception\AuthenticationException;
201199
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
@@ -1157,29 +1155,9 @@ private function registerRouterConfiguration(array $config, ContainerBuilder $co
11571155
->replaceArgument(0, $config['default_uri']);
11581156
}
11591157

1160-
$container->register('routing.loader.annotation', AnnotatedRouteControllerLoader::class)
1161-
->setPublic(false)
1162-
->addTag('routing.loader', ['priority' => -10])
1163-
->setArguments([
1164-
new Reference('annotation_reader', ContainerInterface::NULL_ON_INVALID_REFERENCE),
1165-
'%kernel.environment%',
1166-
]);
1167-
1168-
$container->register('routing.loader.annotation.directory', AnnotationDirectoryLoader::class)
1169-
->setPublic(false)
1170-
->addTag('routing.loader', ['priority' => -10])
1171-
->setArguments([
1172-
new Reference('file_locator'),
1173-
new Reference('routing.loader.annotation'),
1174-
]);
1175-
1176-
$container->register('routing.loader.annotation.file', AnnotationFileLoader::class)
1177-
->setPublic(false)
1178-
->addTag('routing.loader', ['priority' => -10])
1179-
->setArguments([
1180-
new Reference('file_locator'),
1181-
new Reference('routing.loader.annotation'),
1182-
]);
1158+
if (!class_exists(Psr4DirectoryLoader::class)) {
1159+
$container->removeDefinition('routing.loader.psr4');
1160+
}
11831161
}
11841162

11851163
private function registerSessionConfiguration(array $config, ContainerBuilder $container, PhpFileLoader $loader)

Resources/config/routing.php

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
use Symfony\Bundle\FrameworkBundle\CacheWarmer\RouterCacheWarmer;
1616
use Symfony\Bundle\FrameworkBundle\Controller\RedirectController;
1717
use Symfony\Bundle\FrameworkBundle\Controller\TemplateController;
18+
use Symfony\Bundle\FrameworkBundle\Routing\AnnotatedRouteControllerLoader;
1819
use Symfony\Bundle\FrameworkBundle\Routing\DelegatingLoader;
1920
use Symfony\Bundle\FrameworkBundle\Routing\RedirectableCompiledUrlMatcher;
2021
use Symfony\Bundle\FrameworkBundle\Routing\Router;
@@ -23,10 +24,13 @@
2324
use Symfony\Component\Routing\Generator\CompiledUrlGenerator;
2425
use Symfony\Component\Routing\Generator\Dumper\CompiledUrlGeneratorDumper;
2526
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
27+
use Symfony\Component\Routing\Loader\AnnotationDirectoryLoader;
28+
use Symfony\Component\Routing\Loader\AnnotationFileLoader;
2629
use Symfony\Component\Routing\Loader\ContainerLoader;
2730
use Symfony\Component\Routing\Loader\DirectoryLoader;
2831
use Symfony\Component\Routing\Loader\GlobFileLoader;
2932
use Symfony\Component\Routing\Loader\PhpFileLoader;
33+
use Symfony\Component\Routing\Loader\Psr4DirectoryLoader;
3034
use Symfony\Component\Routing\Loader\XmlFileLoader;
3135
use Symfony\Component\Routing\Loader\YamlFileLoader;
3236
use Symfony\Component\Routing\Matcher\Dumper\CompiledUrlMatcherDumper;
@@ -88,6 +92,33 @@
8892
])
8993
->tag('routing.loader')
9094

95+
->set('routing.loader.annotation', AnnotatedRouteControllerLoader::class)
96+
->args([
97+
service('annotation_reader')->nullOnInvalid(),
98+
'%kernel.environment%',
99+
])
100+
->tag('routing.loader', ['priority' => -10])
101+
102+
->set('routing.loader.annotation.directory', AnnotationDirectoryLoader::class)
103+
->args([
104+
service('file_locator'),
105+
service('routing.loader.annotation'),
106+
])
107+
->tag('routing.loader', ['priority' => -10])
108+
109+
->set('routing.loader.annotation.file', AnnotationFileLoader::class)
110+
->args([
111+
service('file_locator'),
112+
service('routing.loader.annotation'),
113+
])
114+
->tag('routing.loader', ['priority' => -10])
115+
116+
->set('routing.loader.psr4', Psr4DirectoryLoader::class)
117+
->args([
118+
service('file_locator'),
119+
])
120+
->tag('routing.loader', ['priority' => -10])
121+
91122
->set('routing.loader', DelegatingLoader::class)
92123
->public()
93124
->args([
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
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\Bundle\FrameworkBundle\Tests\Functional;
13+
14+
abstract class AbstractAttributeRoutingTest extends AbstractWebTestCase
15+
{
16+
/**
17+
* @dataProvider getRoutes
18+
*/
19+
public function testAnnotatedController(string $path, string $expectedValue)
20+
{
21+
$client = $this->createClient(['test_case' => $this->getTestCaseApp(), 'root_config' => 'config.yml']);
22+
$client->request('GET', '/annotated'.$path);
23+
24+
$this->assertSame(200, $client->getResponse()->getStatusCode());
25+
$this->assertSame($expectedValue, $client->getResponse()->getContent());
26+
27+
$router = self::getContainer()->get('router');
28+
29+
$this->assertSame('/annotated/create-transaction', $router->generate('symfony_framework_tests_functional_test_annotated_createtransaction'));
30+
}
31+
32+
public function getRoutes(): array
33+
{
34+
return [
35+
['/null_request', 'Symfony\Component\HttpFoundation\Request'],
36+
['/null_argument', ''],
37+
['/null_argument_with_route_param', ''],
38+
['/null_argument_with_route_param/value', 'value'],
39+
['/argument_with_route_param_and_default', 'value'],
40+
['/argument_with_route_param_and_default/custom', 'custom'],
41+
];
42+
}
43+
44+
abstract protected function getTestCaseApp(): string;
45+
}

Tests/Functional/AnnotatedControllerTest.php

Lines changed: 3 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -11,33 +11,10 @@
1111

1212
namespace Symfony\Bundle\FrameworkBundle\Tests\Functional;
1313

14-
class AnnotatedControllerTest extends AbstractWebTestCase
14+
class AnnotatedControllerTest extends AbstractAttributeRoutingTest
1515
{
16-
/**
17-
* @dataProvider getRoutes
18-
*/
19-
public function testAnnotatedController($path, $expectedValue)
16+
protected function getTestCaseApp(): string
2017
{
21-
$client = $this->createClient(['test_case' => 'AnnotatedController', 'root_config' => 'config.yml']);
22-
$client->request('GET', '/annotated'.$path);
23-
24-
$this->assertSame(200, $client->getResponse()->getStatusCode());
25-
$this->assertSame($expectedValue, $client->getResponse()->getContent());
26-
27-
$router = self::getContainer()->get('router');
28-
29-
$this->assertSame('/annotated/create-transaction', $router->generate('symfony_framework_tests_functional_test_annotated_createtransaction'));
30-
}
31-
32-
public function getRoutes()
33-
{
34-
return [
35-
['/null_request', 'Symfony\Component\HttpFoundation\Request'],
36-
['/null_argument', ''],
37-
['/null_argument_with_route_param', ''],
38-
['/null_argument_with_route_param/value', 'value'],
39-
['/argument_with_route_param_and_default', 'value'],
40-
['/argument_with_route_param_and_default/custom', 'custom'],
41-
];
18+
return 'AnnotatedController';
4219
}
4320
}

Tests/Functional/Bundle/TestBundle/Controller/AnnotatedController.php

Lines changed: 10 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -17,42 +17,32 @@
1717

1818
class AnnotatedController
1919
{
20-
/**
21-
* @Route("/null_request", name="null_request")
22-
*/
23-
public function requestDefaultNullAction(Request $request = null)
20+
#[Route('/null_request', name: 'null_request')]
21+
public function requestDefaultNullAction(Request $request = null): Response
2422
{
2523
return new Response($request ? $request::class : null);
2624
}
2725

28-
/**
29-
* @Route("/null_argument", name="null_argument")
30-
*/
31-
public function argumentDefaultNullWithoutRouteParamAction($value = null)
26+
#[Route('/null_argument', name: 'null_argument')]
27+
public function argumentDefaultNullWithoutRouteParamAction($value = null): Response
3228
{
3329
return new Response($value);
3430
}
3531

36-
/**
37-
* @Route("/null_argument_with_route_param/{value}", name="null_argument_with_route_param")
38-
*/
39-
public function argumentDefaultNullWithRouteParamAction($value = null)
32+
#[Route('/null_argument_with_route_param/{value}', name: 'null_argument_with_route_param')]
33+
public function argumentDefaultNullWithRouteParamAction($value = null): Response
4034
{
4135
return new Response($value);
4236
}
4337

44-
/**
45-
* @Route("/argument_with_route_param_and_default/{value}", defaults={"value": "value"}, name="argument_with_route_param_and_default")
46-
*/
47-
public function argumentWithoutDefaultWithRouteParamAndDefaultAction($value)
38+
#[Route('/argument_with_route_param_and_default/{value}', defaults: ['value' => 'value'], name: 'argument_with_route_param_and_default')]
39+
public function argumentWithoutDefaultWithRouteParamAndDefaultAction($value): Response
4840
{
4941
return new Response($value);
5042
}
5143

52-
/**
53-
* @Route("/create-transaction")
54-
*/
55-
public function createTransaction()
44+
#[Route('/create-transaction')]
45+
public function createTransaction(): Response
5646
{
5747
return new Response();
5848
}

Tests/Functional/Psr4RoutingTest.php

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
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\Bundle\FrameworkBundle\Tests\Functional;
13+
14+
/**
15+
* @requires function Symfony\Component\Routing\Loader\Psr4DirectoryLoader::__construct
16+
*/
17+
final class Psr4RoutingTest extends AbstractAttributeRoutingTest
18+
{
19+
protected function getTestCaseApp(): string
20+
{
21+
return 'Psr4Routing';
22+
}
23+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
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+
use Symfony\Bundle\FrameworkBundle\FrameworkBundle;
13+
use Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\TestBundle\TestBundle;
14+
15+
return [
16+
new FrameworkBundle(),
17+
new TestBundle(),
18+
];
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
imports:
2+
- { resource: ../config/default.yml }
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
test_bundle:
2+
prefix: /annotated
3+
resource: "@TestBundle/Controller"
4+
type: attribute@Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\TestBundle\Controller

0 commit comments

Comments
 (0)