Buy Access to Course
16.

Adding a Custom Action

Share this awesome video!

|

Keep on Learning!

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

We 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:

157 lines | app/config/config.yml
// ... 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:

157 lines | app/config/config.yml
// ... 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:

158 lines | app/config/config.yml
// ... 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, class AdminController does not have a method changePublishedStatusAction().

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:

14 lines | app/config/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:

14 lines | app/config/routing.yml
// ... 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.