From 64d82fc17bbbc0cb448b5ca7aa03370b31a8730f Mon Sep 17 00:00:00 2001 From: Javier Eguiluz Date: Wed, 6 Nov 2024 20:35:57 +0100 Subject: [PATCH] Fix performance issue when using pretty URLs --- src/EventListener/AdminRouterSubscriber.php | 21 ++++----- src/Resources/config/services.php | 2 +- src/Router/AdminRouteGenerator.php | 51 +++++++++++++++------ 3 files changed, 49 insertions(+), 25 deletions(-) diff --git a/src/EventListener/AdminRouterSubscriber.php b/src/EventListener/AdminRouterSubscriber.php index 8e810e885e..a534f937f9 100644 --- a/src/EventListener/AdminRouterSubscriber.php +++ b/src/EventListener/AdminRouterSubscriber.php @@ -7,6 +7,8 @@ use EasyCorp\Bundle\EasyAdminBundle\Contracts\Controller\DashboardControllerInterface; use EasyCorp\Bundle\EasyAdminBundle\Factory\AdminContextFactory; use EasyCorp\Bundle\EasyAdminBundle\Factory\ControllerFactory; +use EasyCorp\Bundle\EasyAdminBundle\Router\AdminRouteGenerator; +use Psr\Cache\CacheItemPoolInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Controller\ControllerResolverInterface; @@ -14,7 +16,6 @@ use Symfony\Component\HttpKernel\Event\RequestEvent; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; use Symfony\Component\Routing\Matcher\RequestMatcherInterface; -use Symfony\Component\Routing\RouterInterface; /** * This subscriber acts as a "proxy" of all backend requests. First, if the @@ -36,16 +37,16 @@ class AdminRouterSubscriber implements EventSubscriberInterface private ControllerResolverInterface $controllerResolver; private UrlGeneratorInterface $urlGenerator; private RequestMatcherInterface $requestMatcher; - private RouterInterface $router; + private CacheItemPoolInterface $cache; - public function __construct(AdminContextFactory $adminContextFactory, ControllerFactory $controllerFactory, ControllerResolverInterface $controllerResolver, UrlGeneratorInterface $urlGenerator, RequestMatcherInterface $requestMatcher, RouterInterface $router) + public function __construct(AdminContextFactory $adminContextFactory, ControllerFactory $controllerFactory, ControllerResolverInterface $controllerResolver, UrlGeneratorInterface $urlGenerator, RequestMatcherInterface $requestMatcher, CacheItemPoolInterface $cache) { $this->adminContextFactory = $adminContextFactory; $this->controllerFactory = $controllerFactory; $this->controllerResolver = $controllerResolver; $this->urlGenerator = $urlGenerator; $this->requestMatcher = $requestMatcher; - $this->router = $router; + $this->cache = $cache; } public static function getSubscribedEvents(): array @@ -68,16 +69,14 @@ public function onKernelRequestPrettyUrls(RequestEvent $event): void return; } - $routes = $this->router->getRouteCollection(); - $route = $routes->get($routeName); - - if (null === $route || true !== $route->getOption(EA::ROUTE_CREATED_BY_EASYADMIN)) { + $adminRoutes = $this->cache->getItem(AdminRouteGenerator::CACHE_KEY_ROUTE_TO_FQCN)->get(); + if (null === $adminRoutes || !\array_key_exists($routeName, $adminRoutes)) { return; } $request->attributes->set(EA::ROUTE_CREATED_BY_EASYADMIN, true); - $dashboardControllerFqcn = $route->getOption(EA::DASHBOARD_CONTROLLER_FQCN); + $dashboardControllerFqcn = $adminRoutes[$routeName][EA::DASHBOARD_CONTROLLER_FQCN]; if (null === $dashboardControllerInstance = $this->getDashboardControllerInstance($dashboardControllerFqcn, $request)) { return; } @@ -85,8 +84,8 @@ public function onKernelRequestPrettyUrls(RequestEvent $event): void // creating the context is expensive, so it's created once and stored in the request // if the current request already has an AdminContext object, do nothing if (null === $adminContext = $request->attributes->get(EA::CONTEXT_REQUEST_ATTRIBUTE)) { - $crudControllerFqcn = $route->getOption(EA::CRUD_CONTROLLER_FQCN); - $actionName = $route->getOption(EA::CRUD_ACTION); + $crudControllerFqcn = $adminRoutes[$routeName][EA::CRUD_CONTROLLER_FQCN]; + $actionName = $adminRoutes[$routeName][EA::CRUD_ACTION]; $request->attributes->set(EA::DASHBOARD_CONTROLLER_FQCN, $dashboardControllerFqcn); $request->attributes->set(EA::CRUD_CONTROLLER_FQCN, $crudControllerFqcn); diff --git a/src/Resources/config/services.php b/src/Resources/config/services.php index 4a443de5bd..562bddbcef 100644 --- a/src/Resources/config/services.php +++ b/src/Resources/config/services.php @@ -168,7 +168,7 @@ ->arg(2, service('controller_resolver')) ->arg(3, service('router')) ->arg(4, service('router')) - ->arg(5, service('router')) + ->arg(5, service('cache.easyadmin')) ->tag('kernel.event_subscriber') ->set(ControllerFactory::class) diff --git a/src/Router/AdminRouteGenerator.php b/src/Router/AdminRouteGenerator.php index be9cd1227f..49623cbfaa 100644 --- a/src/Router/AdminRouteGenerator.php +++ b/src/Router/AdminRouteGenerator.php @@ -23,7 +23,9 @@ */ final class AdminRouteGenerator implements AdminRouteGeneratorInterface { - public const ADMIN_ROUTES_CACHE_KEY = 'easyadmin.generated_routes'; + public const CACHE_KEY_ROUTE_TO_FQCN = 'easyadmin.routes.route_to_fqcn'; + public const CACHE_KEY_FQCN_TO_ROUTE = 'easyadmin.routes.fqcn_to_route'; + private const DEFAULT_ROUTES_CONFIG = [ 'index' => [ 'routePath' => '/', @@ -78,16 +80,9 @@ public function generateAll(): RouteCollection $collection->add($routeName, $route); } - // save the generated routes in the cache; this will allow to detect - // if pretty URLs are being used in the application and also improves - // performance when finding a route name using the {dashboard, CRUD controller, action} tuple - $adminRoutesCache = []; - foreach ($adminRoutes as $routeName => $route) { - $adminRoutesCache[$route->getOption(EA::DASHBOARD_CONTROLLER_FQCN)][$route->getOption(EA::CRUD_CONTROLLER_FQCN)][$route->getOption(EA::CRUD_ACTION)] = $routeName; - } - $cachedAdminRoutes = $this->cache->getItem(self::ADMIN_ROUTES_CACHE_KEY); - $cachedAdminRoutes->set($adminRoutesCache); - $this->cache->save($cachedAdminRoutes); + // this dumps all admin routes in a performance-optimized format to later + // find them quickly without having to use Symfony's router service + $this->saveAdminRoutesInCache($adminRoutes); return $collection; } @@ -96,14 +91,14 @@ public function generateAll(): RouteCollection // TODO: remove this method in EasyAdmin 5.x public function usesPrettyUrls(): bool { - $cachedAdminRoutes = $this->cache->getItem(self::ADMIN_ROUTES_CACHE_KEY)->get(); + $cachedAdminRoutes = $this->cache->getItem(self::CACHE_KEY_FQCN_TO_ROUTE)->get(); return null !== $cachedAdminRoutes && [] !== $cachedAdminRoutes; } public function findRouteName(string $dashboardFqcn, string $crudControllerFqcn, string $actionName): ?string { - $adminRoutes = $this->cache->getItem(self::ADMIN_ROUTES_CACHE_KEY)->get(); + $adminRoutes = $this->cache->getItem(self::CACHE_KEY_FQCN_TO_ROUTE)->get(); return $adminRoutes[$dashboardFqcn][$crudControllerFqcn][$actionName] ?? null; } @@ -366,4 +361,34 @@ private function transformCrudControllerNameToSnakeCase(string $crudControllerFq return $shortName; } + + /** + * @param Route[] $adminRoutes + */ + private function saveAdminRoutesInCache(array $adminRoutes): void + { + // to speedup the look up of routes in different parts of the bundle, + // we cache the admin routes in two different maps: + // 1) $cache[route_name] => [dashboard, CRUD controller, action] + // 2) $cache[dashboard][CRUD controller][action] => route_name + $routeNameToFqcn = []; + $fqcnToRouteName = []; + foreach ($adminRoutes as $routeName => $route) { + $routeNameToFqcn[$routeName] = [ + EA::DASHBOARD_CONTROLLER_FQCN => $route->getOption(EA::DASHBOARD_CONTROLLER_FQCN), + EA::CRUD_CONTROLLER_FQCN => $route->getOption(EA::CRUD_CONTROLLER_FQCN), + EA::CRUD_ACTION => $route->getOption(EA::CRUD_ACTION), + ]; + + $fqcnToRouteName[$route->getOption(EA::DASHBOARD_CONTROLLER_FQCN)][$route->getOption(EA::CRUD_CONTROLLER_FQCN)][$route->getOption(EA::CRUD_ACTION)] = $routeName; + } + + $routeNameToFqcnItem = $this->cache->getItem(self::CACHE_KEY_ROUTE_TO_FQCN); + $routeNameToFqcnItem->set($routeNameToFqcn); + $this->cache->save($routeNameToFqcnItem); + + $fqcnToRouteNameItem = $this->cache->getItem(self::CACHE_KEY_FQCN_TO_ROUTE); + $fqcnToRouteNameItem->set($fqcnToRouteName); + $this->cache->save($fqcnToRouteNameItem); + } }