Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Simple Custom GET 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

Let'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
}

Adding the Custom Action in configureActions()

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

Setting the Action to redirect

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!

Customizing How the Action Looks

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.

Leave a comment!

9
Login or Register to join the conversation
Stephan-O Avatar
Stephan-O Avatar Stephan-O | posted 10 days ago

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:

->createAsGlobalAction()
Reply

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!

Reply
Lechu85 Avatar

Hello. You should add semicolon ; in new code on line 10 after }

:)

Reply

Thanks for reporting it!

1 Reply
Ruslan Avatar
Ruslan Avatar Ruslan | posted 11 months ago | edited

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 does not work, because $viewAction is Closure object.

##### It works if :

 ->add(Crud::PAGE_DETAIL, $viewAction()->addCssClass('btn btn-success'))
 ->add(Crud::PAGE_INDEX, $viewAction());

It's mistyping.
Thank you.

Reply

Hey,

Yep you are totally right, sorry for the silly typo, and thanks for your signal!

Cheers!

1 Reply

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?

Reply

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!

Reply

it worked! much better than creating two objects

Reply
Cat in space

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

What PHP libraries does this tutorial use?

// 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
    }
}
userVoice