Chapters
-
Course Code
Subscribe to download the code!
Subscribe to download the code!
-
This Video
Subscribe to download the video!
Subscribe to download the video!
-
Subtitles
Subscribe to download the subtitles!
Subscribe to download the subtitles!
-
Course Script
Subscribe to download the script!
Subscribe to download the script!
The Filter System
Scroll down to the script below, click on any sentence (including terminal blocks) to jump to that spot in the video!
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.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeLet'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()
.
Show Lines
|
// ... lines 1 - 9 |
use EasyCorp\Bundle\EasyAdminBundle\Config\Filters; | |
Show Lines
|
// ... lines 11 - 23 |
class UserCrudController extends AbstractCrudController | |
{ | |
Show Lines
|
// ... 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
.
Show Lines
|
// ... lines 1 - 85 |
public function configureFilters(Filters $filters): Filters | |
{ | |
return parent::configureFilters($filters) | |
->add('enabled'); | |
} | |
Show Lines
|
// ... 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'))
.
Show Lines
|
// ... lines 1 - 86 |
public function configureFilters(Filters $filters): Filters | |
{ | |
return parent::configureFilters($filters) | |
->add(BooleanFilter::new('enabled')); | |
} | |
Show Lines
|
// ... 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)
.
Show Lines
|
// ... lines 1 - 86 |
public function configureFilters(Filters $filters): Filters | |
{ | |
return parent::configureFilters($filters) | |
->add(BooleanFilter::new('enabled')->setFormTypeOption('expanded', false)); | |
} | |
Show Lines
|
// ... 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')
.
Show Lines
|
// ... lines 1 - 10 |
use EasyCorp\Bundle\EasyAdminBundle\Config\Filters; | |
Show Lines
|
// ... lines 12 - 19 |
class QuestionCrudController extends AbstractCrudController | |
{ | |
Show Lines
|
// ... 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
.
Show Lines
|
// ... lines 1 - 94 |
public function configureFilters(Filters $filters): Filters | |
{ | |
return parent::configureFilters($filters) | |
->add('topic') | |
->add('createdAt') | |
->add('votes') | |
->add('name'); | |
} | |
Show Lines
|
// ... 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.
31 Comments
// 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)
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!
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 likeprif
/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 overridingconfigureFilters
, since it sees, thatconfigureFields
already overridden. Orconfa
would suggestconfigureAssets
. When you're completely lazy, you could typeam
/af
with high chance that it would suggestarray_map
/array_filter
, depending on context of caret position and just hitEnter
.
I also like shortening in commands names symfony console m:a:d
, symfony console m:a:c
, symfony console m:vo
.
Thanks for sharing it with others!
Hello, I have EntityFilter for Categories.
Could I add autocomplete and paging to this option list in the filter as in AssotiationField?
https://prnt.sc/W8rjbXwkDqPR
Yo @Ruslan-A!
Hmm. So you want to basically add the AJAX-powered autocomplete and pagination to the filter, correct? Yea, I don't see an easy way to do that, unfortunately :/. In theory, I THINK if you were able to add a data-ea-autocomplete-endpoint-url
attribute to the select
element set to the autocomplete URL, then it might work. That is how the normal AssociationField
works: https://github.com/EasyCorp/EasyAdminBundle/blob/4.x/src/Field/Configurator/AssociationConfigurator.php#L139-L158
So, it might be possible - you CAN call ->add(EntityFilter::new('topic')->setFormTypeOption(...))
but I have no idea if it will actually work. But I'd love to know if you get it working!
Cheers!
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
Hi @Marouane-M!
Ah, interesting! EasyAdmin actually loads those filters via an Ajax call, so I'm not surprised that you're missing some variables when you try to render them in the main template. I've never tried what you're attempting, but the form and rendering of the filter form is actually done in AbstractCrudController::renderFilters()
: https://github.com/EasyCorp/EasyAdminBundle/blob/be6a5f8dab930b36973329dfb6505b3774cfe8ee/src/Controller/AbstractCrudController.php#LL478C21-L478C34
You could, in theory, copy some of the code from this method and pass the missing variable into your template (perhaps by overriding index()
?) Another option would be to continue to load this via Ajax, but load it immediately after the page loads. You can see how the URL is built to this AJAX call here: https://github.com/EasyCorp/EasyAdminBundle/blob/be6a5f8dab930b36973329dfb6505b3774cfe8ee/src/Resources/views/crud/index.html.twig#L54
I hope these hints help - let me know if you get it working!
Cheers!
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
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!
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
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!
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
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!
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!
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!
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!
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!
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.
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!
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!
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.
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!
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.
but this worked:
->setFormTypeOptions([
'comparison_type' => HiddenType::class,
'comparison_type_options' => ['data' => ComparisonType::EQ],
]);
Thank you very much, you still helped me a lot!
Perfect! Thanks for sharing the solution!
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.
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!
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.
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!
"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.4.5
"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
}
}
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