Skip to content

Commit 287f467

Browse files
committedJan 31, 2025·
feature #6769 Update admin dashboard (javiereguiluz)
This PR was squashed before being merged into the 4.x branch. Discussion ---------- Update admin dashboard When we moved to "pretty URLs", the recommended solution to define the dashboard route was this: ```php // ... use Symfony\Component\Routing\Attribute\Route; class DashboardController extends AbstractDashboardController { #[Route('/admin', name: 'admin')] public function index(): Response { return parent::index(); } // ... } ``` In the past weeks we've faced many issues related to the cache and the event listener related to admin URLs. The problem to solve is this: we need to find out very quickly (i.e. performant) and unequivocally if a given URLs belongs to an EasyAdmin backend or not. When using pretty URLs, this is trivial because we generate those routes and apply some special route attributes to them. But, there's a missing route: the main dashboard route. That one is defined by the user and we cannot add those special attributes to it. Trust me, I tried this very hard (even asking internally to the Symfony Core Team). This is not possible technically speaking. So, after thinking a lot about this, I propose to use the recently introduced `#[AdminDashboard]` attribute to define the admin route: ```php use EasyCorp\Bundle\EasyAdminBundle\Attribute\AdminDashboard; use EasyCorp\Bundle\EasyAdminBundle\Config\Dashboard; use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractDashboardController; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Attribute\Route; #[AdminDashboard(routePath: '/admin', routeName: 'admin')] class DashboardController extends AbstractDashboardController { public function index(): Response { return parent::index(); } // ... } ``` This will solve all our problems, because this attribute will be used to generate the associated Symfony route. Since we create that route, we can apply the custom route attributes. This makes the caching hacks unnecessary and improves the performance of the application. So, I propose to add this now and make it mandatory in EasyAdmin 5.x as the only solution that works to define admin routes. I know all these changes are tiring 😫 but this is the last time we'll change this and I think the change is worth it because it will solve all our internal issues related to admin routes 🙏 What do you think? Commits ------- 901a892 Update admin dashboard
2 parents 576f168 + 901a892 commit 287f467

File tree

10 files changed

+358
-28
lines changed

10 files changed

+358
-28
lines changed
 

‎doc/actions.rst

+2
Original file line numberDiff line numberDiff line change
@@ -606,9 +606,11 @@ main menu using the ``configureMenuItems()`` method::
606606
// src/Controller/Admin/DashboardController.php
607607
namespace App\Controller\Admin;
608608

609+
use EasyCorp\Bundle\EasyAdminBundle\Attribute\AdminDashboard;
609610
use EasyCorp\Bundle\EasyAdminBundle\Config\Dashboard;
610611
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractDashboardController;
611612

613+
#[AdminDashboard(routePath: '/admin', routeName: 'admin')]
612614
class DashboardController extends AbstractDashboardController
613615
{
614616
// ...

‎doc/crud.rst

+4-2
Original file line numberDiff line numberDiff line change
@@ -79,12 +79,12 @@ Admin route name Admin route path
7979
on each dahboard to not generate all these routes.
8080

8181
You can customize the route names and/or paths of the actions of all the CRUD controllers
82-
served by some dashboard using the ``#[AdminDashboard]`` attribute::
82+
served by some dashboard using the ``routes`` option of the ``#[AdminDashboard]`` attribute::
8383

8484
use EasyCorp\Bundle\EasyAdminBundle\Attribute\AdminDashboard;
8585
// ...
8686

87-
#[AdminDashboard(routes: [
87+
#[AdminDashboard(routePath: '/admin', routeName: 'admin', routes: [
8888
'index' => ['routePath' => '/all'],
8989
'new' => ['routePath' => '/create', 'routeName' => 'create'],
9090
'edit' => ['routePath' => '/editing-{entityId}', 'routeName' => 'editing'],
@@ -554,10 +554,12 @@ If you want to do the same config in all CRUD controllers, there's no need to
554554
repeat the config in each controller. Instead, add the ``configureCrud()`` method
555555
in your dashboard and all controllers will inherit that configuration::
556556

557+
use EasyCorp\Bundle\EasyAdminBundle\Attribute\AdminDashboard;
557558
use EasyCorp\Bundle\EasyAdminBundle\Config\Crud;
558559
use EasyCorp\Bundle\EasyAdminBundle\Config\Dashboard;
559560
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractDashboardController;
560561

562+
#[AdminDashboard(routePath: '/admin', routeName: 'admin')]
561563
class DashboardController extends AbstractDashboardController
562564
{
563565
// ...

‎doc/dashboards.rst

+67-13
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,8 @@ shortcuts like ``$this->render()`` or ``$this->isGranted()``.
2525
Dashboard controller classes must implement the
2626
``EasyCorp\Bundle\EasyAdminBundle\Contracts\Controller\DashboardControllerInterface``,
2727
which ensures that certain methods are defined in the dashboard. Instead of
28-
implementing the interface, you can also extend from the
29-
``AbstractDashboardController`` class. Run the following command to quickly
30-
generate a dashboard controller:
28+
implementing the interface, you can also extend the ``AbstractDashboardController``
29+
class. Run the following command to quickly generate a dashboard controller:
3130

3231
.. code-block:: terminal
3332
@@ -76,21 +75,22 @@ first in your application. To do so, create this file:
7675
The ``easyadmin.routes`` string is also available as the PHP constant
7776
``\EasyCorp\Bundle\EasyAdminBundle\Router\AdminRouteLoader::ROUTE_LOADER_TYPE``.
7877

79-
Now, define the main route of your dashboard class using a PHP attribute in the
80-
``index()`` method of that controller (if you don't have a Dashboard yet, you can
81-
quickly generate one running the command ``make:admin:dashboard``)::
78+
Now, define the main route of your dashboard class using the following PHP attribute
79+
(if you don't have a Dashboard yet, you can quickly generate one running the command
80+
``make:admin:dashboard``)::
8281

8382
// src/Controller/Admin/DashboardController.php
8483
namespace App\Controller\Admin;
8584

85+
use EasyCorp\Bundle\EasyAdminBundle\Attribute\AdminDashboard;
8686
use EasyCorp\Bundle\EasyAdminBundle\Config\Dashboard;
8787
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractDashboardController;
8888
use Symfony\Component\HttpFoundation\Response;
8989
use Symfony\Component\Routing\Attribute\Route;
9090

91+
#[AdminDashboard(routePath: '/admin', routeName: 'admin')]
9192
class DashboardController extends AbstractDashboardController
9293
{
93-
#[Route('/admin', name: 'admin')]
9494
public function index(): Response
9595
{
9696
return parent::index();
@@ -101,8 +101,25 @@ quickly generate one running the command ``make:admin:dashboard``)::
101101

102102
.. caution::
103103

104-
The dashboard route must be defined using the ``#[Route]`` attribute. None
105-
of the other ways supported by Symfony to configure a route will work.
104+
The dashboard route must be defined using the ``#[AdminDashboard]`` attribute.
105+
None of the other ways supported by Symfony to configure a route will work.
106+
107+
.. versionadded:: 4.24.0
108+
109+
The feature to define the dashboard route using the ``#[AdminDashboard]``
110+
attribute was introduced in EasyAdmin 4.24.0.
111+
112+
EasyAdmin uses the configuration of the ``#[AdminDashboard]`` attribute to create
113+
the main route of your dashboard. You can verify this by running the following command:
114+
115+
.. code-block:: terminal
116+
117+
$ php bin/console debug:router
118+
119+
.. tip::
120+
121+
If you don't see any of the routes that must be generated by EasyAdmin, delete
122+
the cache of your application to force the regeneration of the routes.
106123

107124
.. tip::
108125

@@ -114,15 +131,42 @@ The ``index()`` method is called by EasyAdmin to render your dashboard. Since
114131
to inject dependencies. Instead, inject those dependencies in the constructor
115132
method of the controller.
116133

117-
The name of the ``index()`` route will be used as the prefix of all the routes
118-
associated to this dashboard (e.g. if this route name is ``my_private_backend``,
134+
The name of the dashboard route should be concise because it's used as the prefix
135+
of all the routes associated to this dashboard (e.g. if this route name is ``my_private_backend``,
119136
the generated routes will be like ``my_private_backend_product_index``). The path
120137
of this route will also be used by all the dasboard routes (e.g. if the path is
121138
``/_secret/backend``, the generated routes paths will be like ``/_secret/backend/category/324``).
122139

123140
That's it. Later, when you start adding :doc:`CRUD controllers </crud>`, the route
124141
loader will create all the needed routes for each of them.
125142

143+
Defining the Route in the ``index()`` Method
144+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
145+
146+
Using the ``#[AdminDashboard]`` attribute is the recommended way to define the
147+
dashboard route. However, you can also define the dashboard route aplying the
148+
``#[Route]`` attribute on the ``index()`` method::
149+
150+
// ...
151+
use Symfony\Component\Routing\Attribute\Route;
152+
153+
class DashboardController extends AbstractDashboardController
154+
{
155+
#[Route('/admin', name: 'admin')]
156+
public function index(): Response
157+
{
158+
return parent::index();
159+
}
160+
161+
// ...
162+
}
163+
164+
.. caution::
165+
166+
This alternative still works in EasyAdmin 4.x versions, but **it's deprecated
167+
and it won't work in EasyAdmin 5.x**. It's recommended to update the dashboard
168+
classes as soon as possible to use the ``#[AdminDashboard]`` attribute.
169+
126170
Legacy Admin URLs
127171
-----------------
128172

@@ -315,10 +359,12 @@ explained later)::
315359

316360
namespace App\Controller\Admin;
317361

362+
use EasyCorp\Bundle\EasyAdminBundle\Attribute\AdminDashboard;
318363
use EasyCorp\Bundle\EasyAdminBundle\Config\Dashboard;
319364
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractDashboardController;
320365
use EasyCorp\Bundle\EasyAdminBundle\Dto\LocaleDto;
321366

367+
#[AdminDashboard(routePath: '/admin', routeName: 'admin')]
322368
class DashboardController extends AbstractDashboardController
323369
{
324370
// ...
@@ -409,11 +455,13 @@ provide yet any way of creating those widgets. It's in our list of future featur
409455
but meanwhile you can use `Symfony UX Chart.js`_ bundle to create those charts
410456
and render them in your own Twig template::
411457

458+
use EasyCorp\Bundle\EasyAdminBundle\Attribute\AdminDashboard;
412459
use EasyCorp\Bundle\EasyAdminBundle\Config\Dashboard;
413460
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractDashboardController;
414461
use Symfony\UX\Chartjs\Builder\ChartBuilderInterface;
415462
use Symfony\UX\Chartjs\Model\Chart;
416463

464+
#[AdminDashboard(routePath: '/admin', routeName: 'admin')]
417465
class DashboardController extends AbstractDashboardController
418466
{
419467
public function __construct(
@@ -424,7 +472,6 @@ and render them in your own Twig template::
424472
// ... you'll also need to load some CSS/JavaScript assets to render
425473
// the charts; this is explained later in the chapter about Design
426474

427-
#[Route('/admin')]
428475
public function index(): Response
429476
{
430477
$chart = $this->chartBuilder->createChart(Chart::TYPE_LINE);
@@ -459,15 +506,16 @@ Another popular option is to avoid a dashboard at all and instead redirect to th
459506
for people working on the backend. This requires :ref:`generating admin URLs <generate-admin-urls>`,
460507
and :doc:`CRUD controllers </crud>`, which is explained in detail later::
461508

509+
use EasyCorp\Bundle\EasyAdminBundle\Attribute\AdminDashboard;
462510
use EasyCorp\Bundle\EasyAdminBundle\Config\Dashboard;
463511
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractDashboardController;
464512
use EasyCorp\Bundle\EasyAdminBundle\Router\AdminUrlGenerator;
465513

514+
#[AdminDashboard(routePath: '/admin', routeName: 'admin')]
466515
class DashboardController extends AbstractDashboardController
467516
{
468517
// ...
469518

470-
#[Route('/admin', name: 'admin')]
471519
public function index(): Response
472520
{
473521
// when using pretty admin URLs, you can redirect directly to some route
@@ -504,9 +552,11 @@ the look and behavior of each menu item::
504552
use App\Entity\Category;
505553
use App\Entity\Comment;
506554
use App\Entity\User;
555+
use EasyCorp\Bundle\EasyAdminBundle\Attribute\AdminDashboard;
507556
use EasyCorp\Bundle\EasyAdminBundle\Config\Dashboard;
508557
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractDashboardController;
509558

559+
#[AdminDashboard(routePath: '/admin', routeName: 'admin')]
510560
class DashboardController extends AbstractDashboardController
511561
{
512562
// ...
@@ -789,11 +839,13 @@ The user name is the result of calling to the ``__toString()`` method on the
789839
current user object. The user avatar is a generic avatar icon. Use the
790840
``configureUserMenu()`` method to configure the features and items of this menu::
791841

842+
use EasyCorp\Bundle\EasyAdminBundle\Attribute\AdminDashboard;
792843
use EasyCorp\Bundle\EasyAdminBundle\Config\MenuItem;
793844
use EasyCorp\Bundle\EasyAdminBundle\Config\UserMenu;
794845
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractDashboardController;
795846
use Symfony\Component\Security\Core\User\UserInterface;
796847

848+
#[AdminDashboard(routePath: '/admin', routeName: 'admin')]
797849
class DashboardController extends AbstractDashboardController
798850
{
799851
// ...
@@ -899,6 +951,7 @@ The rest of the contents (e.g. the label of the menu items, entity and field
899951
names, etc.) use the ``messages`` translation domain by default. You can change
900952
this value with the ``translationDomain()`` method::
901953

954+
#[AdminDashboard(routePath: '/admin', routeName: 'admin')]
902955
class DashboardController extends AbstractDashboardController
903956
{
904957
// ...
@@ -941,6 +994,7 @@ HTML text direction is set to ``rtl`` (right-to-left) automatically. Otherwise,
941994
the text is displayed as ``ltr`` (left-to-right), but you can configure this
942995
value explicitly::
943996

997+
#[AdminDashboard(routePath: '/admin', routeName: 'admin')]
944998
class DashboardController extends AbstractDashboardController
945999
{
9461000
// ...

‎doc/design.rst

+6
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,12 @@ If you prefer to use other icons, call the ``useCustomIconSet()`` in your dashbo
3838

3939
namespace App\Controller\Admin;
4040

41+
use EasyCorp\Bundle\EasyAdminBundle\Attribute\AdminDashboard;
4142
use EasyCorp\Bundle\EasyAdminBundle\Config\Assets;
4243
use EasyCorp\Bundle\EasyAdminBundle\Config\Option\IconSet;
4344
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractDashboardController;
4445

46+
#[AdminDashboard(routePath: '/admin', routeName: 'admin')]
4547
class DashboardController extends AbstractDashboardController
4648
{
4749
public function configureAssets(): Assets
@@ -139,9 +141,11 @@ This option allows you to render certain parts of the backend with your own Twig
139141
templates. First, you can replace some templates globally in the
140142
:doc:`dashboard </dashboards>`::
141143

144+
use EasyCorp\Bundle\EasyAdminBundle\Attribute\AdminDashboard;
142145
use EasyCorp\Bundle\EasyAdminBundle\Config\Crud;
143146
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractDashboardController;
144147

148+
#[AdminDashboard(routePath: '/admin', routeName: 'admin')]
145149
class DashboardController extends AbstractDashboardController
146150
{
147151
// ...
@@ -390,9 +394,11 @@ To override any of them, create a CSS file and redefine the variable values:
390394
391395
Then, load this CSS file in your dashboard and/or resource admin::
392396

397+
use EasyCorp\Bundle\EasyAdminBundle\Attribute\AdminDashboard;
393398
use EasyCorp\Bundle\EasyAdminBundle\Config\Assets;
394399
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractDashboardController;
395400

401+
#[AdminDashboard(routePath: '/admin', routeName: 'admin')]
396402
class DashboardController extends AbstractDashboardController
397403
{
398404
// ...

‎doc/security.rst

+6-1
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,12 @@ cumbersome because you must apply it to all dashboard controllers and to all the
4141
:doc:`CRUD controllers </crud>`::
4242

4343
// app/Controller/Admin/DashboardController.php
44+
use EasyCorp\Bundle\EasyAdminBundle\Attribute\AdminDashboard;
4445
use EasyCorp\Bundle\EasyAdminBundle\Config\Dashboard;
4546
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractDashboardController;
4647
use Symfony\Component\Security\Http\Attribute\IsGranted;
4748

49+
#[AdminDashboard(routePath: '/admin', routeName: 'admin')]
4850
#[IsGranted('ROLE_ADMIN')]
4951
class DashboardController extends AbstractDashboardController
5052
{
@@ -75,7 +77,10 @@ is to use the ``#[AdminDashboard]`` attribute::
7577
use EasyCorp\Bundle\EasyAdminBundle\Attribute\AdminDashboard;
7678
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractDashboardController;
7779

78-
#[AdminDashboard(allowedControllers: [BlogPostCrudController::class, BlogCategoryCrudController::class])]
80+
#[AdminDashboard(routePath: '/admin', routeName: 'admin', allowedControllers: [
81+
BlogPostCrudController::class,
82+
BlogCategoryCrudController::class,
83+
])]
7984
class DashboardController extends AbstractDashboardController
8085
{
8186
// ...

‎src/Attribute/AdminDashboard.php

+55-1
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,66 @@
99
class AdminDashboard
1010
{
1111
public function __construct(
12-
/** @var array<string, array{routeName?: string, routePath?: string}>|null */
12+
/**
13+
* @param string|null $routePath The path of the Symfony route that will be created for the dashboard (e.g. '/admin)
14+
*/
15+
public /* ?string */ $routePath = null,
16+
/**
17+
* @param string|null $routeName The name of the Symfony route that will be created for the dashboard (e.g. 'admin')
18+
*/
19+
public /* ?string */ $routeName = null,
20+
/**
21+
* @param array{
22+
* requirements?: array,
23+
* options?: array,
24+
* defaults?: array,
25+
* host?: string,
26+
* methods?: array|string,
27+
* schemes?: array|string,
28+
* condition?: string,
29+
* locale?: string,
30+
* format?: string,
31+
* utf8?: bool,
32+
* stateless?: bool,
33+
* } $routeOptions The configuration used when creating the Symfony route for the dashboard (these values are passed "as is" without any additional validation)
34+
*/
35+
public array $routeOptions = [],
36+
/** @var array<string, array{routeName?: string, routePath?: string}>|null Allows to change the default route name and/or path of the CRUD actions for this dashboard */
1337
public ?array $routes = null,
1438
/** @var class-string[]|null $allowedControllers If defined, only these CRUD controllers will have a route defined for them */
1539
public ?array $allowedControllers = null,
1640
/** @var class-string[]|null $deniedControllers If defined, all CRUD controllers will have a route defined for them except these ones */
1741
public ?array $deniedControllers = null,
1842
) {
43+
// when this attribute was first created, the $routes, $allowedControllers, and $deniedControllers
44+
// were the first and only arguments of the class; now we added $routePath and $routeName as the
45+
// first arguments, so we need to move the values of the old arguments to the new ones
46+
if (\func_num_args() > 0 && \is_array(func_get_arg(0))) {
47+
$this->routes = func_get_arg(0);
48+
trigger_deprecation(
49+
'easycorp/easyadmin-bundle',
50+
'4.24.0',
51+
'Passing $routes as the first argument of the "%s" attribute is deprecated and will no longer work in EasyAdmin 5.0.0. Pass the routes as the fourth argument or, better, use the \'routes:\' named argument.',
52+
__CLASS__,
53+
);
54+
}
55+
if (\func_num_args() > 1 && \is_array(func_get_arg(1))) {
56+
$this->allowedControllers = func_get_arg(1);
57+
trigger_deprecation(
58+
'easycorp/easyadmin-bundle',
59+
'4.24.0',
60+
'Passing $allowedControllers as the second argument of the "%s" attribute is deprecated and will no longer work in EasyAdmin 5.0.0. Pass the allowed controllers as the fifth argument or, better, use the \'allowedControllers:\' named argument.',
61+
__CLASS__,
62+
);
63+
}
64+
if (\func_num_args() > 2 && \is_array(func_get_arg(0)) && \is_array(func_get_arg(1)) && \is_array(func_get_arg(2))) {
65+
$this->deniedControllers = func_get_arg(2);
66+
trigger_deprecation(
67+
'easycorp/easyadmin-bundle',
68+
'4.24.0',
69+
'Passing $deniedControllers as the third argument of the "%s" attribute is deprecated and will no longer work in EasyAdmin 5.0.0. Pass the denied controllers as the sixth argument or, better, use the \'deniedControllers:\' named argument.',
70+
__CLASS__,
71+
);
72+
}
1973
}
2074
}

‎src/Resources/skeleton/dashboard.tpl

+7-8
Original file line numberDiff line numberDiff line change
@@ -2,33 +2,32 @@
22

33
namespace <?= $namespace; ?>;
44

5+
use EasyCorp\Bundle\EasyAdminBundle\Attribute\AdminDashboard;
56
use EasyCorp\Bundle\EasyAdminBundle\Config\Dashboard;
67
use EasyCorp\Bundle\EasyAdminBundle\Config\MenuItem;
78
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractDashboardController;
89
use Symfony\Component\HttpFoundation\Response;
9-
<?php
10-
$attribute_class_fqcn = class_exists(\Symfony\Component\Routing\Attribute\Route::class)
11-
? \Symfony\Component\Routing\Attribute\Route::class
12-
: \Symfony\Component\Routing\Annotation\Route::class;
13-
?>
14-
use <?= $attribute_class_fqcn; ?>;
1510

11+
#[AdminDashboard(routePath: '/admin', routeName: 'admin')]
1612
class <?= $class_name; ?> extends AbstractDashboardController
1713
{
18-
#[Route('/admin', name: 'admin')]
1914
public function index(): Response
2015
{
2116
return parent::index();
2217
2318
// Option 1. You can make your dashboard redirect to some common page of your backend
2419
//
20+
// 1.1) If you have enabled the "pretty URLs" feature:
21+
// return $this->redirectToRoute('admin_user_index');
22+
//
23+
// 1.2) Same example but using the "ugly URLs" that were used in previous EasyAdmin versions:
2524
// $adminUrlGenerator = $this->container->get(AdminUrlGenerator::class);
2625
// return $this->redirect($adminUrlGenerator->setController(OneOfYourCrudController::class)->generateUrl());
2726
2827
// Option 2. You can make your dashboard redirect to different pages depending on the user
2928
//
3029
// if ('jane' === $this->getUser()->getUsername()) {
31-
// return $this->redirect('...');
30+
// return $this->redirectToRoute('...');
3231
// }
3332

3433
// Option 3. You can render some custom template to display a proper dashboard with widgets, etc.

‎src/Router/AdminRouteGenerator.php

+116-3
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,13 @@ private function generateAdminRoutes(): array
122122
$defaultRoutesConfig = $this->getDefaultRoutesConfig($dashboardFqcn);
123123
$dashboardRouteConfig = $this->getDashboardsRouteConfig()[$dashboardFqcn];
124124

125+
// first, create the routes of the dashboards if they are defined with the #[AdminDashboard] attribute instead of the Symfony #[Route] attribute
126+
if (null !== $adminRouteData = $this->createDashboardRoute($dashboardFqcn)) {
127+
$adminRoutes[$adminRouteData['routeName']] = $adminRouteData['route'];
128+
$addedRouteNames[] = $adminRouteData['routeName'];
129+
}
130+
131+
// then, create the routes of the CRUD controllers associated with the dashboard
125132
foreach ($this->crudControllers as $crudController) {
126133
$crudControllerFqcn = $crudController::class;
127134

@@ -224,21 +231,59 @@ private function getDashboardsRouteConfig(): array
224231

225232
foreach ($this->dashboardControllers as $dashboardController) {
226233
$reflectionClass = new \ReflectionClass($dashboardController);
227-
$indexMethod = $reflectionClass->getMethod('index');
228234

235+
// first, check if the dashboard uses the #[AdminDashboard] attribute to define its route configuration;
236+
// this is the recommended way for modern EasyAdmin applications
237+
$attributes = $reflectionClass->getAttributes(AdminDashboard::class);
238+
$usesAdminDashboardAttribute = [] !== $attributes;
239+
if ($usesAdminDashboardAttribute) {
240+
$adminDashboardAttribute = $attributes[0]->newInstance();
241+
$routeName = $adminDashboardAttribute->routeName;
242+
$routePath = $adminDashboardAttribute->routePath;
243+
if (null !== $routePath) {
244+
$routePath = rtrim($adminDashboardAttribute->routePath, '/');
245+
}
246+
247+
if (null !== $routeName && null !== $routePath) {
248+
$config[$reflectionClass->getName()] = [
249+
'routeName' => $routeName,
250+
'routePath' => $routePath,
251+
];
252+
253+
continue;
254+
} else {
255+
@trigger_deprecation(
256+
'easycorp/easyadmin-bundle',
257+
'4.24.0',
258+
'The "%s" dashboard controller applies the #[AdminDashboard] attribute, but it doesn\'t use it to define the route path and route name of the dashboard. Using the default #[Route] attribute from Symfony on the "index()" method of the dashboard instead of the #[AdminDashboard] attribute (e.g. #[AdminDashboard(routePath: \'/admin\', routeName: \'admin\')]) is deprecated and it will no longer work in EasyAdmin 5.0.0.',
259+
$reflectionClass->getName()
260+
);
261+
}
262+
} else {
263+
@trigger_deprecation(
264+
'easycorp/easyadmin-bundle',
265+
'4.24.0',
266+
'The "%s" dashboard controller does not apply the #[AdminDashboard] attribute. Applying this attribute is the recommended way to define the route path and route name of the dashboard, instead of using the default #[Route] attribute from Symfony (e.g. #[AdminDashboard(routePath: \'/admin\', routeName: \'admin\')]). Not applying the #[AdminDashboard] attribute is deprecated because it will be mandatory in EasyAdmin 5.0.0.',
267+
$reflectionClass->getName()
268+
);
269+
}
270+
271+
// this is the legacy way to define the route configuration of the dashboard: using the Symfony #[Route]
272+
// attribute on the "index()" method of the dashboard controller;
229273
// for BC reasons, the Symfony Route attribute is available under two different namespaces;
230274
// true first the recommended namespace and then fall back to the legacy namespace
275+
$indexMethod = $reflectionClass->getMethod('index');
231276
$attributes = $indexMethod->getAttributes('Symfony\Component\Routing\Attribute\Route');
232277
if ([] === $attributes) {
233278
$attributes = $indexMethod->getAttributes('Symfony\Component\Routing\Annotation\Route');
234279
}
235280

236281
if ([] === $attributes) {
237-
throw new \RuntimeException(sprintf('When using pretty URLs, the "%s" EasyAdmin dashboard controller must define its route configuration (route name and path) using Symfony\'s #[Route] attribute applied to its "index()" method.', $reflectionClass->getName()));
282+
throw new \RuntimeException(sprintf('When using pretty URLs, it\'s recommended to define the dashboard route name and path using the #[AdminDashboard] attribute on the dashboard class. Alternatively, you can apply Symfony\'s #[Route] attribute to the "index()" method of the "%s" controller. However, this alternative will no longer work in EasyAdmin 5.0.', $reflectionClass->getName()));
238283
}
239284

240285
if (\count($attributes) > 1) {
241-
throw new \RuntimeException(sprintf('When using pretty URLs, the "%s" EasyAdmin dashboard controller must define only one #[Route] attribute applied on its "index()" method.', $reflectionClass->getName()));
286+
throw new \RuntimeException(sprintf('When using pretty URLs, it\'s recommended to define the dashboard route name and path using the #[AdminDashboard] attribute on the dashboard class. Alternatively, you can apply Symfony\'s #[Route] attribute to the "index()" method of the "%s" controller. In that case, you cannot apply more than one #[Route] attribute to the "index()" method. Also, this alternative will no longer work in EasyAdmin 5.0.', $reflectionClass->getName()));
242287
}
243288

244289
$routeAttribute = $attributes[0]->newInstance();
@@ -349,6 +394,71 @@ private function getCustomActionsConfig(string $crudControllerFqcn): array
349394
return $customActionsConfig;
350395
}
351396

397+
/**
398+
* @return array{routeName: string, route: Route}|null
399+
*/
400+
private function createDashboardRoute(string $dashboardFqcn): ?array
401+
{
402+
/** @var AdminDashboard|null $attribute */
403+
$attribute = $this->getPhpAttributeInstance($dashboardFqcn, AdminDashboard::class);
404+
if (null === $attribute) {
405+
return null;
406+
}
407+
408+
if (null === $attribute->routeName || null === $attribute->routePath) {
409+
// TODO: in EasyAdmin 5.0, this should throw an exception instead of returning an empty array
410+
return null;
411+
}
412+
413+
$routeName = $attribute->routeName;
414+
$routePath = $attribute->routePath;
415+
$routeOptions = $attribute->routeOptions;
416+
417+
$route = new Route($routePath);
418+
419+
if (isset($routeOptions['requirements'])) {
420+
$route->setRequirements($routeOptions['requirements']);
421+
}
422+
if (isset($routeOptions['host'])) {
423+
$route->setHost($routeOptions['host']);
424+
}
425+
if (isset($routeOptions['methods'])) {
426+
$route->setMethods($routeOptions['methods']);
427+
}
428+
if (isset($routeOptions['schemes'])) {
429+
$route->setSchemes($routeOptions['schemes']);
430+
}
431+
if (isset($routeOptions['condition'])) {
432+
$route->setCondition($routeOptions['condition']);
433+
}
434+
435+
$defaults = $routeOptions['defaults'] ?? [];
436+
if (isset($routeOptions['locale'])) {
437+
$defaults['_locale'] = $routeOptions['locale'];
438+
}
439+
if (isset($routeOptions['format'])) {
440+
$defaults['_format'] = $routeOptions['format'];
441+
}
442+
if (isset($routeOptions['stateless'])) {
443+
$defaults['_stateless'] = $routeOptions['stateless'];
444+
}
445+
$defaults['_controller'] = $dashboardFqcn.'::index';
446+
$defaults[EA::ROUTE_CREATED_BY_EASYADMIN] = true;
447+
$defaults[EA::DASHBOARD_CONTROLLER_FQCN] = $dashboardFqcn;
448+
$defaults[EA::CRUD_CONTROLLER_FQCN] = null;
449+
$defaults[EA::CRUD_ACTION] = null;
450+
$route->setDefaults($defaults);
451+
452+
if (isset($routeOptions['utf8'])) {
453+
$routeOptions['options']['utf8'] = $routeOptions['utf8'];
454+
}
455+
if (isset($routeOptions['options'])) {
456+
$route->setOptions($routeOptions['options']);
457+
}
458+
459+
return ['routeName' => $routeName, 'route' => $route];
460+
}
461+
352462
private function getPhpAttributeInstance(string $classFqcn, string $attributeFqcn): ?object
353463
{
354464
$reflectionClass = new \ReflectionClass($classFqcn);
@@ -391,6 +501,9 @@ private function saveAdminRoutesInCache(array $adminRoutes): void
391501
// first, add the routes of all the application dashboards; this is needed because in
392502
// applications with multiple dashboards, EasyAdmin must be able to find the route data associated
393503
// to each dashboard; otherwise, the URLs of the menu items when visiting the dashboard route will be wrong
504+
// TODO: remove this in EasyAdmin 5.0.0, when all the dashboard routes are created using the #[AdminDashboard] attribute;
505+
// there's no need to remove it now, because when using the #[AdminDashboard] attribute, the admin route created here
506+
// will be immediately overwritten below by the one created using the attribute
394507
foreach ($this->getDashboardsRouteConfig() as $dashboardFqcn => $dashboardRouteConfig) {
395508
$routeNameToFqcn[$dashboardRouteConfig['routeName']] = [
396509
EA::DASHBOARD_CONTROLLER_FQCN => $dashboardFqcn,

‎tests/Controller/PrettyUrls/PrettyUrlsControllerTest.php

+41
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
use EasyCorp\Bundle\EasyAdminBundle\Tests\PrettyUrlsTestApplication\Entity\Category;
1313
use EasyCorp\Bundle\EasyAdminBundle\Tests\PrettyUrlsTestApplication\Kernel;
1414
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
15+
use Symfony\Component\Routing\Router;
1516

1617
/**
1718
* @group pretty_urls
@@ -73,9 +74,21 @@ public function testGeneratedRoutes()
7374
$expectedRoutes['second_dashboard_external_user_editor_detail'] = '/second/dashboard/user-editor/custom/path-for-detail/{entityId}';
7475
$expectedRoutes['second_dashboard_external_user_editor_foobar'] = '/second/dashboard/user-editor/bar/foo';
7576
$expectedRoutes['second_dashboard_external_user_editor_foofoo'] = '/second/dashboard/user-editor/bar/bar';
77+
$expectedRoutes['admin3'] = '/backend/three/';
78+
$expectedRoutes['admin3_external_user_editor_custom_route_for_index'] = '/backend/three/user-editor/custom/path-for-index';
79+
$expectedRoutes['admin3_external_user_editor_custom_route_for_new'] = '/backend/three/user-editor/new';
80+
$expectedRoutes['admin3_external_user_editor_batch_delete'] = '/backend/three/user-editor/batch-delete';
81+
$expectedRoutes['admin3_external_user_editor_autocomplete'] = '/backend/three/user-editor/autocomplete';
82+
$expectedRoutes['admin3_external_user_editor_render_filters'] = '/backend/three/user-editor/render-filters';
83+
$expectedRoutes['admin3_external_user_editor_edit'] = '/backend/three/user-editor/{entityId}/edit';
84+
$expectedRoutes['admin3_external_user_editor_delete'] = '/backend/three/user-editor/{entityId}/delete';
85+
$expectedRoutes['admin3_external_user_editor_detail'] = '/backend/three/user-editor/custom/path-for-detail/{entityId}';
86+
$expectedRoutes['admin3_external_user_editor_foobar'] = '/backend/three/user-editor/bar/foo';
87+
$expectedRoutes['admin3_external_user_editor_foofoo'] = '/backend/three/user-editor/bar/bar';
7688

7789
self::bootKernel();
7890
$container = static::getContainer();
91+
/** @var Router $router */
7992
$router = $container->get('router');
8093
$generatedRoutes = [];
8194
foreach ($router->getRouteCollection() as $name => $route) {
@@ -354,6 +367,34 @@ public function testUglyUrlsAreRedirectedToPrettyUrls(string $crudControllerFqcn
354367
$this->assertSame($expectedPrettyUrlRedirect, $client->getResponse()->headers->get('Location'));
355368
}
356369

370+
public function testPrettyAdminUrlCreatedWithAdminDashboardAttribute(): void
371+
{
372+
$container = static::getContainer();
373+
/** @var Router $router */
374+
$router = $container->get('router');
375+
$route = $router->getRouteCollection()->get('admin3');
376+
377+
$this->assertSame('/backend/three/', $route->getPath());
378+
$this->assertSame('example.com', $route->getHost());
379+
$this->assertSame(['https'], $route->getSchemes());
380+
$this->assertSame(['GET', 'HEAD'], $route->getMethods());
381+
$this->assertSame(['foo' => '.*'], $route->getRequirements());
382+
$this->assertSame('Symfony\Component\Routing\RouteCompiler', $route->getOption('compiler_class'));
383+
$this->assertTrue($route->getOption('utf8'));
384+
$this->assertSame('context.getMethod() in ["GET", "HEAD"]', $route->getCondition());
385+
$this->assertSame([
386+
'foo' => 'bar',
387+
'_locale' => 'es',
388+
'_format' => 'html',
389+
'_stateless' => true,
390+
'_controller' => 'EasyCorp\Bundle\EasyAdminBundle\Tests\PrettyUrlsTestApplication\Controller\ThirdDashboardController::index',
391+
'routeCreatedByEasyAdmin' => true,
392+
'dashboardControllerFqcn' => 'EasyCorp\Bundle\EasyAdminBundle\Tests\PrettyUrlsTestApplication\Controller\ThirdDashboardController',
393+
'crudControllerFqcn' => null,
394+
'crudAction' => null,
395+
], $route->getDefaults());
396+
}
397+
357398
public static function provideActiveMenuUrls(): iterable
358399
{
359400
yield ['/admin/pretty/urls/category'];
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
<?php
2+
3+
namespace EasyCorp\Bundle\EasyAdminBundle\Tests\PrettyUrlsTestApplication\Controller;
4+
5+
use EasyCorp\Bundle\EasyAdminBundle\Attribute\AdminDashboard;
6+
use EasyCorp\Bundle\EasyAdminBundle\Config\Dashboard;
7+
use EasyCorp\Bundle\EasyAdminBundle\Config\MenuItem;
8+
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractDashboardController;
9+
use EasyCorp\Bundle\EasyAdminBundle\Tests\PrettyUrlsTestApplication\Entity\User;
10+
use Symfony\Component\HttpFoundation\Response;
11+
12+
#[AdminDashboard(
13+
routePath: '/backend/three/',
14+
routeName: 'admin3',
15+
routeOptions: [
16+
'requirements' => [
17+
'foo' => '.*',
18+
],
19+
'options' => [
20+
'compiler_class' => 'Symfony\Component\Routing\RouteCompiler',
21+
],
22+
'defaults' => [
23+
'foo' => 'bar',
24+
],
25+
'host' => 'example.com',
26+
'methods' => ['GET', 'HEAD'],
27+
'schemes' => 'https',
28+
'condition' => 'context.getMethod() in ["GET", "HEAD"]',
29+
'locale' => 'es',
30+
'format' => 'html',
31+
'utf8' => true,
32+
'stateless' => true,
33+
],
34+
allowedControllers: [UserCrudController::class],
35+
)]
36+
class ThirdDashboardController extends AbstractDashboardController
37+
{
38+
public function index(): Response
39+
{
40+
return parent::index();
41+
}
42+
43+
public function configureDashboard(): Dashboard
44+
{
45+
return Dashboard::new()
46+
->setTitle('EasyAdmin Tests');
47+
}
48+
49+
public function configureMenuItems(): iterable
50+
{
51+
yield MenuItem::linktoDashboard('Dashboard', 'fa fa-home');
52+
yield MenuItem::linkToCrud('Users', 'fas fa-users', User::class);
53+
}
54+
}

0 commit comments

Comments
 (0)
Please sign in to comment.