mirror of
https://github.com/symfony/http-kernel.git
synced 2026-03-24 01:12:09 +01:00
[HttpKernel] Return attributes as a flat list when using Controller[Arguments]Event::getAttributes(*)
This commit is contained in:
@@ -7,11 +7,13 @@ CHANGELOG
|
||||
* Add support for `UploadedFile` when using `MapRequestPayload`
|
||||
* Add support for bundles as compiler pass
|
||||
* Add support for `SOURCE_DATE_EPOCH` environment variable
|
||||
* Add `ResponseEvent::getControllerAttributes()`
|
||||
* Add property `$controllerArgumentsEvent` to `ResponseEvent`
|
||||
* Add `Request` attribute `_controller_attributes` to decouple controller attributes from their source code
|
||||
* Return attributes as a flat list when using `Controller[Arguments]Event::getAttributes('*')`
|
||||
* Pass `request` and `args` variables to `Cache` attribute expressions containing the `Request` object and controller arguments
|
||||
* Allow using closures with the `Cache` attribute
|
||||
* Allow setting a condition when the `Cache` attribute should be applied
|
||||
* Deprecate passing a non-flat list of attributes to `Controller::setController()`
|
||||
|
||||
8.0
|
||||
---
|
||||
|
||||
@@ -53,7 +53,7 @@ final class ControllerArgumentsEvent extends KernelEvent
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<class-string, list<object>>|null $attributes
|
||||
* @param list<object>|null $attributes
|
||||
*/
|
||||
public function setController(callable $controller, ?array $attributes = null): void
|
||||
{
|
||||
@@ -99,9 +99,9 @@ final class ControllerArgumentsEvent extends KernelEvent
|
||||
/**
|
||||
* @template T of object
|
||||
*
|
||||
* @param class-string<T>|null $className
|
||||
* @param class-string<T>|'*'|null $className
|
||||
*
|
||||
* @return ($className is null ? array<class-string, list<object>> : list<T>)
|
||||
* @return ($className is null ? array<class-string, list<object>> : ($className is '*' ? list<object> : list<T>))
|
||||
*/
|
||||
public function getAttributes(?string $className = null): array
|
||||
{
|
||||
|
||||
@@ -48,12 +48,21 @@ final class ControllerEvent extends KernelEvent
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<class-string, list<object>>|null $attributes
|
||||
* @param list<object>|null $attributes
|
||||
*/
|
||||
public function setController(callable $controller, ?array $attributes = null): void
|
||||
{
|
||||
if (null !== $attributes) {
|
||||
$this->getRequest()->attributes->set('_controller_attributes', $attributes);
|
||||
if (!array_is_list($flattenAttributes = $attributes)) {
|
||||
trigger_deprecation('symfony/http-kernel', '8.1', 'Passing an array of attributes grouped by class name to "%s()" is deprecated. Pass a flat list of attributes instead.', __METHOD__);
|
||||
$flattenAttributes = [];
|
||||
foreach ($attributes as $attributes) {
|
||||
foreach (\is_array($attributes) ? $attributes : [$attributes] as $attribute) {
|
||||
$flattenAttributes[] = $attribute;
|
||||
}
|
||||
}
|
||||
}
|
||||
$this->getRequest()->attributes->set('_controller_attributes', $flattenAttributes);
|
||||
}
|
||||
|
||||
if (isset($this->controller) && ($controller instanceof \Closure ? $controller == $this->controller : $controller === $this->controller)) {
|
||||
@@ -66,13 +75,11 @@ final class ControllerEvent extends KernelEvent
|
||||
$this->getRequest()->attributes->remove('_controller_attributes');
|
||||
}
|
||||
|
||||
if (\is_array($controller) && method_exists(...$controller)) {
|
||||
$this->controllerReflector = new \ReflectionMethod(...$controller);
|
||||
} elseif (\is_string($controller) && str_contains($controller, '::')) {
|
||||
$this->controllerReflector = new \ReflectionMethod(...explode('::', $controller, 2));
|
||||
} else {
|
||||
$this->controllerReflector = new \ReflectionFunction($controller(...));
|
||||
}
|
||||
$this->controllerReflector = match (true) {
|
||||
\is_array($controller) && method_exists(...$controller) => new \ReflectionMethod(...$controller),
|
||||
\is_string($controller) && str_contains($controller, '::') => new \ReflectionMethod(...explode('::', $controller, 2)),
|
||||
default => new \ReflectionFunction($controller(...)),
|
||||
};
|
||||
|
||||
$this->controller = $controller;
|
||||
}
|
||||
@@ -80,33 +87,42 @@ final class ControllerEvent extends KernelEvent
|
||||
/**
|
||||
* @template T of object
|
||||
*
|
||||
* @param class-string<T>|null $className
|
||||
* @param class-string<T>|'*'|null $className
|
||||
*
|
||||
* @return ($className is null ? array<class-string, list<object>> : list<T>)
|
||||
* @return ($className is null ? array<class-string, list<object>> : ($className is '*' ? list<object> : list<T>))
|
||||
*/
|
||||
public function getAttributes(?string $className = null): array
|
||||
{
|
||||
if (null !== $attributes = $this->getRequest()->attributes->get('_controller_attributes')) {
|
||||
return null === $className ? $attributes : $attributes[$className] ?? [];
|
||||
}
|
||||
if (null === $attributes = $this->getRequest()->attributes->get('_controller_attributes')) {
|
||||
$class = match (true) {
|
||||
\is_array($this->controller) && method_exists(...$this->controller) => new \ReflectionClass($this->controller[0]),
|
||||
\is_string($this->controller) && false !== $i = strpos($this->controller, '::') => new \ReflectionClass(substr($this->controller, 0, $i)),
|
||||
$this->controllerReflector instanceof \ReflectionFunction => $this->controllerReflector->isAnonymous() ? null : $this->controllerReflector->getClosureCalledClass(),
|
||||
};
|
||||
$attributes = [];
|
||||
|
||||
if (\is_array($this->controller) && method_exists(...$this->controller)) {
|
||||
$class = new \ReflectionClass($this->controller[0]);
|
||||
} elseif (\is_string($this->controller) && false !== $i = strpos($this->controller, '::')) {
|
||||
$class = new \ReflectionClass(substr($this->controller, 0, $i));
|
||||
} else {
|
||||
$class = $this->controllerReflector instanceof \ReflectionFunction && $this->controllerReflector->isAnonymous() ? null : $this->controllerReflector->getClosureCalledClass();
|
||||
}
|
||||
$attributes = [];
|
||||
|
||||
foreach (array_merge($class?->getAttributes() ?? [], $this->controllerReflector->getAttributes()) as $attribute) {
|
||||
if (class_exists($attribute->getName())) {
|
||||
$attributes[$attribute->getName()][] = $attribute->newInstance();
|
||||
foreach (array_merge($class?->getAttributes() ?? [], $this->controllerReflector->getAttributes()) as $attribute) {
|
||||
if (class_exists($attribute->getName())) {
|
||||
$attributes[] = $attribute->newInstance();
|
||||
}
|
||||
}
|
||||
|
||||
$this->getRequest()->attributes->set('_controller_attributes', $attributes);
|
||||
}
|
||||
|
||||
$this->getRequest()->attributes->set('_controller_attributes', $attributes);
|
||||
if ('*' === $className) {
|
||||
return $attributes;
|
||||
}
|
||||
|
||||
return null === $className ? $attributes : $attributes[$className] ?? [];
|
||||
if (null !== $className) {
|
||||
return array_values(array_filter($attributes, static fn ($attr) => $attr instanceof $className));
|
||||
}
|
||||
|
||||
$grouped = [];
|
||||
foreach ($attributes as $attribute) {
|
||||
$grouped[$attribute::class][] = $attribute;
|
||||
}
|
||||
|
||||
return $grouped;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ final class ResponseEvent extends KernelEvent
|
||||
Request $request,
|
||||
int $requestType,
|
||||
private Response $response,
|
||||
private ?ControllerEvent $controllerEvent = null,
|
||||
public readonly ?ControllerArgumentsEvent $controllerArgumentsEvent = null,
|
||||
) {
|
||||
parent::__construct($kernel, $request, $requestType);
|
||||
}
|
||||
@@ -45,18 +45,4 @@ final class ResponseEvent extends KernelEvent
|
||||
{
|
||||
$this->response = $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* @template T of class-string|null
|
||||
*
|
||||
* @param T $className
|
||||
*
|
||||
* @return array<class-string, list<object>>|list<object>
|
||||
*
|
||||
* @psalm-return (T is null ? array<class-string, list<object>> : list<object>)
|
||||
*/
|
||||
public function getControllerAttributes(?string $className = null): array
|
||||
{
|
||||
return $this->controllerEvent?->getAttributes($className) ?? [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -167,14 +167,14 @@ class HttpKernel implements HttpKernelInterface, TerminableInterface
|
||||
throw new NotFoundHttpException(\sprintf('Unable to find the controller for path "%s". The route is wrongly configured.', $request->getPathInfo()));
|
||||
}
|
||||
|
||||
$controllerEvent = $event = new ControllerEvent($this, $controller, $request, $type);
|
||||
$event = new ControllerEvent($this, $controller, $request, $type);
|
||||
$this->dispatcher->dispatch($event, KernelEvents::CONTROLLER);
|
||||
$controller = $event->getController();
|
||||
|
||||
// controller arguments
|
||||
$arguments = $this->argumentResolver->getArguments($request, $controller, $event->getControllerReflector());
|
||||
|
||||
$event = new ControllerArgumentsEvent($this, $event, $arguments, $request, $type);
|
||||
$controllerArgumentsEvent = $event = new ControllerArgumentsEvent($this, $event, $arguments, $request, $type);
|
||||
$this->dispatcher->dispatch($event, KernelEvents::CONTROLLER_ARGUMENTS);
|
||||
$controller = $event->getController();
|
||||
$arguments = $event->getArguments();
|
||||
@@ -201,7 +201,7 @@ class HttpKernel implements HttpKernelInterface, TerminableInterface
|
||||
}
|
||||
}
|
||||
|
||||
return $this->filterResponse($response, $request, $type, $controllerEvent);
|
||||
return $this->filterResponse($response, $request, $type, $controllerArgumentsEvent);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -209,9 +209,9 @@ class HttpKernel implements HttpKernelInterface, TerminableInterface
|
||||
*
|
||||
* @throws \RuntimeException if the passed object is not a Response instance
|
||||
*/
|
||||
private function filterResponse(Response $response, Request $request, int $type, ?ControllerEvent $controllerEvent = null): Response
|
||||
private function filterResponse(Response $response, Request $request, int $type, ?ControllerArgumentsEvent $controllerArgumentsEvent = null): Response
|
||||
{
|
||||
$event = new ResponseEvent($this, $request, $type, $response, $controllerEvent);
|
||||
$event = new ResponseEvent($this, $request, $type, $response, $controllerArgumentsEvent);
|
||||
|
||||
$this->dispatcher->dispatch($event, KernelEvents::RESPONSE);
|
||||
|
||||
|
||||
@@ -59,10 +59,26 @@ class ControllerArgumentsEventTest extends TestCase
|
||||
|
||||
$this->assertEquals($expected, $event->getAttributes());
|
||||
|
||||
$expected[Bar::class][] = new Bar('foo');
|
||||
$event->setController($controller, $expected);
|
||||
$attributes = [
|
||||
new Bar('class'),
|
||||
new Bar('method'),
|
||||
new Bar('foo'),
|
||||
new Baz(),
|
||||
];
|
||||
$event->setController($controller, $attributes);
|
||||
|
||||
$this->assertEquals($expected, $event->getAttributes());
|
||||
$grouped = [
|
||||
Bar::class => [
|
||||
new Bar('class'),
|
||||
new Bar('method'),
|
||||
new Bar('foo'),
|
||||
],
|
||||
Baz::class => [
|
||||
new Baz(),
|
||||
],
|
||||
];
|
||||
$this->assertEquals($grouped, $event->getAttributes());
|
||||
$this->assertEquals($attributes, $event->getAttributes('*'));
|
||||
$this->assertSame($controllerEvent->getAttributes(), $event->getAttributes());
|
||||
}
|
||||
|
||||
@@ -82,10 +98,20 @@ class ControllerArgumentsEventTest extends TestCase
|
||||
|
||||
$this->assertEquals($expected, $event->getAttributes(Bar::class));
|
||||
|
||||
$expected[] = new Bar('foo');
|
||||
$event->setController($controller, [Bar::class => $expected]);
|
||||
// When setting attributes, provide as flat list
|
||||
$flatAttributes = [
|
||||
new Bar('class'),
|
||||
new Bar('method'),
|
||||
new Bar('foo'),
|
||||
];
|
||||
$event->setController($controller, $flatAttributes);
|
||||
|
||||
$this->assertEquals($expected, $event->getAttributes(Bar::class));
|
||||
$expectedAfterSet = [
|
||||
new Bar('class'),
|
||||
new Bar('method'),
|
||||
new Bar('foo'),
|
||||
];
|
||||
$this->assertEquals($expectedAfterSet, $event->getAttributes(Bar::class));
|
||||
$this->assertSame($controllerEvent->getAttributes(Bar::class), $event->getAttributes(Bar::class));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,8 @@
|
||||
namespace Symfony\Component\HttpKernel\Tests\Event;
|
||||
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use PHPUnit\Framework\Attributes\Group;
|
||||
use PHPUnit\Framework\Attributes\IgnoreDeprecations;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
@@ -76,7 +78,11 @@ class ControllerEventTest extends TestCase
|
||||
$attributes = $event->getAttributes();
|
||||
|
||||
$this->assertTrue($request->attributes->has('_controller_attributes'));
|
||||
$this->assertEquals($attributes, $request->attributes->get('_controller_attributes'));
|
||||
$stored = $request->attributes->get('_controller_attributes');
|
||||
$this->assertIsArray($stored);
|
||||
$this->assertCount(3, $stored);
|
||||
$this->assertIsArray($attributes);
|
||||
$this->assertArrayHasKey(Bar::class, $attributes);
|
||||
}
|
||||
|
||||
public function testSetControllerWithAttributesStoresInRequest()
|
||||
@@ -85,11 +91,33 @@ class ControllerEventTest extends TestCase
|
||||
$controller = [new AttributeController(), '__invoke'];
|
||||
$event = new ControllerEvent(new TestHttpKernel(), $controller, $request, HttpKernelInterface::MAIN_REQUEST);
|
||||
|
||||
$customAttributes = [Bar::class => [new Bar('custom')]];
|
||||
// Provide attributes as flat list
|
||||
$customAttributes = [new Bar('custom')];
|
||||
|
||||
$event->setController($controller, $customAttributes);
|
||||
|
||||
$this->assertEquals($customAttributes, $request->attributes->get('_controller_attributes'));
|
||||
$stored = $request->attributes->get('_controller_attributes');
|
||||
$this->assertIsArray($stored);
|
||||
$this->assertCount(1, $stored);
|
||||
$this->assertInstanceOf(Bar::class, $stored[0]);
|
||||
}
|
||||
|
||||
#[IgnoreDeprecations]
|
||||
#[Group('legacy')]
|
||||
public function testSetControllerWithGroupedAttributesConvertsToFlat()
|
||||
{
|
||||
$request = new Request();
|
||||
$controller = [new AttributeController(), '__invoke'];
|
||||
$event = new ControllerEvent(new TestHttpKernel(), $controller, $request, HttpKernelInterface::MAIN_REQUEST);
|
||||
|
||||
$groupedAttributes = [Bar::class => [new Bar('custom')]];
|
||||
|
||||
$event->setController($controller, $groupedAttributes);
|
||||
|
||||
$stored = $request->attributes->get('_controller_attributes');
|
||||
$this->assertIsArray($stored);
|
||||
$this->assertCount(1, $stored);
|
||||
$this->assertInstanceOf(Bar::class, $stored[0]);
|
||||
}
|
||||
|
||||
public function testSetControllerWithoutAttributesRemovesFromRequestWhenControllerChanges()
|
||||
@@ -100,7 +128,7 @@ class ControllerEventTest extends TestCase
|
||||
$event = new ControllerEvent(new TestHttpKernel(), $controller1, $request, HttpKernelInterface::MAIN_REQUEST);
|
||||
|
||||
// First set some attributes
|
||||
$customAttributes = [Bar::class => [new Bar('custom')]];
|
||||
$customAttributes = [new Bar('custom')];
|
||||
$event->setController($controller1, $customAttributes);
|
||||
$this->assertEquals($customAttributes, $request->attributes->get('_controller_attributes'));
|
||||
|
||||
|
||||
@@ -1,95 +0,0 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* 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\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Event\ControllerEvent;
|
||||
use Symfony\Component\HttpKernel\Event\ResponseEvent;
|
||||
use Symfony\Component\HttpKernel\HttpKernelInterface;
|
||||
use Symfony\Component\HttpKernel\Tests\Fixtures\Attribute\Bar;
|
||||
use Symfony\Component\HttpKernel\Tests\Fixtures\Attribute\Baz;
|
||||
use Symfony\Component\HttpKernel\Tests\Fixtures\Controller\AttributeController;
|
||||
use Symfony\Component\HttpKernel\Tests\TestHttpKernel;
|
||||
|
||||
class ResponseEventTest extends TestCase
|
||||
{
|
||||
public function testGetControllerAttributesWithoutControllerEvent()
|
||||
{
|
||||
$kernel = new TestHttpKernel();
|
||||
$request = new Request();
|
||||
$response = new Response();
|
||||
|
||||
$event = new ResponseEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST, $response);
|
||||
|
||||
$this->assertEquals([], $event->getControllerAttributes());
|
||||
$this->assertEquals([], $event->getControllerAttributes(Bar::class));
|
||||
}
|
||||
|
||||
public function testGetControllerAttributesWithControllerEvent()
|
||||
{
|
||||
$kernel = new TestHttpKernel();
|
||||
$request = new Request();
|
||||
$response = new Response();
|
||||
|
||||
$controller = [new AttributeController(), '__invoke'];
|
||||
$controllerEvent = new ControllerEvent($kernel, $controller, $request, HttpKernelInterface::MAIN_REQUEST);
|
||||
|
||||
$event = new ResponseEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST, $response, $controllerEvent);
|
||||
|
||||
$expected = [
|
||||
Bar::class => [
|
||||
new Bar('class'),
|
||||
new Bar('method'),
|
||||
],
|
||||
Baz::class => [
|
||||
new Baz(),
|
||||
],
|
||||
];
|
||||
|
||||
$this->assertEquals($expected, $event->getControllerAttributes());
|
||||
}
|
||||
|
||||
public function testGetControllerAttributesByClassName()
|
||||
{
|
||||
$kernel = new TestHttpKernel();
|
||||
$request = new Request();
|
||||
$response = new Response();
|
||||
|
||||
$controller = [new AttributeController(), '__invoke'];
|
||||
$controllerEvent = new ControllerEvent($kernel, $controller, $request, HttpKernelInterface::MAIN_REQUEST);
|
||||
|
||||
$event = new ResponseEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST, $response, $controllerEvent);
|
||||
|
||||
$expected = [
|
||||
new Bar('class'),
|
||||
new Bar('method'),
|
||||
];
|
||||
|
||||
$this->assertEquals($expected, $event->getControllerAttributes(Bar::class));
|
||||
}
|
||||
|
||||
public function testGetControllerAttributesByInvalidClassName()
|
||||
{
|
||||
$kernel = new TestHttpKernel();
|
||||
$request = new Request();
|
||||
$response = new Response();
|
||||
|
||||
$controller = [new AttributeController(), '__invoke'];
|
||||
$controllerEvent = new ControllerEvent($kernel, $controller, $request, HttpKernelInterface::MAIN_REQUEST);
|
||||
|
||||
$event = new ResponseEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST, $response, $controllerEvent);
|
||||
|
||||
$this->assertEquals([], $event->getControllerAttributes(\stdClass::class));
|
||||
}
|
||||
}
|
||||
@@ -31,7 +31,6 @@ use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
use Symfony\Component\HttpKernel\HttpKernel;
|
||||
use Symfony\Component\HttpKernel\HttpKernelInterface;
|
||||
use Symfony\Component\HttpKernel\KernelEvents;
|
||||
use Symfony\Component\HttpKernel\Tests\Fixtures\Attribute\Bar;
|
||||
|
||||
class HttpKernelTest extends TestCase
|
||||
{
|
||||
@@ -493,30 +492,6 @@ class HttpKernelTest extends TestCase
|
||||
Request::setTrustedProxies([], -1);
|
||||
}
|
||||
|
||||
public function testResponseEventCanAccessControllerAttributes()
|
||||
{
|
||||
$dispatcher = new EventDispatcher();
|
||||
$capturedAttributes = null;
|
||||
|
||||
$dispatcher->addListener(KernelEvents::CONTROLLER, static function ($event) {
|
||||
// Set some attributes on the controller event
|
||||
$event->setController($event->getController(), [Bar::class => [new Bar('test')]]);
|
||||
});
|
||||
|
||||
$dispatcher->addListener(KernelEvents::RESPONSE, static function ($event) use (&$capturedAttributes) {
|
||||
$capturedAttributes = $event->getControllerAttributes();
|
||||
});
|
||||
|
||||
$kernel = $this->getHttpKernel($dispatcher);
|
||||
|
||||
$kernel->handle(new Request(), HttpKernelInterface::MAIN_REQUEST, false);
|
||||
|
||||
// Should have the attributes we set
|
||||
$this->assertIsArray($capturedAttributes);
|
||||
$this->assertArrayHasKey(Bar::class, $capturedAttributes);
|
||||
$this->assertEquals([new Bar('test')], $capturedAttributes[Bar::class]);
|
||||
}
|
||||
|
||||
private function getHttpKernel(EventDispatcherInterface $eventDispatcher, $controller = null, ?RequestStack $requestStack = null, array $arguments = [], bool $handleAllThrowables = false)
|
||||
{
|
||||
$controller ??= static fn () => new Response('Hello');
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
"require": {
|
||||
"php": ">=8.4",
|
||||
"psr/log": "^1|^2|^3",
|
||||
"symfony/deprecation-contracts": "^2.5|^3",
|
||||
"symfony/error-handler": "^7.4|^8.0",
|
||||
"symfony/event-dispatcher": "^7.4|^8.0",
|
||||
"symfony/http-foundation": "^7.4|^8.0",
|
||||
|
||||
Reference in New Issue
Block a user