Skip to content

Commit efeefc6

Browse files
[FrameworkBundle] Auto-register #[Route] attributes found on services tagged with routing.controller
1 parent ed38673 commit efeefc6

File tree

7 files changed

+79
-3
lines changed

7 files changed

+79
-3
lines changed

src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ CHANGELOG
44
7.4
55
---
66

7+
* Auto-register `#[Route]` attributes found on services tagged with `routing.controller`
78
* Add `ControllerHelper`; the helpers from AbstractController as a standalone service
89
* Allow using their name without added suffix when using `#[Target]` for custom services
910
* Deprecate `Symfony\Bundle\FrameworkBundle\Console\Application::add()` in favor of `Symfony\Bundle\FrameworkBundle\Console\Application::addCommand()`
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
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\DependencyInjection\Compiler;
13+
14+
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
15+
use Symfony\Component\DependencyInjection\Compiler\PriorityTaggedServiceTrait;
16+
use Symfony\Component\DependencyInjection\ContainerBuilder;
17+
18+
/**
19+
* @author Nicolas Grekas <p@tchwork.com>
20+
*/
21+
class RoutingControllerPass implements CompilerPassInterface
22+
{
23+
use PriorityTaggedServiceTrait;
24+
25+
public function process(ContainerBuilder $container): void
26+
{
27+
if (!$container->hasDefinition('routing.loader.attribute')) {
28+
return;
29+
}
30+
31+
$resolve = $container->getParameterBag()->resolveValue(...);
32+
$taggedClasses = [];
33+
foreach ($this->findAndSortTaggedServices('routing.controller', $container) as $id) {
34+
$taggedClasses[$resolve($container->getDefinition($id)->getClass())] = true;
35+
}
36+
37+
$container->getDefinition('routing.loader.attribute')
38+
->replaceArgument(1, array_keys($taggedClasses));
39+
}
40+
}

src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/UnusedTagsPass.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ class UnusedTagsPass implements CompilerPassInterface
7878
'proxy',
7979
'remote_event.consumer',
8080
'routing.condition_service',
81+
'routing.controller',
8182
'routing.expression_language_function',
8283
'routing.expression_language_provider',
8384
'routing.loader',

src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -692,7 +692,7 @@ public function load(array $configs, ContainerBuilder $container): void
692692
$container->registerForAutoconfiguration(ValueResolverInterface::class)
693693
->addTag('controller.argument_value_resolver');
694694
$container->registerForAutoconfiguration(AbstractController::class)
695-
->addTag('controller.service_arguments');
695+
->addTag('controller.service_arguments')->addTag('routing.controller');
696696
$container->registerForAutoconfiguration(DataCollectorInterface::class)
697697
->addTag('data_collector');
698698
$container->registerForAutoconfiguration(FormTypeInterface::class)
@@ -765,10 +765,10 @@ public function load(array $configs, ContainerBuilder $container): void
765765
$definition->addTag('kernel.event_listener', $tagAttributes);
766766
});
767767
$container->registerAttributeForAutoconfiguration(AsController::class, static function (ChildDefinition $definition, AsController $attribute): void {
768-
$definition->addTag('controller.service_arguments');
768+
$definition->addTag('controller.service_arguments')->addTag('routing.controller');
769769
});
770770
$container->registerAttributeForAutoconfiguration(Route::class, static function (ChildDefinition $definition, Route $attribute, \ReflectionClass|\ReflectionMethod $reflection): void {
771-
$definition->addTag('controller.service_arguments');
771+
$definition->addTag('controller.service_arguments')->addTag('routing.controller');
772772
});
773773
$container->registerAttributeForAutoconfiguration(AsRemoteEventConsumer::class, static function (ChildDefinition $definition, AsRemoteEventConsumer $attribute): void {
774774
$definition->addTag('remote_event.consumer', ['consumer' => $attribute->name]);

src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\ErrorLoggerCompilerPass;
1919
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\ProfilerPass;
2020
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\RemoveUnusedSessionMarshallingHandlerPass;
21+
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\RoutingControllerPass;
2122
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\TestServiceContainerRealRefPass;
2223
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\TestServiceContainerWeakRefPass;
2324
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\TranslationLintCommandPass;
@@ -146,6 +147,7 @@ public function build(ContainerBuilder $container): void
146147
$container->addCompilerPass(new RegisterControllerArgumentLocatorsPass());
147148
$container->addCompilerPass(new RemoveEmptyControllerArgumentLocatorsPass(), PassConfig::TYPE_BEFORE_REMOVING);
148149
$container->addCompilerPass(new RoutingResolverPass());
150+
$container->addCompilerPass(new RoutingControllerPass());
149151
$this->addCompilerPassIfExists($container, DataCollectorTranslatorPass::class);
150152
$container->addCompilerPass(new ProfilerPass());
151153
// must be registered before removing private services as some might be listeners/subscribers

src/Symfony/Bundle/FrameworkBundle/Resources/config/routing.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@
9595
->set('routing.loader.attribute', AttributeRouteControllerLoader::class)
9696
->args([
9797
'%kernel.environment%',
98+
abstract_arg('classes tagged with "routing.controller"'),
9899
])
99100
->tag('routing.loader', ['priority' => -10])
100101

src/Symfony/Bundle/FrameworkBundle/Routing/AttributeRouteControllerLoader.php

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
use Symfony\Component\Routing\Loader\AttributeClassLoader;
1515
use Symfony\Component\Routing\Route;
16+
use Symfony\Component\Routing\RouteCollection;
1617

1718
/**
1819
* AttributeRouteControllerLoader is an implementation of AttributeClassLoader
@@ -23,6 +24,36 @@
2324
*/
2425
class AttributeRouteControllerLoader extends AttributeClassLoader
2526
{
27+
/**
28+
* @param class-string[] $taggedClasses
29+
*/
30+
public function __construct(
31+
?string $env = null,
32+
private array $taggedClasses = [],
33+
) {
34+
parent::__construct($env);
35+
}
36+
37+
public function load(mixed $class, ?string $type = null): RouteCollection
38+
{
39+
if (['tag' => 'routing.controller'] !== $class) {
40+
return parent::load($class, $type);
41+
}
42+
43+
$collection = new RouteCollection();
44+
45+
foreach ($this->taggedClasses as $class) {
46+
$collection->addCollection(parent::load($class, $type));
47+
}
48+
49+
return $collection;
50+
}
51+
52+
public function supports(mixed $resource, ?string $type = null): bool
53+
{
54+
return parent::supports($resource, $type) || 'attribute' === $type && ['tag' => 'routing.controller'] === $resource;
55+
}
56+
2657
/**
2758
* Configures the _controller default parameter of a given Route instance.
2859
*/

0 commit comments

Comments
 (0)