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!

18
Login or Register to join the conversation
Chris N. Avatar
Chris N. Avatar Chris N. | posted 8 months 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

1 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
Gianluca-F Avatar
Gianluca-F Avatar Gianluca-F | posted 2 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 3 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 3 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 4 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 4 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 4 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 3 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.... Behind the scenes, the form type for that filter is EntityFilterType: https://github.com/EasyCorp...

So let's look at that class: https://github.com/EasyCorp...

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...

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/sc..., 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.0.2",
        "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
    }
}