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.

Start your All-Access Pass
Buy just this tutorial for $8.00

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:

... 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, 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:

... 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.

Leave a comment!

  • 2019-12-10 Diego Aguiar

    Hey @Igor

    You just need to update the resource key value. It should point to your EasyAdmin controller(s) folder or file (depending if you pretend to have more than one controller to manage EasyAdmin actions) and it should work


    // config/routes/easy_admin.yaml
    easy_admin_bundle:
    resource: "../../Controller/EasyAdmin/AdminController.php" # or "../../Controller/EasyAdmin/"

    Cheers!

  • 2019-12-07 Igor

    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.

    official documentation:
    https://symfony.com/doc/master/bundles/EasyAdminBundle/book/complex-dynamic-backends.html
    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 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
    {


    public function changeSomething()
    {}
    }

  • 2019-04-29 weaverryan

    Nice job! You've been beating us to replying :). Thanks for posting all of your solutions - I'm sure it will be helpful to others!

  • 2019-04-26 Duri3l

    Did it a bit after i said it there....
    i did add a js event listener on the adress select change (and call it on page load if it is edit page) to get all adress selected and add them in another select (right under it) so i can select One (and only one) adress to be the primary adress for this user !

    it's fully functionnal now, just need to implement password encryption and some other stuff (like email validation and every little functionnality like these) and i'll be good for this one ^^

  • 2019-04-26 Dur3l

    Thanks !

    I must say i'm trying to do something nice and tidy for about 2 or 3 week so... i'm starting to get some clues on what's going on :P
    Now i'm ok with both new and edit form, but i changed a bit my database : an adress can be "owned" by many users, but only one adress at a time can be the "primary adress" for a user. I just don't really know how i can design the form for this...

    I first had a thought about a list whith checkbox to add adress for this user, and a couple radio to set them all "notPrimary" except for one, but i'm pretty sure this will be real big and ugly if i have a couple hundred adress in database... So i don't really know how i can do this...........
    if you have any idea about this, i'd be happy :D

  • 2019-04-24 Diego Aguiar

    Hey @Duri3l

    Looks like you are already a pro on EasyAdmin! Congrats!
    Thanks for sharing your case and solution to others

    Cheers!

  • 2019-04-24 Duri3l

    I finally found what i was looking for. I did put the entity controller in easy_admin.yaml and then i customized the new action so it is now a route (user_new), then i created a route named user_new and inside it i call
    return $this->executeDynamicMethod('render<entityname>Template', array('new', $this->entity['templates']['new'], $parameters));

    so i do have my customized template and all that goes within.
    Then inside this same function i check if the form is set and valid, and if it is i grab all the info i need and i put it into my new entity.

  • 2019-04-23 Duri3l

    Hem, i forgot...
    I'm using 'normal forms' to feed my database with each other entities so... Can i tell EasyAdmin to use a custom Controller only for this custom entity form and only this one ?

  • 2019-04-23 Duri3l

    Hello there !
    Firstly, a huge thanks for this gorgeous cast ! :)
    I'm actually building a custom form in which there are custom fields that contains 2 thing :
    a list of entity to link to the main entity
    a form to add a new entity that will be listed when registered

    i do have 3 of those (and one of them contains itself a couple of those).
    looks like:

    MainForm
    - normal fields

    - custom field 1 :
    ---- entitylist1
    ---- customFieldForEntitylist1

    - custom field 2 :
    ---- entitylist2
    ---- customFieldForEntitylist2

    - custom field 3 :
    ---- entitylist3
    ---- customFieldForEntitylist3
    ------- entitylist4
    ------- customFieldForEntitylist4 (inside customFieldForEntitylist3)
    Form End
    (if it's not clear enough i'll add a link to a picture)

    My actual problem is when i submit the main form, i don't have those custom data.
    So i tried submiting using your 'custom action' (genus_feed) and i don't have any data at all...
    Is there a way to 'customize' the basic EasyAdmin insertEntity so i can there use my custom data fields ?

    thanks again :D

  • 2018-08-14 Ricard Espinàs Llovet

    Thank you very much! the config parameter was enough! :)

  • 2018-08-13 Diego Aguiar

    Hey Ricard Espinàs Llovet

    If what you want is to disable the "delete" action entirely for an entity, then you can do it by specifying that action as disabled


    easy_admin:
    entities:
    YourEntity:
    disabled_actions: ['delete']


    If not, then you will have to remove the action from the "edit" action as well (just as you did it for the "list" action)

    Cheers!

  • 2018-08-13 Ricard Espinàs Llovet

    Here it shows how to add a new action button, but I would like to know how to remove the delete button action when I'm editing a record from an entity. I already deactivate the delete button from the list of items from the entity as it shows in the video number 9 "Dynamically Remove the delete Action Link" but if I still having this button when I edit is not useful at all. Thank you for advance!