Skip to content

Commit bb46897

Browse files
committed
minor #7494 Misc changes related to custom CRUD actions (javiereguiluz)
This PR was squashed before being merged into the 4.x branch. Discussion ---------- Misc changes related to custom CRUD actions This is closely related to #7493 and backports many of those change to 4.x to smooth the upgrade when using custom CRUD actions. Commits ------- 3bbf921 Misc changes related to custom CRUD actions
2 parents 94f5e90 + 3bbf921 commit bb46897

9 files changed

Lines changed: 146 additions & 60 deletions

File tree

UPGRADE.md

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,76 @@
11
Upgrade between EasyAdmin 4.x versions
22
======================================
33

4+
EasyAdmin 4.29.5
5+
----------------
6+
7+
When using pretty URLs, it's deprecated to define custom CRUD actions without applying the
8+
`#[AdminRoute]` attribute to them. In EasyAdmin 5.x, custom actions without this attribute
9+
will be ignored and code like `->linkToCrudAction('foo')` will no longer work:
10+
11+
// Before
12+
13+
use App\Entity\Comment;
14+
use EasyCorp\Bundle\EasyAdminBundle\Config\Action;
15+
use EasyCorp\Bundle\EasyAdminBundle\Config\Actions;
16+
use EasyCorp\Bundle\EasyAdminBundle\Context\AdminContext;
17+
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController;
18+
use Symfony\Component\HttpFoundation\Response;
19+
20+
class CommentCrudController extends AbstractCrudController
21+
{
22+
// ...
23+
24+
public function configureActions(Actions $actions): Actions
25+
{
26+
return $actions
27+
->add(
28+
Crud::PAGE_INDEX,
29+
Action::new('markSpam', 'action.mark_spam')->linkToCrudAction('markCommentAsSpam')
30+
)
31+
;
32+
}
33+
34+
public function markCommentAsSpam(AdminContext $context): Response
35+
{
36+
/** @var Comment $comment */
37+
$comment = $context->getEntity()->getInstance();
38+
39+
$comment->markAsSpam();
40+
$this->entityManager->flush();
41+
42+
return $this->redirectToRoute('admin_comment_index');
43+
}
44+
}
45+
46+
// After
47+
use EasyCorp\Bundle\EasyAdminBundle\Attribute\AdminRoute;
48+
// ...
49+
50+
class CommentCrudController extends AbstractCrudController
51+
{
52+
// ...
53+
54+
public function configureActions(Actions $actions): Actions
55+
{
56+
return $actions
57+
->add(
58+
Crud::PAGE_INDEX,
59+
Action::new('markSpam', 'action.mark_spam')->linkToCrudAction('markCommentAsSpam')
60+
)
61+
;
62+
}
63+
64+
#[AdminRoute('/{entityId:comment.id}/mark-as-spam')]
65+
public function markCommentAsSpam(Comment $comment): Response
66+
{
67+
$comment->markAsSpam();
68+
$this->entityManager->flush();
69+
70+
return $this->redirectToRoute('admin_comment_index');
71+
}
72+
}
73+
474
EasyAdmin 4.29.0
575
----------------
676

doc/actions.rst

Lines changed: 9 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -703,56 +703,26 @@ The following example shows all kinds of actions in practice::
703703
;
704704
}
705705

706-
public function renderInvoice(AdminContext $context)
706+
#[AdminRoute('/{entityId:order.id}/invoice')]
707+
public function renderInvoice(Order $order): Response
707708
{
708-
$order = $context->getEntity()->getInstance();
709-
710-
// add your logic here...
709+
// add your custom order logic here...
711710
}
712711
}
713712

713+
Apply the ``#[AdminRoute]`` attribute to turn CRUD controller methods into custom
714+
CRUD actions with their own admin routes. In the above example, if the dashboard
715+
uses ``admin`` as the main route name, EasyAdmin generates a route named
716+
``admin_order_render_invoice`` with the path ``/admin/order/{entityId}/invoice``.
717+
You can :ref:`customize the name, path, and methods <crud_routes>` of this route.
718+
714719
.. tip::
715720

716721
CRUD controllers in EasyAdmin extend the `Symfony base controller class`_.
717722
When actions are defined as methods of CRUD controllers, they can use any
718723
of the shortcuts and utilities available in regular `Symfony controllers`_,
719724
such as ``$this->render()``, ``$this->redirect()``, and others.
720725

721-
It's recommended to apply the ``#[AdminRoute]`` attribute to your custom actions
722-
to :ref:`customize their route name, path and methods <crud_routes>`. This is
723-
recommended even for custom actions defined as methods in the CRUD controllers::
724-
725-
namespace App\Controller\Admin;
726-
727-
use EasyCorp\Bundle\EasyAdminBundle\Attribute\AdminRoute;
728-
use EasyCorp\Bundle\EasyAdminBundle\Config\Action;
729-
use EasyCorp\Bundle\EasyAdminBundle\Config\Actions;
730-
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController;
731-
732-
class OrderCrudController extends AbstractCrudController
733-
{
734-
public function configureActions(Actions $actions): Actions
735-
{
736-
$viewInvoice = Action::new('viewInvoice', 'Invoice', 'fa fa-file-invoice')
737-
->linkToCrudAction('renderInvoice');
738-
739-
// ...
740-
}
741-
742-
// ...
743-
744-
#[AdminRoute(path: '/invoice', name: 'view_invoice')]
745-
public function renderInvoice(AdminContext $context)
746-
{
747-
// if the dashboard uses 'admin' as the main route name, the resulting
748-
// route of this action will be:
749-
// path: /admin/order/invoice
750-
// name: admin_order_view_invoice
751-
752-
// ...
753-
}
754-
}
755-
756726
.. _global-actions:
757727

758728
Global Actions

src/Attribute/AdminAction.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,6 @@ public function __construct(
1616
public ?string $routeName = null,
1717
public array $methods = ['GET'],
1818
) {
19+
@trigger_deprecation('easycorp/easyadmin-bundle', '4.29.5', 'The "%s()" attribute is deprecated and will be removed in EasyAdmin 5.1.0. Use the #[AdminRoute] attribute instead.', __METHOD__);
1920
}
2021
}

src/Attribute/AdminCrud.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,6 @@ public function __construct(
1212
public ?string $routePath = null,
1313
public ?string $routeName = null,
1414
) {
15+
@trigger_deprecation('easycorp/easyadmin-bundle', '4.29.5', 'The "%s()" attribute is deprecated and will be removed in EasyAdmin 5.1.0. Use the #[AdminRoute] attribute instead.', __METHOD__);
1516
}
1617
}

src/Factory/ActionFactory.php

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace EasyCorp\Bundle\EasyAdminBundle\Factory;
44

5+
use EasyCorp\Bundle\EasyAdminBundle\Attribute\AdminRoute;
56
use EasyCorp\Bundle\EasyAdminBundle\Collection\ActionCollection;
67
use EasyCorp\Bundle\EasyAdminBundle\Collection\EntityCollection;
78
use EasyCorp\Bundle\EasyAdminBundle\Config\Action;
@@ -17,6 +18,7 @@
1718
use EasyCorp\Bundle\EasyAdminBundle\Dto\ActionDto;
1819
use EasyCorp\Bundle\EasyAdminBundle\Dto\ActionGroupDto;
1920
use EasyCorp\Bundle\EasyAdminBundle\Dto\EntityDto;
21+
use EasyCorp\Bundle\EasyAdminBundle\Router\AdminRouteGenerator;
2022
use EasyCorp\Bundle\EasyAdminBundle\Router\AdminUrlGeneratorInterface;
2123
use EasyCorp\Bundle\EasyAdminBundle\Security\Permission;
2224
use EasyCorp\Bundle\EasyAdminBundle\Translation\TranslatableMessageBuilder;
@@ -396,10 +398,31 @@ private function generateActionUrl(Request $request, ActionDto $actionDto, ?Enti
396398
return $this->adminUrlGenerator->unsetAllExcept(EA::FILTERS, EA::PAGE, EA::QUERY, EA::SORT)->setRoute($routeName, $routeParameters)->generateUrl();
397399
}
398400

401+
// when using pretty URLs, the data is in the request attributes instead of the query string
402+
$crudControllerFqcn = $request->attributes->get(EA::CRUD_CONTROLLER_FQCN) ?? $request->query->get(EA::CRUD_CONTROLLER_FQCN);
403+
$crudActionName = $actionDto->getCrudActionName();
404+
405+
if (null !== $crudControllerFqcn && null !== $crudActionName && !\in_array($crudActionName, AdminRouteGenerator::BUILT_IN_ACTION_NAMES, true)) {
406+
try {
407+
$reflMethod = new \ReflectionMethod($crudControllerFqcn, $crudActionName);
408+
if ([] === $reflMethod->getAttributes(AdminRoute::class)) {
409+
trigger_deprecation(
410+
'easycorp/easyadmin-bundle',
411+
'4.29.5',
412+
'The "%s()" method in "%s" is used as a custom CRUD action (via "linkToCrudAction()") but it is missing the #[AdminRoute] attribute. In EasyAdmin 5.x, you must add the #[AdminRoute] attribute to the "%s()" method to enable it as a CRUD action. See the UPGRADE.md file.',
413+
$crudActionName,
414+
$crudControllerFqcn,
415+
$crudActionName
416+
);
417+
}
418+
} catch (\ReflectionException) {
419+
// the method doesn't exist; this will be caught elsewhere
420+
}
421+
}
422+
399423
$requestParameters = [
400-
// when using pretty URLs, the data is in the request attributes instead of the query string
401-
EA::CRUD_CONTROLLER_FQCN => $request->attributes->get(EA::CRUD_CONTROLLER_FQCN) ?? $request->query->get(EA::CRUD_CONTROLLER_FQCN),
402-
EA::CRUD_ACTION => $actionDto->getCrudActionName(),
424+
EA::CRUD_CONTROLLER_FQCN => $crudControllerFqcn,
425+
EA::CRUD_ACTION => $crudActionName,
403426
];
404427

405428
if (\in_array($actionDto->getName(), [Action::INDEX, Action::NEW, Action::SAVE_AND_ADD_ANOTHER], true)) {

src/Router/AdminRouteGenerator.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
use EasyCorp\Bundle\EasyAdminBundle\Contracts\Controller\DashboardControllerInterface;
1313
use EasyCorp\Bundle\EasyAdminBundle\Contracts\Router\AdminRouteGeneratorInterface;
1414
use Psr\Cache\CacheItemPoolInterface;
15+
use Symfony\Component\Config\Resource\ReflectionClassResource;
1516
use Symfony\Component\Filesystem\Filesystem;
1617
use Symfony\Component\Routing\Route;
1718
use Symfony\Component\Routing\RouteCollection;
@@ -21,6 +22,12 @@ final class AdminRouteGenerator implements AdminRouteGeneratorInterface
2122
public const CACHE_KEY_ROUTE_TO_FQCN = 'easyadmin.routes.route_to_fqcn';
2223
public const CACHE_KEY_FQCN_TO_ROUTE = 'easyadmin.routes.fqcn_to_route';
2324

25+
public const BUILT_IN_ACTION_NAMES = [
26+
Action::INDEX, Action::NEW, Action::EDIT, Action::DETAIL, Action::DELETE,
27+
Action::BATCH_DELETE, Action::SAVE_AND_ADD_ANOTHER, Action::SAVE_AND_CONTINUE,
28+
Action::SAVE_AND_RETURN, 'autocomplete', 'renderFilters',
29+
];
30+
2431
private const DEFAULT_ROUTES_CONFIG = [
2532
Action::INDEX => [
2633
'actionName' => Action::INDEX,
@@ -98,6 +105,18 @@ public function generateAll(): RouteCollection
98105
$collection->add($routeName, $route);
99106
}
100107

108+
// track controller files as resources so Symfony's routing cache
109+
// is rebuilt automatically when adding/changing route attributes
110+
foreach ($this->dashboardControllers as $dashboardController) {
111+
$collection->addResource(new ReflectionClassResource(new \ReflectionClass($dashboardController)));
112+
}
113+
foreach ($this->crudControllers as $crudController) {
114+
$collection->addResource(new ReflectionClassResource(new \ReflectionClass($crudController)));
115+
}
116+
foreach ($this->adminRouteControllers as $adminRouteController) {
117+
$collection->addResource(new ReflectionClassResource(new \ReflectionClass($adminRouteController)));
118+
}
119+
101120
// this dumps all admin routes in a performance-optimized format to later
102121
// find them quickly without having to use Symfony's router service
103122
$this->saveAdminRoutesInCache($adminRoutes);

tests/Functional/Apps/DefaultApp/src/Controller/CategoryCrudController.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@
22

33
namespace EasyCorp\Bundle\EasyAdminBundle\Tests\Functional\Apps\DefaultApp\Controller;
44

5+
use EasyCorp\Bundle\EasyAdminBundle\Attribute\AdminRoute;
56
use EasyCorp\Bundle\EasyAdminBundle\Config\Action;
67
use EasyCorp\Bundle\EasyAdminBundle\Config\Actions;
78
use EasyCorp\Bundle\EasyAdminBundle\Config\Assets;
89
use EasyCorp\Bundle\EasyAdminBundle\Config\Crud;
910
use EasyCorp\Bundle\EasyAdminBundle\Config\Filters;
10-
use EasyCorp\Bundle\EasyAdminBundle\Context\AdminContext;
1111
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController;
1212
use EasyCorp\Bundle\EasyAdminBundle\Field\BooleanField;
1313
use EasyCorp\Bundle\EasyAdminBundle\Field\IdField;
@@ -62,7 +62,8 @@ public function configureFilters(Filters $filters): Filters
6262
->add('active');
6363
}
6464

65-
public function customAction(AdminContext $context): Response
65+
#[AdminRoute]
66+
public function customAction(): Response
6667
{
6768
return new Response('Custom action page');
6869
}

tests/Functional/Apps/DefaultApp/src/Controller/Synthetic/ActionTestEntityCrudController.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@ public function configureActions(Actions $actions): Actions
141141
});
142142
}
143143

144+
#[AdminRoute]
144145
public function noop(AdminContext $context): Response
145146
{
146147
return $this->redirect($this->container->get(AdminUrlGenerator::class)->setAction(Action::INDEX)->generateUrl());

tests/Unit/Menu/MenuItemMatcherTest.php

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
namespace EasyCorp\Bundle\EasyAdminBundle\Tests\Unit\Menu;
44

5-
use EasyCorp\Bundle\EasyAdminBundle\Config\Crud;
5+
use EasyCorp\Bundle\EasyAdminBundle\Config\Action;
66
use EasyCorp\Bundle\EasyAdminBundle\Config\Option\EA;
77
use EasyCorp\Bundle\EasyAdminBundle\Dto\MenuItemDto;
88
use EasyCorp\Bundle\EasyAdminBundle\Menu\MenuItemMatcher;
@@ -61,7 +61,7 @@ public function testIsSelectedWithCrudControllers(): void
6161
$categoryIndexUrl = $adminUrlGenerator->unsetAll()
6262
->setDashboard(DashboardController::class)
6363
->setController(CategoryCrudController::class)
64-
->setAction(Crud::PAGE_INDEX)
64+
->setAction(Action::INDEX)
6565
->generateUrl();
6666
$categoryIndexPath = parse_url($categoryIndexUrl, \PHP_URL_PATH);
6767

@@ -87,52 +87,52 @@ public function testIsSelectedWithCrudControllers(): void
8787
$categoryEditUrl = $adminUrlGenerator->unsetAll()
8888
->setDashboard(DashboardController::class)
8989
->setController(CategoryCrudController::class)
90-
->setAction(Crud::PAGE_EDIT)
90+
->setAction(Action::EDIT)
9191
->setEntityId('57')
9292
->generateUrl();
9393
$categoryEditPath = parse_url($categoryEditUrl, \PHP_URL_PATH);
9494

9595
$request = $this->createRequest(
9696
crudControllerFqcn: CategoryCrudController::class,
9797
entityId: '57',
98-
action: 'edit',
98+
action: Action::EDIT,
9999
requestPath: $categoryEditPath,
100100
);
101101

102-
$menuItemDto = $this->getMenuItemDtoWithUrl($adminUrlGenerator, CategoryCrudController::class, Crud::PAGE_EDIT, '57');
102+
$menuItemDto = $this->getMenuItemDtoWithUrl($adminUrlGenerator, CategoryCrudController::class, Action::EDIT, '57');
103103
$menuItemMatcher->markSelectedMenuItem([$menuItemDto], $request);
104104
$this->assertTrue($menuItemDto->isSelected(), 'The CRUD controller and the entity ID match');
105105

106-
$menuItemDto = $this->getMenuItemDtoWithUrl($adminUrlGenerator, CategoryCrudController::class, Crud::PAGE_EDIT, 'NOT_57');
106+
$menuItemDto = $this->getMenuItemDtoWithUrl($adminUrlGenerator, CategoryCrudController::class, Action::EDIT, 'NOT_57');
107107
$menuItemMatcher->markSelectedMenuItem([$menuItemDto], $request);
108108
$this->assertFalse($menuItemDto->isSelected(), 'The entity ID of the menu item does not match');
109109

110110
// test detail action with entityId
111111
$categoryDetailUrl = $adminUrlGenerator->unsetAll()
112112
->setDashboard(DashboardController::class)
113113
->setController(CategoryCrudController::class)
114-
->setAction(Crud::PAGE_DETAIL)
114+
->setAction(Action::DETAIL)
115115
->setEntityId('57')
116116
->generateUrl();
117117
$categoryDetailPath = parse_url($categoryDetailUrl, \PHP_URL_PATH);
118118

119119
$request = $this->createRequest(
120120
crudControllerFqcn: CategoryCrudController::class,
121121
entityId: '57',
122-
action: 'detail',
122+
action: Action::DETAIL,
123123
requestPath: $categoryDetailPath,
124124
);
125125

126-
$menuItemDto = $this->getMenuItemDtoWithUrl($adminUrlGenerator, CategoryCrudController::class, Crud::PAGE_DETAIL, '57');
126+
$menuItemDto = $this->getMenuItemDtoWithUrl($adminUrlGenerator, CategoryCrudController::class, Action::DETAIL, '57');
127127
$menuItemMatcher->markSelectedMenuItem([$menuItemDto], $request);
128128
$this->assertTrue($menuItemDto->isSelected(), 'The CRUD controller, entity ID and action match');
129129

130-
$menuItemDto = $this->getMenuItemDtoWithUrl($adminUrlGenerator, CategoryCrudController::class, 'NOT_'.Crud::PAGE_DETAIL, '57');
130+
$menuItemDto = $this->getMenuItemDtoWithUrl($adminUrlGenerator, CategoryCrudController::class, 'NOT_'.Action::DETAIL, '57');
131131
$menuItemMatcher->markSelectedMenuItem([$menuItemDto], $request);
132132
$this->assertFalse($menuItemDto->isSelected(), 'The CRUD controller and entity ID match but the action does not match');
133133
}
134134

135-
private function getMenuItemDtoWithUrl(AdminUrlGenerator $adminUrlGenerator, string $controllerFqcn, string $action = Crud::PAGE_INDEX, ?string $entityId = null): MenuItemDto
135+
private function getMenuItemDtoWithUrl(AdminUrlGenerator $adminUrlGenerator, string $controllerFqcn, string $action = Action::INDEX, ?string $entityId = null): MenuItemDto
136136
{
137137
$menuItemDto = new MenuItemDto();
138138

@@ -258,8 +258,8 @@ public function testComplexMenu(): void
258258
$menuItemMatcher = new MenuItemMatcher($adminUrlGenerator, $adminRouteGenerator);
259259

260260
// generate proper pretty URL paths for the requests
261-
$categoryIndexPath = parse_url($adminUrlGenerator->unsetAll()->setDashboard(DashboardController::class)->setController(CategoryCrudController::class)->setAction(Crud::PAGE_INDEX)->generateUrl(), \PHP_URL_PATH);
262-
$blogPostNewPath = parse_url($adminUrlGenerator->unsetAll()->setDashboard(DashboardController::class)->setController(BlogPostCrudController::class)->setAction(Crud::PAGE_NEW)->generateUrl(), \PHP_URL_PATH);
261+
$categoryIndexPath = parse_url($adminUrlGenerator->unsetAll()->setDashboard(DashboardController::class)->setController(CategoryCrudController::class)->setAction(Action::INDEX)->generateUrl(), \PHP_URL_PATH);
262+
$blogPostNewPath = parse_url($adminUrlGenerator->unsetAll()->setDashboard(DashboardController::class)->setController(BlogPostCrudController::class)->setAction(Action::NEW)->generateUrl(), \PHP_URL_PATH);
263263

264264
// test 1: Perfect match with INDEX action
265265
$menuItems = $this->getComplexMenuItemsWithPrettyUrls($adminUrlGenerator);
@@ -296,10 +296,10 @@ private function getComplexMenuItemsWithPrettyUrls(AdminUrlGenerator $adminUrlGe
296296
$item3 = $this->getMenuItemDtoWithUrl($adminUrlGenerator, BlogPostCrudController::class);
297297
$item3->setLabel('item3');
298298

299-
$item5 = $this->getMenuItemDtoWithUrl($adminUrlGenerator, CategoryCrudController::class, Crud::PAGE_NEW);
299+
$item5 = $this->getMenuItemDtoWithUrl($adminUrlGenerator, CategoryCrudController::class, Action::NEW);
300300
$item5->setLabel('item5');
301301

302-
$item6 = $this->getMenuItemDtoWithUrl($adminUrlGenerator, BlogPostCrudController::class, Crud::PAGE_EDIT, '57');
302+
$item6 = $this->getMenuItemDtoWithUrl($adminUrlGenerator, BlogPostCrudController::class, Action::EDIT, '57');
303303
$item6->setLabel('item6');
304304

305305
$item7 = $this->getMenuItemDtoWithUrl($adminUrlGenerator, ActionsCrudController::class);
@@ -367,7 +367,7 @@ private function getMenuItemDto(?string $crudControllerFqcn = null, ?string $act
367367
if (null !== $action) {
368368
$menuItemRouteParameters[EA::CRUD_ACTION] = $action;
369369
} elseif (null === $action && null === $routeName) {
370-
$menuItemRouteParameters[EA::CRUD_ACTION] = Crud::PAGE_INDEX;
370+
$menuItemRouteParameters[EA::CRUD_ACTION] = Action::INDEX;
371371
}
372372

373373
if (null !== $entityId) {

0 commit comments

Comments
 (0)