Adding a Custom Action
Keep on Learning!
If you liked what you've learned so far, dive in! Subscribe to get access to this tutorial plus video, code and script downloads.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeWe know there are a bunch of built-in actions, like "delete" and "edit. But sometimes you need to manipulate an entity in a different way! Like, how could we add a "publish" button next to each Genus?
There are... two different ways to do that. Click into the show view for a Genus. On show, the actions show up at the bottom. Before we talk about publishing, I want to add a new button down here called "feed"... ya know... because Genuses get hungry. When we click that, it should send the user to a custom controller where we can write whatever crazy code we want.
Custom Route Actions
The first step should feel very natural. We already know how to add actions, remove actions and customize how they look. Under Genus
, add a new show
key and actions
. Use the expanded configuration, with name: genus_feed
and type: route
:
// ... lines 1 - 80 | |
easy_admin: | |
// ... lines 82 - 97 | |
entities: | |
Genus: | |
// ... lines 100 - 118 | |
show: | |
actions: | |
- | |
name: 'genus_feed' | |
type: 'route' | |
// ... lines 124 - 157 |
There are two different custom action "types": route
and action
. Route is simple: it creates a new link to the genus_feed
route. And you can use any of the normal action-configuring options, like label
, css_class: 'btn btn-info
or an icon
:
// ... lines 1 - 80 | |
easy_admin: | |
// ... lines 82 - 97 | |
entities: | |
Genus: | |
// ... lines 100 - 118 | |
show: | |
actions: | |
- | |
name: 'genus_feed' | |
type: 'route' | |
label: 'Feed genus' | |
css_class: 'btn btn-info' | |
icon: 'cutlery' | |
// ... lines 127 - 157 |
Adding the Route Action Endpoint
Next, we need to actually create that route and controller. In src/AppBundle/Controller
, open GenusController
. At the top, add feedAction()
with @Route("/genus/feed")
and name="genus_feed
to match what we put in the config:
// ... lines 1 - 15 | |
class GenusController extends Controller | |
{ | |
/** | |
* @Route("/genus/feed", name="genus_feed") | |
*/ | |
public function feedAction(Request $request) | |
{ | |
// ... lines 23 - 36 | |
} | |
// ... lines 38 - 167 | |
} |
Notice the URL for this is just /genus/feed
. It does not start with /easyadmin
. And so, it's not protected by our access_control
security.
That should be enough to get started. Refresh! There's our link! Click it and... good! Error! I love errors! Our action is still empty.
So here's the question: when we click feed on the Genus
show page... the EasyAdminBundle must somehow pass us the id of that genus... right? Yes! It does it via query parameters... which are a bit ugly! So I'll open up my profiler and go to "Request / Response". Here are the GET parameters. We have entity
and id
!
Now that we know that, this will be a pretty traditional controller. I'll type-hint the Request
object as an argument:
// ... lines 1 - 12 | |
use Symfony\Component\HttpFoundation\Request; | |
// ... lines 14 - 15 | |
class GenusController extends Controller | |
{ | |
// ... lines 18 - 20 | |
public function feedAction(Request $request) | |
{ | |
// ... lines 23 - 36 | |
} | |
// ... lines 38 - 167 | |
} |
Then, fetch the entity manager and the $id
via $request->query->get('id')
. Use that to get the $genus
object: $em->getRepository(Genus::class)->find($id)
.
// ... lines 1 - 15 | |
class GenusController extends Controller | |
{ | |
// ... lines 18 - 20 | |
public function feedAction(Request $request) | |
{ | |
$em = $this->getDoctrine()->getManager(); | |
$id = $request->query->get('id'); | |
$genus = $em->getRepository('AppBundle:Genus')->find($id); | |
// ... lines 26 - 36 | |
} | |
// ... lines 38 - 167 | |
} |
Cool! To feed the Genus
, we'll re-use a feed()
method from a previous tutorial. Start by creating a menu of delicious food: shrimp
, clams
, lobsters
and... dolphin
! Then choose a random food, add a flash message and call $genus->feed()
:
// ... lines 1 - 15 | |
class GenusController extends Controller | |
{ | |
// ... lines 18 - 20 | |
public function feedAction(Request $request) | |
{ | |
$em = $this->getDoctrine()->getManager(); | |
$id = $request->query->get('id'); | |
$genus = $em->getRepository('AppBundle:Genus')->find($id); | |
$menu = ['shrimp', 'clams', 'lobsters', 'dolphin']; | |
$meal = $menu[random_int(0, 3)]; | |
$this->addFlash('info', $genus->feed([$meal])); | |
// ... lines 31 - 36 | |
} | |
// ... lines 38 - 167 | |
} |
Now that all this hard work is done, I want to redirect back to the show view for this genus. Like normal, return $this->redirectToRoute()
. And actually, EasyAdminBundle only has one route... called easyadmin
:
// ... lines 1 - 15 | |
class GenusController extends Controller | |
{ | |
// ... lines 18 - 20 | |
public function feedAction(Request $request) | |
{ | |
$em = $this->getDoctrine()->getManager(); | |
$id = $request->query->get('id'); | |
$genus = $em->getRepository('AppBundle:Genus')->find($id); | |
$menu = ['shrimp', 'clams', 'lobsters', 'dolphin']; | |
$meal = $menu[random_int(0, 3)]; | |
$this->addFlash('info', $genus->feed([$meal])); | |
return $this->redirectToRoute('easyadmin', [ | |
// ... lines 33 - 35 | |
]); | |
} | |
// ... lines 38 - 167 | |
} |
We tell it where to go via query parameters, like action
set to show
, entity
set to $request->query->get('entity')
... or we could just say Genus
, and id
set to $id
:
// ... lines 1 - 15 | |
class GenusController extends Controller | |
{ | |
// ... lines 18 - 20 | |
public function feedAction(Request $request) | |
{ | |
// ... lines 23 - 31 | |
return $this->redirectToRoute('easyadmin', [ | |
'action' => 'show', | |
'entity' => $request->query->get('entity'), | |
'id' => $id | |
]); | |
} | |
// ... lines 38 - 167 | |
} |
That is it! Refresh the show page! And feed the genus. Got it! We can hit that over and over again. Hello custom action.
Custom Controller Action
There's also another way of creating a custom action. It's a bit simpler and a bit stranger... but has one advantage: it allows you to create different implementations of the action for different entities.
Let's try it! In config.yml
, add another action. This time, set the name to changePublishedStatus
with a css_class
set to btn
:
// ... lines 1 - 80 | |
easy_admin: | |
// ... lines 82 - 97 | |
entities: | |
Genus: | |
// ... lines 100 - 118 | |
show: | |
actions: | |
// ... lines 121 - 126 | |
- { name: 'changePublishedStatus', css_class: 'btn' } | |
// ... lines 128 - 158 |
Let's do as little work as possible! So...refresh! We have a button! Click it! Bah! Big error! But, it explains how the feature works:
Warning:
call_user_func_array()
expects parameter 1 to be a valid callback, classAdminController
does not have a methodchangePublishedStatusAction()
.
Eureka! All we need to do is create that method... then celebrate!
Overriding the AdminController
To do that, we need to sub-class the core AdminController
. Create a new directory in Controller
called EasyAdmin
. Then inside, a new PHP class called AdminController
. To make this extend the normal AdminController
, add a use
statement for it: use AdminController as BaseAdminController
. Extend that: BaseAdminController
:
// ... lines 1 - 2 | |
namespace AppBundle\Controller\EasyAdmin; | |
use JavierEguiluz\Bundle\EasyAdminBundle\Controller\AdminController as BaseAdminController; | |
class AdminController extends BaseAdminController | |
{ | |
// ... lines 9 - 25 | |
} |
Next, create that action method: changePublishedStatusAction()
:
// ... lines 1 - 6 | |
class AdminController extends BaseAdminController | |
{ | |
public function changePublishedStatusAction() | |
{ | |
// ... lines 11 - 24 | |
} | |
} |
Notice the config key is just changePublishedStatus
- EasyAdminBundle automatically expects that Action
suffix.
And now that we're in a controller method... we're comfortable! I mean, we could write a killer action in our sleep. But... there's a gotcha. This method is not, exactly, like a traditional controller. That's because it's not called by Symfony's routing system... it's called directly by EasyAdminBundle, which is trying to "fake" things.
In practice, this means one important thing: we cannot add a Request
argument. Actually, all of the normal controller argument tricks will not work.. because this isn't really a real controller.
Fetching the Request & the Entity
Instead, the base AdminController
has a few surprises for us: protected properties with handy things like the entity manager, the request and some EasyAdmin configuration.
Let's use this! Get the id query parameter via $this->request->query->get('id')
. Then, fetch the object with $entity =
$this->em->getRepository(Genus::class)->find($id):
// ... lines 1 - 6 | |
class AdminController extends BaseAdminController | |
{ | |
public function changePublishedStatusAction() | |
{ | |
$id = $this->request->query->get('id'); | |
$entity = $this->em->getRepository('AppBundle:Genus')->find($id); | |
// ... lines 13 - 24 | |
} | |
} |
Now things are easier. Change the published status to whatever it is not currently. Then, $this->em->flush()
:
// ... lines 1 - 6 | |
class AdminController extends BaseAdminController | |
{ | |
public function changePublishedStatusAction() | |
{ | |
$id = $this->request->query->get('id'); | |
$entity = $this->em->getRepository('AppBundle:Genus')->find($id); | |
$entity->setIsPublished(!$entity->getIsPublished()); | |
$this->em->flush(); | |
// ... lines 17 - 24 | |
} | |
} |
Set a fancy flash message that says whether the genus was just published or unpublished:
// ... lines 1 - 6 | |
class AdminController extends BaseAdminController | |
{ | |
public function changePublishedStatusAction() | |
{ | |
$id = $this->request->query->get('id'); | |
$entity = $this->em->getRepository('AppBundle:Genus')->find($id); | |
$entity->setIsPublished(!$entity->getIsPublished()); | |
$this->em->flush(); | |
$this->addFlash('success', sprintf('Genus %spublished!', $entity->getIsPublished() ? '' : 'un')); | |
// ... lines 19 - 24 | |
} | |
} |
And finally, at the bottom, I want to redirect back to the show page. Let's go steal that code from GenusController
. The one difference of course is that $request
needs to be $this->request
:
// ... lines 1 - 6 | |
class AdminController extends BaseAdminController | |
{ | |
public function changePublishedStatusAction() | |
{ | |
$id = $this->request->query->get('id'); | |
$entity = $this->em->getRepository('AppBundle:Genus')->find($id); | |
$entity->setIsPublished(!$entity->getIsPublished()); | |
$this->em->flush(); | |
$this->addFlash('success', sprintf('Genus %spublished!', $entity->getIsPublished() ? '' : 'un')); | |
return $this->redirectToRoute('easyadmin', [ | |
'action' => 'show', | |
'entity' => $this->request->query->get('entity'), | |
'id' => $id, | |
]); | |
} | |
} |
Pointing to our AdminController Classs
Ok friends. Refresh! It works! Ahem... I mean, we totally get the exact same error! What!?
This is because we haven't told Symfony to use our AdminController yet: it's still using the one from the bundle. The fix is actually in routing.yml
:
// ... lines 1 - 9 | |
easy_admin_bundle: | |
resource: "@EasyAdminBundle/Controller/" | |
type: annotation | |
prefix: /easyadmin |
This tells Symfony to import the annotation routes from the bundle's AdminController
class... which means that class is used when we go to those routes. Change this to import routes from @AppBundle/Controller/EasyAdmin/AdminController.php
instead:
// ... lines 1 - 9 | |
easy_admin_bundle: | |
resource: "@AppBundle/Controller/EasyAdmin/AdminController.php" | |
// ... lines 12 - 14 |
It will still read the same route annotations from the base class, because we're extending it. But now, it will use our class when that route is matched.
That should be all we need. Try it. Boom! Genus published. Do it again! Genus unpublished! The power... it's intoxicating...
Next! We're going to go rogue... and start adding our own custom hooks... like right before or after an entity is inserted or updated.
Hi guys, I have an issue.
When I set a new "resource" in easy_admin.yaml :
easy_admin_bundle:
resource: "@App/Controller/EasyAdmin/AdminController.php"
prefix: /easyadmin
type: annotation
Getting this Error:
An exception has been thrown during the rendering of a template ("Bundle "App" does not exist or it is not enabled. Maybe you forgot to add it in the registerBundles() method of your App\Kernel.php file? in @App/Controller/EasyAdmin/AdminController.php (which is being imported from "/var/www/site.loc/config/routes/easy_admin.yaml"). Make sure the "App" bundle is correctly registered and loaded in the application kernel class. If the bundle is registered, make sure the bundle path "@App/Controller/EasyAdmin/AdminController.php" is not empty.").
And the whole project doesn't work.
<b>official documentation:</b>
<a href="#">https://symfony.com/doc/master/bundles/EasyAdminBundle/book/complex-dynamic-backends.html</a>
<i>The default Symfony routing config loads first the annotations of controllers defined in src/Controller/. If you override the AdminController in that directory, the routing config defined in config/routes/easy_admin.yaml will be ignored. You can comment the contents of that file and use instead the @Route annotation of AdminController to configure that route.</i>
I changed the controller directory(src/Admin), but that didn't help either.
Any ideas what this could be?
And of course my controller:
`
namespace App\Controller\EasyAdmin;
use EasyCorp\Bundle\EasyAdminBundle\Controller\EasyAdminController as BaseAdminController;
class AdminController extends BaseAdminController
{
}
`