Skip to content

Commit fca9c02

Browse files
committed
feature #61492 [FrameworkBundle][Routing] Auto-register routes from attributes found on controller services (nicolas-grekas)
This PR was merged into the 7.4 branch. Discussion ---------- [FrameworkBundle][Routing] Auto-register routes from attributes found on controller services | Q | A | ------------- | --- | Branch? | 7.4 | Bug fix? | no | New feature? | yes | Deprecations? | no | Issues | #60946 | License | MIT Currently, controllers have to be put in predetermined directories for their route attributes to be parsed. This PR enables auto-registration of controllers based on their service definition, independently of where they're located in the app - which means they could be put anywhere now. config/routes.yaml before: ```yaml controllers: resource: path: ../src/Controller/ namespace: App\Controller type: attribute ``` config/routes.yaml after: ```yaml controllers: resource: attributes type: tagged_services ``` Recipe update at symfony/recipes#1448 Commits ------- d0bbf04cefc [Routing][FrameworkBundle] Auto-register routes from attributes found on controller services
2 parents 06a94e2 + 8d17959 commit fca9c02

File tree

5 files changed

+57
-1
lines changed

5 files changed

+57
-1
lines changed

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 routes from attributes found on controller services
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()`

DependencyInjection/FrameworkExtension.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,7 @@
172172
use Symfony\Component\RemoteEvent\Attribute\AsRemoteEventConsumer;
173173
use Symfony\Component\RemoteEvent\RemoteEvent;
174174
use Symfony\Component\Routing\Attribute\Route;
175+
use Symfony\Component\Routing\Loader\AttributeServicesLoader;
175176
use Symfony\Component\Scheduler\Attribute\AsCronTask;
176177
use Symfony\Component\Scheduler\Attribute\AsPeriodicTask;
177178
use Symfony\Component\Scheduler\Attribute\AsSchedule;
@@ -768,7 +769,7 @@ public function load(array $configs, ContainerBuilder $container): void
768769
$definition->addTag('controller.service_arguments');
769770
});
770771
$container->registerAttributeForAutoconfiguration(Route::class, static function (ChildDefinition $definition, Route $attribute, \ReflectionClass|\ReflectionMethod $reflection): void {
771-
$definition->addTag('controller.service_arguments');
772+
$definition->addTag('controller.service_arguments')->addTag('routing.controller');
772773
});
773774
$container->registerAttributeForAutoconfiguration(AsRemoteEventConsumer::class, static function (ChildDefinition $definition, AsRemoteEventConsumer $attribute): void {
774775
$definition->addTag('remote_event.consumer', ['consumer' => $attribute->name]);
@@ -1307,6 +1308,10 @@ private function registerRouterConfiguration(array $config, ContainerBuilder $co
13071308

13081309
$loader->load('routing.php');
13091310

1311+
if (!class_exists(AttributeServicesLoader::class)) {
1312+
$container->removeDefinition('routing.loader.attribute.services');
1313+
}
1314+
13101315
if ($config['utf8']) {
13111316
$container->getDefinition('routing.loader')->replaceArgument(1, ['utf8' => true]);
13121317
}

FrameworkBundle.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@
6161
use Symfony\Component\PropertyInfo\DependencyInjection\PropertyInfoConstructorPass;
6262
use Symfony\Component\PropertyInfo\DependencyInjection\PropertyInfoPass;
6363
use Symfony\Component\Routing\DependencyInjection\AddExpressionLanguageProvidersPass;
64+
use Symfony\Component\Routing\DependencyInjection\RoutingControllerPass;
6465
use Symfony\Component\Routing\DependencyInjection\RoutingResolverPass;
6566
use Symfony\Component\Runtime\SymfonyRuntime;
6667
use Symfony\Component\Scheduler\DependencyInjection\AddScheduleMessengerPass;
@@ -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+
$this->addCompilerPassIfExists($container, RoutingControllerPass::class);
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

Resources/config/routing.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
2727
use Symfony\Component\Routing\Loader\AttributeDirectoryLoader;
2828
use Symfony\Component\Routing\Loader\AttributeFileLoader;
29+
use Symfony\Component\Routing\Loader\AttributeServicesLoader;
2930
use Symfony\Component\Routing\Loader\ContainerLoader;
3031
use Symfony\Component\Routing\Loader\DirectoryLoader;
3132
use Symfony\Component\Routing\Loader\GlobFileLoader;
@@ -98,6 +99,12 @@
9899
])
99100
->tag('routing.loader', ['priority' => -10])
100101

102+
->set('routing.loader.attribute.services', AttributeServicesLoader::class)
103+
->args([
104+
abstract_arg('classes tagged with "routing.controller"'),
105+
])
106+
->tag('routing.loader', ['priority' => -10])
107+
101108
->set('routing.loader.attribute.directory', AttributeDirectoryLoader::class)
102109
->args([
103110
service('file_locator'),
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
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\Routing;
13+
14+
use PHPUnit\Framework\TestCase;
15+
use Symfony\Bundle\FrameworkBundle\Routing\AttributeRouteControllerLoader;
16+
use Symfony\Component\Routing\Tests\Fixtures\AttributeFixtures\InvokableController;
17+
use Symfony\Component\Routing\Tests\Fixtures\AttributeFixtures\MethodActionControllers;
18+
19+
class AttributeRouteControllerLoaderTest extends TestCase
20+
{
21+
public function testConfigureRouteSetsControllerForInvokable()
22+
{
23+
$loader = new AttributeRouteControllerLoader();
24+
$collection = $loader->load(InvokableController::class);
25+
26+
$route = $collection->get('lol');
27+
$this->assertSame(InvokableController::class, $route->getDefault('_controller'));
28+
}
29+
30+
public function testConfigureRouteSetsControllerForMethod()
31+
{
32+
$loader = new AttributeRouteControllerLoader();
33+
$collection = $loader->load(MethodActionControllers::class);
34+
35+
$put = $collection->get('put');
36+
$post = $collection->get('post');
37+
38+
$this->assertSame(MethodActionControllers::class.'::put', $put->getDefault('_controller'));
39+
$this->assertSame(MethodActionControllers::class.'::post', $post->getDefault('_controller'));
40+
}
41+
}

0 commit comments

Comments
 (0)