Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Controlling the Dashboard Menu

Video not working?

It looks like your browser may not support the H264 codec. If you're using Linux, try a different browser or try installing the gstreamer0.10-ffmpeg gstreamer0.10-plugins-good packages.

Thanks! This saves us from needing to use Flash or encode videos in multiple formats. And that let's us get back to making more videos :). But as always, please feel free to message us.

There are two things that we can do from our DashboardController. The first is to configure the dashboard itself, which is mostly just the title, menu links, and also controlling the user menu. The second is that we can configure things that affect the CRUD controllers. And we saw an example of that with configureActions() where we globally added a DETAIL action to every index page.

That was an awesome start, but let's look a bit more at some ways that we can configure the dashboard itself.

Linking to the Frontend

The configureMenuItems() method, as we already know, can link to a dashboard and another CRUD section. Now let's add a link to the homepage. Say yield MenuItem::linkToRoute(), passing "Homepage", an icon... and then the route name and optionally route parameters. The name of our homepage route is app_homepage:

... lines 1 - 18
class DashboardController extends AbstractDashboardController
{
... lines 21 - 33
public function configureMenuItems(): iterable
{
... lines 36 - 40
yield MenuItem::linkToRoute('Homepage', 'fas fa-home', 'app_homepage');
}
... lines 43 - 48
}

Cool. Head over, refresh and... yea! We now have a nice homepage link on every page. And if we click, it works!

linkToRoute() vs linkToUrl()

But whoa... check out the URL! That does not look like the homepage. The URL starts with /admin and then has a bunch of query parameters. Yup, it's rendering our homepage controller through the admin dashboard... much like how our CRUD controllers render through the dashboard. This works, but it's not what we intended.

So let's try again. The linkToRoute() method really means:

Link to somewhere... but run that controller through the admin section.

This can be useful if you have a custom controller but want to leverage some of the EasyAdmin tools from inside it. If you just want to link to a page, use linkToUrl() instead. This will have the same label and icon... but instead of passing the route name, say $this->generateUrl() and pass app_homepage:

... lines 1 - 18
class DashboardController extends AbstractDashboardController
{
... lines 21 - 33
public function configureMenuItems(): iterable
{
... lines 36 - 40
yield MenuItem::linkToUrl('Homepage', 'fas fa-home', $this->generateUrl('app_homepage'));
}
... lines 43 - 48
}

Go back to the admin page, refresh... click and... much better!

Linking to the Admin from the Frontend

But what about a link back to the admin page, like up here in the header? For that, open templates/base.html.twig and scroll down to the navbar... here it is.

There's nothing here yet. Add <li class="navbar-nav></li> and a few other classes. Inside, add an <a> with href="" set to path(). To link to the admin section, there's nothing special. Our DashboardController has a real route, and its name is admin:

... lines 1 - 18
class DashboardController extends AbstractDashboardController
{
... line 21
#[Route('/admin', name: 'admin')]
public function index(): Response
{
... line 25
}
... lines 27 - 48
}

So we can just link to that: admin. Give our anchor a class to make it look good... and I'll say "Admin":

... line 1
<html lang="en">
... lines 3 - 14
<body>
<nav
class="navbar navbar-expand-lg navbar-light bg-light px-1"
{{ is_granted('ROLE_PREVIOUS_ADMIN') ? 'style="background-color: red !important"' }}
>
<div class="container-fluid">
... lines 21 - 30
<div class="collapse navbar-collapse" id="navbar-collapsable">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
... line 33
<li class="nav-item">
<a class="nav-link" href="{{ path('admin') }}">Admin</a>
</li>
... line 37
</ul>
... lines 39 - 72
</div>
</div>
</nav>
... lines 76 - 81
</body>
</html>

And we really only want to render this link if we have ROLE_ADMIN. So {% if isGranted('ROLE_ADMIN') %}... then {% endif %} on the other side:

... line 1
<html lang="en">
... lines 3 - 14
<body>
<nav
class="navbar navbar-expand-lg navbar-light bg-light px-1"
{{ is_granted('ROLE_PREVIOUS_ADMIN') ? 'style="background-color: red !important"' }}
>
<div class="container-fluid">
... lines 21 - 30
<div class="collapse navbar-collapse" id="navbar-collapsable">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
{% if is_granted('ROLE_ADMIN') %}
<li class="nav-item">
<a class="nav-link" href="{{ path('admin') }}">Admin</a>
</li>
{% endif %}
</ul>
... lines 39 - 72
</div>
</div>
</nav>
... lines 76 - 81
</body>
</html>

Beautiful! Let's test it. Refresh... there's the link and... we're right back on our admin section!

Customizing the User Menu

One other thing that the dashboard controls is this nice little user menu up here. It shows who you're logged in as, an avatar that doesn't work yet, and a "Sign out" link. In our system, users actually do have avatars on the frontend. You can see this: this is an avatar for a user... and my user's avatar shows up in the upper right. But EasyAdmin doesn't know that our users have avatars. We need to tell it.

Back in DashboardController... it doesn't matter where... go to "Code"->"Generate..." or Cmd+N on a Mac, click "Override Methods", and select configureUserMenu():

... lines 1 - 13
use EasyCorp\Bundle\EasyAdminBundle\Config\UserMenu;
... lines 15 - 18
use Symfony\Component\Security\Core\User\UserInterface;
class DashboardController extends AbstractDashboardController
{
... lines 23 - 48
public function configureUserMenu(UserInterface $user): UserMenu
{
return parent::configureUserMenu($user);
}
... lines 53 - 58
}

This has several methods on it. We can add other menu items (we'll do that in second), set the avatar URL, and a few other things. I'll say setAvatarUrl() and pass it $user->getAvatarUrl(): this is a custom method on our User class:

... lines 1 - 20
class DashboardController extends AbstractDashboardController
{
... lines 23 - 48
public function configureUserMenu(UserInterface $user): UserMenu
{
return parent::configureUserMenu($user)
->setAvatarUrl($user->getAvatarUrl());
}
... lines 54 - 59
}

Notice that I'm not getting auto-completion on the method. That's because PhpStorm doesn't know that this is our custom User class. So if you want to code defensively, add if (!$user instanceof User), then throw new \Exception('Wrong user');.

use App\Entity\User;

class DashboardController extends AbstractDashboardController
{
    public function configureUserMenu(UserInterface $user): UserMenu
    {
        if (!$user instanceof User) {
            throw new \Exception('Wrong user');
        }

        return parent::configureUserMenu($user)
            ->setAvatarUrl($user->getAvatarUrl());
    }
}

Tip

Or you can just add a PHPDoc instead that will help PhpStorm to show more methods for autocompletion:

... lines 1 - 7
use App\Entity\User;
... lines 9 - 18
use Symfony\Component\Security\Core\User\UserInterface;
class DashboardController extends AbstractDashboardController
{
... lines 23 - 45
/**
* @param UserInterface|User $user
*/
public function configureUserMenu(UserInterface $user): UserMenu
{
return parent::configureUserMenu($user)
->setAvatarUrl($user->getAvatarUrl());
}
... lines 54 - 59
}

That won't ever happen, but now if I retype $user->getAvatarUrl()... that fixes it!

And when we refresh... perfect! We have an avatar!

The last thing I want to add is a link on the user menu that goes to my profile. We noticed before that another method you can call is setMenuItems()... where you pass it in array of MenuItem objects. These items are the same ones that we've been building in configureMenuItems(). So we can say, for example, MenuItem::linkToUrl with "My Profile"... some icons, and then $this->generateUrl(). The name of the route for my profile page is app_profile_show:

... lines 1 - 20
class DashboardController extends AbstractDashboardController
{
... lines 23 - 48
public function configureUserMenu(UserInterface $user): UserMenu
{
return parent::configureUserMenu($user)
... line 52
->addMenuItems([
MenuItem::linkToUrl('My Profile', 'fas fa-user', $this->generateUrl('app_profile_show'))
]);
}
... lines 57 - 62
}

That's it! Refresh and... new link! Click and... that works too!

So there's nothing too complicated here: we can very easily control all of the menus in the admin.

So next, let's talk about assets inside of EasyAdmin. This is how we can add custom CSS and custom JavaScript to any section, including assets that are processed through Webpack Encore.

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