Skip to content

Commit

Permalink
Fix performance issue when using pretty URLs
Browse files Browse the repository at this point in the history
  • Loading branch information
javiereguiluz committed Nov 7, 2024
1 parent 2c73dde commit 64d82fc
Show file tree
Hide file tree
Showing 3 changed files with 49 additions and 25 deletions.
21 changes: 10 additions & 11 deletions src/EventListener/AdminRouterSubscriber.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,15 @@
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;
use Symfony\Component\HttpKernel\Event\ControllerEvent;
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
Expand All @@ -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
Expand All @@ -68,25 +69,23 @@ 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;
}

// 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);
Expand Down
2 changes: 1 addition & 1 deletion src/Resources/config/services.php
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
51 changes: 38 additions & 13 deletions src/Router/AdminRouteGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -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' => '/',
Expand Down Expand Up @@ -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;
}
Expand All @@ -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;
}
Expand Down Expand Up @@ -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);
}
}

0 comments on commit 64d82fc

Please sign in to comment.