Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

True 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 $12.00

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

Login Subscribe

The whole point of this "Pending Approval" section is to allow moderators to approve or delete questions. We can delete questions... but there's no way to approve them. Sure, we could add a little "Is Approved" checkbox to the form. But a true "approve" action with a button on the detail or index pages would be a lot nicer. It would also allow us to run custom code on approval if we need to. So let's create another custom action.

Adding the Action as a Button

Over in QuestionCrudController, say $approveAction = Action::new()... and I'll make up the word approve. Down at the bottom, add that to the detail page: ->add(Crud::PAGE_DETAIL, $approveAction).

... lines 1 - 21
class QuestionCrudController extends AbstractCrudController
... lines 24 - 37
public function configureActions(Actions $actions): Actions
... lines 40 - 48
$approveAction = Action::new('approve');
... line 50
return parent::configureActions($actions)
... lines 52 - 66
->add(Crud::PAGE_DETAIL, $approveAction);
... lines 69 - 155

Before we try that, call ->addCssClass('btn btn-success') and ->setIcon('fa fa-check-circle'). Also add ->displayAsButton().

... lines 1 - 37
public function configureActions(Actions $actions): Actions
... lines 40 - 48
$approveAction = Action::new('approve')
->addCssClass('btn btn-success')
->setIcon('fa fa-check-circle')
... lines 53 - 70
... lines 72 - 158

By default, an action renders as a link... where the URL is wherever you want it to go. But in this case, we don't want approval to be done with a simple link that makes a "GET" request. Approving something will modify data on the server... and so it should really be a "POST" request. This will cause the action to render as a button instead of a link. We'll see how that works in a minute.

Linking to a CRUD Action

Ok, we have now created the action... but we need to link it to a URL or to a CRUD action. In this case, we need a CRUD action where we can write the approve logic. So say linkToCrudAction() passing the name of a method that we're going to create later. Let's call it approve.

... lines 1 - 37
public function configureActions(Actions $actions): Actions
... lines 40 - 48
$approveAction = Action::new('approve')
... lines 51 - 71
... lines 73 - 159

Sweet! Refresh and... duh! The button won't be here... but if we go to the detail page... got it! "Approve"!

Overriding the Template to Add a Form

Inspect element and check out the source code. Yup! This literally rendered as a button... and that's it. There's no form around this... and no JavaScript magic to make it submit. We can click this all day long and absolutely nothing happens. To make it work, we need to wrap it in a form so that, on click, it submits a POST request to the new action.

How can we do that? By leveraging a custom template. We know that EasyAdmin has lots of templates. Inside EasyAdmin... in its Resources/views/crud/ directory, there's an action.html.twig file. This is the template that's responsible for rendering every action. You can see that it's either an a tag or a button based on our config.

Copy the three lines on top that document the variables we have... and let's go create our own custom template. Inside templates/admin/, add a new file called approve_action.html.twig. Paste in the comments... and then... just to further help us know what's going on, dump that action variable: dump(action).

{# @var ea \EasyCorp\Bundle\EasyAdminBundle\Context\AdminContext #}
{# @var action \EasyCorp\Bundle\EasyAdminBundle\Dto\ActionDto #}
{# @var entity \EasyCorp\Bundle\EasyAdminBundle\Dto\EntityDto #}
{{ dump(action) }}

To use this template, over in QuestionCrudController... right on the action, add ->setTemplatePath('admin/approve_action.html.twig').

... lines 1 - 21
class QuestionCrudController extends AbstractCrudController
... lines 24 - 37
public function configureActions(Actions $actions): Actions
... lines 40 - 48
$approveAction = Action::new('approve')
... lines 51 - 72
... lines 74 - 158

Let's try it. Refresh and... cool! We see the dump and all the data on that ActionDto object. The most important thing for us is linkURL. This contains the URL we can use to execute the approve() action that we'll create in a minute.

And because this new template is only being used by our one action... we're free to do whatever we want! All the other actions are still using the core action.html.twig template. Add a form... with action="{{ action.linkUrl }}"... and then method="POST". Inside, we need the button. We could create it ourselves... or we can be lazy and {{ include('@EasyAdmin/crud/action.html.twig') }}.

... lines 1 - 3
<form action="{{ action.linkUrl }}" method="POST">
{{ include('@EasyAdmin/crud/action.html.twig') }}

That's all we need! Reload the page... and inspect that element to see... exactly what we want: a form with the correct action... and our button inside. Though, we do need to fix the styling a little bit. Add class="me-2".

... lines 1 - 3
<form action="{{ action.linkUrl }}" method="POST" class="me-2">
... lines 5 - 7

Refresh and... looks better!

Try clicking this. We get... a giant error! Progress!

The controller for URI "/admin" is not callable: Expected method "approve" on
[our class].

Let's add that custom controller method next, and learn how to generate URLs to other EasyAdmin pages from inside PHP.

Leave a comment!

Login or Register to join the conversation
Scott S. Avatar
Scott S. Avatar Scott S. | posted 4 months ago

He weaverryan

Is there any other way to get the current entity in configureActions without using


The reason i am asking this is because i need a few parameters that is in the entity. I also have a custom template because the call is made by my stimulus controller. The only method i have found now is using callable on one of the link methods:

->linkToUrl(function (Blocks $block) {
return $block->getUrl();

I have tried $this->getContext()->getEntity() but getContext is null. I'm a bit stuck here, hope you can help.


Hey Scott

You can only get the entity instance on the detail or edit page, so, you have to check that first, then you can get the instance through the AdminContext, or you can do it manually by getting the entityId from the request


Fabrice Avatar

Hello ! Unfortunately with this kind of action in POST, it causes something bad at the front-end. The action becomes a form as agreed, but it will take up a whole line, and the other actions will be shifted below. See for yourself



Hey kiuega!

Hmm, you might be right! But, where/how are you seeing the icons that you posted? On the "index" page, by default, these should be rendered as a drop-down. And if I call showEntityActionsInlined() to expand them, I still don't see the bad behavior. Is this on some other page?


Fabrice Avatar

Hello! Yes, this is on one of my projects, in which the actions are rendered inlined. So maybe it's no wonder it doesn't do it for you. Yet the method of application is the same.



Yea... that's super weird. I can't explain why your version looks bad but mine does not. But, in theory, it makes sense: a form will often be styled differently (often with display: block) than a normal button, which could cause this. In that case, you'll need to add some custom CSS, likely to set the form to display: inline. I'm doing some guessing, but that's the most likely culprit!


Cat in space

"Houston: no signs of life"
Start the conversation!

What PHP libraries does this tutorial use?

// composer.json
    "require": {
        "php": ">=8.0.2",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "composer/package-versions-deprecated": "^1.11", //
        "doctrine/doctrine-bundle": "^2.1", // 2.5.5
        "doctrine/doctrine-migrations-bundle": "^3.0", // 3.2.1
        "doctrine/orm": "^2.7", // 2.10.4
        "easycorp/easyadmin-bundle": "^4.0", // v4.0.2
        "handcraftedinthealps/goodby-csv": "^1.4", // 1.4.0
        "knplabs/knp-markdown-bundle": "dev-symfony6", // dev-symfony6
        "knplabs/knp-time-bundle": "^1.11", // 1.17.0
        "sensio/framework-extra-bundle": "^6.0", // v6.2.5
        "stof/doctrine-extensions-bundle": "^1.4", // v1.7.0
        "symfony/asset": "6.0.*", // v6.0.1
        "symfony/console": "6.0.*", // v6.0.2
        "symfony/dotenv": "6.0.*", // v6.0.2
        "symfony/flex": "^2.0.0", // v2.0.1
        "symfony/framework-bundle": "6.0.*", // v6.0.2
        "symfony/mime": "6.0.*", // v6.0.2
        "symfony/monolog-bundle": "^3.0", // v3.7.1
        "symfony/runtime": "6.0.*", // v6.0.0
        "symfony/security-bundle": "6.0.*", // v6.0.2
        "symfony/stopwatch": "6.0.*", // v6.0.0
        "symfony/twig-bundle": "6.0.*", // v6.0.1
        "symfony/ux-chartjs": "^2.0", // v2.0.1
        "symfony/webpack-encore-bundle": "^1.7", // v1.13.2
        "symfony/yaml": "6.0.*", // v6.0.2
        "twig/extra-bundle": "^2.12|^3.0", // v3.3.7
        "twig/twig": "^2.12|^3.0" // v3.3.7
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.3", // 3.4.1
        "symfony/debug-bundle": "6.0.*", // v6.0.2
        "symfony/maker-bundle": "^1.15", // v1.36.4
        "symfony/var-dumper": "6.0.*", // v6.0.2
        "symfony/web-profiler-bundle": "6.0.*", // v6.0.2
        "zenstruck/foundry": "^1.1" // v1.16.0