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!

5
Login or Register to join the conversation
Szabolcs Avatar
Szabolcs Avatar Szabolcs | posted 2 days 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
Chris N. Avatar
Chris N. Avatar Chris N. | posted 4 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

Reply

Hey Chris

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

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 Altungi!

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