Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

The Filter System

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $12.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

Let's go log out... and then log back in as our "super admin" user: "superadmin@example.com"... with "adminpass". Now head back to the admin area, find the Users list and... perfect! As promised, we can see every user in the system.

Our user list is pretty short right now, but it's going to get longer and longer as people realize... just how amazing our site is. It would be great if we could filter the records in this index section by some criteria, for example, to only show users that are enabled or not enabled. Fortunately, EasyAdmin has a system for this called, well, filters!

Hello configureFilters()

Over in UserCrudController, I'll go to the bottom and override yet another method called configureFilters().

... lines 1 - 9
use EasyCorp\Bundle\EasyAdminBundle\Config\Filters;
... lines 11 - 23
class UserCrudController extends AbstractCrudController
{
... lines 26 - 85
public function configureFilters(Filters $filters): Filters
{
return parent::configureFilters($filters);
}
}

This looks and feels a lot like configureFields(): we can call ->add() and then put the name of a field like enabled.

... lines 1 - 85
public function configureFilters(Filters $filters): Filters
{
return parent::configureFilters($filters)
->add('enabled');
}
... lines 91 - 92

And... that's all we need! If we refresh the page, watch this section around the top. We have a new "Filters" button! That opens up a modal where we can filter by whichever fields are available. Let's say "Enabled", "No" and... all of these are gone because all of our users are enabled.

We can go and change that... or clear the filter entirely.

Filter Types

Ok: notice that enabled in our entity is a boolean field... and EasyAdmin detected that. It knew to make this as a "Yes" or "No" checkbox. Just like with the field system, there are also many different types of filters. And if you just add a filter by saying ->add() and then the property name, EasyAdmin tries to guess the correct filter type to use.

But, you can be explicit. What we have now is, in practice, identical to saying ->add(BooleanFilter::new('enabled')).

... lines 1 - 86
public function configureFilters(Filters $filters): Filters
{
return parent::configureFilters($filters)
->add(BooleanFilter::new('enabled'));
}
... lines 92 - 93

When we refresh now... and check the filters... that makes no difference because that was already the filter type it was guessing.

Each filter class controls how that filter looks in the form up here, and also how it modifies the query for the page. Hold cmd or ctrl and open the BooleanFilter class. It has a new() method just like fields, and this sets some basic information: the most important being the form type and any form type options.

The apply() method is the method that will be called when the filter is applied: it's where the filter modifies the query.

Filter Form Type Options

Back in new(), this uses a form field called BooleanFilterType. Hold cmd or ctrl to open that. Like all form types, this exposes a bunch of options that allow us to control its behavior. Apparently there's an expanded option, which is the reason that we're seeing this field as expanded radio buttons.

Just to see if we can, let's try changing that. Close that file... and after the filter, add ->setFormTypeOption('expanded', false).

... lines 1 - 86
public function configureFilters(Filters $filters): Filters
{
return parent::configureFilters($filters)
->add(BooleanFilter::new('enabled')->setFormTypeOption('expanded', false));
}
... lines 92 - 93

Try it now: refresh... head to the filters and... awesome! The non-expanded version means it's rendered as a dropdown.

The Many Filter Type Classes

Let's add some filters to the Questions section. Open QuestionCrudController and, near the bottom, override configureFilters(). Start with an entity relation. Each question has a ManyToOne relationship to Topic, so let's ->add('topic').

... lines 1 - 10
use EasyCorp\Bundle\EasyAdminBundle\Config\Filters;
... lines 12 - 19
class QuestionCrudController extends AbstractCrudController
{
... lines 22 - 94
public function configureFilters(Filters $filters): Filters
{
return parent::configureFilters($filters)
->add('topic');
}
}

Go refresh. We get the new filter section... and "Topic" is... this cool dropdown list where we can select whatever topic we want!

To know how you can control this - or any - filter, you need to know what type it is. Just like with fields, if you click on the filter class, you can see there's a src/Filter/ directory deep in the bundle. So vendor/easycorp/easyadmin-bundle/src/Filter/... and here is the full list of all possible filters.

I bet EntityFilter is the filter that's being used for the relationship. By opening this up, we can learn about any methods it might have that will let us configure it or how the query logic is done behind the scenes.

Let's add a few more filters, like createdAt... votes... and name.

... lines 1 - 94
public function configureFilters(Filters $filters): Filters
{
return parent::configureFilters($filters)
->add('topic')
->add('createdAt')
->add('votes')
->add('name');
}
... lines 103 - 104

And... no surprise, those all show up! The coolest thing is what they look like. The createdAt field has a really easy way to choose dates, or even filter between two dates. For Votes, you can choose "is equal", "is greater than", "is less than", etc. And Name has different types of fuzzy searches that you can apply. Super powerful.

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.

Ok, right now, we have one "crud controller" per entity. But it's totally legal to have multiple CRUD controllers for the same entity: you may have a situation where each section shows a different filtered list. But even if you don't have this use-case, adding a second CRUD controller for an entity will help us dive deeper into how EasyAdmin works. That's next.

Leave a comment!

28
Login or Register to join the conversation
Chris N. Avatar
Chris N. Avatar Chris N. | posted 1 year ago

Not sure if I missed something in a previous step, but adding the filters to QuestionCrudController resulted in an Exception being thrown:

`strlen(): Argument #1 ($string) must be of type string, null given`

I modified line 38 of `src/EasyAdmin/TruncateLongTextConfigurator.php`
to be

`if (null === $field->getFormattedValue() || strlen($field->getFormattedValue()) <= self::MAX_LENGTH) {`

and all is good

2 Reply
yaroslavche Avatar
yaroslavche Avatar yaroslavche | Chris N. | posted 3 months ago | edited
// or use null-coalesce
if (strlen($field->getFormattedValue() ?? '') <= self::MAX_LENGTH)
// or cast to string
if (strlen((string)$field->getFormattedValue()) <= self::MAX_LENGTH)
// or use strval function
if (strlen(strval($field->getFormattedValue())) <= self::MAX_LENGTH)
Reply

Hey Chris N.

I don't think you missed something, it looks like you hit a tiny sneaky bug on the TruncateLongTextConfigurator logic. Since it works with all the Textarea fields, it should assume that there will be fields with null values, so, your solution looks good to me

Cheers!

Reply
yaroslavche Avatar
yaroslavche Avatar yaroslavche | posted 3 months ago | edited

Maybe would be helpful for someone, Intellij tip: Instead of Ctrl+O and typing method name (3:58), you could just try start typing method name for autocompletion (IDK how this feature called in Intellij). Benefits:

  • you're able to type not only for methods that can be overridden (Ctrl+O), but also which can be implemented (Ctrl+I), and setters/getters, magic methods, some stubs from Code Generate (Alt+I) and other things, like accessing methods/properties, handy shortcuts like prif/pubf/pubsf/etc. (in class body), thr/forek/etc. (in method body).
  • smart search (alphabetic semantic) for typed characters: they can be shortened, for instance, conff will give just one suggestion for overriding configureFilters, since it sees, that configureFields already overridden. Or confa would suggest configureAssets. When you're completely lazy, you could type am/af with high chance that it would suggest array_map/array_filter, depending on context of caret position and just hit Enter.

I also like shortening in commands names symfony console m:a:d, symfony console m:a:c, symfony console m:vo.

1 Reply

Thanks for sharing it with others!

Reply
Marouane-M Avatar
Marouane-M Avatar Marouane-M | posted 1 day ago

Hello there thanks for this clear course i have a question. i want to display filters directly on crud page i overided the index page of the crud but when modifing the block filters i can't use form_widget to display the filterform. any response in this topic will be helpfull thanks

Reply
Alexey-P Avatar
Alexey-P Avatar Alexey-P | posted 1 month ago

Many thanks! I have a little question. How to make a filter preselected? for example date in exercise enabled = true; or date preselect to current date?
I would suggest using AdminUrlGenerator, but this is a theory. Thank you in Advance

Reply

Hey Alexey,

For this, you need to understand how filters work. EA filters work by query parameters, so to make the filter pre-enabled by default - you need to pass some specific parameters in the URL. Which exactly? You can enable and apply that filter and check the URL to know the correct extra parameters. And then, for the link that you want to click and see the filter pre-enabled - I suppose you want to do it for the link in the dashboard menu - you need to add those extra parameters. Then every time you will click that link - filter will be pre-enabled. That's not simple, but should do the trick :)

I hope this helps!

Cheers!

Reply
Delenclos-A Avatar
Delenclos-A Avatar Delenclos-A | posted 1 month ago | edited

Hi,
I created 2 custom filters, as in the documentation. Each of the filters works perfectly, but both don't. The query is applied with the last filter, ignoring the first filter ($filterDataDto->getValue()

The two filters are the same entitytype (Many to one)

class AssociationFilter implements FilterInterface
{
    use FilterTrait;
    public static function new(string $propertyName, $label = null): self
    {
        return (new self())
            ->setFilterFqcn(__CLASS__)
            ->setProperty($propertyName)
            ->setLabel($label)
            ->setFormType(AssociationFilterType::class);
    }
    public function apply(QueryBuilder $queryBuilder, FilterDataDto $filterDataDto, ?FieldDto $fieldDto, EntityDto $entityDto): void
    {

        if ('' !== $filterDataDto->getValue()) {
            $queryBuilder
                ->andWhere('entity.association = :value')
                ->setParameter('value', $filterDataDto->getValue());
        }
    }
}

for the other filter

    public static function new(string $propertyName, $label = null): self
    {
        return (new self())
            ->setFilterFqcn(__CLASS__)
            ->setProperty($propertyName)
            ->setLabel($label)
            ->setFormType(CoupleFilterType::class);
    }
    public function apply(QueryBuilder $queryBuilder, FilterDataDto $filterDataDto, ?FieldDto $fieldDto, EntityDto $entityDto): void
    {

        if ('' !== $filterDataDto->getValue()) {
            $queryBuilder
                ->andWhere('entity.couple = :value')
                ->setParameter('value', $filterDataDto->getValue());
        }
    }

the request takes the same Id for couple and association.

SELECT count(DISTINCT d0_.id) AS sclr_0 FROM dance d0_ LEFT JOIN couple c1_ ON d0_.couple_id = c1_.id LEFT JOIN association a2_ ON d0_.association_id = a2_.id WHERE d0_.couple_id = ? AND d0_.association_id = ? AND d0_.association_id IN (?, ?, ?, ?)
Parameters:
[▼
  57
  57
  55
  56
  57
  59
]

Thank's for your help.
Alan from France

Reply

Hey Delenclos,

Please, first, make sure you hit both filters, e.g. add some dump statements in both to see if they both are called. Also, just in case, clear the cache, I'd recommend to do it with rm -rf var/cache/ just in case. But fairly speaking, I see that the final query holds both entity.association and entity.couple fields, so I may suppose it should work well because it's in the query?

Cheers!

Reply
Delenclos-A Avatar
Delenclos-A Avatar Delenclos-A | Victor | posted 1 month ago

Hi,

I found the solution, it's just because the parameter name (:value) must be different in each apply custom filter function. Thank's for all.

Alain

Reply

Hey Delenclos,

Awesome, thanks for sharing the final solution with others! Yep, it should be unique, otherwise it will be overwritten in the URL :)

Cheers!

Reply
Gianluca-F Avatar
Gianluca-F Avatar Gianluca-F | posted 6 months ago

Hi all,

actually, the search filter opens a modal with all filters added; is there a way to customize or override the template for filters container?
let you want to do something different from opening a modal ... can we do this ?

thanks in advance!

Reply

Hi Gianluca-F!

Sorry for the slow reply - we're all getting back from Symfony conference week :).

So, let's see...

is there a way to customize or override the template for filters container?

Probably, though depending on what you want to do, simply overriding the template for the modal may not give you what you need. The template lives here: https://github.com/EasyCorp/EasyAdminBundle/blob/4.x/src/Resources/views/crud/includes/_filters_modal.html.twig

And so, you should be able to follow Symfony's normal process for overriding a template from a bundle: https://symfony.com/doc/current/bundles/override.html#templates

let you want to do something different from opening a modal ... can we do this ?

I am more doubtful that this is possible... at least easily. The code that renders the "Filters" button lives here: https://github.com/EasyCorp/EasyAdminBundle/blob/76d8585b25d400a75234d714a83962d76ab0aeb1/src/Resources/views/crud/index.html.twig#L49-L65

You can see the <a> tag that is built to work with the Bootstrap modal. In theory, you could override this template... and try to change JUST that area. Or, you could allow this to render like normal, but then add some custom JavaScript to try to override the modal behavior and do something different. It may or may not be super tricky, depending on what you need to do. But hopefully this can help :).

Cheers!

Reply
Szabolcs Avatar
Szabolcs Avatar Szabolcs | posted 7 months ago | edited

I just have a very last question! Is it in EntityFilter's somehow possible to control the query which is responsible to create the filter-options which are shown in the select-form? It just fetching all the data but I would like to apply some exclude conditions, but I have no idea how to access the query-builder. This should be somehow possible, but there is really no documentation. Thanks!

Reply

Hey again!

Yes, good question :). I think the answer may be similar to your previous question. There are two fields for each filter the "comparison field" (e.g. equals, less than, etc) and the "value field", which in this case would be the select menu of options. Both of these are confirable. I would try something like:

->setFormTypeOptions([
    'value_type_options' => ['query_builder' => function(...) {}],
]);

If I'm reading the code correctly, for an EntityFilter, internally, it will use an EntityType for that select element and pass value_type_options TO that EntityType. So whatever options EntityType has, you should be able to pass those to value_type_options.

Let me know how that goes!

Cheers!

Reply
Szabolcs Avatar
Szabolcs Avatar Szabolcs | weaverryan | posted 7 months ago | edited

Thank you very much, it works! Once again you solved my problem :-D! I looked in the EntityFilterType, EntityType, Filter-Configurators and even in the templates, but I had no luck. I think the filter should provide a helper-method or at least, this should be somehow mentioned in the documentation.

Reply

Yay! Yea, this isn't a use-case I had ever thought of (modifying the filters), but now that you mention it, it probably should be documented. If you have some time, you could add a section here - https://github.com/EasyCorp/EasyAdminBundle/blob/4.x/doc/filters.rst - e.g. Advanced Filter Configuration where you mention these options we've learned about.

Cheers!

Reply
Szabolcs Avatar

Thank you, this is a good idea. I think, having access to the query-builder on filters should be also very common, maybe to exclude entities which are marked as deleted and so on. Cheers!

Reply
Szabolcs Avatar
Szabolcs Avatar Szabolcs | posted 8 months ago

Hello! Is it somehow possible to hide these comparison-options in EntityFilters? I want only provide "is equal" without this select-box. Sadly there is nothing in the documentation which options I can provide to the EntityFilter or to any Filter. I also tried to find this part in the view to maybe override the template.

Reply

Hey Szabolcs!

Ah, that's an interesting question! So here's the flow:

A) You use the EntityFilter directly from EasyAdmin - https://github.com/EasyCorp/EasyAdminBundle/blob/4.x/src/Filter/EntityFilter.php
B) Internally, its form type is EntityFilterType: https://github.com/EasyCorp/EasyAdminBundle/blob/4.x/src/Form/Filter/Type/EntityFilterType.php
C) That class extends (via getParent()) several classes, but eventually ComparisonFilterType - https://github.com/EasyCorp/EasyAdminBundle/blob/4.x/src/Form/Filter/Type/ComparisonFilterType.php
D) ComparisonFilterType adds a comparison field set to ComparisonType, which is the field that, finally, shows the "select box" with things like "is equal", etc.

So, how can you hook into this? I'm doing some guessing, but I can think of a few ways:

1) You could call ->setFormTypeOption('comparison_type_options', ['choices' => ['filter.label.is_same' => ComparisonType::EQ]]); on your filter field in your controller. I believe this would change the choice to only show the ONE option. Not exactly what you were asking for, but closer.
2) You might also be able to change this to a hidden field, via:

->setFormTypeOption('comparison_type_options', [
    'comparison_type' => HiddenType::class,
    'comparison_type_options' => ['data' => ComparisonType::EQ],
])

I might be missing something, but that should change the select field to a hidden field... and then set the data on the hidden field to the "equal" value so that everything works.

There are also other options - like using a form type extension to more directly alter one of the fields. But try this first :).

Cheers!

Reply
Szabolcs Avatar
Szabolcs Avatar Szabolcs | weaverryan | posted 8 months ago | edited

Hi Ryan

thank you very much for your answer! This solution throws a nice error :-D, it says >The options "comparison_type", "comparison_type_options" do not exist<. I think I tried this before.

I also don't know, what is the approach when I want to place some filter (or an action-button) on a complete different place in the template - in my admin-tool, the admin-users must select some categories at first in order to continue and so on.

I have to say, building CRUD-based controllers in API-Platform and just write the frontend-logic in Vue or in React is much easier. For a simple todo-list, easyAdmin works, but when it comes to a little complexity, it is really difficult to customize it.

Reply
Szabolcs Avatar
Szabolcs Avatar Szabolcs | Szabolcs | posted 8 months ago | edited

but this worked:

->setFormTypeOptions([
    'comparison_type' => HiddenType::class,
    'comparison_type_options' => ['data' => ComparisonType::EQ],
]);

Thank you very much, you still helped me a lot!

Reply

Perfect! Thanks for sharing the solution!

Reply
Szabolcs Avatar
Szabolcs Avatar Szabolcs | weaverryan | posted 7 months ago | edited

Hello! I just want to mention, that this solution to hiding the comparison-options (which are in most cases not necessary) leads to an error when I use multiple entity-filters. This error appears:

EasyCorp\Bundle\EasyAdminBundle\Dto\FilterDataDto::getComparison(): Return value must be of type string, null returned<br />

With only one filter, it works fine. So my solution ist just to hide them with css :-D

form[name=filters] .filter-content .form-widget-compound > div > div:first-child {
    display: none;
}

I really really recommend to avoid this easy-admin-bundle and use api-platform to implement crud-action and build the UI's with Vue or React.

Reply

Hey Szabolcs!

Ah, that's a shame! I'm guessing there is a way to work around that, but your solution is fine of course - thanks for sharing!

I really really recommend to avoid this easy-admin-bundle and use api-platform to implement crud-action and build the UI's with Vue or React.

I really like EasyAdmin - but it's a fair point - we have multiple excellent options :).

Cheers!

Reply

Many thanks! I have a little question. Can we change the display name of the intities inside the filter? Because I've already changed the entities names in the table and they don't look the same right now.

Reply

Hey Lubna!

That's an excellent question! So, for a relation filter, you're using EntityFilter: https://github.com/EasyCorp/EasyAdminBundle/blob/4.x/src/Filter/EntityFilter.php. Behind the scenes, the form type for that filter is EntityFilterType: https://github.com/EasyCorp/EasyAdminBundle/blob/025bc2b5e5add6961c884dca7338e01c20f4c71f/src/Filter/EntityFilter.php#L33

So let's look at that class: https://github.com/EasyCorp/EasyAdminBundle/blob/4.x/src/Form/Filter/Type/EntityFilterType.php

Notice it has a "value_type" of EntityType. I don't really know how "value_type" is used, but it sounds like, at some level, it's using EntityType to build the filter form element. That EntityFilterType's "parent type" is ChoiceFilterType, which means it inherits all of its options: https://github.com/EasyCorp/EasyAdminBundle/blob/4.x/src/Form/Filter/Type/ChoiceFilterType.php

Finally, here we can see how value_type is used and also there is a value_type_options option. Since we know that the EntityType's "display" can be controlled via the "choice_label" option (ref: https://symfonycasts.com/screencast/easyadminbundle/association-many#configuring-the-choice-label-field-option), we can put the whole thing together. I believe it would look like this (inside configureFilters):


->add(
    EntityFilter::new('topic')->setFormTypeOption('value_type_options', [
        'choice_label' => 'name' // where "name" is the property on Topic to use. Or pass a callback to this
    ])
)

It's very possible I messed up some syntax here, but the idea should work: we pass a "form type option" to EntityFilter called "value_type_options". This is eventually passed to EntityFilterType/ChoiceFilterType, which uses it when constructing the EntityType to build that field.

Phew! Let me know if that works.

Cheers!

1 Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": ">=8.1.0",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "composer/package-versions-deprecated": "^1.11", // 1.11.99.4
        "doctrine/doctrine-bundle": "^2.1", // 2.5.5
        "doctrine/doctrine-migrations-bundle": "^3.0", // 3.2.1
        "doctrine/orm": "^2.7", // 2.10.4
        "easycorp/easyadmin-bundle": "^4.0", // v4.0.2
        "handcraftedinthealps/goodby-csv": "^1.4", // 1.4.0
        "knplabs/knp-markdown-bundle": "dev-symfony6", // dev-symfony6
        "knplabs/knp-time-bundle": "^1.11", // 1.17.0
        "sensio/framework-extra-bundle": "^6.0", // v6.2.5
        "stof/doctrine-extensions-bundle": "^1.4", // v1.7.0
        "symfony/asset": "6.0.*", // v6.0.1
        "symfony/console": "6.0.*", // v6.0.2
        "symfony/dotenv": "6.0.*", // v6.0.2
        "symfony/flex": "^2.0.0", // v2.0.1
        "symfony/framework-bundle": "6.0.*", // v6.0.2
        "symfony/mime": "6.0.*", // v6.0.2
        "symfony/monolog-bundle": "^3.0", // v3.7.1
        "symfony/runtime": "6.0.*", // v6.0.0
        "symfony/security-bundle": "6.0.*", // v6.0.2
        "symfony/stopwatch": "6.0.*", // v6.0.0
        "symfony/twig-bundle": "6.0.*", // v6.0.1
        "symfony/ux-chartjs": "^2.0", // v2.0.1
        "symfony/webpack-encore-bundle": "^1.7", // v1.13.2
        "symfony/yaml": "6.0.*", // v6.0.2
        "twig/extra-bundle": "^2.12|^3.0", // v3.3.7
        "twig/twig": "^2.12|^3.0" // v3.3.7
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.3", // 3.4.1
        "symfony/debug-bundle": "6.0.*", // v6.0.2
        "symfony/maker-bundle": "^1.15", // v1.36.4
        "symfony/var-dumper": "6.0.*", // v6.0.2
        "symfony/web-profiler-bundle": "6.0.*", // v6.0.2
        "zenstruck/foundry": "^1.1" // v1.16.0
    }
}
userVoice