Slide 1

Slide 1 text

Christian Flothmann - SymfonyLive Berlin 2023 Anatomie des Request Handlings

Slide 2

Slide 2 text

Anatomie des Request Handling - SymfonyLive Berlin 2023 Christian Flothmann Software Developer bei QOSSMIC @xabbuh Symfony Core Team

Slide 3

Slide 3 text

Anatomie des Request Handling - SymfonyLive Berlin 2023 symfony new --demo symfony-live-demo symfony server:start

Slide 4

Slide 4 text

Anatomie des Request Handling - SymfonyLive Berlin 2023

Slide 5

Slide 5 text

Anatomie des Request Handling - SymfonyLive Berlin 2023 A controller is a PHP function you create that reads information from the Request object and creates and returns a Response object. https://symfony.com/doc/current/controller.html

Slide 6

Slide 6 text

Anatomie des Request Handling - SymfonyLive Berlin 2023 The HttpKernel component provides a structured process for converting a Request into a Response by making use of the EventDispatcher component. The HttpKernel component provides a structured process for converting a Request into a Response by making use of the EventDispatcher component. The HttpKernel component provides a structured process for converting a Request into a Response by making use of the EventDispatcher component. https://symfony.com/doc/current/components/http_kernel.html

Slide 7

Slide 7 text

Anatomie des Request Handling - SymfonyLive Berlin 2023 use App\Kernel; require_once dirname(__DIR__).'/vendor/autoload_runtime.php'; return function (array $context) { return new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']); };

Slide 8

Slide 8 text

Anatomie des Request Handling - SymfonyLive Berlin 2023 $runtime = $_SERVER['APP_RUNTIME'] ?? $_ENV['APP_RUNTIME'] ?? 'Symfony\\Component\\Runtime\\Symfo $runtime = new $runtime(($_SERVER['APP_RUNTIME_OPTIONS'] ?? $_ENV['APP_RUNTIME_OPTIONS'] ?? []) + 'project_dir' => dirname(__DIR__, 1), ]); [$app, $args] = $runtime ->getResolver($app) ->resolve(); $app = $app(...$args); exit( $runtime ->getRunner($app) ->run() ); return function (array $context) { return new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']); };

Slide 9

Slide 9 text

Anatomie des Request Handling - SymfonyLive Berlin 2023 $runtime = $_SERVER['APP_RUNTIME'] ?? $_ENV['APP_RUNTIME'] ?? 'Symfony\\Component\\Runtime\\Symfo $runtime = new $runtime(($_SERVER['APP_RUNTIME_OPTIONS'] ?? $_ENV['APP_RUNTIME_OPTIONS'] ?? []) + 'project_dir' => dirname(__DIR__, 1), ]); [$app, $args] = $runtime ->getResolver($app) ->resolve(); $app = $app(...$args); exit( $runtime ->getRunner($app) ->run() );

Slide 10

Slide 10 text

Anatomie des Request Handling - SymfonyLive Berlin 2023 public function getRunner(?object $application): RunnerInterface { if ($application instanceof HttpKernelInterface) { return new HttpKernelRunner($application, Request::createFromGlobals()); } if ($application instanceof Response) { return new ResponseRunner($application); } if ($application instanceof Command) { $console = $this->console ??= new Application(); $console->setName($application->getName() ?: $console->getName()); if (!$application->getName() || !$console->has($application->getName())) { $application->setName($_SERVER['argv'][0]); $console->add($application); } Anatomie des Request Handling - SymfonyLive Berlin 2023

Slide 11

Slide 11 text

Anatomie des Request Handling - SymfonyLive Berlin 2023 public static function createFromGlobals(): static { $request = self::createRequestFromFactory($_GET, $_POST, [], $_COOKIE, $_FILES, $_SERVER); if (str_starts_with($request->headers->get('CONTENT_TYPE', ''), 'application/x-www-form-urlencoded') && \in_array(strtoupper($request->server->get('REQUEST_METHOD', 'GET')), ['PUT', 'DELETE', 'PATCH']) ) { parse_str($request->getContent(), $data); $request->request = new InputBag($data); } return $request; }

Slide 12

Slide 12 text

Anatomie des Request Handling - SymfonyLive Berlin 2023 public function run(): int { $response = $this->kernel->handle($this->request); $response->send(); if ($this->kernel instanceof TerminableInterface) { $this->kernel->terminate($this->request, $response); } return 0; }

Slide 13

Slide 13 text

Anatomie des Request Handling - SymfonyLive Berlin 2023 public function handle(Request $request, int $type = HttpKernelInterface::MAIN_REQUEST, bool $catch = true): Response { if (!$this->booted) { $container = $this->container ?? $this->preBoot(); if ($container->has('http_cache')) { return $container->get('http_cache')->handle($request, $type, $catch); } } $this->boot(); ++$this->requestStackSize; $this->resetServices = true; try { return $this->getHttpKernel()->handle($request, $type, $catch); } finally { --$this->requestStackSize; } }

Slide 14

Slide 14 text

Anatomie des Request Handling - SymfonyLive Berlin 2023 public function handle(Request $request, int $type = HttpKernelInterface::MAIN_REQUEST, bool $catch = true): Response { if (!$this->booted) { $container = $this->container ?? $this->preBoot(); if ($container->has('http_cache')) { return $container->get('http_cache')->handle($request, $type, $catch); } } $this->boot(); ++$this->requestStackSize; $this->resetServices = true; try { return $this->getHttpKernel()->handle($request, $type, $catch); } finally { --$this->requestStackSize; } }

Slide 15

Slide 15 text

Anatomie des Request Handling - SymfonyLive Berlin 2023 public function handle(Request $request, int $type = HttpKernelInterface::MAIN_REQUEST, bool $catch = true): Response { $request->headers->set('X-Php-Ob-Level', (string) ob_get_level()); $this->requestStack->push($request); try { return $this->handleRaw($request, $type); } catch (\Throwable $e) { if ($e instanceof \Error && !$this->handleAllThrowables) { throw $e; } if ($e instanceof RequestExceptionInterface) { $e = new BadRequestHttpException($e->getMessage(), $e); } if (false === $catch) { $this->finishRequest($request, $type); throw $e; } return $this->handleThrowable($e, $request, $type); } finally { $this->requestStack->pop(); } }

Slide 16

Slide 16 text

Anatomie des Request Handling - SymfonyLive Berlin 2023 public function handle(Request $request, int $type = HttpKernelInterface::MAIN_REQUEST, bool $catch = true): Response { $request->headers->set('X-Php-Ob-Level', (string) ob_get_level()); $this->requestStack->push($request); try { return $this->handleRaw($request, $type); } catch (\Throwable $e) { if ($e instanceof \Error && !$this->handleAllThrowables) { throw $e; } if ($e instanceof RequestExceptionInterface) { $e = new BadRequestHttpException($e->getMessage(), $e); } if (false === $catch) { $this->finishRequest($request, $type); throw $e; } return $this->handleThrowable($e, $request, $type); } finally { $this->requestStack->pop(); } }

Slide 17

Slide 17 text

Anatomie des Request Handling - SymfonyLive Berlin 2023 private function handleRaw(Request $request, int $type = self::MAIN_REQUEST): Response { // request $event = new RequestEvent($this, $request, $type); $this->dispatcher->dispatch($event, KernelEvents::REQUEST); if ($event->hasResponse()) { return $this->filterResponse($event->getResponse(), $request, $type); } // load controller if (false === $controller = $this->resolver->getController($request)) { 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); $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); $this->dispatcher->dispatch($event, KernelEvents::CONTROLLER_ARGUMENTS); $controller = $event->getController(); Anatomie des Request Handling - SymfonyLive Berlin 2023

Slide 18

Slide 18 text

Anatomie des Request Handling - SymfonyLive Berlin 2023

Slide 19

Slide 19 text

Anatomie des Request Handling - SymfonyLive Berlin 2023 bin/console debug:event-dispatcher kernel.request

Slide 20

Slide 20 text

Anatomie des Request Handling - SymfonyLive Berlin 2023 #[AsEventListener(event: RequestEvent::class)] class MaintenanceModeListener { public function __construct( private readonly bool $enabled, private readonly Environment $twig, ) { } public function __invoke(RequestEvent $event) { if ($this->enabled) { $event->setResponse(new Response($this->twig->render('maintenance.html.twig'))); } } }

Slide 21

Slide 21 text

Anatomie des Request Handling - SymfonyLive Berlin 2023 #[AsEventListener(event: RequestEvent::class)] class MaintenanceModeListener { public function __construct( private readonly bool $enabled, private readonly Environment $twig, ) { } public function __invoke(RequestEvent $event) { if ($this->enabled) { $event->setResponse(new Response($this->twig->render('maintenance.html.twig'))); } } }

Slide 22

Slide 22 text

Anatomie des Request Handling - SymfonyLive Berlin 2023 #[AsEventListener(event: RequestEvent::class)] class MaintenanceModeListener { public function __construct( private readonly bool $enabled, private readonly Environment $twig, ) { } public function __invoke(RequestEvent $event) { if ($this->enabled) { $event->setResponse(new Response($this->twig->render('maintenance.html.twig'))); } } }

Slide 23

Slide 23 text

Anatomie des Request Handling - SymfonyLive Berlin 2023

Slide 24

Slide 24 text

Anatomie des Request Handling - SymfonyLive Berlin 2023 private function handleRaw(Request $request, int $type = self::MAIN_REQUEST): Response { // request $event = new RequestEvent($this, $request, $type); $this->dispatcher->dispatch($event, KernelEvents::REQUEST); if ($event->hasResponse()) { return $this->filterResponse($event->getResponse(), $request, $type); } // load controller if (false === $controller = $this->resolver->getController($request)) { 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); $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); $this->dispatcher->dispatch($event, KernelEvents::CONTROLLER_ARGUMENTS); $controller = $event->getController(); Anatomie des Request Handling - SymfonyLive Berlin 2023

Slide 25

Slide 25 text

Anatomie des Request Handling - SymfonyLive Berlin 2023 private function handleRaw(Request $request, int $type = self::MAIN_REQUEST): Response { // request $event = new RequestEvent($this, $request, $type); $this->dispatcher->dispatch($event, KernelEvents::REQUEST); if ($event->hasResponse()) { return $this->filterResponse($event->getResponse(), $request, $type); } // load controller if (false === $controller = $this->resolver->getController($request)) { 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); $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); $this->dispatcher->dispatch($event, KernelEvents::CONTROLLER_ARGUMENTS); $controller = $event->getController(); Anatomie des Request Handling - SymfonyLive Berlin 2023

Slide 26

Slide 26 text

Anatomie des Request Handling - SymfonyLive Berlin 2023 private function handleRaw(Request $request, int $type = self::MAIN_REQUEST): Response { // request $event = new RequestEvent($this, $request, $type); $this->dispatcher->dispatch($event, KernelEvents::REQUEST); if ($event->hasResponse()) { return $this->filterResponse($event->getResponse(), $request, $type); } // load controller if (false === $controller = $this->resolver->getController($request)) { 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); $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); $this->dispatcher->dispatch($event, KernelEvents::CONTROLLER_ARGUMENTS); $controller = $event->getController(); Anatomie des Request Handling - SymfonyLive Berlin 2023

Slide 27

Slide 27 text

Anatomie des Request Handling - SymfonyLive Berlin 2023

Slide 28

Slide 28 text

Anatomie des Request Handling - SymfonyLive Berlin 2023 } $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); $this->dispatcher->dispatch($event, KernelEvents::CONTROLLER_ARGUMENTS); $controller = $event->getController(); $arguments = $event->getArguments(); // call controller $response = $controller(...$arguments); // view if (!$response instanceof Response) { $event = new ViewEvent($this, $request, $type, $response, $event); $this->dispatcher->dispatch($event, KernelEvents::VIEW); if ($event->hasResponse()) { $response = $event->getResponse(); } else { $msg = sprintf('The controller must return a "Symfony\Component\HttpFoundation\Response" object but it returned %s.', $this- Anatomie des Request Handling - SymfonyLive Berlin 2023

Slide 29

Slide 29 text

Anatomie des Request Handling - SymfonyLive Berlin 2023 #[Route('/', name: 'blog_index', defaults: ['page' => '1', '_format' => 'html'], methods: ['GET'])] #[Route('/page/{page<[1-9]\d{0,8}>}', name: 'blog_index_paginated', methods: ['GET'])] public function index(Request $request, int $page, PostRepository $posts): Response { } #[Route('/comment/{postSlug}/new', name: 'comment_new', methods: ['POST'])] public function commentNew( #[CurrentUser] User $user, Request $request, #[MapEntity(mapping: ['postSlug' => 'slug'])] Post $post, EventDispatcherInterface $eventDispatcher, EntityManagerInterface $entityManager, ): Response { }

Slide 30

Slide 30 text

Anatomie des Request Handling - SymfonyLive Berlin 2023 namespace Symfony\Component\HttpKernel\Controller; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; interface ValueResolverInterface { public function resolve(Request $request, ArgumentMetadata $argument): iterable; }

Slide 31

Slide 31 text

Anatomie des Request Handling - SymfonyLive Berlin 2023 $controller = $event->getController(); $arguments = $event->getArguments(); // call controller $response = $controller(...$arguments); // view if (!$response instanceof Response) { $event = new ViewEvent($this, $request, $type, $response, $event); $this->dispatcher->dispatch($event, KernelEvents::VIEW); if ($event->hasResponse()) { $response = $event->getResponse(); } else { $msg = sprintf('The controller must return a "Symfony\Component\HttpFoundation\Response" object but it returned %s.', $this- >varToString($response)); // the user may have forgotten to return something if (null === $response) { $msg .= ' Did you forget to add a return statement somewhere in your controller?'; } throw new ControllerDoesNotReturnResponseException($msg, $controller, __FILE__, __LINE__ - 17); } } Anatomie des Request Handling - SymfonyLive Berlin 2023

Slide 32

Slide 32 text

Anatomie des Request Handling - SymfonyLive Berlin 2023 $controller = $event->getController(); $arguments = $event->getArguments(); // call controller $response = $controller(...$arguments); // view if (!$response instanceof Response) { $event = new ViewEvent($this, $request, $type, $response, $event); $this->dispatcher->dispatch($event, KernelEvents::VIEW); if ($event->hasResponse()) { $response = $event->getResponse(); } else { $msg = sprintf('The controller must return a "Symfony\Component\HttpFoundation\Response" object but it returned %s.', $this- >varToString($response)); // the user may have forgotten to return something if (null === $response) { $msg .= ' Did you forget to add a return statement somewhere in your controller?'; } throw new ControllerDoesNotReturnResponseException($msg, $controller, __FILE__, __LINE__ - 17); } } Anatomie des Request Handling - SymfonyLive Berlin 2023

Slide 33

Slide 33 text

Anatomie des Request Handling - SymfonyLive Berlin 2023

Slide 34

Slide 34 text

Anatomie des Request Handling - SymfonyLive Berlin 2023 private function filterResponse(Response $response, Request $request, int $type): Response { $event = new ResponseEvent($this, $request, $type, $response); $this->dispatcher->dispatch($event, KernelEvents::RESPONSE); $this->finishRequest($request, $type); return $event->getResponse(); } private function finishRequest(Request $request, int $type): void { $this->dispatcher->dispatch(new FinishRequestEvent($this, $request, $type), KernelEvents::FINISH_REQUEST); }

Slide 35

Slide 35 text

Anatomie des Request Handling - SymfonyLive Berlin 2023 use Symfony\Component\EventDispatcher\Attribute\AsEventListener; use Symfony\Component\HttpKernel\Event\ResponseEvent; #[AsEventListener(event: ResponseEvent::class)] class RequestIdResponseListener { public function __invoke(ResponseEvent $event): void { $request = $event->getRequest(); $response = $event->getResponse(); if ($request->attributes->has('_request_id')) { $response->headers->set('X-Request-Id', $request->attributes->get('_request_id')); } } }

Slide 36

Slide 36 text

Anatomie des Request Handling - SymfonyLive Berlin 2023

Slide 37

Slide 37 text

Anatomie des Request Handling - SymfonyLive Berlin 2023 private function filterResponse(Response $response, Request $request, int $type): Response { $event = new ResponseEvent($this, $request, $type, $response); $this->dispatcher->dispatch($event, KernelEvents::RESPONSE); $this->finishRequest($request, $type); return $event->getResponse(); } private function finishRequest(Request $request, int $type): void { $this->dispatcher->dispatch(new FinishRequestEvent($this, $request, $type), KernelEvents::FINISH_REQUEST); }

Slide 38

Slide 38 text

Anatomie des Request Handling - SymfonyLive Berlin 2023

Slide 39

Slide 39 text

Anatomie des Request Handling - SymfonyLive Berlin 2023 public function handle(Request $request, int $type = HttpKernelInterface::MAIN_REQUEST, bool $catch = true): Response { $request->headers->set('X-Php-Ob-Level', (string) ob_get_level()); $this->requestStack->push($request); try { return $this->handleRaw($request, $type); } catch (\Throwable $e) { if ($e instanceof \Error && !$this->handleAllThrowables) { throw $e; } if ($e instanceof RequestExceptionInterface) { $e = new BadRequestHttpException($e->getMessage(), $e); } if (false === $catch) { $this->finishRequest($request, $type); throw $e; } return $this->handleThrowable($e, $request, $type); } finally { $this->requestStack->pop(); } }

Slide 40

Slide 40 text

Anatomie des Request Handling - SymfonyLive Berlin 2023

Slide 41

Slide 41 text

Anatomie des Request Handling - SymfonyLive Berlin 2023 private function handleThrowable(\Throwable $e, Request $request, int $type): Response { $event = new ExceptionEvent($this, $request, $type, $e); $this->dispatcher->dispatch($event, KernelEvents::EXCEPTION); // a listener might have replaced the exception $e = $event->getThrowable(); if (!$event->hasResponse()) { $this->finishRequest($request, $type); throw $e; } $response = $event->getResponse(); // the developer asked for a specific status code if (!$event->isAllowingCustomResponseCode() && !$response->isClientError() && !$response- >isServerError() && !$response->isRedirect()) { // ensure that we actually have an error response if ($e instanceof HttpExceptionInterface) { Anatomie des Request Handling - SymfonyLive Berlin 2023

Slide 42

Slide 42 text

Anatomie des Request Handling - SymfonyLive Berlin 2023 #[AsEventListener(event: ExceptionEvent::class)] class UnprocessableEntityListener { public function __invoke(ExceptionEvent $event): void { if ($event->getThrowable() instanceof DiscountExpiredException) { $event->setResponse(new Response('', Response::HTTP_UNPROCESSABLE_ENTITY)); } } }

Slide 43

Slide 43 text

Anatomie des Request Handling - SymfonyLive Berlin 2023 public function handle(Request $request, int $type = HttpKernelInterface::MAIN_REQUEST, bool $catch = true): Response { $request->headers->set('X-Php-Ob-Level', (string) ob_get_level()); $this->requestStack->push($request); try { return $this->handleRaw($request, $type); } catch (\Throwable $e) { if ($e instanceof \Error && !$this->handleAllThrowables) { throw $e; } if ($e instanceof RequestExceptionInterface) { $e = new BadRequestHttpException($e->getMessage(), $e); } if (false === $catch) { $this->finishRequest($request, $type); throw $e; } return $this->handleThrowable($e, $request, $type); } finally { $this->requestStack->pop(); } }

Slide 44

Slide 44 text

Anatomie des Request Handling - SymfonyLive Berlin 2023 public function run(): int { $response = $this->kernel->handle($this->request); $response->send(); if ($this->kernel instanceof TerminableInterface) { $this->kernel->terminate($this->request, $response); } return 0; }

Slide 45

Slide 45 text

Anatomie des Request Handling - SymfonyLive Berlin 2023 public function run(): int { $response = $this->kernel->handle($this->request); $response->send(); if ($this->kernel instanceof TerminableInterface) { $this->kernel->terminate($this->request, $response); } return 0; }

Slide 46

Slide 46 text

Anatomie des Request Handling - SymfonyLive Berlin 2023 public function terminate(Request $request, Response $response) { $this->dispatcher->dispatch(new TerminateEvent($this, $request, $response), KernelEvents::TERMINATE); }

Slide 47

Slide 47 text

Anatomie des Request Handling - SymfonyLive Berlin 2023

Slide 48

Slide 48 text

Anatomie des Request Handling - SymfonyLive Berlin 2023 TLDR HTTP Kernel: Herz jeder Symfony Anwendung Event Dispatcher: mächtiger Erweiterungspunkt WARNUNG: dosierter Einsatz!

Slide 49

Slide 49 text

Anatomie des Request Handling - SymfonyLive Berlin 2023 Vielen Dank!