Skip to content

Commit 71d8ba8

Browse files
committed
bug #6507 Improve the detection of pretty URLs usage (javiereguiluz)
This PR was squashed before being merged into the 4.x branch. Discussion ---------- Improve the detection of pretty URLs usage Fixes #6499. ----- The bug: if you enable pretty URLs but visit a page that doesn't use them (e.g. a Symfony route embedded in a EasyAdmin dashboard), then all URLs of the dashboard (menus, actions, etc.) use ugly URLs. Why: because we detected if pretty URLs should be used based on the current request. This is wrong. How to solve this: 1) We could create a config option `easyadmin.use_pretty_urls: true` ... but that would require creating a config file (that soon will be useless because EasyAdmin 5 will only use pretty URLs). It goes a bit against DX, so I don't like that solution. 2) A good technical solution would be to detect if our custom route loader is enabled and it generated the admin routes. Ideally in a Compiler Pass. Sadly, this doesn't work. I asked in Symfony Slack and smart folks like `@stof` confirmed that this can't work. 3) So, I ended up with the following solution: 3.1) The custom route loader now dumps all the generated routes using Symfony's cache component 3.2) We detect if pretty URLs are used by checking if that cached item exists and it's not empty 3.3) Indirectly, this improves a lot the performance of finding the route name for a given tuple of Dashboard + CRUD controller + Action. I was going to do this change in the future, but doing it now will help us solve this issue. ----- I tested in my apps and everything worked as expected, so I'll try to merge this very soon. Thanks. Commits ------- 16c0f13 Improve the detection of pretty URLs usage
2 parents 2bc71ea + 16c0f13 commit 71d8ba8

File tree

4 files changed

+75
-30
lines changed

4 files changed

+75
-30
lines changed

src/Context/AdminContext.php

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,9 @@ final class AdminContext
4343
private TemplateRegistry $templateRegistry;
4444
private ?MainMenuDto $mainMenuDto = null;
4545
private ?UserMenuDto $userMenuDto = null;
46+
private bool $usePrettyUrls;
4647

47-
public function __construct(Request $request, ?UserInterface $user, I18nDto $i18nDto, CrudControllerRegistry $crudControllers, DashboardDto $dashboardDto, DashboardControllerInterface $dashboardController, AssetsDto $assetDto, ?CrudDto $crudDto, ?EntityDto $entityDto, ?SearchDto $searchDto, MenuFactoryInterface $menuFactory, TemplateRegistry $templateRegistry)
48+
public function __construct(Request $request, ?UserInterface $user, I18nDto $i18nDto, CrudControllerRegistry $crudControllers, DashboardDto $dashboardDto, DashboardControllerInterface $dashboardController, AssetsDto $assetDto, ?CrudDto $crudDto, ?EntityDto $entityDto, ?SearchDto $searchDto, MenuFactoryInterface $menuFactory, TemplateRegistry $templateRegistry, bool $usePrettyUrls = false)
4849
{
4950
$this->request = $request;
5051
$this->user = $user;
@@ -58,6 +59,7 @@ public function __construct(Request $request, ?UserInterface $user, I18nDto $i18
5859
$this->searchDto = $searchDto;
5960
$this->menuFactory = $menuFactory;
6061
$this->templateRegistry = $templateRegistry;
62+
$this->usePrettyUrls = $usePrettyUrls;
6163
}
6264

6365
public function getRequest(): Request
@@ -109,7 +111,7 @@ public function getAbsoluteUrls(): bool
109111

110112
public function usePrettyUrls(): bool
111113
{
112-
return true === (bool) $this->request->attributes->get(EA::ROUTE_CREATED_BY_EASYADMIN);
114+
return $this->usePrettyUrls;
113115
}
114116

115117
public function getDashboardTitle(): string

src/Factory/AdminContextFactory.php

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
use EasyCorp\Bundle\EasyAdminBundle\Dto\SearchDto;
2121
use EasyCorp\Bundle\EasyAdminBundle\Registry\CrudControllerRegistry;
2222
use EasyCorp\Bundle\EasyAdminBundle\Registry\TemplateRegistry;
23+
use EasyCorp\Bundle\EasyAdminBundle\Router\AdminRouteGenerator;
2324
use Symfony\Component\HttpFoundation\Request;
2425
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
2526
use Symfony\Component\Security\Core\User\UserInterface;
@@ -37,14 +38,16 @@ final class AdminContextFactory
3738
private MenuFactoryInterface $menuFactory;
3839
private CrudControllerRegistry $crudControllers;
3940
private EntityFactory $entityFactory;
41+
private AdminRouteGenerator $adminRouteGenerator;
4042

41-
public function __construct(string $cacheDir, ?TokenStorageInterface $tokenStorage, MenuFactoryInterface $menuFactory, CrudControllerRegistry $crudControllers, EntityFactory $entityFactory)
43+
public function __construct(string $cacheDir, ?TokenStorageInterface $tokenStorage, MenuFactoryInterface $menuFactory, CrudControllerRegistry $crudControllers, EntityFactory $entityFactory, AdminRouteGenerator $adminRouteGenerator)
4244
{
4345
$this->cacheDir = $cacheDir;
4446
$this->tokenStorage = $tokenStorage;
4547
$this->menuFactory = $menuFactory;
4648
$this->crudControllers = $crudControllers;
4749
$this->entityFactory = $entityFactory;
50+
$this->adminRouteGenerator = $adminRouteGenerator;
4851
}
4952

5053
public function create(Request $request, DashboardControllerInterface $dashboardController, ?CrudControllerInterface $crudController, ?string $actionName = null): AdminContext
@@ -65,7 +68,9 @@ public function create(Request $request, DashboardControllerInterface $dashboard
6568
$templateRegistry = $this->getTemplateRegistry($dashboardController, $crudDto);
6669
$user = $this->getUser($this->tokenStorage);
6770

68-
return new AdminContext($request, $user, $i18nDto, $this->crudControllers, $dashboardDto, $dashboardController, $assetDto, $crudDto, $entityDto, $searchDto, $this->menuFactory, $templateRegistry);
71+
$usePrettyUrls = $this->adminRouteGenerator->usesPrettyUrls();
72+
73+
return new AdminContext($request, $user, $i18nDto, $this->crudControllers, $dashboardDto, $dashboardController, $assetDto, $crudDto, $entityDto, $searchDto, $this->menuFactory, $templateRegistry, $usePrettyUrls);
6974
}
7075

7176
private function getDashboardDto(Request $request, DashboardControllerInterface $dashboardControllerInstance): DashboardDto

src/Resources/config/services.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,7 @@
185185
->arg(2, new Reference(MenuFactory::class))
186186
->arg(3, new Reference(CrudControllerRegistry::class))
187187
->arg(4, new Reference(EntityFactory::class))
188+
->arg(5, service(AdminRouteGenerator::class))
188189

189190
->set(AdminUrlGenerator::class)
190191
// I don't know if we truly need the share() method to get a new instance of the
@@ -200,9 +201,14 @@
200201
->args([[AdminUrlGenerator::class => service(AdminUrlGenerator::class)]])
201202
->tag('container.service_locator')
202203

204+
->set('cache.easyadmin')
205+
->parent('cache.system')
206+
->tag('cache.pool')
207+
203208
->set(AdminRouteGenerator::class)
204209
->arg(0, tagged_iterator(EasyAdminExtension::TAG_DASHBOARD_CONTROLLER))
205210
->arg(1, tagged_iterator(EasyAdminExtension::TAG_CRUD_CONTROLLER))
211+
->arg(2, service('cache.easyadmin'))
206212

207213
->set(AdminRouteLoader::class)
208214
->arg(0, service(AdminRouteGenerator::class))

src/Router/AdminRouteGenerator.php

Lines changed: 58 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use EasyCorp\Bundle\EasyAdminBundle\Attribute\AdminDashboard;
88
use EasyCorp\Bundle\EasyAdminBundle\Config\Option\EA;
99
use EasyCorp\Bundle\EasyAdminBundle\Contracts\Router\AdminRouteGeneratorInterface;
10+
use Psr\Cache\CacheItemPoolInterface;
1011
use Symfony\Component\Routing\Route;
1112
use Symfony\Component\Routing\RouteCollection;
1213

@@ -22,6 +23,7 @@
2223
*/
2324
final class AdminRouteGenerator implements AdminRouteGeneratorInterface
2425
{
26+
public const ADMIN_ROUTES_CACHE_KEY = 'easyadmin.generated_routes';
2527
private const DEFAULT_ROUTES_CONFIG = [
2628
'index' => [
2729
'routePath' => '/',
@@ -63,13 +65,59 @@ final class AdminRouteGenerator implements AdminRouteGeneratorInterface
6365
public function __construct(
6466
private iterable $dashboardControllers,
6567
private iterable $crudControllers,
68+
private CacheItemPoolInterface $cache,
6669
) {
6770
}
6871

6972
public function generateAll(): RouteCollection
7073
{
7174
$collection = new RouteCollection();
75+
$adminRoutes = $this->generateAdminRoutes();
76+
77+
foreach ($adminRoutes as $routeName => $route) {
78+
$collection->add($routeName, $route);
79+
}
80+
81+
// save the generated routes in the cache; this will allow to detect
82+
// if pretty URLs are being used in the application and also improves
83+
// performance when finding a route name using the {dashboard, CRUD controller, action} tuple
84+
$adminRoutesCache = [];
85+
foreach ($adminRoutes as $routeName => $route) {
86+
$adminRoutesCache[$route->getOption(EA::DASHBOARD_CONTROLLER_FQCN)][$route->getOption(EA::CRUD_CONTROLLER_FQCN)][$route->getOption(EA::CRUD_ACTION)] = $routeName;
87+
}
88+
$cachedAdminRoutes = $this->cache->getItem(self::ADMIN_ROUTES_CACHE_KEY);
89+
$cachedAdminRoutes->set($adminRoutesCache);
90+
$this->cache->save($cachedAdminRoutes);
91+
92+
return $collection;
93+
}
94+
95+
// Temporary utility method to be removed in EasyAdmin 5, when the pretty URLs will be mandatory
96+
// TODO: remove this method in EasyAdmin 5.x
97+
public function usesPrettyUrls(): bool
98+
{
99+
$cachedAdminRoutes = $this->cache->getItem(self::ADMIN_ROUTES_CACHE_KEY)->get();
100+
101+
return null !== $cachedAdminRoutes && [] !== $cachedAdminRoutes;
102+
}
103+
104+
public function findRouteName(string $dashboardFqcn, string $crudControllerFqcn, string $actionName): ?string
105+
{
106+
$adminRoutes = $this->cache->getItem(self::ADMIN_ROUTES_CACHE_KEY)->get();
107+
108+
return $adminRoutes[$dashboardFqcn][$crudControllerFqcn][$actionName] ?? null;
109+
}
110+
111+
/**
112+
* @return array<string, Route>
113+
*/
114+
private function generateAdminRoutes(): array
115+
{
116+
/** @var array<string, Route> $adminRoutes Stores the collection of admin routes created for the app */
117+
$adminRoutes = [];
118+
/** @var array<string> $addedRouteNames Temporary cache that stores the route names to ensure that we don't add duplicated admin routes */
72119
$addedRouteNames = [];
120+
73121
foreach ($this->dashboardControllers as $dashboardController) {
74122
$dashboardFqcn = $dashboardController::class;
75123
[$allowedCrudControllers, $deniedCrudControllers] = $this->getAllowedAndDeniedControllers($dashboardFqcn);
@@ -104,8 +152,12 @@ public function generateAll(): RouteCollection
104152

105153
foreach (array_keys($actionsRouteConfig) as $actionName) {
106154
$actionRouteConfig = $actionsRouteConfig[$actionName];
107-
$crudActionPath = sprintf('%s/%s/%s', $dashboardRouteConfig['routePath'], $crudControllerRouteConfig['routePath'], ltrim($actionRouteConfig['routePath'], '/'));
108-
$crudActionRouteName = sprintf('%s_%s_%s', $dashboardRouteConfig['routeName'], $crudControllerRouteConfig['routeName'], $actionRouteConfig['routeName']);
155+
$adminRoutePath = sprintf('%s/%s/%s', $dashboardRouteConfig['routePath'], $crudControllerRouteConfig['routePath'], ltrim($actionRouteConfig['routePath'], '/'));
156+
$adminRouteName = sprintf('%s_%s_%s', $dashboardRouteConfig['routeName'], $crudControllerRouteConfig['routeName'], $actionRouteConfig['routeName']);
157+
158+
if (\in_array($adminRouteName, $addedRouteNames, true)) {
159+
throw new \RuntimeException(sprintf('When using pretty URLs, all CRUD controllers must have unique PHP class names to generate unique route names. However, your application has at least two controllers with the FQCN "%s", generating the route "%s". Even if both CRUD controllers are in different namespaces, they cannot have the same class name. Rename one of these controllers to resolve the issue.', $crudControllerFqcn, $adminRouteName));
160+
}
109161

110162
$defaults = [
111163
'_controller' => $crudControllerFqcn.'::'.$actionName,
@@ -117,34 +169,14 @@ public function generateAll(): RouteCollection
117169
EA::CRUD_ACTION => $actionName,
118170
];
119171

120-
$route = new Route($crudActionPath, defaults: $defaults, options: $options, methods: $actionRouteConfig['methods']);
121-
122-
if (\in_array($crudActionRouteName, $addedRouteNames, true)) {
123-
throw new \RuntimeException(sprintf('When using pretty URLs, all CRUD controllers must have unique PHP class names to generate unique route names. However, your application has at least two controllers with the FQCN "%s", generating the route "%s". Even if both CRUD controllers are in different namespaces, they cannot have the same class name. Rename one of these controllers to resolve the issue.', $crudControllerFqcn, $crudActionRouteName));
124-
}
125-
126-
$collection->add($crudActionRouteName, $route);
127-
$addedRouteNames[] = $crudActionRouteName;
172+
$adminRoute = new Route($adminRoutePath, defaults: $defaults, options: $options, methods: $actionRouteConfig['methods']);
173+
$adminRoutes[$adminRouteName] = $adminRoute;
174+
$addedRouteNames[] = $adminRouteName;
128175
}
129176
}
130177
}
131178

132-
return $collection;
133-
}
134-
135-
public function findRouteName(string $dashboardFqcn, string $crudControllerFqcn, string $actionName): ?string
136-
{
137-
$defaultRoutesConfig = $this->getDefaultRoutesConfig($dashboardFqcn);
138-
$actionsRouteConfig = array_replace_recursive($defaultRoutesConfig, $this->getCustomActionsConfig($crudControllerFqcn));
139-
if (!isset($actionsRouteConfig[$actionName])) {
140-
return null;
141-
}
142-
143-
$dashboardRouteConfig = $this->getDashboardsRouteConfig()[$dashboardFqcn];
144-
$crudControllerRouteConfig = $this->getCrudControllerRouteConfig($crudControllerFqcn);
145-
$actionRouteConfig = $actionsRouteConfig[$actionName];
146-
147-
return sprintf('%s_%s_%s', $dashboardRouteConfig['routeName'], $crudControllerRouteConfig['routeName'], $actionRouteConfig['routeName']);
179+
return $adminRoutes;
148180
}
149181

150182
/**

0 commit comments

Comments
 (0)