vendor/easycorp/easyadmin-bundle/src/EventListener/AdminRouterSubscriber.php line 144

Open in your IDE?
  1. <?php
  2. namespace EasyCorp\Bundle\EasyAdminBundle\EventListener;
  3. use EasyCorp\Bundle\EasyAdminBundle\Config\Option\EA;
  4. use EasyCorp\Bundle\EasyAdminBundle\Contracts\Controller\CrudControllerInterface;
  5. use EasyCorp\Bundle\EasyAdminBundle\Contracts\Controller\DashboardControllerInterface;
  6. use EasyCorp\Bundle\EasyAdminBundle\Factory\AdminContextFactory;
  7. use EasyCorp\Bundle\EasyAdminBundle\Factory\ControllerFactory;
  8. use EasyCorp\Bundle\EasyAdminBundle\Registry\CrudControllerRegistry;
  9. use EasyCorp\Bundle\EasyAdminBundle\Registry\DashboardControllerRegistry;
  10. use EasyCorp\Bundle\EasyAdminBundle\Router\UrlSigner;
  11. use Symfony\Component\EventDispatcher\EventSubscriberInterface;
  12. use Symfony\Component\HttpFoundation\RedirectResponse;
  13. use Symfony\Component\HttpFoundation\Request;
  14. use Symfony\Component\HttpKernel\Controller\ControllerResolverInterface;
  15. use Symfony\Component\HttpKernel\Event\ControllerEvent;
  16. use Symfony\Component\HttpKernel\Event\RequestEvent;
  17. use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
  18. use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
  19. use Symfony\Component\Routing\Matcher\RequestMatcherInterface;
  20. use Twig\Environment;
  21. /**
  22.  * This subscriber acts as a "proxy" of all backend requests. First, if the
  23.  * request is related to EasyAdmin, it creates the AdminContext variable and
  24.  * stores it in the Request as an attribute.
  25.  *
  26.  * Second, it uses Symfony events to serve all backend requests using a single
  27.  * route. The trick is to change dynamically the controller to execute when
  28.  * the request is related to a CRUD action or a normal Symfony route/action.
  29.  *
  30.  * @author Javier Eguiluz <javier.eguiluz@gmail.com>
  31.  * @author Maxime Steinhausser <maxime.steinhausser@gmail.com>
  32.  * @author Yonel Ceruto <yonelceruto@gmail.com>
  33.  */
  34. class AdminRouterSubscriber implements EventSubscriberInterface
  35. {
  36.     private $adminContextFactory;
  37.     private $dashboardControllerRegistry;
  38.     private $crudControllerRegistry;
  39.     private $controllerFactory;
  40.     private $controllerResolver;
  41.     private $urlGenerator;
  42.     private $requestMatcher;
  43.     private $twig;
  44.     private $urlSigner;
  45.     public function __construct(AdminContextFactory $adminContextFactoryDashboardControllerRegistry $dashboardControllerRegistryCrudControllerRegistry $crudControllerRegistryControllerFactory $controllerFactoryControllerResolverInterface $controllerResolverUrlGeneratorInterface $urlGeneratorRequestMatcherInterface $requestMatcherEnvironment $twigUrlSigner $urlSigner)
  46.     {
  47.         $this->adminContextFactory $adminContextFactory;
  48.         $this->dashboardControllerRegistry $dashboardControllerRegistry;
  49.         $this->crudControllerRegistry $crudControllerRegistry;
  50.         $this->controllerFactory $controllerFactory;
  51.         $this->controllerResolver $controllerResolver;
  52.         $this->urlGenerator $urlGenerator;
  53.         $this->requestMatcher $requestMatcher;
  54.         $this->twig $twig;
  55.         $this->urlSigner $urlSigner;
  56.     }
  57.     public static function getSubscribedEvents(): array
  58.     {
  59.         return [
  60.             RequestEvent::class => [
  61.                 ['handleLegacyEaContext'10],
  62.                 ['onKernelRequest'0],
  63.             ],
  64.             // the priority must be higher than 0 to run it before ParamConverterListener
  65.             ControllerEvent::class => ['onKernelController'128],
  66.         ];
  67.     }
  68.     /**
  69.      * It adds support to legacy EasyAdmin requests that include the EA::CONTEXT_NAME query
  70.      * parameter. It creates the new equivalent URL and redirects to it transparently.
  71.      */
  72.     public function handleLegacyEaContext(RequestEvent $event): void
  73.     {
  74.         $request $event->getRequest();
  75.         if (null === $eaContext $request->query->get(EA::CONTEXT_NAME)) {
  76.             return;
  77.         }
  78.         trigger_deprecation('easycorp/easyadmin-bundle''3.2.0''The "%s" query parameter is deprecated and you no longer need to add it when using custom actions inside EasyAdmin. Read the UPGRADE guide at https://github.com/EasyCorp/EasyAdminBundle/blob/master/UPGRADE.md.'EA::CONTEXT_NAME);
  79.         if (null === $dashboardControllerFqcn $this->dashboardControllerRegistry->getControllerFqcnByContextId($eaContext)) {
  80.             return;
  81.         }
  82.         $dashboardControllerRoute $this->dashboardControllerRegistry->getRouteByControllerFqcn($dashboardControllerFqcn);
  83.         $request->query->remove(EA::CONTEXT_NAME);
  84.         $request->query->set(EA::ROUTE_NAME$request->attributes->get('_route'));
  85.         $request->query->set(EA::ROUTE_PARAMS$request->attributes->all()['_route_params'] ?? []);
  86.         $newUrl $this->urlGenerator->generate($dashboardControllerRoute$request->query->all());
  87.         $dashboardControllerInstance $this->getDashboardControllerInstance($dashboardControllerFqcn$request);
  88.         $adminContext $this->adminContextFactory->create($request$dashboardControllerInstancenull);
  89.         if ($adminContext->getSignedUrls()) {
  90.             $newUrl $this->urlSigner->sign($newUrl);
  91.         }
  92.         $event->setResponse(new RedirectResponse($newUrl));
  93.     }
  94.     /**
  95.      * If this is an EasyAdmin request, it creates the AdminContext variable, stores it
  96.      * in the Request as an attribute and injects it as a global Twig variable.
  97.      */
  98.     public function onKernelRequest(RequestEvent $event): void
  99.     {
  100.         $request $event->getRequest();
  101.         if (null === $dashboardControllerFqcn $this->getDashboardControllerFqcn($request)) {
  102.             return;
  103.         }
  104.         if (null === $dashboardControllerInstance $this->getDashboardControllerInstance($dashboardControllerFqcn$request)) {
  105.             return;
  106.         }
  107.         // creating the context is expensive, so it's created once and stored in the request
  108.         // if the current request already has an AdminContext object, do nothing
  109.         if (null === $adminContext $request->attributes->get(EA::CONTEXT_REQUEST_ATTRIBUTE)) {
  110.             $crudControllerInstance $this->getCrudControllerInstance($request);
  111.             $adminContext $this->adminContextFactory->create($request$dashboardControllerInstance$crudControllerInstance);
  112.         }
  113.         $request->attributes->set(EA::CONTEXT_REQUEST_ATTRIBUTE$adminContext);
  114.         // this makes the AdminContext available in all templates as a short named variable
  115.         $this->twig->addGlobal('ea'$adminContext);
  116.         if ($adminContext->getSignedUrls() && false === $this->urlSigner->check($request->getUri())) {
  117.             throw new AccessDeniedHttpException('The signature of the URL is not valid.');
  118.         }
  119.     }
  120.     /**
  121.      * In EasyAdmin all backend requests are served via the same route (that allows to
  122.      * detect under which dashboard you want to process the request). This method handles
  123.      * the requests related to "CRUD controller actions" and "custom Symfony actions".
  124.      * The trick used is to change dynamically the controller executed by Symfony.
  125.      */
  126.     public function onKernelController(ControllerEvent $event): void
  127.     {
  128.         $request $event->getRequest();
  129.         if (null === $request->attributes->get(EA::CONTEXT_REQUEST_ATTRIBUTE)) {
  130.             return;
  131.         }
  132.         // if the request is related to a CRUD controller, change the controller to be executed
  133.         if (null !== $crudControllerInstance $this->getCrudControllerInstance($request)) {
  134.             $symfonyControllerFqcnCallable = [$crudControllerInstance$request->query->get(EA::CRUD_ACTION)];
  135.             $symfonyControllerStringCallable = [\get_class($crudControllerInstance), $request->query->get(EA::CRUD_ACTION)];
  136.             // this makes Symfony believe that another controller is being executed
  137.             // (e.g. this is needed for the autowiring of controller action arguments)
  138.             // VERY IMPORTANT: here the Symfony controller must be passed as a string (['App\Controller\Foo', 'index'])
  139.             // Otherwise, the param converter of the controller method doesn't work
  140.             $event->getRequest()->attributes->set('_controller'$symfonyControllerStringCallable);
  141.             // this actually makes Symfony to execute the other controller
  142.             $event->setController($symfonyControllerFqcnCallable);
  143.         }
  144.         // if the request is related to a custom action, change the controller to be executed
  145.         if (null !== $request->query->get(EA::ROUTE_NAME)) {
  146.             $symfonyControllerAsString $this->getSymfonyControllerFqcn($request);
  147.             $symfonyControllerCallable $this->getSymfonyControllerInstance($symfonyControllerAsString$request->query->all()[EA::ROUTE_PARAMS] ?? []);
  148.             if (false !== $symfonyControllerCallable) {
  149.                 // this makes Symfony believe that another controller is being executed
  150.                 // (e.g. this is needed for the autowiring of controller action arguments)
  151.                 // VERY IMPORTANT: here the Symfony controller must be passed as a string ('App\Controller\Foo::index')
  152.                 // Otherwise, the param converter of the controller method doesn't work
  153.                 $event->getRequest()->attributes->set('_controller'$symfonyControllerAsString);
  154.                 // route params must be added as route attribute; otherwise, param converters don't work
  155.                 $event->getRequest()->attributes->replace(array_merge(
  156.                     $request->query->all()[EA::ROUTE_PARAMS] ?? [],
  157.                     $event->getRequest()->attributes->all()
  158.                 ));
  159.                 // this actually makes Symfony to execute the other controller
  160.                 $event->setController($symfonyControllerCallable);
  161.             }
  162.         }
  163.     }
  164.     /**
  165.      * It returns the FQCN of the EasyAdmin Dashboard controller used to serve this
  166.      * request or null if this is not an EasyAdmin request.
  167.      * Because of how EasyAdmin works, all backend requests are handled via the
  168.      * Dashboard controller, so its enough to check if the request controller implements
  169.      * the DashboardControllerInterface.
  170.      */
  171.     private function getDashboardControllerFqcn(Request $request): ?string
  172.     {
  173.         $controller $request->attributes->get('_controller');
  174.         $controllerFqcn null;
  175.         if (\is_string($controller)) {
  176.             [$controllerFqcn, ] = explode('::'$controller);
  177.         }
  178.         if (\is_array($controller)) {
  179.             $controllerFqcn $controller[0];
  180.         }
  181.         if (\is_object($controller)) {
  182.             $controllerFqcn \get_class($controller);
  183.         }
  184.         return is_subclass_of($controllerFqcnDashboardControllerInterface::class) ? $controllerFqcn null;
  185.     }
  186.     private function getDashboardControllerInstance(string $dashboardControllerFqcnRequest $request): ?DashboardControllerInterface
  187.     {
  188.         return $this->controllerFactory->getDashboardControllerInstance($dashboardControllerFqcn$request);
  189.     }
  190.     private function getCrudControllerInstance(Request $request): ?CrudControllerInterface
  191.     {
  192.         if (null !== $crudId $request->query->get(EA::CRUD_ID)) {
  193.             $crudControllerFqcn $this->crudControllerRegistry->findCrudFqcnByCrudId($crudId);
  194.         } else {
  195.             $crudControllerFqcn $request->query->get(EA::CRUD_CONTROLLER_FQCN);
  196.         }
  197.         $crudAction $request->query->get(EA::CRUD_ACTION);
  198.         return $this->controllerFactory->getCrudControllerInstance($crudControllerFqcn$crudAction$request);
  199.     }
  200.     private function getSymfonyControllerFqcn(Request $request): ?string
  201.     {
  202.         $routeName $request->query->get(EA::ROUTE_NAME);
  203.         $routeParams $request->query->all()[EA::ROUTE_PARAMS] ?? [];
  204.         $url $this->urlGenerator->generate($routeName$routeParams);
  205.         $newRequest $request->duplicate();
  206.         $newRequest->attributes->remove('_controller');
  207.         $newRequest->attributes->set('_route'$routeName);
  208.         $newRequest->attributes->add($routeParams);
  209.         $newRequest->server->set('REQUEST_URI'$url);
  210.         $parameters $this->requestMatcher->matchRequest($newRequest);
  211.         return $parameters['_controller'] ?? null;
  212.     }
  213.     /**
  214.      * @return callable|false
  215.      */
  216.     private function getSymfonyControllerInstance(string $controllerFqcn, array $routeParams)
  217.     {
  218.         $newRequest = new Request([], [], ['_controller' => $controllerFqcn'_route_params' => $routeParams], [], [], []);
  219.         return $this->controllerResolver->getController($newRequest);
  220.     }
  221. }