mirror of
https://github.com/symfony/http-kernel.git
synced 2026-03-24 01:12:09 +01:00
[HttpKernel] Expose controller metadata throughout the request lifecycle
This commit is contained in:
@@ -7,7 +7,7 @@ CHANGELOG
|
||||
* Add support for `UploadedFile` when using `MapRequestPayload`
|
||||
* Add support for bundles as compiler pass
|
||||
* Add support for `SOURCE_DATE_EPOCH` environment variable
|
||||
* Add property `$controllerArgumentsEvent` to `ResponseEvent`
|
||||
* Add property `$controllerMetadata` to several kernel events to give listeners access to controller metadata
|
||||
* 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
|
||||
@@ -16,6 +16,7 @@ CHANGELOG
|
||||
* Deprecate passing a non-flat list of attributes to `Controller::setController()`
|
||||
* Deprecate the `Symfony\Component\HttpKernel\DependencyInjection\Extension` class, use the parent `Symfony\Component\DependencyInjection\Extension\Extension` class instead
|
||||
* Allow using Expression or \Closure for `validationGroups` in `#[MapRequestPayload]` and `#[MapQueryString]`
|
||||
* Deprecate passing a `ControllerArgumentsEvent` to the `ViewEvent` constructor; pass a `ControllerArgumentsMetadata` instead
|
||||
|
||||
8.0
|
||||
---
|
||||
|
||||
@@ -61,17 +61,26 @@ final class ControllerArgumentsEvent extends KernelEvent
|
||||
unset($this->namedArguments);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<mixed>
|
||||
*/
|
||||
public function getArguments(): array
|
||||
{
|
||||
return $this->arguments;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<mixed> $arguments
|
||||
*/
|
||||
public function setArguments(array $arguments): void
|
||||
{
|
||||
$this->arguments = $arguments;
|
||||
unset($this->namedArguments);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function getNamedArguments(): array
|
||||
{
|
||||
if (isset($this->namedArguments)) {
|
||||
|
||||
43
Event/ControllerArgumentsMetadata.php
Normal file
43
Event/ControllerArgumentsMetadata.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?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\Event;
|
||||
|
||||
/**
|
||||
* Provides read-only access to controller metadata.
|
||||
*
|
||||
* @author Nicolas Grekas <p@tchwork.com>
|
||||
*/
|
||||
class ControllerArgumentsMetadata extends ControllerMetadata
|
||||
{
|
||||
public function __construct(
|
||||
ControllerEvent $controllerEvent,
|
||||
private ControllerArgumentsEvent $controllerArgumentsEvent,
|
||||
) {
|
||||
parent::__construct($controllerEvent);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<mixed>
|
||||
*/
|
||||
public function getArguments(): array
|
||||
{
|
||||
return $this->controllerArgumentsEvent->getArguments();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function getNamedArguments(): array
|
||||
{
|
||||
return $this->controllerArgumentsEvent->getNamedArguments();
|
||||
}
|
||||
}
|
||||
@@ -21,8 +21,6 @@ use Symfony\Component\HttpKernel\HttpKernelInterface;
|
||||
* setController() you can set a new controller that is used in the processing
|
||||
* of the request.
|
||||
*
|
||||
* Controllers should be callables.
|
||||
*
|
||||
* @author Bernhard Schussek <bschussek@gmail.com>
|
||||
*/
|
||||
final class ControllerEvent extends KernelEvent
|
||||
|
||||
47
Event/ControllerMetadata.php
Normal file
47
Event/ControllerMetadata.php
Normal file
@@ -0,0 +1,47 @@
|
||||
<?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\Event;
|
||||
|
||||
/**
|
||||
* Provides read-only access to controller metadata.
|
||||
*
|
||||
* @author Nicolas Grekas <p@tchwork.com>
|
||||
*/
|
||||
class ControllerMetadata
|
||||
{
|
||||
public function __construct(
|
||||
private ControllerEvent $controllerEvent,
|
||||
) {
|
||||
}
|
||||
|
||||
public function getController(): callable
|
||||
{
|
||||
return $this->controllerEvent->getController();
|
||||
}
|
||||
|
||||
public function getReflector(): \ReflectionFunctionAbstract
|
||||
{
|
||||
return $this->controllerEvent->getControllerReflector();
|
||||
}
|
||||
|
||||
/**
|
||||
* @template T of object
|
||||
*
|
||||
* @param class-string<T>|'*'|null $className
|
||||
*
|
||||
* @return ($className is null ? array<class-string, list<object>> : ($className is '*' ? list<object> : list<T>))
|
||||
*/
|
||||
public function getAttributes(?string $className = null): array
|
||||
{
|
||||
return $this->controllerEvent->getAttributes($className);
|
||||
}
|
||||
}
|
||||
@@ -38,6 +38,7 @@ final class ExceptionEvent extends RequestEvent
|
||||
int $requestType,
|
||||
\Throwable $e,
|
||||
private bool $isKernelTerminating = false,
|
||||
public readonly ?ControllerMetadata $controllerMetadata = null,
|
||||
) {
|
||||
parent::__construct($kernel, $request, $requestType);
|
||||
|
||||
|
||||
@@ -11,6 +11,9 @@
|
||||
|
||||
namespace Symfony\Component\HttpKernel\Event;
|
||||
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpKernel\HttpKernelInterface;
|
||||
|
||||
/**
|
||||
* Triggered whenever a request is fully processed.
|
||||
*
|
||||
@@ -18,4 +21,12 @@ namespace Symfony\Component\HttpKernel\Event;
|
||||
*/
|
||||
final class FinishRequestEvent extends KernelEvent
|
||||
{
|
||||
public function __construct(
|
||||
HttpKernelInterface $kernel,
|
||||
Request $request,
|
||||
?int $requestType,
|
||||
public readonly ?ControllerMetadata $controllerMetadata = null,
|
||||
) {
|
||||
parent::__construct($kernel, $request, $requestType);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ final class ResponseEvent extends KernelEvent
|
||||
Request $request,
|
||||
int $requestType,
|
||||
private Response $response,
|
||||
public readonly ?ControllerArgumentsEvent $controllerArgumentsEvent = null,
|
||||
public readonly ?ControllerArgumentsMetadata $controllerMetadata = null,
|
||||
) {
|
||||
parent::__construct($kernel, $request, $requestType);
|
||||
}
|
||||
|
||||
@@ -25,13 +25,38 @@ use Symfony\Component\HttpKernel\HttpKernelInterface;
|
||||
*/
|
||||
final class ViewEvent extends RequestEvent
|
||||
{
|
||||
public readonly ?ControllerArgumentsMetadata $controllerMetadata;
|
||||
|
||||
/**
|
||||
* @deprecated since Symfony 8.1, use $controllerMetadata instead
|
||||
*/
|
||||
public private(set) ?ControllerArgumentsEvent $controllerArgumentsEvent {
|
||||
get {
|
||||
trigger_deprecation('symfony/http-kernel', '8.1', 'Accessing the "controllerArgumentsEvent" property of the "%s" class is deprecated. Use "controllerMetadata" instead.', __CLASS__);
|
||||
|
||||
if (!$m = $this->controllerMetadata) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->controllerArgumentsEvent ??= new ControllerArgumentsEvent($this->getKernel(), \Closure::bind(fn () => $this->controllerEvent, $m, ControllerMetadata::class)(), $m->getArguments(), $this->getRequest(), $this->getRequestType());
|
||||
}
|
||||
}
|
||||
|
||||
public function __construct(
|
||||
HttpKernelInterface $kernel,
|
||||
Request $request,
|
||||
int $requestType,
|
||||
private mixed $controllerResult,
|
||||
public readonly ?ControllerArgumentsEvent $controllerArgumentsEvent = null,
|
||||
ControllerArgumentsMetadata|ControllerArgumentsEvent|null $controllerMetadata = null,
|
||||
) {
|
||||
if ($controllerMetadata instanceof ControllerArgumentsEvent) {
|
||||
trigger_deprecation('symfony/http-kernel', '8.1', 'Passing a ControllerArgumentsEvent to the ViewEvent constructor is deprecated. Pass a ControllerArgumentsMetadata instance instead.');
|
||||
$this->controllerArgumentsEvent = $controllerMetadata;
|
||||
$controllerEvent = \Closure::bind(fn () => $this->controllerEvent, $controllerMetadata, ControllerArgumentsEvent::class)();
|
||||
$controllerMetadata = new ControllerArgumentsMetadata($controllerEvent, $controllerMetadata);
|
||||
}
|
||||
$this->controllerMetadata = $controllerMetadata;
|
||||
|
||||
parent::__construct($kernel, $request, $requestType);
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,9 @@ use Symfony\Component\HttpKernel\Controller\ArgumentResolver;
|
||||
use Symfony\Component\HttpKernel\Controller\ArgumentResolverInterface;
|
||||
use Symfony\Component\HttpKernel\Controller\ControllerResolverInterface;
|
||||
use Symfony\Component\HttpKernel\Event\ControllerArgumentsEvent;
|
||||
use Symfony\Component\HttpKernel\Event\ControllerArgumentsMetadata;
|
||||
use Symfony\Component\HttpKernel\Event\ControllerEvent;
|
||||
use Symfony\Component\HttpKernel\Event\ControllerMetadata;
|
||||
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
|
||||
use Symfony\Component\HttpKernel\Event\FinishRequestEvent;
|
||||
use Symfony\Component\HttpKernel\Event\RequestEvent;
|
||||
@@ -35,6 +37,7 @@ use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
|
||||
|
||||
// Help opcache.preload discover always-needed symbols
|
||||
class_exists(ControllerArgumentsEvent::class);
|
||||
class_exists(ControllerArgumentsMetadata::class);
|
||||
class_exists(ControllerEvent::class);
|
||||
class_exists(ExceptionEvent::class);
|
||||
class_exists(FinishRequestEvent::class);
|
||||
@@ -73,7 +76,7 @@ class HttpKernel implements HttpKernelInterface, TerminableInterface
|
||||
$this->requestStack->push($request);
|
||||
$response = null;
|
||||
try {
|
||||
return $response = $this->handleRaw($request, $type);
|
||||
return $response = $this->handleRaw($request, $type, $controllerMetadata);
|
||||
} catch (\Throwable $e) {
|
||||
if ($e instanceof \Error && !$this->handleAllThrowables) {
|
||||
throw $e;
|
||||
@@ -83,12 +86,12 @@ class HttpKernel implements HttpKernelInterface, TerminableInterface
|
||||
$e = new BadRequestHttpException($e->getMessage(), $e);
|
||||
}
|
||||
if (false === $catch) {
|
||||
$this->finishRequest($request, $type);
|
||||
$this->finishRequest($request, $type, $controllerMetadata);
|
||||
|
||||
throw $e;
|
||||
}
|
||||
|
||||
return $response = $this->handleThrowable($e, $request, $type);
|
||||
return $response = $this->handleThrowable($e, $request, $type, $controllerMetadata);
|
||||
} finally {
|
||||
$this->requestStack->pop();
|
||||
|
||||
@@ -152,7 +155,7 @@ class HttpKernel implements HttpKernelInterface, TerminableInterface
|
||||
* @throws \LogicException If one of the listener does not behave as expected
|
||||
* @throws NotFoundHttpException When controller cannot be found
|
||||
*/
|
||||
private function handleRaw(Request $request, int $type = self::MAIN_REQUEST): Response
|
||||
private function handleRaw(Request $request, int $type = self::MAIN_REQUEST, ?ControllerMetadata &$controllerMetadata = null): Response
|
||||
{
|
||||
// request
|
||||
$event = new RequestEvent($this, $request, $type);
|
||||
@@ -167,14 +170,16 @@ class HttpKernel implements HttpKernelInterface, TerminableInterface
|
||||
throw new NotFoundHttpException(\sprintf('Unable to find the controller for path "%s". The route is wrongly configured.', $request->getPathInfo()));
|
||||
}
|
||||
|
||||
$event = new ControllerEvent($this, $controller, $request, $type);
|
||||
$controllerEvent = $event = new ControllerEvent($this, $controller, $request, $type);
|
||||
$controllerMetadata = new ControllerMetadata($event);
|
||||
$this->dispatcher->dispatch($event, KernelEvents::CONTROLLER);
|
||||
$controller = $event->getController();
|
||||
|
||||
// controller arguments
|
||||
$arguments = $this->argumentResolver->getArguments($request, $controller, $event->getControllerReflector());
|
||||
|
||||
$controllerArgumentsEvent = $event = new ControllerArgumentsEvent($this, $event, $arguments, $request, $type);
|
||||
$event = new ControllerArgumentsEvent($this, $event, $arguments, $request, $type);
|
||||
$controllerMetadata = new ControllerArgumentsMetadata($controllerEvent, $event);
|
||||
$this->dispatcher->dispatch($event, KernelEvents::CONTROLLER_ARGUMENTS);
|
||||
$controller = $event->getController();
|
||||
$arguments = $event->getArguments();
|
||||
@@ -184,7 +189,7 @@ class HttpKernel implements HttpKernelInterface, TerminableInterface
|
||||
|
||||
// view
|
||||
if (!$response instanceof Response) {
|
||||
$event = new ViewEvent($this, $request, $type, $response, $event);
|
||||
$event = new ViewEvent($this, $request, $type, $response, $controllerMetadata);
|
||||
$this->dispatcher->dispatch($event, KernelEvents::VIEW);
|
||||
|
||||
if ($event->hasResponse()) {
|
||||
@@ -201,7 +206,7 @@ class HttpKernel implements HttpKernelInterface, TerminableInterface
|
||||
}
|
||||
}
|
||||
|
||||
return $this->filterResponse($response, $request, $type, $controllerArgumentsEvent);
|
||||
return $this->filterResponse($response, $request, $type, $controllerMetadata);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -209,13 +214,13 @@ 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, ?ControllerArgumentsEvent $controllerArgumentsEvent = null): Response
|
||||
private function filterResponse(Response $response, Request $request, int $type, ?ControllerMetadata $controllerMetadata = null): Response
|
||||
{
|
||||
$event = new ResponseEvent($this, $request, $type, $response, $controllerArgumentsEvent);
|
||||
$event = new ResponseEvent($this, $request, $type, $response, $controllerMetadata);
|
||||
|
||||
$this->dispatcher->dispatch($event, KernelEvents::RESPONSE);
|
||||
|
||||
$this->finishRequest($request, $type);
|
||||
$this->finishRequest($request, $type, $controllerMetadata);
|
||||
|
||||
return $event->getResponse();
|
||||
}
|
||||
@@ -227,24 +232,24 @@ class HttpKernel implements HttpKernelInterface, TerminableInterface
|
||||
* operations such as {@link RequestStack::getParentRequest()} can lead to
|
||||
* weird results.
|
||||
*/
|
||||
private function finishRequest(Request $request, int $type): void
|
||||
private function finishRequest(Request $request, int $type, ?ControllerMetadata $controllerMetadata = null): void
|
||||
{
|
||||
$this->dispatcher->dispatch(new FinishRequestEvent($this, $request, $type), KernelEvents::FINISH_REQUEST);
|
||||
$this->dispatcher->dispatch(new FinishRequestEvent($this, $request, $type, $controllerMetadata), KernelEvents::FINISH_REQUEST);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a throwable by trying to convert it to a Response.
|
||||
*/
|
||||
private function handleThrowable(\Throwable $e, Request $request, int $type): Response
|
||||
private function handleThrowable(\Throwable $e, Request $request, int $type, ?ControllerMetadata $controllerMetadata = null): Response
|
||||
{
|
||||
$event = new ExceptionEvent($this, $request, $type, $e, isKernelTerminating: $this->terminating);
|
||||
$event = new ExceptionEvent($this, $request, $type, $e, isKernelTerminating: $this->terminating, controllerMetadata: $controllerMetadata);
|
||||
$this->dispatcher->dispatch($event, KernelEvents::EXCEPTION);
|
||||
|
||||
// a listener might have replaced the exception
|
||||
$e = $event->getThrowable();
|
||||
|
||||
if (!$event->hasResponse()) {
|
||||
$this->finishRequest($request, $type);
|
||||
$this->finishRequest($request, $type, $controllerMetadata);
|
||||
|
||||
throw $e;
|
||||
}
|
||||
@@ -264,7 +269,7 @@ class HttpKernel implements HttpKernelInterface, TerminableInterface
|
||||
}
|
||||
|
||||
try {
|
||||
return $this->filterResponse($response, $request, $type);
|
||||
return $this->filterResponse($response, $request, $type, $controllerMetadata);
|
||||
} catch (\Throwable $e) {
|
||||
if ($e instanceof \Error && !$this->handleAllThrowables) {
|
||||
throw $e;
|
||||
|
||||
@@ -31,6 +31,7 @@ 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
|
||||
{
|
||||
@@ -492,6 +493,83 @@ class HttpKernelTest extends TestCase
|
||||
Request::setTrustedProxies([], -1);
|
||||
}
|
||||
|
||||
public function testResponseEventCanAccessControllerAttributes()
|
||||
{
|
||||
$dispatcher = new EventDispatcher();
|
||||
$capturedAttributes = null;
|
||||
|
||||
$dispatcher->addListener(KernelEvents::CONTROLLER, static function ($event) {
|
||||
$event->setController($event->getController(), [new Bar('test')]);
|
||||
});
|
||||
|
||||
$dispatcher->addListener(KernelEvents::RESPONSE, static function ($event) use (&$capturedAttributes) {
|
||||
$capturedAttributes = $event->controllerMetadata->getAttributes('*');
|
||||
});
|
||||
|
||||
$kernel = $this->getHttpKernel($dispatcher);
|
||||
|
||||
$kernel->handle(new Request(), HttpKernelInterface::MAIN_REQUEST, false);
|
||||
|
||||
$this->assertEquals([new Bar('test')], $capturedAttributes);
|
||||
}
|
||||
|
||||
public function testViewEventProvidesControllerArgumentsViaMetadata()
|
||||
{
|
||||
$dispatcher = new EventDispatcher();
|
||||
$capturedArguments = $capturedNamedArguments = null;
|
||||
|
||||
$dispatcher->addListener(KernelEvents::VIEW, static function ($event) use (&$capturedArguments, &$capturedNamedArguments) {
|
||||
$capturedArguments = $event->controllerMetadata->getArguments();
|
||||
$capturedNamedArguments = $event->controllerMetadata->getNamedArguments();
|
||||
$event->setResponse(new Response('ok'));
|
||||
});
|
||||
|
||||
$kernel = $this->getHttpKernel($dispatcher, static fn ($value) => $value, arguments: ['resolved']);
|
||||
|
||||
$kernel->handle(new Request(), HttpKernelInterface::MAIN_REQUEST, false);
|
||||
|
||||
$this->assertSame(['resolved'], $capturedArguments);
|
||||
$this->assertSame(['value' => 'resolved'], $capturedNamedArguments);
|
||||
}
|
||||
|
||||
public function testExceptionEventProvidesControllerMetadata()
|
||||
{
|
||||
$dispatcher = new EventDispatcher();
|
||||
$capturedController = $capturedArguments = null;
|
||||
$controller = static fn (string $value) => throw new \RuntimeException('boom');
|
||||
|
||||
$dispatcher->addListener(KernelEvents::EXCEPTION, static function ($event) use (&$capturedController, &$capturedArguments) {
|
||||
$capturedController = $event->controllerMetadata->getController();
|
||||
$capturedArguments = $event->controllerMetadata->getArguments();
|
||||
$event->setResponse(new Response('handled'));
|
||||
});
|
||||
|
||||
$kernel = $this->getHttpKernel($dispatcher, $controller, arguments: ['meta']);
|
||||
|
||||
$response = $kernel->handle(new Request(), HttpKernelInterface::MAIN_REQUEST, true);
|
||||
|
||||
$this->assertSame('handled', $response->getContent());
|
||||
$this->assertSame($controller, $capturedController);
|
||||
$this->assertSame(['meta'], $capturedArguments);
|
||||
}
|
||||
|
||||
public function testFinishRequestEventKeepsControllerMetadata()
|
||||
{
|
||||
$dispatcher = new EventDispatcher();
|
||||
$capturedArguments = null;
|
||||
|
||||
$dispatcher->addListener(KernelEvents::FINISH_REQUEST, static function ($event) use (&$capturedArguments) {
|
||||
$capturedArguments = $event->controllerMetadata->getArguments();
|
||||
});
|
||||
|
||||
$kernel = $this->getHttpKernel($dispatcher, static fn ($value) => new Response($value), arguments: ['done']);
|
||||
|
||||
$response = $kernel->handle(new Request(), HttpKernelInterface::MAIN_REQUEST, false);
|
||||
|
||||
$this->assertSame('done', $response->getContent());
|
||||
$this->assertSame(['done'], $capturedArguments);
|
||||
}
|
||||
|
||||
private function getHttpKernel(EventDispatcherInterface $eventDispatcher, $controller = null, ?RequestStack $requestStack = null, array $arguments = [], bool $handleAllThrowables = false)
|
||||
{
|
||||
$controller ??= static fn () => new Response('Hello');
|
||||
|
||||
Reference in New Issue
Block a user