Using a ChoiceType (or EnumType) with multiple / expanded set to true as a filter breaks URL generation
Hello Kreyu,
First of all, thank you for this amazing bundle! I've been using and following it for months, and it has been incredibly helpful.
However, I’ve encountered an issue when trying to implement a ChoiceType or EnumType filter with multiple: true and expanded: true. This setup seems to break URL generation in the DataTable. Specifically, every action within the DataTable appends all filter choices to the query, regardless of whether any are selected.
Here’s what I’ve done:
- Overriding the filter_clear_button block: To avoid translation errors when the filter value is an array, I had to override the filter_clear_button block as follows:
{% block filter_clear_button %}
...
<span class="">{{ filter.vars.value is iterable ? filter.vars.value|map(value => value|trans)|join(', ') : filter.vars.value|trans({}, filter.vars.translation_domain) }}</span>
...
{% endblock %}
- Defining the filter: I configured the filter with these options:
->addFilter('color', StringFilterType::class, [
'label' => 'Color',
'form_type' => EnumType::class,
'form_options' => [
'class' => ColorEnum::class,
'multiple' => true,
'expanded' => true,
],
'default_operator' => Operator::In,
])
- Observing the issue: Any interaction within the DataTable (e.g., sorting, pagination) results in all filter choices being appended to the query, even if none were selected.
To help debug this issue, I’ve created a small reproducer repository: https://github.com/j0r1s/datatable-choice-filter-multiple.
I’ve tried to investigate the code to identify the root cause but haven’t been able to pinpoint it yet. I’d be happy to assist further if I can!
Thank you in advance for looking into this.
I had some time to investigate the issue, and it seems the culprit is the FormUtil::getFormViewValueRecursive method. When the expanded and multiple options are both set to true, the form becomes compound, and this method iterates over the children of the filter.
As a result, all checkbox values are added to the query parameters, regardless of whether they are checked or not.
To address this, I added a small check inside the method to omit unchecked checkbox values. This appears to resolve the issue:
class FormUtil
{
public static function getFormViewValueRecursive(FormView $view): mixed
{
$value = $view->vars['value'];
if (!empty($view->children)) {
$value = [];
foreach ($view->children as $child) {
$childValue = static::getFormViewValueRecursive($child);
// Skip unchecked checkboxes for expanded and multiple options
if ($view->vars['expanded'] && $view->vars['multiple'] && isset($child->vars['checked']) && !$child->vars['checked']) {
continue;
}
$value[$child->vars['name']] = $childValue;
}
}
return $value;
}
}
Would you be interested in me creating a PR with this fix? I believe the filter_clear_button block would also need the is iterable check to fully support filters with multiple values.
Additionally, would you be open to a PR implementing ChoiceFilterType and EnumFilterType? I think these could be valuable additions to the core of the bundle.
Hey @j0r1s, thank you for your kind words and time! Great explanation of the issue, and reproducer repository.
That is an interesting case I haven't tested before. I wonder whether there's other way to handle this situation without relying on checking the type-specific variables in its view (such as expanded, multiple, etc.). I'll take a look into it.
Additionally, would you be open to a PR implementing ChoiceFilterType and EnumFilterType? I think these could be valuable additions to the core of the bundle.
Sure!
Hi! I have tried @j0r1s check in FormUtil, but it looks like it's not enough to make multiple filters usable.
- When multiple filter is removed (by click), then filter form has an error (it's visible in profiler). When using multiple filter with other filters at the same time, then removing multiple filter results in all filters being removed.
- When multiple filter is configured but not used, then using other filters breaks table functionality and no data is displayed.
- Also exporting with multiple filter results in filter form error, so filters are not applied to export at all.
On a positive note - pagination and sorting works fine.
Okay so, initially I assumed that we can retrieve value of each form field from its view object. This is because we have to convert FormView into query parameters to automatically append them on render. Thanks to this case I see this method will not suffice. I'll think of other ways to handle this.
Overriding the filter_clear_button block: To avoid translation errors when the filter value is an array, I had to override the filter_clear_button block as follows: [...]
This should be fixed in 0.25.8.
Any interaction within the DataTable (e.g., sorting, pagination) results in all filter choices being appended to the query, even if none were selected.
This is (unfortunately) still work in progress, to make it work regardless of form type used.
I think I also ran into this issue.
I got the following filters:
$builder->addFilter(DataTableBuilderInterface::SEARCH_FILTER_NAME, SearchFilterType::class, [
'handler' => fn (...$args) => $this->handleSearch(...$args),
]);
$builder->addFilter('status', StringFilterType::class, [
'label' => t('Status'),
'query_path' => 'formSubmission.status',
'form_type' => EnumType::class,
'form_options' => [
'class' => FinancingFormSubmissionStatus::class,
'multiple' => true,
],
'default_operator' => Operator::In,
]);
In some cases using the filters leads to empty results. I'll explain further with a matrix. I did set breakpoints at two places Form (HttpFoundationRequestHandler::filter, L55) and Filter (DataTable::filter, L575).
Field: What's set in the frontend Form: The Symfony Form data value Filter: The DataTable Filter value
| Field Search | Form Search | Filter Search | Field Status | Form Status | Filter Status | Correct Result | Query Params | |
|---|---|---|---|---|---|---|---|---|
| 1 | null |
null |
processing, approved | ['processing', 'approved] |
['processing', 'approved] |
yes | ?filter_financing_index[status][value][]=processing&filter_financing_index[status][value][]=approved |
|
| 2 | test | test | test | processing, approved | ['processing', 'approved] |
['processing', 'approved] |
yes | ?filter_financing_index[__search][value]=test&filter_financing_index[status][value][]=processing&filter_financing_index[status][value][]=approved |
| 3 | test | test | test | [] |
[] |
no (empty result) | ?filter_financing_index[__search][value]=test |
|
| 4 | null |
null |
[] |
[] |
no (empty result) | ?filter_financing_index[__search][value]= |
Line 1 is correct, there are results when filtering for status only. Also line 2 with search filter value and status filter value works.
I would expect line 3 to also lead to a result, but there is no result as the query includes a WHERE status IN (:status) and the :status parameter is an empty array. The Filter Status should probably be null in that case.
Now when resetting status filters and clearing the search input field, I still get no result, as the status filter is also applied as an empty array. The query parameter for line 4 looks like ?filter_financing_index[__search][value]=.
On initial load (without any query parameter) it shows the correct result.
I thought maybe I could just set 'empty_value' => null inside form_options of the status field. But this makes the form invalid ("The selected choice is invalid." as status "Expected an array."). Due to this the breakpoint HttpFoundationRequestHandler::filter, L55 is not hit when the status field is empty, so the matrix looks as follows:
| Field Search | Form Search | Filter Search | Field Status | Form Status | Filter Status | Correct Result | Query Params | |
|---|---|---|---|---|---|---|---|---|
| 1 | null |
null |
processing, approved | ['processing', 'approved] |
['processing', 'approved] |
yes | ?filter_financing_index[status][value][]=processing&filter_financing_index[status][value][]=approved |
|
| 2 | test | test | test | processing, approved | ['processing', 'approved] |
['processing', 'approved] |
yes | ?filter_financing_index[__search][value]=test&filter_financing_index[status][value][]=processing&filter_financing_index[status][value][]=approved |
| 3 | test | <breakpoint not hit> |
null |
<breakpoint not hit> |
null |
no (unfiltered) | ?filter_financing_index[__search][value]=test |
|
| 4 | <breakpoint not hit> |
null |
<breakpoint not hit> |
null |
yes (unfiltered) | ?filter_financing_index[__search][value]= |
Also after submitting line 3 the query parameter shows test for search, but the search input field is empty due to the form error. Submitting this again leads to line 4.
My current workaround is to add a DataTableEvents::PRE_FILTER listener:
$builder->addEventListener(
DataTableEvents::PRE_FILTER,
function (DataTableFiltrationEvent $event) {
$data = $event->getFiltrationData();
$statusFilterData = $data->getFilterData('status');
if (!$statusFilterData) {
return;
}
$statusFilterDataValue = $statusFilterData->getValue();
if (\is_array($statusFilterDataValue) && empty($statusFilterDataValue)) {
$statusFilterData->setValue(null);
}
},
);