From 0f2293c983744b744085e2dd67d89d79f54b21db Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Mon, 11 Apr 2022 21:49:53 +0200 Subject: [PATCH] [HttpKernel] Add `ControllerEvent::getAttributes()` to handle attributes on controllers --- src/Symfony/Component/HttpKernel/CHANGELOG.md | 1 + .../Controller/ArgumentResolver.php | 3 +- .../ArgumentMetadataFactory.php | 17 ++--- .../Event/ControllerArgumentsEvent.php | 27 ++++++-- .../HttpKernel/Event/ControllerEvent.php | 42 ++++++++++++- .../Component/HttpKernel/HttpKernel.php | 3 +- .../ArgumentMetadataFactoryTest.php | 10 +-- .../Event/ControllerArgumentsEventTest.php | 42 ++++++++++++- .../Tests/Event/ControllerEventTest.php | 63 +++++++++++++++++++ .../Tests/Fixtures/Attribute/Bar.php | 21 +++++++ .../Controller/AttributeController.php | 7 +++ 11 files changed, 208 insertions(+), 28 deletions(-) create mode 100644 src/Symfony/Component/HttpKernel/Tests/Event/ControllerEventTest.php create mode 100644 src/Symfony/Component/HttpKernel/Tests/Fixtures/Attribute/Bar.php diff --git a/src/Symfony/Component/HttpKernel/CHANGELOG.md b/src/Symfony/Component/HttpKernel/CHANGELOG.md index 097b533bba94f..c79f5150a02ed 100644 --- a/src/Symfony/Component/HttpKernel/CHANGELOG.md +++ b/src/Symfony/Component/HttpKernel/CHANGELOG.md @@ -5,6 +5,7 @@ CHANGELOG --- * Add constructor argument `bool $catchThrowable` to `HttpKernel` + * Add `ControllerEvent::getAttributes()` to handle attributes on controllers 6.1 --- diff --git a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver.php b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver.php index 30fae1b3df3a9..6359611e5c0cd 100644 --- a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver.php +++ b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver.php @@ -45,8 +45,9 @@ public function __construct(ArgumentMetadataFactoryInterface $argumentMetadataFa public function getArguments(Request $request, callable $controller): array { $arguments = []; + $reflectors = $request->attributes->get('_controller_reflectors') ?? []; - foreach ($this->argumentMetadataFactory->createArgumentMetadata($controller) as $metadata) { + foreach ($this->argumentMetadataFactory->createArgumentMetadata($controller, ...$reflectors) as $metadata) { foreach ($this->argumentValueResolvers as $resolver) { if (!$resolver->supports($request, $metadata)) { continue; diff --git a/src/Symfony/Component/HttpKernel/ControllerMetadata/ArgumentMetadataFactory.php b/src/Symfony/Component/HttpKernel/ControllerMetadata/ArgumentMetadataFactory.php index 921fba03c204f..b6f1580b03134 100644 --- a/src/Symfony/Component/HttpKernel/ControllerMetadata/ArgumentMetadataFactory.php +++ b/src/Symfony/Component/HttpKernel/ControllerMetadata/ArgumentMetadataFactory.php @@ -21,22 +21,15 @@ final class ArgumentMetadataFactory implements ArgumentMetadataFactoryInterface /** * {@inheritdoc} */ - public function createArgumentMetadata(string|object|array $controller): array + public function createArgumentMetadata(string|object|array $controller, \ReflectionClass $class = null, \ReflectionFunction $reflection = null): array { $arguments = []; - if (\is_array($controller)) { - $reflection = new \ReflectionMethod($controller[0], $controller[1]); - $class = $reflection->class; - } elseif (\is_object($controller) && !$controller instanceof \Closure) { - $reflection = new \ReflectionMethod($controller, '__invoke'); - $class = $reflection->class; - } else { - $reflection = new \ReflectionFunction($controller); - if ($class = str_contains($reflection->name, '{closure}') ? null : $reflection->getClosureScopeClass()) { - $class = $class->name; - } + if (null === $reflection) { + $reflection = new \ReflectionFunction($controller(...)); + $class = str_contains($reflection->name, '{closure}') ? null : $reflection->getClosureScopeClass(); } + $class = $class?->name; foreach ($reflection->getParameters() as $param) { $attributes = []; diff --git a/src/Symfony/Component/HttpKernel/Event/ControllerArgumentsEvent.php b/src/Symfony/Component/HttpKernel/Event/ControllerArgumentsEvent.php index 824a3325a3163..4039924df6ec7 100644 --- a/src/Symfony/Component/HttpKernel/Event/ControllerArgumentsEvent.php +++ b/src/Symfony/Component/HttpKernel/Event/ControllerArgumentsEvent.php @@ -28,25 +28,32 @@ */ final class ControllerArgumentsEvent extends KernelEvent { - private $controller; + private ControllerEvent $controllerEvent; private array $arguments; - public function __construct(HttpKernelInterface $kernel, callable $controller, array $arguments, Request $request, ?int $requestType) + public function __construct(HttpKernelInterface $kernel, callable|ControllerEvent $controller, array $arguments, Request $request, ?int $requestType) { parent::__construct($kernel, $request, $requestType); - $this->controller = $controller; + if (!$controller instanceof ControllerEvent) { + $controller = new ControllerEvent($kernel, $controller, $request, $requestType); + } + + $this->controllerEvent = $controller; $this->arguments = $arguments; } public function getController(): callable { - return $this->controller; + return $this->controllerEvent->getController(); } - public function setController(callable $controller) + /** + * @param array>|null $attributes + */ + public function setController(callable $controller, array $attributes = null): void { - $this->controller = $controller; + $this->controllerEvent->setController($controller, $attributes); } public function getArguments(): array @@ -58,4 +65,12 @@ public function setArguments(array $arguments) { $this->arguments = $arguments; } + + /** + * @return array> + */ + public function getAttributes(): array + { + return $this->controllerEvent->getAttributes(); + } } diff --git a/src/Symfony/Component/HttpKernel/Event/ControllerEvent.php b/src/Symfony/Component/HttpKernel/Event/ControllerEvent.php index 7b6ae3ee6599d..6d8283325fd0a 100644 --- a/src/Symfony/Component/HttpKernel/Event/ControllerEvent.php +++ b/src/Symfony/Component/HttpKernel/Event/ControllerEvent.php @@ -28,6 +28,7 @@ final class ControllerEvent extends KernelEvent { private string|array|object $controller; + private array $attributes; public function __construct(HttpKernelInterface $kernel, callable $controller, Request $request, ?int $requestType) { @@ -41,8 +42,47 @@ public function getController(): callable return $this->controller; } - public function setController(callable $controller): void + /** + * @param array>|null $attributes + */ + public function setController(callable $controller, array $attributes = null): void { + if (null !== $attributes) { + $this->attributes = $attributes; + } + + if (isset($this->controller) && ($controller instanceof \Closure ? $controller == $this->controller : $controller === $this->controller)) { + $this->controller = $controller; + + return; + } + + if (null === $attributes) { + unset($this->attributes); + } + + $action = new \ReflectionFunction($controller(...)); + $this->getRequest()->attributes->set('_controller_reflectors', [str_contains($action->name, '{closure}') ? null : $action->getClosureScopeClass(), $action]); $this->controller = $controller; } + + /** + * @return array> + */ + public function getAttributes(): array + { + if (isset($this->attributes) || ![$class, $action] = $this->getRequest()->attributes->get('_controller_reflectors')) { + return $this->attributes ??= []; + } + + $this->attributes = []; + + foreach (array_merge($class?->getAttributes() ?? [], $action->getAttributes()) as $attribute) { + if (class_exists($attribute->getName())) { + $this->attributes[$attribute->getName()][] = $attribute->newInstance(); + } + } + + return $this->attributes; + } } diff --git a/src/Symfony/Component/HttpKernel/HttpKernel.php b/src/Symfony/Component/HttpKernel/HttpKernel.php index 0096596a52a05..015d8b51f9e51 100644 --- a/src/Symfony/Component/HttpKernel/HttpKernel.php +++ b/src/Symfony/Component/HttpKernel/HttpKernel.php @@ -152,10 +152,11 @@ private function handleRaw(Request $request, int $type = self::MAIN_REQUEST): Re // controller arguments $arguments = $this->argumentResolver->getArguments($request, $controller); - $event = new ControllerArgumentsEvent($this, $controller, $arguments, $request, $type); + $event = new ControllerArgumentsEvent($this, $event, $arguments, $request, $type); $this->dispatcher->dispatch($event, KernelEvents::CONTROLLER_ARGUMENTS); $controller = $event->getController(); $arguments = $event->getArguments(); + $request->attributes->remove('_controller_reflectors'); // call controller $response = $controller(...$arguments); diff --git a/src/Symfony/Component/HttpKernel/Tests/ControllerMetadata/ArgumentMetadataFactoryTest.php b/src/Symfony/Component/HttpKernel/Tests/ControllerMetadata/ArgumentMetadataFactoryTest.php index 7ec66e9df61f5..ffbb6549b89fe 100644 --- a/src/Symfony/Component/HttpKernel/Tests/ControllerMetadata/ArgumentMetadataFactoryTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/ControllerMetadata/ArgumentMetadataFactoryTest.php @@ -154,23 +154,23 @@ public function testIssue41478() ], $arguments); } - private function signature1(self $foo, array $bar, callable $baz) + public function signature1(self $foo, array $bar, callable $baz) { } - private function signature2(self $foo = null, FakeClassThatDoesNotExist $bar = null, ImportedAndFake $baz = null) + public function signature2(self $foo = null, FakeClassThatDoesNotExist $bar = null, ImportedAndFake $baz = null) { } - private function signature3(FakeClassThatDoesNotExist $bar, ImportedAndFake $baz) + public function signature3(FakeClassThatDoesNotExist $bar, ImportedAndFake $baz) { } - private function signature4($foo = 'default', $bar = 500, $baz = []) + public function signature4($foo = 'default', $bar = 500, $baz = []) { } - private function signature5(array $foo = null, $bar = null) + public function signature5(array $foo = null, $bar = null) { } } diff --git a/src/Symfony/Component/HttpKernel/Tests/Event/ControllerArgumentsEventTest.php b/src/Symfony/Component/HttpKernel/Tests/Event/ControllerArgumentsEventTest.php index 87dab37a84cb3..3f53847bac609 100644 --- a/src/Symfony/Component/HttpKernel/Tests/Event/ControllerArgumentsEventTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/Event/ControllerArgumentsEventTest.php @@ -14,13 +14,51 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Event\ControllerArgumentsEvent; +use Symfony\Component\HttpKernel\Event\ControllerEvent; +use Symfony\Component\HttpKernel\HttpKernelInterface; +use Symfony\Component\HttpKernel\Tests\Fixtures\Attribute\Bar; +use Symfony\Component\HttpKernel\Tests\Fixtures\Controller\AttributeController; use Symfony\Component\HttpKernel\Tests\TestHttpKernel; class ControllerArgumentsEventTest extends TestCase { public function testControllerArgumentsEvent() { - $filterController = new ControllerArgumentsEvent(new TestHttpKernel(), function () {}, ['test'], new Request(), 1); - $this->assertEquals($filterController->getArguments(), ['test']); + $event = new ControllerArgumentsEvent(new TestHttpKernel(), function () {}, ['test'], new Request(), HttpKernelInterface::MAIN_REQUEST); + $this->assertEquals($event->getArguments(), ['test']); + } + + public function testSetAttributes() + { + $controller = function () {}; + $event = new ControllerArgumentsEvent(new TestHttpKernel(), $controller, ['test'], new Request(), HttpKernelInterface::MAIN_REQUEST); + $event->setController($controller, []); + + $this->assertSame([], $event->getAttributes()); + } + + public function testGetAttributes() + { + $controller = new AttributeController(); + $request = new Request(); + + $controllerEvent = new ControllerEvent(new TestHttpKernel(), $controller, $request, HttpKernelInterface::MAIN_REQUEST); + + $event = new ControllerArgumentsEvent(new TestHttpKernel(), $controllerEvent, ['test'], new Request(), HttpKernelInterface::MAIN_REQUEST); + + $expected = [ + Bar::class => [ + new Bar('class'), + new Bar('method'), + ], + ]; + + $this->assertEquals($expected, $event->getAttributes()); + + $expected[Bar::class][] = new Bar('foo'); + $event->setController($controller, $expected); + + $this->assertEquals($expected, $event->getAttributes()); + $this->assertSame($controllerEvent->getAttributes(), $event->getAttributes()); } } diff --git a/src/Symfony/Component/HttpKernel/Tests/Event/ControllerEventTest.php b/src/Symfony/Component/HttpKernel/Tests/Event/ControllerEventTest.php new file mode 100644 index 0000000000000..e4bdb4f266abb --- /dev/null +++ b/src/Symfony/Component/HttpKernel/Tests/Event/ControllerEventTest.php @@ -0,0 +1,63 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpKernel\Tests\Event; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpKernel\Event\ControllerEvent; +use Symfony\Component\HttpKernel\HttpKernelInterface; +use Symfony\Component\HttpKernel\Tests\Fixtures\Attribute\Bar; +use Symfony\Component\HttpKernel\Tests\Fixtures\Controller\AttributeController; +use Symfony\Component\HttpKernel\Tests\TestHttpKernel; + +class ControllerEventTest extends TestCase +{ + public function testSetAttributes() + { + $request = new Request(); + $request->attributes->set('_controller_reflectors', [1, 2]); + $controller = [new AttributeController(), 'action']; + $event = new ControllerEvent(new TestHttpKernel(), $controller, $request, HttpKernelInterface::MAIN_REQUEST); + $event->setController($controller, []); + + $this->assertSame([], $event->getAttributes()); + } + + /** + * @dataProvider provideGetAttributes + */ + public function testGetAttributes(callable $controller) + { + $request = new Request(); + $reflector = new \ReflectionFunction($controller(...)); + $request->attributes->set('_controller_reflectors', [str_contains($reflector->name, '{closure}') ? null : $reflector->getClosureScopeClass(), $reflector]); + + $event = new ControllerEvent(new TestHttpKernel(), $controller, $request, HttpKernelInterface::MAIN_REQUEST); + + $expected = [ + Bar::class => [ + new Bar('class'), + new Bar('method'), + ], + ]; + + $this->assertEquals($expected, $event->getAttributes()); + } + + public function provideGetAttributes() + { + yield [[new AttributeController(), '__invoke']]; + yield [new AttributeController()]; + yield [(new AttributeController())->__invoke(...)]; + yield [#[Bar('class'), Bar('method')] static function () {}]; + } +} diff --git a/src/Symfony/Component/HttpKernel/Tests/Fixtures/Attribute/Bar.php b/src/Symfony/Component/HttpKernel/Tests/Fixtures/Attribute/Bar.php new file mode 100644 index 0000000000000..5c722bf0b3310 --- /dev/null +++ b/src/Symfony/Component/HttpKernel/Tests/Fixtures/Attribute/Bar.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\HttpKernel\Tests\Fixtures\Attribute; + +#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD | \Attribute::TARGET_FUNCTION | \Attribute::IS_REPEATABLE)] +class Bar +{ + public function __construct( + public mixed $foo, + ) { + } +} diff --git a/src/Symfony/Component/HttpKernel/Tests/Fixtures/Controller/AttributeController.php b/src/Symfony/Component/HttpKernel/Tests/Fixtures/Controller/AttributeController.php index 92e54f400d014..0385a07e64b4f 100644 --- a/src/Symfony/Component/HttpKernel/Tests/Fixtures/Controller/AttributeController.php +++ b/src/Symfony/Component/HttpKernel/Tests/Fixtures/Controller/AttributeController.php @@ -11,10 +11,17 @@ namespace Symfony\Component\HttpKernel\Tests\Fixtures\Controller; +use Symfony\Component\HttpKernel\Tests\Fixtures\Attribute\Bar; use Symfony\Component\HttpKernel\Tests\Fixtures\Attribute\Foo; +#[Bar('class'), Undefined('class')] class AttributeController { + #[Bar('method'), Undefined('method')] + public function __invoke() + { + } + public function action(#[Foo('bar')] string $baz) { }