easy-admin-bundle

📁 kgslotwinski/skills 📅 5 days ago
4
总安装量
4
周安装量
#49607
全站排名
安装命令
npx skills add https://github.com/kgslotwinski/skills --skill easy-admin-bundle

Agent 安装分布

amp 4
github-copilot 4
codex 4
kimi-cli 4
gemini-cli 4
cursor 4

Skill 文档

EasyAdmin Bundle Skill

Build powerful Symfony admin panels quickly with EasyAdminBundle. This skill provides complete examples and patterns for creating production-ready admin interfaces.

Version: This skill covers EasyAdminBundle 4.x (current stable) Requirements: PHP 8.1+, Symfony 5.4/6.x/7.x/8.x, Doctrine ORM Official Docs: https://github.com/EasyCorp/EasyAdminBundle/tree/4.x/doc Repository: https://github.com/EasyCorp/EasyAdminBundle

Quick Start

# Install EasyAdmin bundle
composer require easycorp/easyadmin-bundle

# Create a dashboard controller
php bin/console make:admin:dashboard

# Create CRUD controllers for your entities
php bin/console make:admin:crud

Complete Dashboard Setup

Basic Dashboard Controller

// src/Controller/Admin/DashboardController.php
namespace App\Controller\Admin;

use App\Entity\User;
use App\Entity\Post;
use App\Entity\Category;
use App\Entity\Comment;
use EasyCorp\Bundle\EasyAdminBundle\Config\Dashboard;
use EasyCorp\Bundle\EasyAdminBundle\Config\MenuItem;
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractDashboardController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;

class DashboardController extends AbstractDashboardController
{
    #[Route('/admin', name: 'admin')]
    public function index(): Response
    {
        return $this->render('admin/dashboard.html.twig');
    }

    public function configureDashboard(): Dashboard
    {
        return Dashboard::new()
            ->setTitle('My Admin Panel')
            ->setFaviconPath('favicon.ico')
            ->setTranslationDomain('admin')
            ->setTextDirection('ltr')
            ->renderContentMaximized()
            ->renderSidebarMinimized()
            ->disableDarkMode();
    }

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

        yield MenuItem::section('Content');
        yield MenuItem::linkToCrud('Posts', 'fa fa-newspaper', Post::class);
        yield MenuItem::linkToCrud('Categories', 'fa fa-tags', Category::class);
        yield MenuItem::linkToCrud('Comments', 'fa fa-comments', Comment::class);

        yield MenuItem::section('Users');
        yield MenuItem::linkToCrud('Users', 'fa fa-user', User::class);

        yield MenuItem::section('External');
        yield MenuItem::linkToUrl('Homepage', 'fa fa-globe', '/');
        yield MenuItem::linkToLogout('Logout', 'fa fa-sign-out');
    }
}

Dashboard with Submenus

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

    // Submenu example
    yield MenuItem::subMenu('Content', 'fa fa-file')->setSubItems([
        MenuItem::linkToCrud('Posts', 'fa fa-newspaper', Post::class),
        MenuItem::linkToCrud('Pages', 'fa fa-file-text', Page::class),
        MenuItem::linkToCrud('Media', 'fa fa-images', Media::class),
    ]);

    yield MenuItem::subMenu('E-commerce', 'fa fa-shopping-cart')->setSubItems([
        MenuItem::linkToCrud('Products', 'fa fa-box', Product::class),
        MenuItem::linkToCrud('Orders', 'fa fa-shopping-bag', Order::class),
        MenuItem::linkToCrud('Customers', 'fa fa-users', Customer::class),
    ]);
}

Field Types Reference

Basic Fields

use EasyCorp\Bundle\EasyAdminBundle\Field\TextField;
use EasyCorp\Bundle\EasyAdminBundle\Field\TextEditorField;
use EasyCorp\Bundle\EasyAdminBundle\Field\TextareaField;
use EasyCorp\Bundle\EasyAdminBundle\Field\NumberField;
use EasyCorp\Bundle\EasyAdminBundle\Field\IntegerField;
use EasyCorp\Bundle\EasyAdminBundle\Field\EmailField;
use EasyCorp\Bundle\EasyAdminBundle\Field\UrlField;
use EasyCorp\Bundle\EasyAdminBundle\Field\TelephoneField;

public function configureFields(string $pageName): iterable
{
    yield TextField::new('title')->setMaxLength(255);
    yield TextEditorField::new('content'); // Rich text editor
    yield TextareaField::new('description')->setMaxLength(500);
    yield NumberField::new('price')->setNumDecimals(2);
    yield IntegerField::new('quantity');
    yield EmailField::new('email');
    yield UrlField::new('website');
    yield TelephoneField::new('phone');
}

Date and Time Fields

use EasyCorp\Bundle\EasyAdminBundle\Field\DateField;
use EasyCorp\Bundle\EasyAdminBundle\Field\DateTimeField;
use EasyCorp\Bundle\EasyAdminBundle\Field\TimeField;

public function configureFields(string $pageName): iterable
{
    yield DateField::new('publishedAt')
        ->setFormat('dd/MM/yyyy');

    yield DateTimeField::new('createdAt')
        ->setFormat('dd/MM/yyyy HH:mm')
        ->hideOnForm();

    yield TimeField::new('openingTime')
        ->setFormat('HH:mm');
}

Boolean and Choice Fields

use EasyCorp\Bundle\EasyAdminBundle\Field\BooleanField;
use EasyCorp\Bundle\EasyAdminBundle\Field\ChoiceField;

public function configureFields(string $pageName): iterable
{
    yield BooleanField::new('isPublished')
        ->renderAsSwitch();

    yield ChoiceField::new('status')
        ->setChoices([
            'Draft' => 'draft',
            'Published' => 'published',
            'Archived' => 'archived',
        ])
        ->renderAsBadges([
            'draft' => 'warning',
            'published' => 'success',
            'archived' => 'secondary',
        ]);

    yield ChoiceField::new('roles')
        ->setChoices([
            'User' => 'ROLE_USER',
            'Editor' => 'ROLE_EDITOR',
            'Admin' => 'ROLE_ADMIN',
        ])
        ->allowMultipleChoices()
        ->renderExpanded(); // Show as checkboxes
}

Association Fields

use EasyCorp\Bundle\EasyAdminBundle\Field\AssociationField;

public function configureFields(string $pageName): iterable
{
    // Many-to-One
    yield AssociationField::new('category')
        ->setRequired(true);

    // One-to-Many / Many-to-Many
    yield AssociationField::new('tags')
        ->setFormTypeOptions([
            'by_reference' => false,
        ])
        ->autocomplete(); // Ajax autocomplete for large datasets

    // Custom query for association
    yield AssociationField::new('author')
        ->setQueryBuilder(
            fn (QueryBuilder $qb) => $qb
                ->andWhere('entity.isActive = :active')
                ->setParameter('active', true)
        );
}

File and Image Fields

use EasyCorp\Bundle\EasyAdminBundle\Field\ImageField;
use EasyCorp\Bundle\EasyAdminBundle\Field\FileField;

public function configureFields(string $pageName): iterable
{
    yield ImageField::new('thumbnail')
        ->setBasePath('uploads/images')
        ->setUploadDir('public/uploads/images')
        ->setUploadedFileNamePattern('[randomhash].[extension]')
        ->setRequired(false);

    yield ImageField::new('gallery')
        ->setBasePath('uploads/gallery')
        ->setUploadDir('public/uploads/gallery')
        ->setUploadedFileNamePattern('[slug]-[timestamp].[extension]')
        ->setFormTypeOptions(['multiple' => true]);

    yield FileField::new('attachment')
        ->setBasePath('uploads/files')
        ->setUploadDir('public/uploads/files');
}

Money and Percent Fields

use EasyCorp\Bundle\EasyAdminBundle\Field\MoneyField;
use EasyCorp\Bundle\EasyAdminBundle\Field\PercentField;

public function configureFields(string $pageName): iterable
{
    yield MoneyField::new('price')
        ->setCurrency('USD')
        ->setStoredAsCents(false);

    yield MoneyField::new('priceInCents')
        ->setCurrency('EUR')
        ->setStoredAsCents(true);

    yield PercentField::new('discount')
        ->setNumDecimals(2);
}

Other Useful Fields

use EasyCorp\Bundle\EasyAdminBundle\Field\SlugField;
use EasyCorp\Bundle\EasyAdminBundle\Field\ColorField;
use EasyCorp\Bundle\EasyAdminBundle\Field\CountryField;
use EasyCorp\Bundle\EasyAdminBundle\Field\LanguageField;
use EasyCorp\Bundle\EasyAdminBundle\Field\LocaleField;
use EasyCorp\Bundle\EasyAdminBundle\Field\TimezoneField;
use EasyCorp\Bundle\EasyAdminBundle\Field\CodeEditorField;

public function configureFields(string $pageName): iterable
{
    yield SlugField::new('slug')
        ->setTargetFieldName('title');

    yield ColorField::new('brandColor');

    yield CountryField::new('country');

    yield LanguageField::new('language');

    yield LocaleField::new('locale');

    yield TimezoneField::new('timezone');

    yield CodeEditorField::new('customCss')
        ->setLanguage('css')
        ->setNumOfRows(10);
}

Complete CRUD Controller Examples

User CRUD with All Features

namespace App\Controller\Admin;

use App\Entity\User;
use EasyCorp\Bundle\EasyAdminBundle\Config\Action;
use EasyCorp\Bundle\EasyAdminBundle\Config\Actions;
use EasyCorp\Bundle\EasyAdminBundle\Config\Crud;
use EasyCorp\Bundle\EasyAdminBundle\Config\Filters;
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController;
use EasyCorp\Bundle\EasyAdminBundle\Field\ArrayField;
use EasyCorp\Bundle\EasyAdminBundle\Field\BooleanField;
use EasyCorp\Bundle\EasyAdminBundle\Field\DateTimeField;
use EasyCorp\Bundle\EasyAdminBundle\Field\EmailField;
use EasyCorp\Bundle\EasyAdminBundle\Field\IdField;
use EasyCorp\Bundle\EasyAdminBundle\Field\ImageField;
use EasyCorp\Bundle\EasyAdminBundle\Field\TextField;
use EasyCorp\Bundle\EasyAdminBundle\Filter\BooleanFilter;
use EasyCorp\Bundle\EasyAdminBundle\Filter\DateTimeFilter;

class UserCrudController extends AbstractCrudController
{
    public static function getEntityFqcn(): string
    {
        return User::class;
    }

    public function configureCrud(Crud $crud): Crud
    {
        return $crud
            ->setEntityLabelInSingular('User')
            ->setEntityLabelInPlural('Users')
            ->setPageTitle('index', 'User Management')
            ->setPageTitle('new', 'Create User')
            ->setPageTitle('edit', 'Edit %entity_label_singular%')
            ->setSearchFields(['username', 'email', 'firstName', 'lastName'])
            ->setDefaultSort(['createdAt' => 'DESC'])
            ->setPaginatorPageSize(30)
            ->setEntityPermission('ROLE_ADMIN');
    }

    public function configureFields(string $pageName): iterable
    {
        yield IdField::new('id')->hideOnForm();

        yield TextField::new('username')
            ->setColumns(6);

        yield EmailField::new('email')
            ->setColumns(6);

        yield TextField::new('firstName')
            ->setColumns(6)
            ->hideOnIndex();

        yield TextField::new('lastName')
            ->setColumns(6)
            ->hideOnIndex();

        yield ImageField::new('avatar')
            ->setBasePath('uploads/avatars')
            ->setUploadDir('public/uploads/avatars')
            ->setUploadedFileNamePattern('[slug]-[timestamp].[extension]')
            ->hideOnIndex();

        yield ArrayField::new('roles')
            ->hideOnIndex();

        yield BooleanField::new('isVerified')
            ->renderAsSwitch(false);

        yield BooleanField::new('isActive')
            ->renderAsSwitch(false);

        yield DateTimeField::new('createdAt')
            ->hideOnForm();

        yield DateTimeField::new('lastLoginAt')
            ->hideOnForm()
            ->hideOnIndex();
    }

    public function configureFilters(Filters $filters): Filters
    {
        return $filters
            ->add('username')
            ->add('email')
            ->add(BooleanFilter::new('isActive'))
            ->add(BooleanFilter::new('isVerified'))
            ->add(DateTimeFilter::new('createdAt'))
            ->add(DateTimeFilter::new('lastLoginAt'));
    }

    public function configureActions(Actions $actions): Actions
    {
        $impersonate = Action::new('impersonate', 'Impersonate')
            ->linkToCrudAction('impersonate')
            ->setCssClass('btn btn-warning')
            ->displayIf(fn (User $user) => $this->isGranted('ROLE_SUPER_ADMIN'));

        return $actions
            ->add(Crud::PAGE_INDEX, Action::DETAIL)
            ->add(Crud::PAGE_INDEX, $impersonate)
            ->update(Crud::PAGE_INDEX, Action::DELETE, function (Action $action) {
                return $action->displayIf(fn (User $user) => $user->getId() !== $this->getUser()->getId());
            });
    }
}

Product CRUD with Categories and Images

namespace App\Controller\Admin;

use App\Entity\Product;
use EasyCorp\Bundle\EasyAdminBundle\Config\Crud;
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController;
use EasyCorp\Bundle\EasyAdminBundle\Field\AssociationField;
use EasyCorp\Bundle\EasyAdminBundle\Field\BooleanField;
use EasyCorp\Bundle\EasyAdminBundle\Field\DateTimeField;
use EasyCorp\Bundle\EasyAdminBundle\Field\IdField;
use EasyCorp\Bundle\EasyAdminBundle\Field\ImageField;
use EasyCorp\Bundle\EasyAdminBundle\Field\IntegerField;
use EasyCorp\Bundle\EasyAdminBundle\Field\MoneyField;
use EasyCorp\Bundle\EasyAdminBundle\Field\SlugField;
use EasyCorp\Bundle\EasyAdminBundle\Field\TextEditorField;
use EasyCorp\Bundle\EasyAdminBundle\Field\TextField;

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

    public function configureCrud(Crud $crud): Crud
    {
        return $crud
            ->setEntityLabelInSingular('Product')
            ->setEntityLabelInPlural('Products')
            ->setSearchFields(['name', 'sku', 'description'])
            ->setDefaultSort(['createdAt' => 'DESC']);
    }

    public function configureFields(string $pageName): iterable
    {
        yield IdField::new('id')->onlyOnIndex();

        yield TextField::new('name')
            ->setColumns(8);

        yield SlugField::new('slug')
            ->setTargetFieldName('name')
            ->setColumns(4)
            ->hideOnIndex();

        yield TextField::new('sku')
            ->setColumns(6);

        yield MoneyField::new('price')
            ->setCurrency('USD')
            ->setColumns(6);

        yield IntegerField::new('stock')
            ->setColumns(6);

        yield AssociationField::new('category')
            ->setColumns(6)
            ->autocomplete();

        yield AssociationField::new('tags')
            ->hideOnIndex()
            ->autocomplete();

        yield TextEditorField::new('description')
            ->hideOnIndex();

        yield ImageField::new('image')
            ->setBasePath('uploads/products')
            ->setUploadDir('public/uploads/products')
            ->setUploadedFileNamePattern('[slug]-[timestamp].[extension]');

        yield BooleanField::new('isFeatured')
            ->renderAsSwitch(false);

        yield BooleanField::new('isActive')
            ->renderAsSwitch(false);

        yield DateTimeField::new('createdAt')
            ->hideOnForm();
    }
}

Blog Post CRUD with Rich Content

namespace App\Controller\Admin;

use App\Entity\Post;
use EasyCorp\Bundle\EasyAdminBundle\Config\Crud;
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController;
use EasyCorp\Bundle\EasyAdminBundle\Field\AssociationField;
use EasyCorp\Bundle\EasyAdminBundle\Field\ChoiceField;
use EasyCorp\Bundle\EasyAdminBundle\Field\DateTimeField;
use EasyCorp\Bundle\EasyAdminBundle\Field\IdField;
use EasyCorp\Bundle\EasyAdminBundle\Field\ImageField;
use EasyCorp\Bundle\EasyAdminBundle\Field\SlugField;
use EasyCorp\Bundle\EasyAdminBundle\Field\TextEditorField;
use EasyCorp\Bundle\EasyAdminBundle\Field\TextField;

class PostCrudController extends AbstractCrudController
{
    public static function getEntityFqcn(): string
    {
        return Post::class;
    }

    public function configureCrud(Crud $crud): Crud
    {
        return $crud
            ->setEntityLabelInSingular('Post')
            ->setEntityLabelInPlural('Blog Posts')
            ->setSearchFields(['title', 'content', 'excerpt'])
            ->setDefaultSort(['publishedAt' => 'DESC']);
    }

    public function configureFields(string $pageName): iterable
    {
        yield IdField::new('id')->onlyOnIndex();

        yield TextField::new('title');

        yield SlugField::new('slug')
            ->setTargetFieldName('title')
            ->hideOnIndex();

        yield TextField::new('excerpt')
            ->setMaxLength(200)
            ->hideOnIndex();

        yield TextEditorField::new('content')
            ->hideOnIndex();

        yield ImageField::new('featuredImage')
            ->setBasePath('uploads/posts')
            ->setUploadDir('public/uploads/posts');

        yield AssociationField::new('author')
            ->autocomplete();

        yield AssociationField::new('category');

        yield AssociationField::new('tags')
            ->hideOnIndex()
            ->autocomplete();

        yield ChoiceField::new('status')
            ->setChoices([
                'Draft' => 'draft',
                'Published' => 'published',
                'Archived' => 'archived',
            ])
            ->renderAsBadges([
                'draft' => 'warning',
                'published' => 'success',
                'archived' => 'secondary',
            ]);

        yield DateTimeField::new('publishedAt')
            ->hideOnIndex();

        yield DateTimeField::new('createdAt')
            ->hideOnForm();

        yield DateTimeField::new('updatedAt')
            ->hideOnForm()
            ->hideOnIndex();
    }
}

Custom Actions

Add Custom Action to CRUD

use EasyCorp\Bundle\EasyAdminBundle\Config\Action;
use EasyCorp\Bundle\EasyAdminBundle\Config\Actions;
use EasyCorp\Bundle\EasyAdminBundle\Config\Crud;
use Symfony\Component\HttpFoundation\Response;

class ProductCrudController extends AbstractCrudController
{
    public function configureActions(Actions $actions): Actions
    {
        // Create custom action
        $exportAction = Action::new('export', 'Export CSV')
            ->linkToCrudAction('exportCsv')
            ->setCssClass('btn btn-success')
            ->createAsGlobalAction(); // Shows in index page header

        $duplicateAction = Action::new('duplicate', 'Duplicate')
            ->linkToCrudAction('duplicateProduct')
            ->setCssClass('btn btn-info');

        return $actions
            ->add(Crud::PAGE_INDEX, $exportAction)
            ->add(Crud::PAGE_DETAIL, $duplicateAction)
            ->add(Crud::PAGE_INDEX, Action::DETAIL)
            ->reorder(Crud::PAGE_INDEX, [Action::DETAIL, Action::EDIT, 'duplicate', Action::DELETE]);
    }

    public function exportCsv(): Response
    {
        $products = $this->entityManager->getRepository(Product::class)->findAll();

        // Generate CSV content
        $csv = "ID,Name,SKU,Price\n";
        foreach ($products as $product) {
            $csv .= sprintf("%d,%s,%s,%.2f\n",
                $product->getId(),
                $product->getName(),
                $product->getSku(),
                $product->getPrice()
            );
        }

        return new Response($csv, 200, [
            'Content-Type' => 'text/csv',
            'Content-Disposition' => 'attachment; filename="products.csv"',
        ]);
    }

    public function duplicateProduct(AdminContext $context): Response
    {
        $product = $context->getEntity()->getInstance();

        $newProduct = clone $product;
        $newProduct->setName($product->getName() . ' (Copy)');
        $newProduct->setSku($product->getSku() . '-copy');

        $this->entityManager->persist($newProduct);
        $this->entityManager->flush();

        $this->addFlash('success', 'Product duplicated successfully');

        return $this->redirect($context->getReferrer());
    }
}

Batch Actions

use EasyCorp\Bundle\EasyAdminBundle\Config\Action;
use EasyCorp\Bundle\EasyAdminBundle\Config\Actions;

public function configureActions(Actions $actions): Actions
{
    $batchPublish = Action::new('batchPublish', 'Publish selected')
        ->linkToCrudAction('batchPublish')
        ->addCssClass('btn btn-success')
        ->setIcon('fa fa-check');

    return $actions
        ->addBatchAction($batchPublish);
}

public function batchPublish(BatchActionDto $batchActionDto): Response
{
    $entityManager = $this->entityManager;

    foreach ($batchActionDto->getEntityIds() as $id) {
        $post = $entityManager->find(Post::class, $id);
        if ($post) {
            $post->setStatus('published');
            $post->setPublishedAt(new \DateTime());
        }
    }

    $entityManager->flush();

    $this->addFlash('success', sprintf('Published %d posts', count($batchActionDto->getEntityIds())));

    return $this->redirect($batchActionDto->getReferrerUrl());
}

Advanced Configurations

Conditional Field Display

public function configureFields(string $pageName): iterable
{
    yield TextField::new('title');

    // Show only on index page
    yield TextField::new('summary')->onlyOnIndex();

    // Show only on forms (new/edit)
    yield TextEditorField::new('content')->onlyOnForms();

    // Show only on detail page
    yield TextField::new('fullDetails')->onlyOnDetail();

    // Hide on specific pages
    yield DateTimeField::new('createdAt')
        ->hideOnForm()
        ->hideOnIndex();

    // Conditional display based on context
    if (Crud::PAGE_EDIT === $pageName) {
        yield DateTimeField::new('updatedAt')->setFormTypeOptions(['disabled' => true]);
    }
}

Custom Form Layouts

use EasyCorp\Bundle\EasyAdminBundle\Field\FormField;

public function configureFields(string $pageName): iterable
{
    yield FormField::addPanel('Basic Information');
    yield TextField::new('name');
    yield TextField::new('sku');

    yield FormField::addPanel('Pricing')->setIcon('fa fa-dollar-sign');
    yield MoneyField::new('price')->setCurrency('USD');
    yield MoneyField::new('costPrice')->setCurrency('USD');
    yield PercentField::new('taxRate');

    yield FormField::addPanel('Inventory');
    yield IntegerField::new('stock');
    yield IntegerField::new('minStock');
    yield BooleanField::new('trackInventory');

    yield FormField::addPanel('SEO')->setHelp('Search engine optimization settings');
    yield TextField::new('metaTitle');
    yield TextareaField::new('metaDescription');
}

Security and Permissions

use EasyCorp\Bundle\EasyAdminBundle\Config\Crud;

public function configureCrud(Crud $crud): Crud
{
    return $crud
        ->setEntityPermission('ROLE_ADMIN') // Required role for all operations
        ->setEntityLabelInSingular('Product')
        ->setEntityLabelInPlural('Products');
}

public function configureActions(Actions $actions): Actions
{
    return $actions
        // Only super admins can delete
        ->setPermission(Action::DELETE, 'ROLE_SUPER_ADMIN')
        // Conditional permissions
        ->update(Crud::PAGE_INDEX, Action::EDIT, function (Action $action) {
            return $action->displayIf(function (Product $product) {
                return $this->isGranted('ROLE_EDITOR') || $product->getAuthor() === $this->getUser();
            });
        });
}

Custom Queries and Filters

use Doctrine\ORM\QueryBuilder;
use EasyCorp\Bundle\EasyAdminBundle\Collection\FieldCollection;
use EasyCorp\Bundle\EasyAdminBundle\Collection\FilterCollection;
use EasyCorp\Bundle\EasyAdminBundle\Dto\EntityDto;
use EasyCorp\Bundle\EasyAdminBundle\Dto\SearchDto;

public function createIndexQueryBuilder(SearchDto $searchDto, EntityDto $entityDto, FieldCollection $fields, FilterCollection $filters): QueryBuilder
{
    $qb = parent::createIndexQueryBuilder($searchDto, $entityDto, $fields, $filters);

    // Only show active products
    $qb->andWhere('entity.isActive = :active')
       ->setParameter('active', true);

    // Only show products from current user's store
    if (!$this->isGranted('ROLE_ADMIN')) {
        $qb->andWhere('entity.store = :store')
           ->setParameter('store', $this->getUser()->getStore());
    }

    return $qb;
}

Event Listeners and Hooks

Modify Entity Before Persist

use EasyCorp\Bundle\EasyAdminBundle\Event\BeforeEntityPersistedEvent;
use EasyCorp\Bundle\EasyAdminBundle\Event\BeforeEntityUpdatedEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

class ProductSubscriber implements EventSubscriberInterface
{
    public static function getSubscribedEvents(): array
    {
        return [
            BeforeEntityPersistedEvent::class => ['setCreatedAt'],
            BeforeEntityUpdatedEvent::class => ['setUpdatedAt'],
        ];
    }

    public function setCreatedAt(BeforeEntityPersistedEvent $event): void
    {
        $entity = $event->getEntityInstance();

        if ($entity instanceof Product) {
            $entity->setCreatedAt(new \DateTime());
        }
    }

    public function setUpdatedAt(BeforeEntityUpdatedEvent $event): void
    {
        $entity = $event->getEntityInstance();

        if ($entity instanceof Product) {
            $entity->setUpdatedAt(new \DateTime());
        }
    }
}

Configuration Files

Route Configuration

# config/routes.yaml
admin:
    resource: App\Controller\Admin\DashboardController
    type: easyadmin
    prefix: /admin

Security Configuration

# config/packages/security.yaml
security:
    # ... other config

    access_control:
        - { path: ^/admin/login, roles: PUBLIC_ACCESS }
        - { path: ^/admin, roles: ROLE_ADMIN }

    role_hierarchy:
        ROLE_EDITOR: ROLE_USER
        ROLE_ADMIN: ROLE_EDITOR
        ROLE_SUPER_ADMIN: ROLE_ADMIN

EasyAdmin Configuration

# config/packages/easyadmin.yaml
easy_admin:
    site_name: 'My Admin Panel'

    formats:
        date: 'd/m/Y'
        time: 'H:i'
        datetime: 'd/m/Y H:i:s'

    design:
        brand_color: '#1976D2'
        menu:
            - { label: 'Dashboard', icon: 'fa fa-home', route: 'admin' }

    # Custom assets
    assets:
        css:
            - 'css/admin.css'
        js:
            - 'js/admin.js'

Troubleshooting

Routes Not Found

# Clear cache
php bin/console cache:clear

# Verify routes
php bin/console debug:router | grep admin

Images Not Displaying

Ensure your public directory structure matches:

public/
  uploads/
    images/
    products/
    avatars/

Association Fields Empty

Add by_reference => false for collections:

yield AssociationField::new('tags')
    ->setFormTypeOptions(['by_reference' => false]);

Autocomplete Not Working

Install and configure autocomplete:

composer require symfony/ux-autocomplete
php bin/console importmap:install

Performance Tips

  1. Use pagination – Keep default page size reasonable (20-50 items)
  2. Limit search fields – Only include fields that need to be searchable
  3. Use autocomplete for associations – Especially with large datasets
  4. Disable unnecessary features – Hide detail pages if not needed
  5. Cache queries – Use query result caching for complex filters

Common Patterns

Multi-Tenant CRUD

public function createIndexQueryBuilder(SearchDto $searchDto, EntityDto $entityDto, FieldCollection $fields, FilterCollection $filters): QueryBuilder
{
    $qb = parent::createIndexQueryBuilder($searchDto, $entityDto, $fields, $filters);
    $qb->andWhere('entity.tenant = :tenant')
       ->setParameter('tenant', $this->getUser()->getTenant());
    return $qb;
}

public function persistEntity(EntityManagerInterface $em, $entityInstance): void
{
    $entityInstance->setTenant($this->getUser()->getTenant());
    parent::persistEntity($em, $entityInstance);
}

Soft Delete Integration

use Doctrine\ORM\QueryBuilder;

public function createIndexQueryBuilder(...): QueryBuilder
{
    $qb = parent::createIndexQueryBuilder(...);
    $qb->andWhere('entity.deletedAt IS NULL');
    return $qb;
}

Audit Trail

public function updateEntity(EntityManagerInterface $em, $entityInstance): void
{
    $entityInstance->setUpdatedBy($this->getUser());
    $entityInstance->setUpdatedAt(new \DateTime());
    parent::updateEntity($em, $entityInstance);
}

Requirements

  • PHP: 8.1 or higher
  • Symfony: 5.4, 6.x, 7.x, or 8.x
  • Doctrine ORM: Required for entity management
  • Twig: Template engine

Installation

# Install bundle (version 4.x)
composer require easycorp/easyadmin-bundle

# Optional: Install UX components for enhanced features
composer require symfony/ux-autocomplete

# Generate dashboard
php bin/console make:admin:dashboard

# Generate CRUD controllers
php bin/console make:admin:crud

Official Documentation

Main Documentation

Key Documentation Files

Getting Latest Information

# Clone the repository to browse documentation locally
git clone https://github.com/EasyCorp/EasyAdminBundle.git
cd EasyAdminBundle

# Switch to 4.x branch
git checkout 4.x

# View documentation
cd doc
ls -la  # List all documentation files

# Read specific documentation
cat fields.rst
cat crud.rst
cat dashboards.rst

Checking for Updates

# Check installed version
composer show easycorp/easyadmin-bundle

# Update to latest 4.x version
composer update easycorp/easyadmin-bundle

# Check for newer versions
composer outdated easycorp/easyadmin-bundle

Version Notes

  • 4.x (Current Stable): Production ready, actively maintained
  • 5.x (Beta): In development, use for testing only
  • This skill is based on 4.x patterns and syntax

Community Resources