symfony easyadmin3 cheat sheet

https://symfony.com/bundles/EasyAdminBundle/3.x/index.html

Font-Awesome Icons: https://fontawesome.com/v5/search?q=check&o=r&m=free&f=classic

Easyadmin3 uses https://tom-select.js.org/ for select widgets. See below for an example.

Dashboard configuration

We had problems with a wrong base url via varnish proxy. Enabling relative urls helped:

  • // src/Controller/Admin/DashboardController.php
        public function configureDashboard(): Dashboard
        {
            return Dashboard::new()
                ->generateRelativeUrls()
            ;
        }

CRUD

Generate new crud:

  • php bin/console make:admin:crud

Actions

  •     public function configureActions(Actions $actions): Actions
        {
            $viewAction = Action::new('viewView', 'View')->linkToUrl(function (Foo $entity) {
                return '/foo/' . $entity->getUrl();
            });
    
            $secondAction = ...;
    
            // These are displayed in reverse order!
            return $actions
                ->add(Crud::PAGE_INDEX, $secondAction)
                ->add(Crud::PAGE_INDEX, $viewAction)
                ->add(Crud::PAGE_EDIT, $viewAction)
            ;
        }

Fields

Association

Add an empty option to a required AssociationField:

  •         yield AssociationField::new('foo')
                ->setRequired(true)
                ->setFormTypeOptions([
                    'placeholder' => 'Please select...', // Force empty option when creating
                ]);
            ;

Filters

Choice Filter

  •     public function configureFilters(Filters $filters): Filters
        {
            $exampleRepository = $this->getDoctrine()->getManager()->getRepository(Example::class);
    
            $filters
                ->add('name')
                ->add(ChoiceFilter::new('interests')
                    ->setChoices(array_combine(
                        $exampleRepository->findDistinctInterests(),
                        $exampleRepository->findDistinctInterests()
                    ))
                )
            ;
    
            return $filters;
        }

Expanded

  • ->add(BooleanFilter::new('enabled')->setFormTypeOption('expanded', false));

We can also create our own custom filter class. That's as easy as creating a custom class, making it implement FilterInterface, and using this FilterTrait. Then all you need to do is implement the new() method where you set the form type and then the apply() method where you modify the query.

Set a default value / set default from filter value

  • // class Example CrudController
    public function createEntity(string $entityFqcn)
    {
        $entity = new MyEntity();
        $entity->setFoo(true);
    
        $barRepository = $this->getDoctrine()->getManager()->getRepository(Bar::class);
        $filters = $this->getContext()->getSearch()->getAppliedFilters();
    
        if (isset($filters['bar'])) {
            $bar = $barRepository->find($filters['bar']['value']);
            $entity->setBar($bar);
        }
    
        return $entity;
    }

Use Association

And add custom order

  • public function configureFields(string $pageName): iterable
    {
        $fields = parent::configureFields($pageName);
    
            $fields[] = AssociationField::new('categories')
                ->setQueryBuilder(function (QueryBuilder $qb) {
                $qb->orderBy('entity.name');
            });
    
        return $fields;
    }

CKEditor

https://symfony.com/bundles/FOSCKEditorBundle/current/installation.html

  • composer require friendsofsymfony/ckeditor-bundle
  • yarn run encore dev
  • ExampleCrudController.php
    • public function configureCrud(Crud $crud): Crud
      {
              $crud
                  ...
                  ->addFormTheme('@FOSCKEditor/Form/ckeditor_widget.html.twig')
              ;
      
              return parent::configureCrud($crud); // TODO: Change the autogenerated stub
      }
    • // configureFields()
      
       yield TextareaField::new('myTextField')
         ->onlyOnForms()
         ->setFormType(CKEditorType::class)
      ;

 

Custom Field for Easyadmin:https://github.com/EasyCorp/EasyAdminBundle/issues/3412#issuecomment-651315841

Configuration / Styles

Global config options for CKEditor can be configured in

The global config above can be adjusted on a crud level:

  • ExampleCrudController.php
    • // configureFields()
      
       yield TextareaField::new('myTextField')
         ->onlyOnForms()
         ->setFormType(CKEditorType::class)
         // Customize CKEditor config
         ->setFormTypeOption('config', [
           'toolbar' => 'full',
         ])
      ;

https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_stylesSet.html
https://symfony.com/bundles/FOSCKEditorBundle/current/usage/style.html

 

A custom field: HtmlField / Unmapped Entity Getter

  • <?php
    // src/Admin/Field/HtmlField.php
    
    namespace App\Admin\Field;
    
    use EasyCorp\Bundle\EasyAdminBundle\Contracts\Field\FieldInterface;
    use EasyCorp\Bundle\EasyAdminBundle\Field\FieldTrait;
    use Symfony\Component\Form\Extension\Core\Type\TextType;
    
    /**
     * Field for index/list view to display html generated by unmapped entity getters
     */
    class HtmlField implements FieldInterface
    {
        use FieldTrait;
    
        /**
         * @param string|false|null $label
         */
        public static function new(string $propertyName, $label = null): self
        {
            return (new self())
                ->setProperty($propertyName)
                ->setLabel($label)
                ->setTemplatePath('admin/field/html_field.html.twig')
                ->setFormType(TextType::class)
            ;
        }
    
    }
  • // src/Entity/Foo.php
    
        // Example for an unmapped getter for EasyAdmin
        public function getViewLink(): string {
    
          return '<a href="/foo/' . $this->getUrl() . '" target="_blank">View</a>';
        }
  • // src/Controller/Admin/FooCrudController.php
    
        public function configureFields(string $pageName): iterable
        {
            yield HtmlField::new('viewLink')->onlyOnIndex();
            ...

Override a specific form type template

Example: add image preview to "ImageField".
First let's investigate:

  • class ImageField
    • // Define which form field to use
      ->setFormType(FileUploadType::class)
  • class FileUploadType
    •     // This is the connection between the form type and the twig template block.
          // It defined the prefix for the form field
          public function getBlockPrefix(): string
          {
              return 'ea_fileupload';
          }
      
          // buildView() defines what data is injected into the template
          public function buildView(FormView $view, FormInterface $form, array $options): void
          {
              ...
              $view->vars['currentFiles'] = $currentFiles;
              $view->vars['multiple'] = $options['multiple'];
              $view->vars['allow_add'] = $options['allow_add'];
              $view->vars['allow_delete'] = $options['allow_delete'];
              $view->vars['download_path'] = $options['download_path'];
          }
  • vendor/easycorp/easyadmin-bundle/src/Resources/views/crud/form_theme.html.twig
    • This is the default form theme for Easyadmin.
      Here we find the corresponding block which we want to override.
      • {% block ea_fileupload_widget %}
            <div class="ea-fileupload">
                <div class="input-group">
                    {% set placeholder = '' %}
                    {% set title = '' %}
        ...

Now let's override this block globally for all easyadmin cruds:

  • class DashboardController
    •     public function configureCrud(): Crud
          {
              return Crud::new()
                  // add a custom form theme
                  ->setFormThemes([
                      '@EasyAdmin/crud/form_theme.html.twig', // the original base form theme
                      'admin/form_theme.html.twig', // custom theme to override specific blocks,
                  ])
              ;
          }
  • templates/admin/form_theme.html.twig
    • {# Copied block from vendor/easycorp/easyadmin-bundle/src/Resources/views/crud/form_theme.html.twig #}
      {# with added image preview html code #}
      {% block ea_fileupload_widget %}
          <div class="ea-fileupload">
      ...
                      <label class="btn" for="{{ form.file.vars.id }}">
                          <i class="fa fa-folder-open-o"></i>
                      </label>
                  </div>
                  {# Added image preview #}
                  {% if currentFiles is iterable and currentFiles|length > 0 %}
                      <img style="margin-top: 2rem; max-height: 100px;" src="{{ '/' ~ download_path ~ (currentFiles|first).filename }}" alt="" />
                  {% endif %}
              </div>
              {% if multiple and currentFiles %}
      ...
      

Use another field as edit link

E.g. link "name" to the edit action instead of separate "Edit" link

  • // src/Controller/Admin/FooCrudController.php
    
        public function configureFields(string $pageName): iterable
        {
            // Add the trigger css class name:
            yield TextField::new('name')->setCssClass('use-as-edit-link');
            ...
        }
    
        public function configureAssets(): Assets
        {
            $assets = parent::configureAssets();
            $assets->addJsFile('js/easyadmin/move-edit-link.js');
    
            return $assets;
        }

     

  • // /public/js/easyadmin/move-edit-link.js
    
    /* jshint esversion: 6 */
    document.addEventListener("DOMContentLoaded", function(event)
    {
        // Remove the "edit" link and link the field with css class 'use-as-edit-link' instead
    
        // Get all elements with class "action-edit"
        const editLinks = document.querySelectorAll('.action-edit');
    
        // Loop through each edit link
        editLinks.forEach(editLink => {
            // Get the href attribute of the current edit link
            const href = editLink.getAttribute('href');
    
            // Hide the original edit link cell by setting its style to "display: none;"
            editLink.style.display = 'none';
    
            // Find the closest parent row of the edit link
            const row = editLink.closest('tr');
    
            // Find the element with class "use-as-edit-link" within the same row
            const useAsEditLink = row.querySelector('.use-as-edit-link');
    
            // Update the innerHTML of the use-as-edit-link element to create a link
            useAsEditLink.innerHTML = `<a href="${href}">${useAsEditLink.innerHTML}</a>`;
        });
    
    });

How to link to another CRUD entity/action

(Unfinished)

  • // src/Controller/Admin/FooCrudController.php        
        public function configureFields(string $pageName): iterable
        {
            $adminUrl = $this->container->get(AdminUrlGenerator::class)
                ->setController(BarCrudController::class)
                ->setAction(Action::EDIT)
                ->setEntityId('__id__')
                ->generateUrl();
            dump($adminUrl);
    

Duplicate / Clone

  • // src/Controller/Admin/ExampleCrudController.php
    
        public function clone(AdminContext $context,
                              AdminUrlGenerator $adminUrlGenerator,
                              KernelInterface $kernel)
        {
            $id = $context->getRequest()->query->get('entityId');
            $entity = $this->getDoctrine()->getRepository(Studiengang::class)->find($id);
    
            $clone = clone $entity;
            $clone->setId(null);
            $clone->setSlug(null);
            $clone->setName($clone->getName() . ' COPY');
            
            $now = new DateTime();
            $clone->setCreatedAt($now);
            $clone->setUpdatedAt($now);        
    
            $this->persistEntity($this->get('doctrine')->getManagerForClass($context->getEntity()->getFqcn()), $clone);
    
            $this->addFlash('success', 'Entity duplicated!');
    
            // Use adminUrlGenerator, the original referer is properly retained
            $url = $adminUrlGenerator->setAll($context->getRequest()->query->all())
                // Override params we want to modify
                ->setAction('edit')
                ->setEntityId($clone->getId())
                ->generateUrl()
            ;
    
            return $this->redirect($url);
        }  

Tom Select

Easyadmin3 uses https://tom-select.js.org/ for select widgets.

If I select an option in a select, add an item to a multiselect:

  • // src/Controller/Admin/ExampleCrudController.php
    ...
        public function configureAssets(Assets $assets): Assets
        {
            // public/js/easyadmin/example.js
            $assets->addJsFile('js/easyadmin/example.js');
    
            return $assets;
        }
  • // public/js/easyadmin/example.js
    document.addEventListener("DOMContentLoaded", function(event)
    {
        const mainTownSelect = document.getElementById("mainTown");
        const townsSelect = document.getElementById("towns");
    
        providerTownSelect.addEventListener("change", function () {
            const selectedValue = mainTownSelect.value;
            const tomSelectInstance = townsSelect.tomselect;
            tomSelectInstance.addItem(selectedValue);
        });
    }); 
  • // Add an empty option
            const tomSelectInstance = myElement.tomselect;
            tomSelectInstance.addOption({ value: '', text: '' });
            tomSelectInstance.addItem("");
            tomSelectInstance.refreshOptions(false);

Custom Action

  • // DasboardController.php
    
       public function configureMenuItems(): iterable
        {
            // https://fontawesome.com/v5/search?q=check&o=r&m=free&f=classic
    
            return[
                MenuItem::linkToCrud('Stats', 'fas fa-chart-bar', Example::class)
                    ->setController(ExampleCrudController::class)
                    ->setAction('stats'),
            ];
        }
  • // ExampleCrudController.php
    
        public function stats()
        {
            ...
    
            return $this->render('admin/example/stats.html.twig', [
                'rows' => $rows,
            ]);
        }
  • // admin/example/stats.html.twig
    
    {% extends '@EasyAdmin/page/content.html.twig' %}
    
    {% block main %}
        <h1>{{ 'Stats' }}</h1>
    
        <table class="table table-bordered table-striped">
            <thead>
            <tr>
                {% for key, _ in rows[0] %}
                    <th>{{ key }}</th>
                {% endfor %}
            </tr>
            </thead>
            <tbody>
            {% for row in rows %}
                <tr>
                    {% for _, value in row %}
                        <td>{{ value }}</td>
                    {% endfor %}
                </tr>
            {% endfor %}
            </tbody>
        </table>
    
    {% endblock %}

Action without a template, but capture output and display it within Easyadmin

  • // ExampleCrudController.php
    
        public function import()
        {
            // Start output buffering
            ob_start();
    
            echo "fooo\n";
    
            $content = ob_get_clean();
    
            return $this->render('admin/content.html.twig', [
                'headline' => 'My Import',
                'pre'   => true,
                'content' => $content,
            ]);
        }
  • // templates/admin/content.html.twig
    
    {% extends '@EasyAdmin/page/content.html.twig' %}
    
    {% block main %}
    
        <h1>{{ headline }}</h1>
    
        {% if (pre|default(null)) %}
            <pre>{{- content|raw -}}</pre>
        {% else %}
            {{- content|raw -}}
        {% endif %}
    
    {% endblock %}

Modify an action

  • ->update(Crud::PAGE_DETAIL, Action::EDIT, static function (Action $action) {
                    return $action->setIcon('fa fa-edit');
                })

Action: use the entity / discover arguments:

  •         $viewAction = Action::new('view')
                ->linkToUrl(function() {
                    dd(func_get_args());
                });
  •         $viewAction = Action::new('view')
                ->linkToUrl(function(Question $question) {
                    return $this->generateUrl('app_question_show', [
                        'slug' => $question->getSlug(),
                    ]);
                });

global action on list view:

  • ->createAsGlobalAction();

custom action with redirect

  • action:
    •     public function approve(AdminContext $adminContext, EntityManagerInterface $entityManager, AdminUrlGenerator $adminUrlGenerator)
          {
              $question = $adminContext->getEntity()->getInstance();
              if (!$question instanceof Question) {
                  throw new \LogicException('Entity is missing or not a Question');
              }
              $question->setIsApproved(true);
              $entityManager->flush();
              $targetUrl = $adminUrlGenerator
                  ->setController(self::class)
                  ->setAction(Crud::PAGE_DETAIL)
                  ->setEntityId($question->getId())
                  ->generateUrl();
              return $this->redirect($targetUrl);
          }
  • template
    • templates/admin/approve_action.html.twig
    • {# @var ea \EasyCorp\Bundle\EasyAdminBundle\Context\AdminContext #}
      {# @var action \EasyCorp\Bundle\EasyAdminBundle\Dto\ActionDto #}
      {# @var entity \EasyCorp\Bundle\EasyAdminBundle\Dto\EntityDto #}
      <form action="{{ action.linkUrl }}" method="POST" class="me-2">
          {{ include('@EasyAdmin/crud/action.html.twig') }}
      </form>

order actions

  •             ->reorder(Crud::PAGE_DETAIL, [
                    'approve',
                    'view',
                    Action::EDIT,
                    Action::INDEX,
                    Action::DELETE,
                ]);

Customization / Structure

  • One route in DashboardController: /admin
  • crudController and crudAction are in every URL.
    • /admin?
    • crudAction=index&
    • crudControllerFqcn=App%5CController%5CAdmin%5CStudiengangCrudController&
    • menuIndex=3&
    • submenuIndex=-1
  • Fields
    • Pseudo properties: Entity::getFullName()
    • No ArrayField in easyadmin3
    • $field->setFormType(ChoiceType::class)->setFormTypOptions(['choices' => [])
    • Field Configurators: vendor/easycorp/easyadmin-bundle/src/Field/Configurator
  • Formatted Value:
    • yield AvatarField::new('avatar')
                  ->formatValue(static function ($value, User $user) {
                      return $user->getAvatarUrl();
                  });
  • Injection
    •     public function index(ChartBuilderInterface $chartBuilder = null): Response
          {
              assert(null !== $chartBuilder);
  • QueryBuilder:
    •     public function createIndexQueryBuilder(SearchDto $searchDto, EntityDto $entityDto, FieldCollection $fields, FilterCollection $filters): QueryBuilder
          {
              return parent::createIndexQueryBuilder($searchDto, $entityDto, $fields, $filters)
                  ->andWhere('entity.isApproved = :approved')
                  ->setParameter('approved', false);
          }
  • Configure Crud, inject entity
    •             ->setPageTitle(Crud::PAGE_DETAIL, static function (Question $question) {
                      return sprintf('#%s %s', $question->getId(), $question->getName());
                  });
  • Events, good for enhancments for multiple entities
  • Controller override methods, e.g. updateEntity()
    •     public function updateEntity(EntityManagerInterface $entityManager, $entityInstance): void
          {
              $user = $this->getUser();
              if (!$user instanceof User) {
                  throw new \LogicException('Currently logged in user is not an instance of User?!');
              }
              $entityInstance->setUpdatedBy($user);
              parent::updateEntity($entityManager, $entityInstance);
          }
    •     public function deleteEntity(EntityManagerInterface $entityManager, $entityInstance): void
          {
              if ($entityInstance->getIsApproved()) {
                  throw new \Exception('Deleting approved questions is forbidden!');
              }
              parent::deleteEntity($entityManager, $entityInstance);
          }
  • Tweak actions with update() e.g displayIf()
    •     public function configureActions(Actions $actions): Actions
          {
              return parent::configureActions($actions)
                  ->update(Crud::PAGE_INDEX, Action::DELETE, static function(Action $action) {
                      $action->displayIf(static function (Question $question) {
                          return !$question->getIsApproved();
                      });
                  })
  • Tamper with action buttons: Events / AdminContext / crudDto:
    • https://symfonycasts.com/screencast/easyadminbundle/dynamic-disable-action#disabling-the-action
    • <?php
      // src/EventSubscriber/HideActionSubscriber.php

      namespace App\EventSubscriber;

      use App\Entity\Question;
      use EasyCorp\Bundle\EasyAdminBundle\Config\Action;
      use EasyCorp\Bundle\EasyAdminBundle\Dto\ActionDto;
      use EasyCorp\Bundle\EasyAdminBundle\Event\BeforeCrudActionEvent;
      use Symfony\Component\EventDispatcher\EventSubscriberInterface;

      class HideActionSubscriber implements EventSubscriberInterface
      {
          public function onBeforeCrudActionEvent(BeforeCrudActionEvent $event)
          {
              if (!$adminContext = $event->getAdminContext()) {
                  return;
              }
              if (!$crudDto = $adminContext->getCrud()) {
                  return;
              }
              if ($crudDto->getEntityFqcn() !== Question::class) {
                  return;
              }

              // disable action entirely delete, detail, edit
              $question = $adminContext->getEntity()->getInstance();
              if ($question instanceof Question && $question->getIsApproved()) {
                  $crudDto->getActionsConfig()->disableActions([Action::DELETE]);
              }

              // This gives you the "configuration for all the actions".
              // Calling ->getActions() returns the array of actual actions that will be
              // enabled for the current page... so then we can modify the one for "delete"
              $actions = $crudDto->getActionsConfig()->getActions();
              if (!$deleteAction = $actions[Action::DELETE] ?? null) {
                  return;
              }
              $deleteAction->setDisplayCallable(function(Question $question) {
                  return !$question->getIsApproved();
              });
          }

          public static function getSubscribedEvents()
          {
              return [
                  BeforeCrudActionEvent::class => 'onBeforeCrudActionEvent',
              ];
          }
      }

       

  •    public function onBeforeCrudActionEvent(BeforeCrudActionEvent $event)
        {
            if (!$adminContext = $event->getAdminContext()) {
                return;
            }
            if (!$crudDto = $adminContext->getCrud()) {
                return;
            }

Tamper with filter query

  • public function export(AdminContext $context)
        {
            $fields = FieldCollection::new($this->configureFields(Crud::PAGE_INDEX));
            $filters = $this->container->get(FilterFactory::class)->create($context->getCrud()->getFiltersConfig(), $fields, $context->getEntity());
            $queryBuilder = $this->createIndexQueryBuilder($context->getSearch(), $context->getEntity(), $fields, $filters);
        }

Custom URL with query params

  • class QuestionCrudController extends AbstractCrudController
    {
        private AdminUrlGenerator $adminUrlGenerator;
        private RequestStack $requestStack;
        public function __construct(AdminUrlGenerator $adminUrlGenerator, RequestStack $requestStack)
        {
            $this->adminUrlGenerator = $adminUrlGenerator;
            $this->requestStack = $requestStack;
        }
  •  
  • // Generate the same URL that I have now... but change the action to point to export.
            $exportAction = Action::new('export')
                ->linkToUrl(function () {
                    $request = $this->requestStack->getCurrentRequest();
                    return $this->adminUrlGenerator->setAll($request->query->all())
                        ->setAction('export')
                        ->generateUrl();
                })

twig link to easyadmin

  •                                 <a class="text-white" href="{{ ea_url()
                                        .setController('App\\Controller\\Admin\\QuestionCrudController')
                                        .setAction('edit')
                                        .setEntityId(question.id)
                                    }}">

Custom elements in template / Pass custom data

src/Controller/admin/ExampleCrudController:

  •     public function configureCrud(Crud $crud): Crud
        {
            return $crud
               ...
                ->overrideTemplate('crud/index', 'admin/crud/example/index.html.twig');
        }
    
        public function configureResponseParameters(KeyValueStore $responseParameters): KeyValueStore
        {
            $responseParameters->set('foo', 'bar');
    
            return $responseParameters;
        }

templates/admin/crud/example/index.html.twig:

  • {% extends '@EasyAdmin/crud/index.html.twig' %}
    
    {% block content_title %}
        {{ parent() }}
        {{ foo }}
    {% endblock %}
    
    {% block main %}
        {{ parent() }}
    
        hello
    
        <script src="https://example.com/example.js"></script>
    
    {% endblock %}

 

Filters


https://symfony.com/bundles/EasyAdminBundle/current/filters.html#unmapped-filters

Autocomplete Filter https://gist.github.com/wizhippo/9043a6676ce2920676b730ba2f507655

Sidebar filters prototype: https://github.com/klemens-u/easyadminSidebarFilters

Add a custom filter

  • https://stackoverflow.com/questions/68722242/easyadmin-3-3-create-custom-filter
  • <?php
    //src/Admin/Form/HasContentFilterType.php
    
    namespace App\Admin\Form;
    
    use Symfony\Component\Form\AbstractType;
    use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
    use Symfony\Component\OptionsResolver\OptionsResolver;
    
    class HasContentFilterType extends AbstractType
    {
        public function configureOptions(OptionsResolver $resolver)
        {
            $resolver->setDefaults([
                'choices' => [
                    'Has Content' => 'not_empty',
                    'Is Empty' => 'empty',
                ],
            ]);
        }
    
        public function getParent()
        {
            return ChoiceType::class;
        }
    
    }
  • <?php
    // src/Admin/Filter/HasContentFilter.php
    
    namespace App\Admin\Filter;
    
    use App\Admin\Form\HasContentFilterType;
    use Doctrine\ORM\QueryBuilder;
    use EasyCorp\Bundle\EasyAdminBundle\Contracts\Filter\FilterInterface;
    use EasyCorp\Bundle\EasyAdminBundle\Dto\EntityDto;
    use EasyCorp\Bundle\EasyAdminBundle\Dto\FieldDto;
    use EasyCorp\Bundle\EasyAdminBundle\Dto\FilterDataDto;
    use EasyCorp\Bundle\EasyAdminBundle\Filter\FilterTrait;
    
    class HasContentFilter implements FilterInterface
    {
        use FilterTrait;
    
        public static function new(string $propertyName, $label = null): self
        {
            return (new self())
                ->setFilterFqcn(__CLASS__)
                ->setProperty($propertyName)
                ->setLabel($label)
                ->setFormType(HasContentFilterType::class);
        }
    
        public function apply(QueryBuilder $queryBuilder, FilterDataDto $filterDataDto, ?FieldDto $fieldDto, EntityDto $entityDto): void
        {
            if ('not_empty' === $filterDataDto->getValue()) {
                $queryBuilder->andWhere(sprintf('%s.%s IS NOT NULL AND %s.%s != \'\'', $filterDataDto->getEntityAlias(), $filterDataDto->getProperty(), $filterDataDto->getEntityAlias(), $filterDataDto->getProperty()));
            } elseif ('empty' === $filterDataDto->getValue()) {
                $queryBuilder->andWhere(sprintf('%s.%s IS NULL OR %s.%s = \'\'', $filterDataDto->getEntityAlias(), $filterDataDto->getProperty(), $filterDataDto->getEntityAlias(), $filterDataDto->getProperty()));
            }
        }
    }
    
    
  • // FooCrudController.php
    
        public function configureFilters(Filters $filters): Filters
        {
            $filters
                ->add('name')        
                ->add(HasContentFilter::new('logo'))
            ;
    
            return $filters;
        }