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 SubscribeLet's add a totally custom action. What if, when we're on the detail page for a question, we add a new action called "view" that takes us to the frontend for that question? Sounds good! Start in QuestionCrudController
. To add a new action... we'll probably need to do some work inside of configureActions()
. We already know how to add actions to different pages: with the ->add()
method. Let's try adding, to Crud::PAGE_DETAIL
, a new action called view
.
... lines 1 - 21 | |
class QuestionCrudController extends AbstractCrudController | |
{ | |
... lines 24 - 37 | |
public function configureActions(Actions $actions): Actions | |
{ | |
return parent::configureActions($actions) | |
... lines 41 - 53 | |
->add(Crud::PAGE_DETAIL, 'view'); | |
} | |
... lines 56 - 140 | |
} |
There are a bunch of built-in action names - like index
or delete
- and we usually reference those via their Action
constant. But in this case, we're making a new action... so let's just "invent" a string called view
... and see what happens.
Refresh and... what happened was... an error!
The "view" action is not a built-in action, so you can't
add or configure it via its name. Either refer to one of
the built-in actions or create a custom action called "view".
In the last chapters, we talked about how, behind-the-scenes, each action is actually an Action
object. We don't really think about that most of the time... but when we create a custom action, we need to deal with this object directly.
Above the return
, create an Action
object with $viewAction = Action::new()
... and pass this the action name that we just invented: view
. Then, below, instead of the string, this argument accepts an $actionNameOrObject
. Pass in that new $viewAction
variable.
... lines 1 - 37 | |
public function configureActions(Actions $actions): Actions | |
{ | |
$viewAction = Action::new('view'); | |
... line 41 | |
return parent::configureActions($actions) | |
... lines 43 - 55 | |
->add(Crud::PAGE_DETAIL, $viewAction); | |
} | |
... lines 58 - 144 |
Refresh again to see... another error:
Actions must link to either a route, a CRUD action, or a URL.
And then it gives us three different methods we can use to set that up. That's a pretty great error message. It sounds like linkToRoute()
or linkToUrl()
is what we need.
So, up here, let's modify our action. We could use ->linkToRoute()
... but as we learned earlier, that would generate a URL through the admin section, complete with all the admin query parameters. Not what we want. Instead, use ->linkToUrl()
.
But, hmm. We can't use $this->generateUrl()
yet... because we need to know which Question
we're generating the URL for. And we don't have that! Fortunately, the argument accepts a string or callable. Let's try that: pass a function()
... and then to see what arguments this receives, let's use a trick: dd(func_get_args())
.
... lines 1 - 37 | |
public function configureActions(Actions $actions): Actions | |
{ | |
$viewAction = Action::new('view') | |
->linkToUrl(function() { | |
dd(func_get_args()); | |
}); | |
... lines 44 - 59 | |
} | |
... lines 61 - 147 |
Back in the browser... awesome! We are apparently passed one argument, which is the Question
object. We're dangerous! Use that: return $this->generateUrl()
, passing the frontend route name: which is app_question_show
. This route has a slug
route wildcard... so add the Question $question
argument to the function and set slug
to $question->getSlug()
.
... lines 1 - 37 | |
public function configureActions(Actions $actions): Actions | |
{ | |
$viewAction = Action::new('view') | |
->linkToUrl(function(Question $question) { | |
return $this->generateUrl('app_question_show', [ | |
'slug' => $question->getSlug(), | |
]); | |
}); | |
... lines 46 - 61 | |
} | |
... lines 63 - 149 |
Testing time! And now... yes! We have a "View" button. If we click it... it works!
And just like any other action, we can modify how this looks. Let's ->addCssClass('btn btn-success')
, ->setIcon('fa fa-eye
), and ->setLabel('View
on site'): all things that we've done before for other actions.
... lines 1 - 37 | |
public function configureActions(Actions $actions): Actions | |
{ | |
$viewAction = Action::new('view') | |
... lines 41 - 45 | |
->addCssClass('btn btn-success') | |
->setIcon('fa fa-eye') | |
->setLabel('View on site'); | |
... lines 49 - 64 | |
} | |
... lines 66 - 152 |
Refresh and... that looks great! If we want to include this action on other pages, we can. Because, if you go to the index page, there's no "view on frontend" action. Thankfully, we created this nice $viewAction
variable, so, at the bottom, we can reuse it: ->add(Crud::PAGE_INDEX, $viewAction)
.
... lines 1 - 37 | |
public function configureActions(Actions $actions): Actions | |
{ | |
... lines 40 - 49 | |
return parent::configureActions($actions) | |
... lines 51 - 63 | |
->add(Crud::PAGE_DETAIL, $viewAction) | |
->add(Crud::PAGE_INDEX, $viewAction); | |
} | |
... lines 67 - 153 |
Refresh and... got it! Though... you can see the btn
styling doesn't really work well here. I won't do it, but you could clone the Action
object and then customize each one.
Tip
I was wrong! Cloning will not work, due to the fact that "clones" are shallow in
PHP... and the data inside an "action" object is stored in the internal ActionDto
.
Anyways, try this solution instead:
$viewAction = function() {
return Action::new('view')
->linkToUrl(function(Question $question) {
return $this->generateUrl('app_question_show', [
'slug' => $question->getSlug(),
]);
})
->setIcon('fa fa-eye')
->setLabel('View on site');
};
// ...
return parent::configureActions($actions)
// ...
->add(Crud::PAGE_DETAIL, $viewAction()->addCssClass('btn btn-success'))
->add(Crud::PAGE_INDEX, $viewAction());
Okay, so creating an action that links somewhere is cool. But what about a true custom action that connects to a custom controller with custom logic... that does custom... stuff? Let's add a custom action that allows moderators to approve questions, next.
Hey Stephan,
Yep, that's on purpose :) We talk about createAsGlobalAction()
in the further chapters, e.g. see https://symfonycasts.com/screencast/easyadminbundle/global-action
Cheers!
Hello. You should add semicolon ; in new code on line 10 after }
:)
Hi,
Check this, please.
In your Tip :
return parent::configureActions($actions)
// ...
->add(Crud::PAGE_DETAIL, $viewAction->addCssClass('btn btn-success'))
->add(Crud::PAGE_INDEX, $viewAction);
##### It works if :
->add(Crud::PAGE_DETAIL, $viewAction()->addCssClass('btn btn-success'))
->add(Crud::PAGE_INDEX, $viewAction());
It's mistyping.
Thank you.
Hey,
Yep you are totally right, sorry for the silly typo, and thanks for your signal!
Cheers!
I try cloning with
`
$viewIndexAction = Action::new('view')
->linkToUrl(function (Question $question) {
return $this->generateUrl('app_question_show', [
'slug' => $question->getSlug(),
]);
});
$viewDetailAction = (clone $viewIndexAction)
->addCssClass('btn btn-success')
->setIcon('fa fa-eye')
->setLabel('View on site');
`
and then
`
// ...
->add(Crud::PAGE_DETAIL, $viewDetailAction)
->add(Crud::PAGE_INDEX, $viewIndexAction);
`
But it didn't work. I had to create another Action.
If I dd those vars
<br />QuestionCrudController.php on line 100:<br />EasyCorp\Bundle\EasyAdminBundle\Config\Action {#1605 ▼<br /> -dto: EasyCorp\Bundle\EasyAdminBundle\Dto\ActionDto {#1602 ▶}<br />}<br />QuestionCrudController.php on line 100:<br />EasyCorp\Bundle\EasyAdminBundle\Config\Action {#1603 ▼<br /> -dto: EasyCorp\Bundle\EasyAdminBundle\Dto\ActionDto {#1602 ▶}<br />}<br />
So the Action object is cloned, but the dto object inside it is not.
Did I get it right or have I missed something?
Hey rcapile!
It's funny you posted this, because I just finished answering another comment where I realized, for the first time, that my cloning solution won't work (for the exact reasons you said)! Boo Ryan!
So... we just "make up" our own clone, which really creates a fresh object ;)
$viewAction = function() {
return Action::new('view')
->linkToUrl(function (Question $question) {
return $this->generateUrl('app_question_show', [
'slug' => $question->getSlug(),
]);
});
};
$viewDetailAction = $viewAction()
->addCssClass('btn btn-success')
->setIcon('fa fa-eye')
->setLabel('View on site');
I'll add a note to the video about this - thank you for bringing up the issue! And let me know if this works for you :).
Cheers!
// composer.json
{
"require": {
"php": ">=8.1.0",
"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
}
}
Following this guide, I had the issue that the custom action was only being displayed for each individual row instead of on the top for the whole Crud like in the video. If you encounter this issue, you can fix it by adding the following method to your custom action: