Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Having Fun with the Menu

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

Our menu on the left is getting a little long and... kind of confusing... since we now have two question links. To make this more user-friendly, let's divide this into a sub-menu. We do that inside of DashboardController... because that's, of course, where we configure the menu items.

Adding a Sub Menu

To create a sub-menu, say yield MenuItem::subMenu() and then give that a name - like Questions - and an icon... just like we do with normal menu items.

To populate the items in this menu, say ->setSubItems(), pass this an array, and then we'll wrap our other two menu item objects inside of this. Of course, now we need to indent, remove the yield, and... replace the semicolons with commas.

Perfect! Now change Questions to... how about All... and let's play with the icons. Change the first to fa fa-list... and the second to fa fa-warning.

... lines 1 - 24
class DashboardController extends AbstractDashboardController
... lines 27 - 57
public function configureMenuItems(): iterable
... line 60
yield MenuItem::subMenu('Questions', 'fa fa-question-circle')
MenuItem::linkToCrud('All', 'fa fa-list', Question::class)
MenuItem::linkToCrud('Pending Approval', 'fa fa-warning', Question::class)
... lines 70 - 73
... lines 75 - 134

Let's try that. Move over... refresh and... ahhh, much cleaner!

But wait, there's more we can do with the menu... like adding separators... technically called "sections". Right after linkToDashboard(), add yield MenuItem::section() and pass it Content.

... lines 1 - 57
public function configureMenuItems(): iterable
... line 60
yield MenuItem::section('Content');
... lines 62 - 75
... lines 77 - 138

Let's put one more down here - yield MenuItem::section()... but this time leave the label blank. So unlike sub-menus, which wrap menu items, you can just pop a section anywhere that you want a separator.

... lines 1 - 57
public function configureMenuItems(): iterable
... lines 60 - 72
yield MenuItem::linkToCrud('Users', 'fas fa-users', User::class);
yield MenuItem::section();
yield MenuItem::linkToUrl('Homepage', 'fas fa-home', $this->generateUrl('app_homepage'));
... lines 77 - 138

Let's go check it out. Refresh and... very nice! Separator one says "Content"... and separator two gives us a little gap without any text.

We saw earlier that you can add menu links to point to a dashboard, other CRUD sections... or just any URL you want, like the Homepage. So, not surprisingly, you can also link to external sites. For instance, let's say that I love StackOverflow so much, that I want to link to it. We can tweak the icons, and for the URL, pass whatever you want, like https://stackoverflow.com.

... lines 1 - 57
public function configureMenuItems(): iterable
... lines 60 - 75
yield MenuItem::linkToUrl('StackOverflow', 'fab fa-stack-overflow', 'https://stackoverflow.com');
... lines 78 - 139

Oh, but let me fix my icon name. Great! Now when we refresh... no surprise, that works fine.

More Menu Item Options

If you look closer at these menu items, you'll see that they have a lot of options on them! We know we have things like setPermission() and setController(), but we also have methods like setLinkTarget(), setLinkRel(), setCssClass(), or setQueryParameter(). For this case, let's ->setLinkTarget('_blank')... so that now if I click "StackOverflow", it pops up in a new tab.

... lines 1 - 57
public function configureMenuItems(): iterable
... lines 60 - 75
yield MenuItem::linkToUrl('StackOverflow', 'fab fa-stack-overflow', 'https://stackoverflow.com')
... lines 79 - 140

Next: what if we need to disable an action on an entity-by-entity basis? Like, we want only want to allow questions to be deleted if they are not approved. Let's dive into that.

Leave a comment!

Login or Register to join the conversation
Diaconescu Avatar
Diaconescu Avatar Diaconescu | posted 4 months ago

I don't see the submenu links unless I ditch ->setController(QuestionCrudController::class)->setPermission('ROLE_MODERATOR') respectively ->setPermission('ROLE_MODERATOR')->setController(QuestionPendingApprovalCrudController::class) portion of the code. Of course that makes those sublinks unfunctional. I tried the code downloaded from symfonicasts site with the same result.
My code is here https://github.com/petre-sy...


Hey Diaconescu,

Most probably your logged in user does not have that ROLE_MODERATOR somehow. Please, try to log out and log in again, then check the Symfony web debug toolbar, in particular, the Security tab there - it will show you roles that your user has. It should be in "Roles" or in "Inherited Roles" row. If the user does not have that ROLE_MODERATOR role somehow - please, add it first and try again (you may need to log our and log in just in case).

I hope this helps!


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