vendor/symfony/ux-live-component/src/EventListener/LiveComponentSubscriber.php line 175

Open in your IDE?
  1. <?php
  2. /*
  3.  * This file is part of the Symfony package.
  4.  *
  5.  * (c) Fabien Potencier <fabien@symfony.com>
  6.  *
  7.  * For the full copyright and license information, please view the LICENSE
  8.  * file that was distributed with this source code.
  9.  */
  10. namespace Symfony\UX\LiveComponent\EventListener;
  11. use Psr\Container\ContainerInterface;
  12. use Symfony\Component\EventDispatcher\EventSubscriberInterface;
  13. use Symfony\Component\HttpFoundation\Request;
  14. use Symfony\Component\HttpFoundation\Response;
  15. use Symfony\Component\HttpKernel\Event\ControllerEvent;
  16. use Symfony\Component\HttpKernel\Event\ExceptionEvent;
  17. use Symfony\Component\HttpKernel\Event\RequestEvent;
  18. use Symfony\Component\HttpKernel\Event\ResponseEvent;
  19. use Symfony\Component\HttpKernel\Event\ViewEvent;
  20. use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
  21. use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException;
  22. use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
  23. use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;
  24. use Symfony\Component\Security\Csrf\CsrfToken;
  25. use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
  26. use Symfony\Contracts\Service\ServiceSubscriberInterface;
  27. use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
  28. use Symfony\UX\LiveComponent\Attribute\LiveArg;
  29. use Symfony\UX\LiveComponent\LiveComponentHydrator;
  30. use Symfony\UX\TwigComponent\ComponentFactory;
  31. use Symfony\UX\TwigComponent\ComponentMetadata;
  32. use Symfony\UX\TwigComponent\ComponentRenderer;
  33. use Symfony\UX\TwigComponent\MountedComponent;
  34. /**
  35.  * @author Kevin Bond <kevinbond@gmail.com>
  36.  * @author Ryan Weaver <ryan@symfonycasts.com>
  37.  *
  38.  * @experimental
  39.  *
  40.  * @internal
  41.  */
  42. class LiveComponentSubscriber implements EventSubscriberInterfaceServiceSubscriberInterface
  43. {
  44.     private const HTML_CONTENT_TYPE 'application/vnd.live-component+html';
  45.     public function __construct(private ContainerInterface $container)
  46.     {
  47.     }
  48.     public static function getSubscribedServices(): array
  49.     {
  50.         return [
  51.             ComponentRenderer::class,
  52.             ComponentFactory::class,
  53.             LiveComponentHydrator::class,
  54.             '?'.CsrfTokenManagerInterface::class,
  55.         ];
  56.     }
  57.     public function onKernelRequest(RequestEvent $event): void
  58.     {
  59.         $request $event->getRequest();
  60.         if (!$this->isLiveComponentRequest($request)) {
  61.             return;
  62.         }
  63.         // the default "action" is get, which does nothing
  64.         $action $request->get('action''get');
  65.         $componentName = (string) $request->get('component');
  66.         $request->attributes->set('_component_name'$componentName);
  67.         try {
  68.             /** @var ComponentMetadata $metadata */
  69.             $metadata $this->container->get(ComponentFactory::class)->metadataFor($componentName);
  70.         } catch (\InvalidArgumentException $e) {
  71.             throw new NotFoundHttpException(sprintf('Component "%s" not found.'$componentName), $e);
  72.         }
  73.         if (!$metadata->get('live'false)) {
  74.             throw new NotFoundHttpException(sprintf('"%s" (%s) is not a Live Component.'$metadata->getClass(), $componentName));
  75.         }
  76.         if ('get' === $action) {
  77.             $defaultAction trim($metadata->get('default_action''__invoke'), '()');
  78.             // set default controller for "default" action
  79.             $request->attributes->set('_controller'sprintf('%s::%s'$metadata->getServiceId(), $defaultAction));
  80.             $request->attributes->set('_component_default_action'true);
  81.             return;
  82.         }
  83.         if (!$request->isMethod('post')) {
  84.             throw new MethodNotAllowedHttpException(['POST']);
  85.         }
  86.         if (
  87.             $this->container->has(CsrfTokenManagerInterface::class) &&
  88.             $metadata->get('csrf') &&
  89.             !$this->container->get(CsrfTokenManagerInterface::class)->isTokenValid(new CsrfToken($componentName$request->headers->get('X-CSRF-TOKEN')))) {
  90.             throw new BadRequestHttpException('Invalid CSRF token.');
  91.         }
  92.         $request->attributes->set('_controller'sprintf('%s::%s'$metadata->getServiceId(), $action));
  93.     }
  94.     public function onKernelController(ControllerEvent $event): void
  95.     {
  96.         $request $event->getRequest();
  97.         if (!$this->isLiveComponentRequest($request)) {
  98.             return;
  99.         }
  100.         if ($request->query->has('data')) {
  101.             // ?data=
  102.             $data json_decode($request->query->get('data'), true512\JSON_THROW_ON_ERROR);
  103.         } else {
  104.             // OR body of the request is JSON
  105.             $data json_decode($request->getContent(), true512\JSON_THROW_ON_ERROR);
  106.         }
  107.         if (!\is_array($controller $event->getController()) || !== \count($controller)) {
  108.             throw new \RuntimeException('Not a valid live component.');
  109.         }
  110.         [$component$action] = $controller;
  111.         if (!\is_object($component)) {
  112.             throw new \RuntimeException('Not a valid live component.');
  113.         }
  114.         if (!$request->attributes->get('_component_default_action'false) && !AsLiveComponent::isActionAllowed($component$action)) {
  115.             throw new NotFoundHttpException(sprintf('The action "%s" either doesn\'t exist or is not allowed in "%s". Make sure it exist and has the LiveAction attribute above it.'$action\get_class($component)));
  116.         }
  117.         $mounted $this->container->get(LiveComponentHydrator::class)->hydrate(
  118.             $component,
  119.             $data,
  120.             $request->attributes->get('_component_name')
  121.         );
  122.         $request->attributes->set('_mounted_component'$mounted);
  123.         if (!\is_string($queryString $request->query->get('args'))) {
  124.             return;
  125.         }
  126.         // extra variables to be made available to the controller
  127.         // (for "actions" only)
  128.         parse_str($queryString$args);
  129.         foreach (LiveArg::liveArgs($component$action) as $parameter => $arg) {
  130.             if (isset($args[$arg])) {
  131.                 $request->attributes->set($parameter$args[$arg]);
  132.             }
  133.         }
  134.     }
  135.     public function onKernelView(ViewEvent $event): void
  136.     {
  137.         if (!$this->isLiveComponentRequest($request $event->getRequest())) {
  138.             return;
  139.         }
  140.         $event->setResponse($this->createResponse($request->attributes->get('_mounted_component')));
  141.     }
  142.     public function onKernelException(ExceptionEvent $event): void
  143.     {
  144.         if (!$this->isLiveComponentRequest($request $event->getRequest())) {
  145.             return;
  146.         }
  147.         if (!$event->getThrowable() instanceof UnprocessableEntityHttpException) {
  148.             return;
  149.         }
  150.         // in case the exception was too early somehow
  151.         if (!$mounted $request->attributes->get('_mounted_component')) {
  152.             return;
  153.         }
  154.         $event->setResponse($this->createResponse($mounted));
  155.     }
  156.     public function onKernelResponse(ResponseEvent $event): void
  157.     {
  158.         $request $event->getRequest();
  159.         $response $event->getResponse();
  160.         if (!$this->isLiveComponentRequest($request)) {
  161.             return;
  162.         }
  163.         if (!\in_array(self::HTML_CONTENT_TYPE$request->getAcceptableContentTypes(), true)) {
  164.             return;
  165.         }
  166.         if (!$response->isRedirection()) {
  167.             return;
  168.         }
  169.         $event->setResponse(new Response(null204, [
  170.             'Location' => $response->headers->get('Location'),
  171.             'Content-Type' => self::HTML_CONTENT_TYPE,
  172.         ]));
  173.     }
  174.     public static function getSubscribedEvents(): array
  175.     {
  176.         return [
  177.             RequestEvent::class => 'onKernelRequest',
  178.             ControllerEvent::class => 'onKernelController',
  179.             ViewEvent::class => 'onKernelView',
  180.             ResponseEvent::class => 'onKernelResponse',
  181.             ExceptionEvent::class => 'onKernelException',
  182.         ];
  183.     }
  184.     private function createResponse(MountedComponent $mounted): Response
  185.     {
  186.         $component $mounted->getComponent();
  187.         foreach (AsLiveComponent::preReRenderMethods($component) as $method) {
  188.             $component->{$method->name}();
  189.         }
  190.         return new Response($this->container->get(ComponentRenderer::class)->render($mounted));
  191.     }
  192.     private function isLiveComponentRequest(Request $request): bool
  193.     {
  194.         return 'live_component' === $request->attributes->get('_route');
  195.     }
  196. }