Skip to content

CRUD Controllers

Edit this page

CRUD controllers provide the CRUD operations (create, show, update, delete) for Doctrine ORM entities. Each CRUD controller can be associated to one or more dashboards.

Technically, these CRUD controllers 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().

CRUD controllers must implement the EasyCorp\Bundle\EasyAdminBundle\Contracts\Controller\CrudControllerInterface, which ensures that certain methods are defined in the controller. Instead of implementing the interface, you can also extend from the AbstractCrudController class. Run the following command to generate the basic structure of a CRUD controller:

1
$ php bin/console make:admin:crud

CRUD Controller Pages

The four main pages of the CRUD controllers are:

  • index, displays a list of entities which can be paginated, sorted by column and refined with search queries and filters;
  • detail, displays the contents of a given entity;
  • new, allows to create new entity instances;
  • edit, allows to update any property of a given entity.

These pages are generated with four actions with the same name in the AbstractCrudController controller. This controller defines other secondary actions (e.g. delete and autocomplete) which don't match any page.

The default behavior of these actions in the AbstractCrudController is appropriate for most backends, but you can customize it in several ways: EasyAdmin events, custom EasyAdmin templates, etc.

Page Names and Constants

Some methods require as argument the name of some CRUD page. You can use any of the following strings: 'index', 'detail', 'edit' and 'new'. If you prefer to use constants for these values, use Crud::PAGE_INDEX, Crud::PAGE_DETAIL, Crud::PAGE_EDIT and Crud::PAGE_NEW (they are defined in the EasyCorp\Bundle\EasyAdminBundle\Config\Crud class).

CRUD Controller Configuration

The only mandatory config option of a CRUD controller is the FQCN of the Doctrine entity being managed by the controller. This is defined as a public static method:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
namespace App\Controller\Admin;

use App\Entity\Product;
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController;

class ProductCrudController extends AbstractCrudController
{
    // it must return a FQCN (fully-qualified class name) of a Doctrine ORM entity
    public static function getEntityFqcn(): string
    {
        return Product::class;
    }

    // ...
}

The rest of CRUD options are configured using the configureCrud() method:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
namespace App\Controller\Admin;

use EasyCorp\Bundle\EasyAdminBundle\Config\Crud;
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController;

class ProductCrudController extends AbstractCrudController
{
    // ...

    public function configureCrud(Crud $crud): Crud
    {
        return $crud
            ->setEntityLabelInSingular('...')
            ->setDateFormat('...')
            // ...
        ;
    }
}

Design Options

1
2
3
4
5
6
7
8
9
10
11
12
public function configureCrud(Crud $crud): Crud
{
    return $crud
        // 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()
    ;
}

Entity Options

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public function configureCrud(Crud $crud): Crud
{
    return $crud
        // the labels used to refer to this entity in titles, buttons, etc.
        ->setEntityLabelInSingular('Product')
        ->setEntityLabelInPlural('Products')

        // in addition to a string, the argument of the singular and plural label methods
        // can be a closure that defines two nullable arguments: entityInstance (which will
        // be null in 'index' and 'new' pages) and the current page name
        ->setEntityLabelInSingular(
            fn (?Product $product, ?string $pageName) => $product ? $product->toString() : 'Product'
        )
        ->setEntityLabelInPlural(function (?Category $category, ?string $pageName) {
            return 'edit' === $pageName ? $category->getLabel() : 'Categories';
        })

        // the Symfony Security permission needed to manage the entity
        // (none by default, so you can manage all instances of the entity)
        ->setEntityPermission('ROLE_EDITOR')
    ;
}

Title and Help Options

By default, the page titles of the index and new pages are based on the entity option values defined with the setEntityLabelInSingular() and setEntityLabelInPlural() methods. In the detail and edit pages, EasyAdmin tries first to convert the entity into a string representation and falls back to a generic title otherwise.

You can override the default page titles with the following methods:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public function configureCrud(Crud $crud): Crud
{
    return $crud
        // the visible title at the top of the page and the content of the <title> element
        // it can include these placeholders:
        //   %entity_name%, %entity_as_string%,
        //   %entity_id%, %entity_short_id%
        //   %entity_label_singular%, %entity_label_plural%
        ->setPageTitle('index', '%entity_label_plural% listing')

        // you can pass a PHP closure as the value of the title
        ->setPageTitle('new', fn () => new \DateTime('now') > new \DateTime('today 13:00') ? 'New dinner' : 'New lunch')

        // in DETAIL and EDIT pages, the closure receives the current entity
        // as the first argument
        ->setPageTitle('detail', fn (Product $product) => (string) $product)
        ->setPageTitle('edit', fn (Category $category) => sprintf('Editing <b>%s</b>', $category->getName()))

        // the help message displayed to end users (it can contain HTML tags)
        ->setHelp('edit', '...')
    ;
}

Date, Time and Number Formatting Options

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public function configureCrud(Crud $crud): Crud
{
    return $crud
        // the argument must be either one of these strings: 'short', 'medium', 'long', 'full', 'none'
        // (the strings are also available as \EasyCorp\Bundle\EasyAdminBundle\Field\DateTimeField::FORMAT_* constants)
        // or a valid ICU Datetime Pattern (see https://unicode-org.github.io/icu/userguide/format_parse/datetime/)
        ->setDateFormat('...')
        ->setTimeFormat('...')

        // first argument = datetime pattern or date format; second optional argument = time format
        ->setDateTimeFormat('...', '...')

        ->setDateIntervalFormat('%%y Year(s) %%m Month(s) %%d Day(s)')
        ->setTimezone('...')

        // this option makes numeric values to be rendered with a sprintf()
        // call using this value as the first argument.
        // this option overrides any formatting option for all numeric values
        // (e.g. setNumDecimals(), setRoundingMode(), etc. are ignored)
        // NumberField and IntegerField can override this value with their
        // own setNumberFormat() methods, which works in the same way
        ->setNumberFormat('%.2d');
    ;
}

Search, Order, and Pagination Options

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
public function configureCrud(Crud $crud): Crud
{
    return $crud
        // the names of the Doctrine entity properties where the search is made on
        // (by default it looks for in all properties)
        ->setSearchFields(['name', 'description'])
        // use dots (e.g. 'seller.email') to search in Doctrine associations
        ->setSearchFields(['name', 'description', 'seller.email', 'seller.address.zipCode'])
        // set it to null to disable and hide the search box
        ->setSearchFields(null)

        // defines the initial sorting applied to the list of entities
        // (user can later change this sorting by clicking on the table columns)
        ->setDefaultSort(['id' => 'DESC'])
        ->setDefaultSort(['id' => 'DESC', 'title' => 'ASC', 'startsAt' => 'DESC'])
        // you can sort by Doctrine associations up to two levels
        ->setDefaultSort(['seller.name' => 'ASC'])

        // the max number of entities to display per page
        ->setPaginatorPageSize(30)
        // the number of pages to display on each side of the current page
        // e.g. if num pages = 35, current page = 7 and you set ->setPaginatorRangeSize(4)
        // the paginator displays: [Previous]  1 ... 3  4  5  6  [7]  8  9  10  11 ... 35  [Next]
        // set this number to 0 to display a simple "< Previous | Next >" pager
        ->setPaginatorRangeSize(4)

        // these are advanced options related to Doctrine Pagination
        // (see https://www.doctrine-project.org/projects/doctrine-orm/en/2.7/tutorials/pagination.html)
        ->setPaginatorUseOutputWalkers(true)
        ->setPaginatorFetchJoinCollection(true)
    ;
}

Note

When using Doctrine filters, listings may not include some items because they were removed by those global Doctrine filters. Use the dashboard route name to not apply the filters when the request URL belongs to the dashboard You can also get the dashboard route name via the application context variable.

The default Doctrine query executed to get the list of entities displayed in the index page takes into account the sorting configuration, the optional search query, the optional filters and the pagination. If you need to fully customize this query, override the createIndexQueryBuilder() method in your CRUD controller.

Templates and Form Options

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
public function configureCrud(Crud $crud): Crud
{
    return $crud
        // this method allows to use your own template to render a certain part
        // of the backend instead of using EasyAdmin default template
        // the first argument is the "template name", which is the same as the
        // Twig path but without the `@EasyAdmin/` prefix and the `.html.twig` suffix
        ->overrideTemplate('crud/field/id', 'admin/fields/my_id.html.twig')

        // the theme/themes to use when rendering the forms of this entity
        // (in addition to EasyAdmin default theme)
        ->addFormTheme('foo.html.twig')
        // this method overrides all existing form themes (including the
        // default EasyAdmin form theme)
        ->setFormThemes(['my_theme.html.twig', 'admin.html.twig'])

        // this sets the options of the entire form (later, you can set the options
        // of each form type via the methods of their associated fields)
        // pass a single array argument to apply the same options for the new and edit forms
        ->setFormOptions([
            'validation_groups' => ['Default', 'my_validation_group']
        ]);

        // pass two array arguments to apply different options for the new and edit forms
        // (pass an empty array argument if you want to apply no options to some form)
        ->setFormOptions(
            ['validation_groups' => ['my_validation_group']],
            ['validation_groups' => ['Default'], '...' => '...'],
        );
    ;
}

Custom Redirect After Creating or Editing Entities

By default, when clicking on "Save" button when creating or editing entities you are redirected to the previous page. If you want to change this behavior, override the getRedirectResponseAfterSave() method.

For example, if you've added a custom action called "Save and view detail", you may prefer to redirect to the detail page after saving the changes:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
protected function getRedirectResponseAfterSave(AdminContext $context, string $action): RedirectResponse
{
    $submitButtonName = $context->getRequest()->request->all()['ea']['newForm']['btn'];

    if ('saveAndViewDetail' === $submitButtonName) {
        $url = $this->get(AdminUrlGenerator::class)
            ->setAction(Action::DETAIL)
            ->setEntityId($context->getEntity()->getPrimaryKeyValue())
            ->generateUrl();

        return $this->redirect($url);
    }

    return parent::getRedirectResponseAfterSave($context, $action);
}

Same Configuration in Different CRUD Controllers

If you want to do the same config in all CRUD controllers, there's no need to repeat the config in each controller. Instead, add the configureCrud() method in your dashboard and all controllers will inherit that configuration:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
use EasyCorp\Bundle\EasyAdminBundle\Config\Crud;
use EasyCorp\Bundle\EasyAdminBundle\Config\Dashboard;
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractDashboardController;

class DashboardController extends AbstractDashboardController
{
    // ...

    public function configureCrud(): Crud
    {
        return Crud::new()
            // this defines the pagination size for all CRUD controllers
            // (each CRUD controller can override this value if needed)
            ->setPaginatorPageSize(30)
        ;
    }
}

Fields

Fields allow to display the contents of your Doctrine entities on each CRUD page. EasyAdmin provides built-in fields to display all the common data types, but you can also create your own fields.

If your CRUD controller extends from the AbstractCrudController, the fields are configured automatically. In the index page you'll see a few fields and in the rest of pages you'll see as many fields as needed to display all the properties of your Doctrine entity.

Read the chapter about Fields to learn how to configure which fields to display on each page, how to configure the way each field is rendered, etc.

Customizing CRUD Actions

The default CRUD actions (index(), detail(), edit(), new() and delete() methods in the controller) implement the most common behaviors used in applications.

The first way to customize their behavior is to override those methods in your own controllers. However, the original actions are so generic that they contain quite a lot of code, so overriding them is not that convenient.

Instead, you can override other smaller methods that implement certain features needed by the CRUD actions. For example, the index() action calls to a method named createIndexQueryBuilder() to create the Doctrine query builder used to get the results displayed on the index listing. If you want to customize that listing, it's better to override the createIndexQueryBuilder() method instead of the entire index() method. There are many of these methods, so you should check the EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController class.

The other alternative to customize CRUD actions is to use the events triggered by EasyAdmin, such as BeforeCrudActionEvent and AfterCrudActionEvent.

Creating, Persisting and Deleting Entities

Most of the actions of a CRUD controller end up creating, persisting or deleting entities. If your CRUD controller extends from the AbstractCrudController, these methods are already implemented, but you can customize them overriding methods and listening to events.

First, you can override the createEntity(), updateEntity(), persistEntity() and deleteEntity() methods. The createEntity() method for example only executes return new $entityFqcn(), so you need to override it if your entity needs to pass constructor arguments or set some of its properties:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
namespace App\Controller\Admin;

use App\Entity\Product;
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController;

class ProductCrudController extends AbstractCrudController
{
    public static function getEntityFqcn(): string
    {
        return Product::class;
    }

    public function createEntity(string $entityFqcn)
    {
        $product = new Product();
        $product->createdBy($this->getUser());

        return $product;
    }

    // ...
}

The other way of overriding this behavior is listening to the events triggered by EasyAdmin when an entity is created, updated, persisted, deleted, etc.

Passing Additional Variables to CRUD Templates

The default CRUD actions implemented in AbstractCrudController don't end with the usual $this->render('...') instruction to render a Twig template and return its contents in a Symfony Response object.

Instead, CRUD actions return a EasyCorp\Bundle\EasyAdminBundle\Config\KeyValueStore object with the variables passed to the template that renders the CRUD action contents. This KeyValueStore object is similar to Symfony's ParameterBag object. It's like an object-oriented array with useful methods such as get(), set(), has(), etc.

Before ending each CRUD action, their KeyValueStore object is passed to a method called configureResponseParameters() which you can override in your own controller to add/remove/change those template variables:

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
namespace App\Controller\Admin;

use App\Entity\Product;
use EasyCorp\Bundle\EasyAdminBundle\Config\Crud;
use EasyCorp\Bundle\EasyAdminBundle\Config\KeyValueStore;
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController;

class ProductCrudController extends AbstractCrudController
{
    // ...

    public function configureResponseParameters(KeyValueStore $responseParameters): KeyValueStore
    {
        if (Crud::PAGE_DETAIL === $responseParameters->get('pageName')) {
            $responseParameters->set('foo', '...');

            // keys support the "dot notation", so you can get/set nested
            // values separating their parts with a dot:
            $responseParameters->setIfNotSet('bar.foo', '...');
            // this is equivalent to: $parameters['bar']['foo'] = '...'
        }

        return $responseParameters;
    }
}

You can add as many or as few parameters to this KeyValueStore object as you need. The only mandatory parameter is either templateName or templatePath to set respectively the name or path of the template to render as the result of the CRUD action.

Template Names and Template Paths

All the templates used by EasyAdmin to render its contents are configurable. That's why EasyAdmin deals with "template names" instead of normal Twig template paths.

A template name is the same as the template path but without the @EasyAdmin prefix and the .html.twig suffix. For example, @EasyAdmin/layout.html.twig refers to the built-in layout template provided by EasyAdmin. However, layout refers to "whichever template is configured as the layout in the application".

Working with template names instead of paths gives you full flexibility to customize the application behavior while keeping all the customized templates. In Twig templates, use the ea.templatePath() function to get the Twig path associated to the given template name:

1
2
3
4
5
6
7
<div id="flash-messages">
    {{ include(ea.templatePath('flash_messages')) }}
</div>

{% if some_value is null %}
    {{ include(ea.templatePath('label/null')) }}
{% endif %}

Generating Admin URLs

3.2

The AdminUrlGenerator class was introduced in EasyAdmin 3.2.0. In earlier versions, you had to use the CrudUrlGenerator class and call the build() method to start building a URL.

As explained in the article about Dashboards, all URLs of a given dashboard use the same route and they only differ in the query string parameters. Instead of having to deal with that, you can use the AdminUrlGenerator service to generate URLs in your PHP code.

When generating a URL, you don't start from scratch. EasyAdmin reuses all the query parameters existing in the current request. This is done on purpose because generating new URLs based on the current URL is the most common scenario. Use the unsetAll() method to remove all existing query parameters:

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
namespace App\Controller\Admin;

use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController;
use EasyCorp\Bundle\EasyAdminBundle\Router\AdminUrlGenerator;

class SomeCrudController extends AbstractCrudController
{
    private $adminUrlGenerator;

    public function __construct(AdminUrlGenerator $adminUrlGenerator)
    {
        $this->adminUrlGenerator = $adminUrlGenerator;
    }

    // ...

    public function someMethod()
    {
        // instead of injecting the AdminUrlGenerator service in the constructor,
        // you can also get it from inside a controller action as follows:
        // $adminUrlGenerator = $this->get(AdminUrlGenerator::class);

        // the existing query parameters are maintained, so you only
        // have to pass the values you want to change.
        $url = $this->adminUrlGenerator->set('page', 2)->generateUrl();

        // you can remove existing parameters
        $url = $this->adminUrlGenerator->unset('menuIndex')->generateUrl();
        $url = $this->adminUrlGenerator->unsetAll()->set('foo', 'someValue')->generateUrl();

        // the URL builder provides shortcuts for the most common parameters
        $url = $this->adminUrlGenerator
            ->setController(SomeCrudController::class)
            ->setAction('theActionName')
            ->generateUrl();

        // ...
    }
}

Tip

If you need to deal with the admin URLs manually for any reason, the names of the query string parameters are defined as constants in the EA class.

The exact same features are available in templates thanks to the ea_url() Twig function. In templates you can omit the call to the generateUrl() method (it will be called automatically for you):

1
2
3
4
5
6
7
8
9
{# both are equivalent #}
{% set url = ea_url({ page: 2 }).generateUrl() %}
{% set url = ea_url({ page: 2 }) %}

{% set url = ea_url().set('page', 2) %}

{% set url = ea_url()
    .setController('App\\Controller\\Admin\\SomeCrudController')
    .setAction('theActionName') %}

Generating CRUD URLs from outside EasyAdmin

When generating URLs of EasyAdmin pages from outside EasyAdmin (e.g. from a regular Symfony controller) the admin context variable is not available. That's why you must always set the CRUD controller associated to the URL. If you have more than one dashboard, you must also set the Dashboard:

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
use App\Controller\Admin\DashboardController;
use App\Controller\Admin\ProductCrudController;
use EasyCorp\Bundle\EasyAdminBundle\Config\Action;
use EasyCorp\Bundle\EasyAdminBundle\Router\AdminUrlGenerator;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;

class SomeSymfonyController extends AbstractController
{
    private $adminUrlGenerator;

    public function __construct(AdminUrlGenerator $adminUrlGenerator)
    {
        $this->adminUrlGenerator = $adminUrlGenerator;
    }

    public function someMethod()
    {
        // if your application only contains one Dashboard, it's enough
        // to define the controller related to this URL
        $url = $this->adminUrlGenerator
            ->setController(ProductCrudController::class)
            ->setAction(Action::INDEX)
            ->generateUrl();

        // in applications containing more than one Dashboard, you must also
        // define the Dashboard associated to the URL
        $url = $this->adminUrlGenerator
            ->setDashboard(DashboardController::class)
            ->setController(ProductCrudController::class)
            ->setAction(Action::INDEX)
            ->generateUrl();

        // some actions may require to pass additional parameters
        $url = $this->adminUrlGenerator
            ->setController(ProductCrudController::class)
            ->setAction(Action::EDIT)
            ->setEntityId($product->getId())
            ->generateUrl();

        // ...
    }
}

The same applies to URLs generated in Twig templates:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{# if your application defines only one Dashboard #}
{% set url = ea_url()
    .setController('App\\Controller\\Admin\\ProductCrudController')
    .setAction('index') %}
{# if you prefer PHP constants, use this:
   .setAction(constant('EasyCorp\\Bundle\\EasyAdminBundle\\Config\\Action::INDEX')) #}

{# if your application defines multiple Dashboards #}
{% set url = ea_url()
    .setDashboard('App\\Controller\\Admin\\DashboardController')
    .setController('App\\Controller\\Admin\\ProductCrudController')
    .setAction('index') %}

{# some actions may require to pass additional parameters #}
{% set url = ea_url()
    .setController('App\\Controller\\Admin\\ProductCrudController')
    .setAction('edit')
    .setEntityId(product.id) %}
This work, including the code samples, is licensed under a Creative Commons BY-SA 3.0 license.
TOC
    Version