diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/controller.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/controller.php new file mode 100644 index 0000000000000..66a9c09260de4 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/controller.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Loader\Configurator; + +use Symfony\Component\HttpKernel\EventListener\ControllerAttributeListener; + +return static function (ContainerConfigurator $container) { + $container->services() + ->set('controller_attribute_listener', ControllerAttributeListener::class) + ->tag('kernel.event_subscriber') + ; +}; diff --git a/src/Symfony/Component/HttpKernel/Attribute/ControllerAttributeInterface.php b/src/Symfony/Component/HttpKernel/Attribute/ControllerAttributeInterface.php new file mode 100644 index 0000000000000..f57ba89414ef6 --- /dev/null +++ b/src/Symfony/Component/HttpKernel/Attribute/ControllerAttributeInterface.php @@ -0,0 +1,10 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpKernel\EventListener; + +use Doctrine\Persistence\Proxy; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\HttpKernel\Attribute\ControllerAttributeInterface; +use Symfony\Component\HttpKernel\Event\ControllerEvent; +use Symfony\Component\HttpKernel\KernelEvents; + +/** + * The ControllerAttributeListener class parses attributes marked + * as controller attributes in controllers. + * + * @author Fabien Potencier + * @author Tim Goudriaan + */ +class ControllerAttributeListener implements EventSubscriberInterface +{ + public function onKernelController(ControllerEvent $event) + { + $controller = $event->getController(); + + if (!\is_array($controller) && method_exists($controller, '__invoke')) { + $controller = [$controller, '__invoke']; + } + + if (!\is_array($controller)) { + return; + } + + $className = self::getRealClass(\get_class($controller[0])); + $object = new \ReflectionClass($className); + $method = $object->getMethod($controller[1]); + + $classAttributes = $object->getAttributes(ControllerAttributeInterface::class, \ReflectionAttribute::IS_INSTANCEOF); + $methodAttributes = $method->getAttributes(ControllerAttributeInterface::class, \ReflectionAttribute::IS_INSTANCEOF); + + $attributes = []; + foreach (array_merge($classAttributes, $methodAttributes) as $attribute) { + if ($attribute->isRepeated()) { + $attributes[$attribute->getName()][] = $attribute->newInstance(); + } else { + // method attribute overrides class attribute + $attributes[$attribute->getName()] = $attribute->newInstance(); + } + } + + $request = $event->getRequest(); + $request->attributes->set('_controller_attributes', $attributes); + } + + /** + * {@inheritdoc} + */ + public static function getSubscribedEvents(): array + { + return [KernelEvents::CONTROLLER => 'onKernelController']; + } + + private static function getRealClass(string $class): string + { + if (class_exists(Proxy::class)) { + if (false === $pos = strrpos($class, '\\'.Proxy::MARKER.'\\')) { + return $class; + } + + return substr($class, $pos + Proxy::MARKER_LENGTH + 2); + } + + return $class; + } +} diff --git a/src/Symfony/Component/HttpKernel/Tests/ControllerMetadata/ArgumentMetadataFactoryTest.php b/src/Symfony/Component/HttpKernel/Tests/ControllerMetadata/ArgumentMetadataFactoryTest.php index ec5a55243fb07..f22664eea99dd 100644 --- a/src/Symfony/Component/HttpKernel/Tests/ControllerMetadata/ArgumentMetadataFactoryTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/ControllerMetadata/ArgumentMetadataFactoryTest.php @@ -15,8 +15,8 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadataFactory; -use Symfony\Component\HttpKernel\Tests\Fixtures\Attribute\Foo; -use Symfony\Component\HttpKernel\Tests\Fixtures\Controller\AttributeController; +use Symfony\Component\HttpKernel\Tests\Fixtures\Attribute\FooParam; +use Symfony\Component\HttpKernel\Tests\Fixtures\Controller\ArgumentAttributeController; use Symfony\Component\HttpKernel\Tests\Fixtures\Controller\BasicTypesController; use Symfony\Component\HttpKernel\Tests\Fixtures\Controller\NullableController; use Symfony\Component\HttpKernel\Tests\Fixtures\Controller\VariadicController; @@ -121,24 +121,24 @@ public function testNullableTypesSignature() public function testAttributeSignature() { - $arguments = $this->factory->createArgumentMetadata([new AttributeController(), 'action']); + $arguments = $this->factory->createArgumentMetadata([new ArgumentAttributeController(), 'action']); $this->assertEquals([ - new ArgumentMetadata('baz', 'string', false, false, null, false, [new Foo('bar')]), + new ArgumentMetadata('baz', 'string', false, false, null, false, [new FooParam('bar')]), ], $arguments); } public function testMultipleAttributes() { - $this->factory->createArgumentMetadata([new AttributeController(), 'multiAttributeArg']); - $this->assertCount(1, $this->factory->createArgumentMetadata([new AttributeController(), 'multiAttributeArg'])[0]->getAttributes()); + $this->factory->createArgumentMetadata([new ArgumentAttributeController(), 'multiAttributeArg']); + $this->assertCount(1, $this->factory->createArgumentMetadata([new ArgumentAttributeController(), 'multiAttributeArg'])[0]->getAttributes()); } public function testIssue41478() { - $arguments = $this->factory->createArgumentMetadata([new AttributeController(), 'issue41478']); + $arguments = $this->factory->createArgumentMetadata([new ArgumentAttributeController(), 'issue41478']); $this->assertEquals([ - new ArgumentMetadata('baz', 'string', false, false, null, false, [new Foo('bar')]), + new ArgumentMetadata('baz', 'string', false, false, null, false, [new FooParam('bar')]), new ArgumentMetadata('bat', 'string', false, false, null, false, []), ], $arguments); } diff --git a/src/Symfony/Component/HttpKernel/Tests/ControllerMetadata/ArgumentMetadataTest.php b/src/Symfony/Component/HttpKernel/Tests/ControllerMetadata/ArgumentMetadataTest.php index 8ef1fe678ecf1..bb83d41c8591d 100644 --- a/src/Symfony/Component/HttpKernel/Tests/ControllerMetadata/ArgumentMetadataTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/ControllerMetadata/ArgumentMetadataTest.php @@ -13,7 +13,7 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; -use Symfony\Component\HttpKernel\Tests\Fixtures\Attribute\Foo; +use Symfony\Component\HttpKernel\Tests\Fixtures\Attribute\FooParam; class ArgumentMetadataTest extends TestCase { @@ -45,7 +45,7 @@ public function testDefaultValueUnavailable() public function testGetAttributes() { - $argument = new ArgumentMetadata('foo', 'string', false, true, 'default value', true, [new Foo('bar')]); - $this->assertEquals([new Foo('bar')], $argument->getAttributes()); + $argument = new ArgumentMetadata('foo', 'string', false, true, 'default value', true, [new FooParam('bar')]); + $this->assertEquals([new FooParam('bar')], $argument->getAttributes()); } } diff --git a/src/Symfony/Component/HttpKernel/Tests/EventListener/ControllerAttributeListenerTest.php b/src/Symfony/Component/HttpKernel/Tests/EventListener/ControllerAttributeListenerTest.php new file mode 100644 index 0000000000000..89016fda2f1de --- /dev/null +++ b/src/Symfony/Component/HttpKernel/Tests/EventListener/ControllerAttributeListenerTest.php @@ -0,0 +1,185 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpKernel\Tests\EventListener; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpKernel\Event\ControllerEvent; +use Symfony\Component\HttpKernel\EventListener\ControllerAttributeListener; +use Symfony\Component\HttpKernel\HttpKernelInterface; +use Symfony\Component\HttpKernel\Tests\Fixtures\Attribute\FooController; +use Symfony\Component\HttpKernel\Tests\Fixtures\Attribute\RepeatableFooController; +use Symfony\Component\HttpKernel\Tests\Fixtures\Controller\ControllerAttributeAtClassAndMethodController; +use Symfony\Component\HttpKernel\Tests\Fixtures\Controller\ControllerAttributeAtClassController; +use Symfony\Component\HttpKernel\Tests\Fixtures\Controller\ControllerAttributeAtMethodController; +use Symfony\Component\HttpKernel\Tests\Fixtures\Controller\RepeatableControllerAttributeAtClassAndMethodController; +use Symfony\Component\HttpKernel\Tests\Fixtures\Controller\RepeatableControllerAttributeAtClassController; +use Symfony\Component\HttpKernel\Tests\Fixtures\Controller\RepeatableControllerAttributeAtMethodController; + +class ControllerAttributeListenerTest extends TestCase +{ + public function testAttributeAtClass() + { + $request = new Request(); + + $event = new ControllerEvent( + $this->getMockBuilder(HttpKernelInterface::class)->getMock(), + [new ControllerAttributeAtClassController(), 'foo'], + $request, + null + ); + + $listener = new ControllerAttributeListener(); + $listener->onKernelController($event); + + $this->assertNotNull($attributes = $request->attributes->get('_controller_attributes')); + $this->assertCount(1, $attributes); + $this->assertEquals('class', $attributes[FooController::class]->bar); + } + + public function testAttributeAtMethod() + { + $request = new Request(); + + $event = new ControllerEvent( + $this->getMockBuilder(HttpKernelInterface::class)->getMock(), + [new ControllerAttributeAtMethodController(), 'foo'], + $request, + null + ); + + $listener = new ControllerAttributeListener(); + $listener->onKernelController($event); + + $this->assertNotNull($attributes = $request->attributes->get('_controller_attributes')); + $this->assertCount(1, $attributes); + $this->assertEquals('method', $attributes[FooController::class]->bar); + } + + public function testAttributeAtClassAndMethod() + { + $listener = new ControllerAttributeListener(); + + $request = new Request(); + + $event = new ControllerEvent( + $this->getMockBuilder(HttpKernelInterface::class)->getMock(), + [new ControllerAttributeAtClassAndMethodController(), 'foo'], + $request, + null + ); + + $listener->onKernelController($event); + + $this->assertNotNull($attributes = $request->attributes->get('_controller_attributes')); + $this->assertCount(1, $attributes); + $this->assertEquals('method', $attributes[FooController::class]->bar); + + $request = new Request(); + + $event = new ControllerEvent( + $this->getMockBuilder(HttpKernelInterface::class)->getMock(), + [new ControllerAttributeAtClassAndMethodController(), 'bar'], + $request, + null + ); + + $listener->onKernelController($event); + + $this->assertNotNull($attributes = $request->attributes->get('_controller_attributes')); + $this->assertCount(1, $attributes); + $this->assertEquals('class', $attributes[FooController::class]->bar); + } + + public function testRepeatableAttributeAtClass() + { + $request = new Request(); + + $event = new ControllerEvent( + $this->getMockBuilder(HttpKernelInterface::class)->getMock(), + [new RepeatableControllerAttributeAtClassController(), 'foo'], + $request, + null + ); + + $listener = new ControllerAttributeListener(); + $listener->onKernelController($event); + + $this->assertNotNull($attributes = $request->attributes->get('_controller_attributes')); + $this->assertCount(1, $attributes); + $this->assertCount(2, $attributes[RepeatableFooController::class]); + $this->assertEquals('class1', $attributes[RepeatableFooController::class][0]->bar); + $this->assertEquals('class2', $attributes[RepeatableFooController::class][1]->bar); + } + + public function testRepeatableAttributeAtMethod() + { + $request = new Request(); + + $event = new ControllerEvent( + $this->getMockBuilder(HttpKernelInterface::class)->getMock(), + [new RepeatableControllerAttributeAtMethodController(), 'foo'], + $request, + null + ); + + $listener = new ControllerAttributeListener(); + $listener->onKernelController($event); + + $this->assertNotNull($attributes = $request->attributes->get('_controller_attributes')); + $this->assertCount(1, $attributes); + $this->assertCount(2, $attributes[RepeatableFooController::class]); + $this->assertEquals('method1', $attributes[RepeatableFooController::class][0]->bar); + $this->assertEquals('method2', $attributes[RepeatableFooController::class][1]->bar); + } + + public function testRepeatableAttributeAtClassAndMethod() + { + $listener = new ControllerAttributeListener(); + + $request = new Request(); + + $event = new ControllerEvent( + $this->getMockBuilder(HttpKernelInterface::class)->getMock(), + [new RepeatableControllerAttributeAtClassAndMethodController(), 'foo'], + $request, + null + ); + + $listener->onKernelController($event); + + $this->assertNotNull($attributes = $request->attributes->get('_controller_attributes')); + $this->assertCount(1, $attributes); + $this->assertCount(4, $attributes[RepeatableFooController::class]); + $this->assertEquals('class1', $attributes[RepeatableFooController::class][0]->bar); + $this->assertEquals('class2', $attributes[RepeatableFooController::class][1]->bar); + $this->assertEquals('method1', $attributes[RepeatableFooController::class][2]->bar); + $this->assertEquals('method2', $attributes[RepeatableFooController::class][3]->bar); + + $request = new Request(); + + $event = new ControllerEvent( + $this->getMockBuilder(HttpKernelInterface::class)->getMock(), + [new RepeatableControllerAttributeAtClassAndMethodController(), 'bar'], + $request, + null + ); + + $listener->onKernelController($event); + + $this->assertNotNull($attributes = $request->attributes->get('_controller_attributes')); + $this->assertCount(1, $attributes); + $this->assertCount(2, $attributes[RepeatableFooController::class]); + $this->assertEquals('class1', $attributes[RepeatableFooController::class][0]->bar); + $this->assertEquals('class2', $attributes[RepeatableFooController::class][1]->bar); + } +} diff --git a/src/Symfony/Component/HttpKernel/Tests/Fixtures/Attribute/FooController.php b/src/Symfony/Component/HttpKernel/Tests/Fixtures/Attribute/FooController.php new file mode 100644 index 0000000000000..b1194f8401f0c --- /dev/null +++ b/src/Symfony/Component/HttpKernel/Tests/Fixtures/Attribute/FooController.php @@ -0,0 +1,13 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpKernel\Tests\Fixtures\Controller; + +use Symfony\Component\HttpKernel\Tests\Fixtures\Attribute\FooParam; + +class ArgumentAttributeController +{ + public function action(#[FooParam('bar')] string $baz) + { + } + + public function multiAttributeArg(#[FooParam('bar'), Undefined('bar')] string $baz) + { + } + + public function issue41478(#[FooParam('bar')] string $baz, string $bat) + { + } +} diff --git a/src/Symfony/Component/HttpKernel/Tests/Fixtures/Controller/AttributeController.php b/src/Symfony/Component/HttpKernel/Tests/Fixtures/Controller/ControllerAttributeAtClassAndMethodController.php similarity index 51% rename from src/Symfony/Component/HttpKernel/Tests/Fixtures/Controller/AttributeController.php rename to src/Symfony/Component/HttpKernel/Tests/Fixtures/Controller/ControllerAttributeAtClassAndMethodController.php index 92e54f400d014..9b02878947fcf 100644 --- a/src/Symfony/Component/HttpKernel/Tests/Fixtures/Controller/AttributeController.php +++ b/src/Symfony/Component/HttpKernel/Tests/Fixtures/Controller/ControllerAttributeAtClassAndMethodController.php @@ -11,19 +11,17 @@ namespace Symfony\Component\HttpKernel\Tests\Fixtures\Controller; -use Symfony\Component\HttpKernel\Tests\Fixtures\Attribute\Foo; +use Symfony\Component\HttpKernel\Tests\Fixtures\Attribute\FooController; -class AttributeController +#[FooController(bar: 'class')] +class ControllerAttributeAtClassAndMethodController { - public function action(#[Foo('bar')] string $baz) + #[FooController(bar: 'method')] + public function foo() { } - public function multiAttributeArg(#[Foo('bar'), Undefined('bar')] string $baz) - { - } - - public function issue41478(#[Foo('bar')] string $baz, string $bat) + public function bar() { } } diff --git a/src/Symfony/Component/HttpKernel/Tests/Fixtures/Controller/ControllerAttributeAtClassController.php b/src/Symfony/Component/HttpKernel/Tests/Fixtures/Controller/ControllerAttributeAtClassController.php new file mode 100644 index 0000000000000..35b07fc85244d --- /dev/null +++ b/src/Symfony/Component/HttpKernel/Tests/Fixtures/Controller/ControllerAttributeAtClassController.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\HttpKernel\Tests\Fixtures\Controller; + +use Symfony\Component\HttpKernel\Tests\Fixtures\Attribute\FooController; + +#[FooController(bar: 'class')] +class ControllerAttributeAtClassController +{ + public function foo() + { + } +} diff --git a/src/Symfony/Component/HttpKernel/Tests/Fixtures/Controller/ControllerAttributeAtMethodController.php b/src/Symfony/Component/HttpKernel/Tests/Fixtures/Controller/ControllerAttributeAtMethodController.php new file mode 100644 index 0000000000000..ea0d2589fc68b --- /dev/null +++ b/src/Symfony/Component/HttpKernel/Tests/Fixtures/Controller/ControllerAttributeAtMethodController.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\HttpKernel\Tests\Fixtures\Controller; + +use Symfony\Component\HttpKernel\Tests\Fixtures\Attribute\FooController; + +class ControllerAttributeAtMethodController +{ + #[FooController(bar: 'method')] + public function foo() + { + } +} diff --git a/src/Symfony/Component/HttpKernel/Tests/Fixtures/Controller/RepeatableControllerAttributeAtClassAndMethodController.php b/src/Symfony/Component/HttpKernel/Tests/Fixtures/Controller/RepeatableControllerAttributeAtClassAndMethodController.php new file mode 100644 index 0000000000000..857c9399e25b2 --- /dev/null +++ b/src/Symfony/Component/HttpKernel/Tests/Fixtures/Controller/RepeatableControllerAttributeAtClassAndMethodController.php @@ -0,0 +1,29 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpKernel\Tests\Fixtures\Controller; + +use Symfony\Component\HttpKernel\Tests\Fixtures\Attribute\RepeatableFooController; + +#[RepeatableFooController(bar: 'class1')] +#[RepeatableFooController(bar: 'class2')] +class RepeatableControllerAttributeAtClassAndMethodController +{ + #[RepeatableFooController(bar: 'method1')] + #[RepeatableFooController(bar: 'method2')] + public function foo() + { + } + + public function bar() + { + } +} diff --git a/src/Symfony/Component/HttpKernel/Tests/Fixtures/Controller/RepeatableControllerAttributeAtClassController.php b/src/Symfony/Component/HttpKernel/Tests/Fixtures/Controller/RepeatableControllerAttributeAtClassController.php new file mode 100644 index 0000000000000..71cc53fc96723 --- /dev/null +++ b/src/Symfony/Component/HttpKernel/Tests/Fixtures/Controller/RepeatableControllerAttributeAtClassController.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\Component\HttpKernel\Tests\Fixtures\Controller; + +use Symfony\Component\HttpKernel\Tests\Fixtures\Attribute\RepeatableFooController; + +#[RepeatableFooController(bar: 'class1')] +#[RepeatableFooController(bar: 'class2')] +class RepeatableControllerAttributeAtClassController +{ + public function foo() + { + } +} diff --git a/src/Symfony/Component/HttpKernel/Tests/Fixtures/Controller/RepeatableControllerAttributeAtMethodController.php b/src/Symfony/Component/HttpKernel/Tests/Fixtures/Controller/RepeatableControllerAttributeAtMethodController.php new file mode 100644 index 0000000000000..0d6be4c250ca9 --- /dev/null +++ b/src/Symfony/Component/HttpKernel/Tests/Fixtures/Controller/RepeatableControllerAttributeAtMethodController.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\Component\HttpKernel\Tests\Fixtures\Controller; + +use Symfony\Component\HttpKernel\Tests\Fixtures\Attribute\RepeatableFooController; + +class RepeatableControllerAttributeAtMethodController +{ + #[RepeatableFooController(bar: 'method1')] + #[RepeatableFooController(bar: 'method2')] + public function foo() + { + } +}