Dashboards

Version: current
  • Maintained versions
Edit this page

Dashboards

Dashboards are the entry point of backends and they link to one or more resources. Dashboards also display a main menu to navigate the resources and the information of the logged in user.

Imagine that you have a simple application with three Doctrine entities: users, blog posts and categories. Your own employees can create and edit any of them but external collaborators can only create blog posts.

You can implement this in EasyAdmin as follows:

  • Create three CRUD controllers (e.g. UserCrudController, BlogPostCrudController and CategoryCrudController);
  • Create a dashboard for your employees (e.g. DashboardController) and link to the three resources;
  • Create a dashboard for your external collaborators (e.g. ExternalDashboardController) and link only to the BlogPostCrudController resource.

Technically, dashboards are regular Symfony controllers so you can do anything you usually do in a controller, such as injecting services and using shortcuts like $this->render() or $this->isGranted().

Dashboard controller classes must implement the EasyCorp\Bundle\EasyAdminBundle\Contracts\Controller\DashboardControllerInterface, which ensures that certain methods are defined in the dashboard. Instead of implementing the interface, you can also extend from the AbstractDashboardController class. Run the following command to quickly generate a dashboard controller:

1
$ php bin/console make:admin:dashboard

Dashboard Route

Each dashboard uses a single Symfony route to serve all its URLs. The needed information is passed using query string parameters. If you generated the dashboard with the make:admin:dashboard command, the route is defined using Symfony route annotations or PHP attributes (if the project requires PHP 8 or newer):

  • Annotations
  • Attributes
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// src/Controller/Admin/DashboardController.php
namespace App\Controller\Admin;

use EasyCorp\Bundle\EasyAdminBundle\Config\Dashboard;
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractDashboardController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;

class DashboardController extends AbstractDashboardController
{
    /**
     * @Route("/admin")
     */
    public function index(): Response
    {
        return parent::index();
    }

    // ...
}

The /admin URL is only a default value, so you can change it. If you do that, don't forget to also update this value in your Symfony security config to restrict access to the entire backend.

There's no need to define an explicit name for this route. Symfony autogenerates a route name and EasyAdmin gets that value at runtime to generate all URLs. However, if you generate URLs pointing to the dashboard in other parts of your application, you can define an explicit route name to simplify your code:

  • Annotations
  • Attributes
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// src/Controller/Admin/DashboardController.php
namespace App\Controller\Admin;

use EasyCorp\Bundle\EasyAdminBundle\Config\Dashboard;
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractDashboardController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;

class DashboardController extends AbstractDashboardController
{
    /**
     * @Route("/admin", name="some_route_name")
     */
    public function index(): Response
    {
        return parent::index();
    }

    // ...
}

If you don't use annotations, you must configure the dashboard route using YAML, XML or PHP config in a separate file:

  • YAML
  • XML
  • PHP
1
2
3
4
5
6
# config/routes.yaml
dashboard:
    path: /admin
    controller: App\Controller\Admin\DashboardController::index

# ...

In practice you won't have to deal with this route or the query string parameters in your application because EasyAdmin provides a service to generate admin URLs.

Note

Using a single route to handle all backend URLs means that generated URLs are a bit long and ugly. This is a reasonable trade-off because it makes many other features, such as generating admin URLs, much simpler.

Dashboard Configuration

The dashboard configuration is defined in the configureDashboard() method (the main menu and the user menu are configured in their own methods, as explained later):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
namespace App\Controller\Admin;

use EasyCorp\Bundle\EasyAdminBundle\Config\Dashboard;
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractDashboardController;

class DashboardController extends AbstractDashboardController
{
    // ...

    public function configureDashboard(): Dashboard
    {
        return Dashboard::new()
            // the name visible to end users
            ->setTitle('ACME Corp.')
            // you can include HTML contents too (e.g. to link to an image)
            ->setTitle('<img src="..."> ACME <span class="text-small">Corp.</span>')

            // the path defined in this method is passed to the Twig asset() function
            ->setFaviconPath('favicon.svg')

            // the domain used by default is 'messages'
            ->setTranslationDomain('my-custom-domain')

            // there's no need to define the "text direction" explicitly because
            // its default value is inferred dynamically from the user locale
            ->setTextDirection('ltr')

            // set this option if you prefer the page content to span the entire
            // browser width, instead of the default design which sets a max width
            ->renderContentMaximized()

            // set this option if you prefer the sidebar (which contains the main menu)
            // to be displayed as a narrow column instead of the default expanded design
            ->renderSidebarMinimized()

            // by default, all backend URLs include a signature hash. If a user changes any
            // query parameter (to "hack" the backend) the signature won't match and EasyAdmin
            // triggers an error. If this causes any issue in your backend, call this method
            // to disable this feature and remove all URL signature checks
            ->disableUrlSignatures()

            // by default, all backend URLs are generated as absolute URLs. If you
            // need to generate relative URLs instead, call this method
            ->generateRelativeUrls()
        ;
    }
}

The main menu links to different CRUD controllers from the dashboard. It's the only way to associate dashboards and resources. For security reasons, a backend can only access to the resources associated to the dashboard via the main menu.

The main menu is a collection of objects implementing EasyCorp\Bundle\EasyAdminBundle\Contracts\Menu\MenuInterface that configure the look and behavior of each menu item:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
use App\Entity\BlogPost;
use App\Entity\Category;
use App\Entity\Comment;
use App\Entity\User;
use EasyCorp\Bundle\EasyAdminBundle\Config\Dashboard;
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractDashboardController;

class DashboardController extends AbstractDashboardController
{
    // ...

    public function configureMenuItems(): iterable
    {
        return [
            MenuItem::linkToDashboard('Dashboard', 'fa fa-home'),

            MenuItem::section('Blog'),
            MenuItem::linkToCrud('Categories', 'fa fa-tags', Category::class),
            MenuItem::linkToCrud('Blog Posts', 'fa fa-file-text', BlogPost::class),

            MenuItem::section('Users'),
            MenuItem::linkToCrud('Comments', 'fa fa-comment', Comment::class),
            MenuItem::linkToCrud('Users', 'fa fa-user', User::class),
        ];
    }
}

The first argument of MenuItem::new() is the label displayed by the item and the second argument is the full CSS class of the FontAwesome icon to display.

All menu items define the following methods to configure some options:

  • setCssClass(string $cssClass), sets the CSS class or classes applied to the <li> parent element of the menu item;
  • setLinkRel(string $rel), sets the rel HTML attribute of the menu item link (check out the allowed values for the "rel" attribute);
  • setLinkTarget(string $target), sets the target HTML attribute of the menu item link (_self by default);
  • setPermission(string $permission), sets the Symfony security permission that the user must have to see this menu item. Read the menu security reference for more details.

The rest of options depend on each menu item type, as explained in the next sections.

CRUD Menu Item

This is the most common menu item type and it links to some action of some CRUD controller. Instead of passing the FQCN (fully-qualified class name) of the CRUD controller, you must pass the FQCN of the Doctrine entity associated to the CRUD controller:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
use App\Entity\Category;
use EasyCorp\Bundle\EasyAdminBundle\Config\MenuItem;

public function configureMenuItems(): iterable
{
    return [
        // ...

        // links to the 'index' action of the Category CRUD controller
        MenuItem::linkToCrud('Categories', 'fa fa-tags', Category::class),

        // links to a different CRUD action
        MenuItem::linkToCrud('Add Category', 'fa fa-tags', Category::class)
            ->setAction('new'),

        MenuItem::linkToCrud('Show Main Category', 'fa fa-tags', Category::class)
            ->setAction('detail')
            ->setEntityId(1),

        // if the same Doctrine entity is associated to more than one CRUD controller,
        // use the 'setController()' method to specify which controller to use
        MenuItem::linkToCrud('Categories', 'fa fa-tags', Category::class)
            ->setController(LegacyCategoryCrudController::class),

        // uses custom sorting options for the listing
        MenuItem::linkToCrud('Categories', 'fa fa-tags', Category::class)
            ->setDefaultSort(['createdAt' => 'DESC']),
    ];
}

Dashboard Menu Item

It links to the homepage of the current dashboard. You can achieve the same with a "route menu item" (explained below) but this one is simpler because you don't have to specify the route name (it's found automatically):

1
2
3
4
5
6
7
8
9
use EasyCorp\Bundle\EasyAdminBundle\Config\MenuItem;

public function configureMenuItems(): iterable
{
    return [
        MenuItem::linkToDashboard('Home', 'fa fa-home'),
        // ...
    ];
}

Route Menu Item

It links to any of the routes defined by your Symfony application:

1
2
3
4
5
6
7
8
9
10
use EasyCorp\Bundle\EasyAdminBundle\Config\MenuItem;

public function configureMenuItems(): iterable
{
    return [
        MenuItem::linkToRoute('The Label', 'fa ...', 'route_name'),
        MenuItem::linkToRoute('The Label', 'fa ...', 'route_name', ['routeParamName' => 'routeParamValue']),
        // ...
    ];
}

Note

Read the section about integrating Symfony controllers/actions in EasyAdmin to fully understand the URLs generated by linkToRoute().

URL Menu Item

It links to a relative or absolute URL:

1
2
3
4
5
6
7
8
9
10
use EasyCorp\Bundle\EasyAdminBundle\Config\MenuItem;

public function configureMenuItems(): iterable
{
    return [
        MenuItem::linkToUrl('Visit public website', null, '/'),
        MenuItem::linkToUrl('Search in Google', 'fab fa-google', 'https://google.com'),
        // ...
    ];
}

To avoid leaking internal backend information to external websites, EasyAdmin adds the rel="noreferrer" attribute to all URL menu items, except if the menu item defines its own rel option.

Section Menu Item

It creates a visual separation between menu items and can optionally display a label which acts as the title of the menu items below:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
use EasyCorp\Bundle\EasyAdminBundle\Config\MenuItem;

public function configureMenuItems(): iterable
{
    return [
        // ...

        MenuItem::section(),
        // ...

        MenuItem::section('Blog'),
        // ...
    ];
}

Logout Menu Item

It links to the URL that the user must visit to log out from the application. If you know the logout route name, you can achieve the same with the "route menu item", but this one is more convenient because it finds the logout URL for the current security firewall automatically:

1
2
3
4
5
6
7
8
9
use EasyCorp\Bundle\EasyAdminBundle\Config\MenuItem;

public function configureMenuItems(): iterable
{
    return [
        // ...
        MenuItem::linkToLogout('Logout', 'fa fa-exit'),
    ];
}

Note

The logout menu item will not work under certain authentication schemes like HTTP Basic because they do not have a default logout path configured due to the nature of how those authentication schemes work.

If you encounter an error like "Unable to find the current firewall LogoutListener, please provide the provider key manually.", you'll need to remove the logout menu item or add a logout provider to your authentication scheme.

Exit Impersonation Menu Item

It links to the URL that the user must visit to stop impersonating other users:

1
2
3
4
5
6
7
8
9
use EasyCorp\Bundle\EasyAdminBundle\Config\MenuItem;

public function configureMenuItems(): iterable
{
    return [
        // ...
        MenuItem::linkToExitImpersonation('Stop impersonation', 'fa fa-exit'),
    ];
}

The main menu can display up to two level nested menus. Submenus are defined using the subMenu() item type:

1
2
3
4
5
6
7
8
9
10
11
12
13
use EasyCorp\Bundle\EasyAdminBundle\Config\MenuItem;

public function configureMenuItems(): iterable
{
    return [
        MenuItem::subMenu('Blog', 'fa fa-article')->setSubItems([
            MenuItem::linkToCrud('Categories', 'fa fa-tags', Category::class),
            MenuItem::linkToCrud('Posts', 'fa fa-file-text', BlogPost::class),
            MenuItem::linkToCrud('Comments', 'fa fa-comment', Comment::class),
        ]),
        // ...
    ];
}

Note

In a submenu, the parent menu item cannot link to any resource, route or URL; it can only expand/collapse the submenu items.

Complex Main Menus

The return type of the configureMenuItems() is iterable, so you don't have to always return an array. For example, if your main menu requires complex logic to decide which items to display for each user, it's more convenient to use a generator to return the menu items:

1
2
3
4
5
6
7
8
9
10
11
12
public function configureMenuItems(): iterable
{
    yield MenuItem::linkToDashboard('Dashboard', 'fa fa-home');

    if ('... some complex expression ...') {
        yield MenuItem::section('Blog');
        yield MenuItem::linkToCrud('Categories', 'fa fa-tags', Category::class);
        yield MenuItem::linkToCrud('Blog Posts', 'fa fa-file-text', BlogPost::class);
    }

    // ...
}

User Menu

When accessing a protected backend, EasyAdmin displays the details of the user who is logged in the application and a menu with some options like "logout" (if Symfony's logout feature is enabled).

The user name is the result of calling to the __toString() method on the current user object. The user avatar is a generic avatar icon. Use the configureUserMenu() method to configure the features and items of this menu:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
use EasyCorp\Bundle\EasyAdminBundle\Config\MenuItem;
use EasyCorp\Bundle\EasyAdminBundle\Config\UserMenu;
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractDashboardController;
use Symfony\Component\Security\Core\User\UserInterface;

class DashboardController extends AbstractDashboardController
{
    // ...

    public function configureUserMenu(UserInterface $user): UserMenu
    {
        // Usually it's better to call the parent method because that gives you a
        // user menu with some menu items already created ("sign out", "exit impersonation", etc.)
        // if you prefer to create the user menu from scratch, use: return UserMenu::new()->...
        return parent::configureUserMenu($user)
            // use the given $user object to get the user name
            ->setName($user->getFullName())
            // use this method if you don't want to display the name of the user
            ->displayUserName(false)

            // you can return an URL with the avatar image
            ->setAvatarUrl('https://...')
            ->setAvatarUrl($user->getProfileImageUrl())
            // use this method if you don't want to display the user image
            ->displayUserAvatar(false)
            // you can also pass an email address to use gravatar's service
            ->setGravatarEmail($user->getMainEmailAddress())

            // you can use any type of menu item, except submenus
            ->addMenuItems([
                MenuItem::linkToRoute('My Profile', 'fa fa-id-card', '...', ['...' => '...']),
                MenuItem::linkToRoute('Settings', 'fa fa-user-cog', '...', ['...' => '...']),
                MenuItem::section(),
                MenuItem::linkToLogout('Logout', 'fa fa-sign-out'),
            ]);
    }
}

Admin Context

EasyAdmin initializes a variable of type EasyCorp\Bundle\EasyAdminBundle\Context\AdminContext automatically on each backend request. This object implements the context object design pattern and stores all the information commonly needed in different parts of the backend.

This context object is automatically injected in every template as a variable called ea (the initials of "EasyAdmin"):

1
2
3
4
5
<h1>{{ ea.dashboardTitle }}</h1>

{% for menuItem in ea.mainMenu.items %}
    {# ... #}
{% endfor %}

The AdminContext variable is created dynamically on each request, so you can't inject it directly in your services. Instead, use the AdminContextProvider service to get the context variable:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
use EasyCorp\Bundle\EasyAdminBundle\Provider\AdminContextProvider;

final class SomeService
{
    private $adminContextProvider;

    public function __construct(AdminContextProvider $adminContextProvider)
    {
        $this->adminContextProvider = $adminContextProvider;
    }

    public function someMethod()
    {
        $context = $this->adminContextProvider->getContext();
    }

    // ...
}

In EasyAdmin's CRUD controllers and in Symfony controllers integrated into EasyAdmin, use the AdminContext type-hint in any argument where you want to inject the context object:

1
2
3
4
5
6
7
8
9
10
use EasyCorp\Bundle\EasyAdminBundle\Context\AdminContext;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;

class SomeController extends AbstractController
{
    public function someMethod(AdminContext $context)
    {
        // ...
    }
}

Translation

The backend interface is fully translated using the Symfony translation features. EasyAdmin own messages and contents use the EasyAdminBundle translation domain (thanks to our community for kindly providing translations for tens of languages).

The rest of the contents (e.g. the label of the menu items, entity and field names, etc.) use the messages translation domain by default. You can change this value with the translationDomain() method:

1
2
3
4
5
6
7
8
9
10
11
12
13
class DashboardController extends AbstractDashboardController
{
    // ...

    public function configureDashboard(): Dashboard
    {
        return Dashboard::new()
            // ...

            // the argument is the name of any valid Symfony translation domain
            ->setTranslationDomain('admin');
    }
}

The backend uses the same language configured in the Symfony application. When the locale is Arabic (ar), Persian (fa) or Hebrew (he), the HTML text direction is set to rtl (right-to-left) automatically. Otherwise, the text is displayed as ltr (left-to-right), but you can configure this value explicitly:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class DashboardController extends AbstractDashboardController
{
    // ...

    public function configureDashboard(): Dashboard
    {
        return Dashboard::new()
            // ...

            // most of the times there's no need to configure this explicitly
            // (default: 'rtl' or 'ltr' depending on the language)
            ->setTextDirection('rtl');
    }
}

Tip

If you want to make the backend use a different language than the public website, you'll need to work with the user locale to set the request locale before the translation service retrieves it.

Note

The contents stored in the database (e.g. the content of a blog post or the name of a product) are not translated. EasyAdmin does not support the translation of the entity property contents into different languages.

Page Templates

EasyAdmin provides several page templates which are useful when adding custom logic in your dashboards.

Login Form Template

Twig Template Path: @EasyAdmin/page/login.html.twig

It displays a simple username + password login form that matches the style of the rest of the backend. The template defines lots of config options, but most applications can rely on its default values:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;

class SecurityController extends AbstractController
{
    /**
     * @Route("/login", name="login")
     */
    public function login(AuthenticationUtils $authenticationUtils): Response
    {
        $error = $authenticationUtils->getLastAuthenticationError();
        $lastUsername = $authenticationUtils->getLastUsername();

        return $this->render('@EasyAdmin/page/login.html.twig', [
            // parameters usually defined in Symfony login forms
            'error' => $error,
            'last_username' => $lastUsername,

            // OPTIONAL parameters to customize the login form:

            // the translation_domain to use (define this option only if you are
            // rendering the login template in a regular Symfony controller; when
            // rendering it from an EasyAdmin Dashboard this is automatically set to
            // the same domain as the rest of the Dashboard)
            'translation_domain' => 'admin',

            // the title visible above the login form (define this option only if you are
            // rendering the login template in a regular Symfony controller; when rendering
            // it from an EasyAdmin Dashboard this is automatically set as the Dashboard title)
            'page_title' => 'ACME login',

            // the string used to generate the CSRF token. If you don't define
            // this parameter, the login form won't include a CSRF token
            'csrf_token_intention' => 'authenticate',

            // the URL users are redirected to after the login (default: '/admin')
            'target_path' => $this->generateUrl('admin_dashboard'),

            // the label displayed for the username form field (the |trans filter is applied to it)
            'username_label' => 'Your username',

            // the label displayed for the password form field (the |trans filter is applied to it)
            'password_label' => 'Your password',

            // the label displayed for the Sign In form button (the |trans filter is applied to it)
            'sign_in_label' => 'Log in',

            // the 'name' HTML attribute of the <input> used for the username field (default: '_username')
            'username_parameter' => 'my_custom_username_field',

            // the 'name' HTML attribute of the <input> used for the password field (default: '_password')
            'password_parameter' => 'my_custom_password_field',

            // whether to enable or not the "forgot password?" link (default: false)
            'forgot_password_enabled' => true,

            // the path (i.e. a relative or absolute URL) to visit when clicking the "forgot password?" link (default: '#')
            'forgot_password_path' => $this->generateUrl('...', ['...' => '...']),

            // the label displayed for the "forgot password?" link (the |trans filter is applied to it)
            'forgot_password_label' => 'Forgot your password?',

            // whether to enable or not the "remember me" checkbox (default: false)
            'remember_me_enabled' => true,

            // remember me name form field (default: '_remember_me')
            'remember_me_parameter' => 'custom_remember_me_param',

            // whether to check by default the "remember me" checkbox (default: false)
            'remember_me_checked' => true,

            // the label displayed for the remember me checkbox (the |trans filter is applied to it)
            'remember_me_label' => 'Remember me',
        ]);
    }
}

Content Page Template

Twig Template Path: @EasyAdmin/page/content.html.twig

It displays a simple page similar to the index/detail/form pages, with the main header, the sidebar menu and the central content section. The only difference is that the content section is completely empty, so it's useful to display your own contents and custom forms, to integrate Symfony actions inside EasyAdmin, etc. Example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
{# templates/admin/my-custom-page.html.twig #}
{% extends '@EasyAdmin/page/content.html.twig' %}

{% block content_title %}The Title of the Page{% endblock %}
{% block page_actions %}
    <a class="btn btn-primary" href="...">Some Action</a>
{% endblock %}

{% block main %}
    <table class="datagrid">
        <thead>
            <tr>
                <td>Some Column</td>
                <td>Another Column</td>
            </tr>
        </thead>
        <tbody>
            {% for data in my_own_data %}
                <tr>
                    <td>{{ data.someColumn }}</td>
                    <td>{{ data.anotherColumn }}</td>
                </tr>
            {% endfor %}
        </tbody>
    </table>
{% endblock %}
This work, including the code samples, is licensed under a Creative Commons BY-SA 3.0 license.