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')
->displayAsButton();
... 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')
->linkToCrudAction('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')
->setTemplatePath('admin/approve_action.html.twig')
... 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') }}
</form>

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!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": ">=8.0.2",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "composer/package-versions-deprecated": "^1.11", // 1.11.99.4
        "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
    }
}