easycorp/easyadmin-bundle
EasyAdmin builds beautiful, responsive admin panels for Symfony apps with no CSS required. Run three commands to generate a dashboard and CRUD controllers, then manage your data through a modern UI with light and dark modes and multiple dashboards. Free and fully documented.
Installation
composer require easycorp/easyadmin-bundle
Register the bundle if it isn't auto-discovered:
// config/bundles.php
return [
// ...
EasyCorp\Bundle\EasyAdminBundle\EasyAdminBundle::class => ['all' => true],
];
Publish assets (needed after every upgrade that touches CSS/JS/icons):
php bin/console assets:install
# or, when using AssetMapper:
php bin/console importmap:install
Generate a dashboard
php bin/console make:admin:dashboard
Creates a dashboard controller rendering at /admin with the default layout and menu.
Generate CRUD for an entity
php bin/console make:admin:crud
Pick an entity (e.g. App\Entity\Post); you get a controller covering index, new, edit, detail and delete.
Open /admin and authenticate with your app's security setup (security.yaml).
*CrudController — configureFields(), configureActions() and configurePages() are the three hooks you'll touch 90% of the time.// src/Controller/Admin/PostCrudController.php
public static function getEntityFqcn(): string
{
return Post::class;
}
public function configureFields(string $pageName): iterable
{
return [
Field::new('title')->setLabel('Blog Post Title')->required(),
Field::new('content')->onlyOnForms()->setFormTypeOption('attr', ['rows' => 8]),
Field::new('publishedAt')->setFormTypeOption('widget', 'single_text'),
];
}
Then register it in the dashboard menu:
public function configureMenuItems(): iterable
{
yield MenuItem::linkToDashboard('Dashboard', 'fa fa-home');
yield MenuItem::linkToCrud('Posts', 'fas fa-newspaper', Post::class);
}
Visit /admin/post — list, filters, pagination, create/edit forms and delete are wired up.
The everyday workflow is: generate a controller, then override small hooks instead of rewriting pages.
public function configureFields(string $pageName): iterable
{
return [
Field::new('title')->setLabel('Title')->required()->autocomplete(),
Field::new('slug')->hideOnIndex(),
Field::new('author')->setFormTypeOption('by_reference', false),
Field::new('image')->setFormType(FileFieldType::class)->setRequired(false),
Field::new('content')->onlyOnDetail(),
Field::new('createdAt')->hideOnForm(),
];
}
Useful patterns:
onlyOnIndex(), onlyOnForms(), onlyOnDetail(), hideOnIndex() for page-scoped fields.setPermission('ROLE_EDITOR') for field-level visibility — the field is removed from forms and list output, not just visually hidden.setFormTypeOption() to pass through Symfony form type options rather than subclassing a field.5.7 documents the props and blocks of the built-in Twig components, and the UI Kit renders them live. For custom templates and dashboards, prefer components over raw HTML so styling tracks the theme:
{# Icon component now accepts a size prop #}
<twig:ea:Icon name="fa:solid:user" size="lg" />
<twig:ea:Badge color="success">Published</twig:ea:Badge>
When a component supports blocks, the docs/UI Kit list the block names — use them instead of CSS overrides wherever possible.
Group long forms into fieldsets/tabs, and pin tab identifiers when you need stability:
yield FormField::addTab('Content', 'fa fa-pen');
yield FormField::addPanel('Basics');
yield Field::new('title');
yield Field::new('summary');
FormField::addTab('SEO')->setTabId('seo'); // new in 5.7
yield Field::new('metaTitle');
yield Field::new('metaDescription');
setTabId() matters when you:
#seo) from another page or a "next error field" flow,Without it, ids are generated and can shift when you reorder fields.
public function configureActions(Actions $actions): Actions
{
$publish = Action::new('publish', 'Publish')
->linkToCrudAction('publishPost')
->displayIf(fn (Post $post) => !$post->isPublished());
return $actions
->add(Crud::PAGE_INDEX, Action::DETAIL)
->add(Crud::PAGE_DETAIL, $publish)
->addBatchAction(BatchAction::new('publish_all', 'Publish all')->linkToCrudAction('publishBatch'));
}
Keep mutation actions on POST (a custom CRUD action with #[Route(methods: ['POST'])] or EasyAdmin's action handling) rather than GET links with side effects. Custom action modals got design fixes in 5.7, so verify your custom modal markup against the UI Kit if you reimplemented any.
yield MenuItem::linkToCrud('Users', 'fas fa-users', User::class)
->setPermission('ROLE_ADMIN');
yield MenuItem::section('Reports');
yield MenuItem::linkToRoute('Sales', 'fas fa-chart-bar', 'app_reports_sales');
make:admin:dashboard again, then set #[AdminDashboard]/route prefix accordingly) and gate them in security.yaml.setPermission() / setPermission(SomeVoter::class, 'edit') on menu items, fields and actions over duplicating controllers per role.ConfigureMenuEvent listener rather than overriding configureMenuItems() with request state.Override by path under templates/bundles/EasyAdminBundle/... — never edit vendor/.
crud/index.html.twig, crud/detail.html.twig, layout.html.twig); wholesale copies drift and break on minor upgrades.assets:install/importmap:install after upgrades so icon and CSS assets match the new bundle version.radius: 0 and previously patched CSS for toggle shape, drop that patch.The EasyAdmin profiler panel was reworked in 5.7. On any admin page it shows the resolved AdminContext, the current CRUD controller/action, the field list actually rendered, and the query used for the index. When a field "disappears", a permission check fails, or you get an unexpected 404, open the panel first — it usually names the culprit without a dump().
Unknown ?routeName= is now a 404, not a 500. If you generated links by string (including a typo'd route name in linkToRoute()), you'll get a bare 404 instead of a loud exception. Add a smoke test that crawls the menu, or validate route names in a menu listener.
setEventDispatcher() / CRUD pages now use the event dispatcher contract. If you extended a CRUD controller or a service and typed the setter against Symfony's concrete dispatcher, switch to Psr\EventDispatcher\EventDispatcherInterface. Autowire the interface, not EventDispatcher. php bin/console debug:autowiring event (the 5.7 docs now describe the listed types) is the quickest way to confirm what's injectable.
Empty file fields now save null. Previously you could end up with an empty string (or a stale path) persisted. Consequences:
#[Assert\NotNull]/setRequired(true) if the file is mandatory.NOT NULL DB columns will now raise an integrity error on "clear the upload" instead of silently storing ''.setUploadDir() / setFileConstraints() / setFormTypeOption('mapped', false) setup, plus listeners that assumed a string.The leftover ea global Twig variable is gone. Custom templates using ea.field_value(...)-style access must move to the current API — inject/use AdminContext/EntityDto (available as template variables) or the ea() Twig function. Grep your templates/ for ea. before upgrading.
Tab ids are generated unless you pin them. Any JS or test keyed on #tab-... is fragile across field reorders; add ->setTabId('seo') and target that.
Icon sizing changed surface in 5.7 (size prop). If you wrapped icons in custom CSS to control dimensions, remove the hack and use the prop — otherwise icons will double-scale.
Re-run asset tooling after upgrading. Symptom of a stale install: square/round switches and tab paddings behaving inconsistently with the release notes. cache:clear alone does not reinstall CSS/JS.
Per-field vs per-action permissions are independent. Field::new('salary')->setPermission('ROLE_ADMIN') protects display, but the value is still submitted/handled if a form is crafted — enforce authorization in the action/controller or a voter, not only in configureFields().
by_reference => false on association fields is the usual fix when a collection isn't updated on the owning side; pair it with proper addX()/removeX() methods and cascade settings.
Don't copy vendor templates wholesale. Override the smallest possible template and rely on Twig components + the UI Kit for anything cosmetic; minor releases touch component markup frequently (tabs, modals, dropdowns in 5.7 alone).
Batch actions run per entity and can be slow. For large selections, queue a job per batch or restrict with ->setPermission() plus a max-selection guard; the default implementation iterates in-request.
Useful console commands when something behaves oddly:
php bin/console debug:router | grep admin
php bin/console debug:autowiring admin
php bin/console cache:clear
Pro features are separate. Audit Log and Content Lock are EasyAdmin Pro (paid) and not part of this release; don't plan core features around them unless you've licensed them.
How can I help you explore Laravel packages today?