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

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 Symfony\Component\EventDispatcher\EventSubscriberInterface;
  10. use Symfony\Component\HttpFoundation\Request;
  11. use Symfony\Component\HttpKernel\Controller\ControllerResolverInterface;
  12. use Symfony\Component\HttpKernel\Event\ControllerEvent;
  13. use Symfony\Component\HttpKernel\Event\RequestEvent;
  14. use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
  15. use Symfony\Component\Routing\Matcher\RequestMatcherInterface;
  16. use Twig\Environment;
  17. /**
  18.  * This subscriber acts as a "proxy" of all backend requests. First, if the
  19.  * request is related to EasyAdmin, it creates the AdminContext variable and
  20.  * stores it in the Request as an attribute.
  21.  *
  22.  * Second, it uses Symfony events to serve all backend requests using a single
  23.  * route. The trick is to change dynamically the controller to execute when
  24.  * the request is related to a CRUD action or a normal Symfony route/action.
  25.  *
  26.  * @author Javier Eguiluz <javier.eguiluz@gmail.com>
  27.  * @author Maxime Steinhausser <maxime.steinhausser@gmail.com>
  28.  * @author Yonel Ceruto <yonelceruto@gmail.com>
  29.  */
  30. class AdminRouterSubscriber implements EventSubscriberInterface
  31. {
  32.     private AdminContextFactory $adminContextFactory;
  33.     private CrudControllerRegistry $crudControllerRegistry;
  34.     private ControllerFactory $controllerFactory;
  35.     private ControllerResolverInterface $controllerResolver;
  36.     private UrlGeneratorInterface $urlGenerator;
  37.     private RequestMatcherInterface $requestMatcher;
  38.     private Environment $twig;
  39.     public function __construct(AdminContextFactory $adminContextFactoryCrudControllerRegistry $crudControllerRegistryControllerFactory $controllerFactoryControllerResolverInterface $controllerResolverUrlGeneratorInterface $urlGeneratorRequestMatcherInterface $requestMatcherEnvironment $twig)
  40.     {
  41.         $this->adminContextFactory $adminContextFactory;
  42.         $this->crudControllerRegistry $crudControllerRegistry;
  43.         $this->controllerFactory $controllerFactory;
  44.         $this->controllerResolver $controllerResolver;
  45.         $this->urlGenerator $urlGenerator;
  46.         $this->requestMatcher $requestMatcher;
  47.         $this->twig $twig;
  48.     }
  49.     public static function getSubscribedEvents(): array
  50.     {
  51.         return [
  52.             RequestEvent::class => [
  53.                 ['onKernelRequest'0],
  54.             ],
  55.             // the priority must be higher than 0 to run it before ParamConverterListener
  56.             ControllerEvent::class => ['onKernelController'128],
  57.         ];
  58.     }
  59.     /**
  60.      * If this is an EasyAdmin request, it creates the AdminContext variable, stores it
  61.      * in the Request as an attribute and injects it as a global Twig variable.
  62.      */
  63.     public function onKernelRequest(RequestEvent $event): void
  64.     {
  65.         $request $event->getRequest();
  66.         if (null === $dashboardControllerFqcn $this->getDashboardControllerFqcn($request)) {
  67.             return;
  68.         }
  69.         if (null === $dashboardControllerInstance $this->getDashboardControllerInstance($dashboardControllerFqcn$request)) {
  70.             return;
  71.         }
  72.         // creating the context is expensive, so it's created once and stored in the request
  73.         // if the current request already has an AdminContext object, do nothing
  74.         if (null === $adminContext $request->attributes->get(EA::CONTEXT_REQUEST_ATTRIBUTE)) {
  75.             $crudControllerInstance $this->getCrudControllerInstance($request);
  76.             $adminContext $this->adminContextFactory->create($request$dashboardControllerInstance$crudControllerInstance);
  77.         }
  78.         $request->attributes->set(EA::CONTEXT_REQUEST_ATTRIBUTE$adminContext);
  79.         // this makes the AdminContext available in all templates as a short named variable
  80.         $this->twig->addGlobal('ea'$adminContext);
  81.     }
  82.     /**
  83.      * In EasyAdmin all backend requests are served via the same route (that allows to
  84.      * detect under which dashboard you want to process the request). This method handles
  85.      * the requests related to "CRUD controller actions" and "custom Symfony actions".
  86.      * The trick used is to change dynamically the controller executed by Symfony.
  87.      */
  88.     public function onKernelController(ControllerEvent $event): void
  89.     {
  90.         $request $event->getRequest();
  91.         if (null === $request->attributes->get(EA::CONTEXT_REQUEST_ATTRIBUTE)) {
  92.             return;
  93.         }
  94.         // if the request is related to a CRUD controller, change the controller to be executed
  95.         if (null !== $crudControllerInstance $this->getCrudControllerInstance($request)) {
  96.             $symfonyControllerFqcnCallable = [$crudControllerInstance$request->query->get(EA::CRUD_ACTION)];
  97.             $symfonyControllerStringCallable = [\get_class($crudControllerInstance), $request->query->get(EA::CRUD_ACTION)];
  98.             // this makes Symfony believe that another controller is being executed
  99.             // (e.g. this is needed for the autowiring of controller action arguments)
  100.             // VERY IMPORTANT: here the Symfony controller must be passed as a string (['App\Controller\Foo', 'index'])
  101.             // Otherwise, the param converter of the controller method doesn't work
  102.             $event->getRequest()->attributes->set('_controller'$symfonyControllerStringCallable);
  103.             // this actually makes Symfony to execute the other controller
  104.             $event->setController($symfonyControllerFqcnCallable);
  105.         }
  106.         // if the request is related to a custom action, change the controller to be executed
  107.         if (null !== $request->query->get(EA::ROUTE_NAME)) {
  108.             $symfonyControllerAsString $this->getSymfonyControllerFqcn($request);
  109.             $symfonyControllerCallable $this->getSymfonyControllerInstance($symfonyControllerAsString$request->query->all()[EA::ROUTE_PARAMS] ?? []);
  110.             if (false !== $symfonyControllerCallable) {
  111.                 // this makes Symfony believe that another controller is being executed
  112.                 // (e.g. this is needed for the autowiring of controller action arguments)
  113.                 // VERY IMPORTANT: here the Symfony controller must be passed as a string ('App\Controller\Foo::index')
  114.                 // Otherwise, the param converter of the controller method doesn't work
  115.                 $event->getRequest()->attributes->set('_controller'$symfonyControllerAsString);
  116.                 // route params must be added as route attribute; otherwise, param converters don't work
  117.                 $event->getRequest()->attributes->replace(array_merge(
  118.                     $request->query->all()[EA::ROUTE_PARAMS] ?? [],
  119.                     $event->getRequest()->attributes->all()
  120.                 ));
  121.                 // this actually makes Symfony to execute the other controller
  122.                 $event->setController($symfonyControllerCallable);
  123.             }
  124.         }
  125.     }
  126.     /**
  127.      * It returns the FQCN of the EasyAdmin Dashboard controller used to serve this
  128.      * request or null if this is not an EasyAdmin request.
  129.      * Because of how EasyAdmin works, all backend requests are handled via the
  130.      * Dashboard controller, so its enough to check if the request controller implements
  131.      * the DashboardControllerInterface.
  132.      */
  133.     private function getDashboardControllerFqcn(Request $request): ?string
  134.     {
  135.         $controller $request->attributes->get('_controller');
  136.         $controllerFqcn null;
  137.         if (\is_string($controller)) {
  138.             [$controllerFqcn, ] = explode('::'$controller);
  139.         }
  140.         if (\is_array($controller)) {
  141.             $controllerFqcn $controller[0];
  142.         }
  143.         if (\is_object($controller)) {
  144.             $controllerFqcn \get_class($controller);
  145.         }
  146.         return is_subclass_of($controllerFqcnDashboardControllerInterface::class) ? $controllerFqcn null;
  147.     }
  148.     private function getDashboardControllerInstance(string $dashboardControllerFqcnRequest $request): ?DashboardControllerInterface
  149.     {
  150.         return $this->controllerFactory->getDashboardControllerInstance($dashboardControllerFqcn$request);
  151.     }
  152.     private function getCrudControllerInstance(Request $request): ?CrudControllerInterface
  153.     {
  154.         $crudControllerFqcn $request->query->get(EA::CRUD_CONTROLLER_FQCN);
  155.         $crudAction $request->query->get(EA::CRUD_ACTION);
  156.         return $this->controllerFactory->getCrudControllerInstance($crudControllerFqcn$crudAction$request);
  157.     }
  158.     private function getSymfonyControllerFqcn(Request $request): ?string
  159.     {
  160.         $routeName $request->query->get(EA::ROUTE_NAME);
  161.         $routeParams $request->query->all()[EA::ROUTE_PARAMS] ?? [];
  162.         $url $this->urlGenerator->generate($routeName$routeParams);
  163.         $newRequest $request->duplicate();
  164.         $newRequest->attributes->remove('_controller');
  165.         $newRequest->attributes->set('_route'$routeName);
  166.         $newRequest->attributes->add($routeParams);
  167.         $newRequest->server->set('REQUEST_URI'$url);
  168.         $parameters $this->requestMatcher->matchRequest($newRequest);
  169.         return $parameters['_controller'] ?? null;
  170.     }
  171.     private function getSymfonyControllerInstance(string $controllerFqcn, array $routeParams): callable|false
  172.     {
  173.         $newRequest = new Request([], [], ['_controller' => $controllerFqcn'_route_params' => $routeParams], [], [], []);
  174.         return $this->controllerResolver->getController($newRequest);
  175.     }
  176. }