Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions docs/src/docs/components/filters.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,43 @@ This method accepts _three_ arguments:

For reference, see [available filter types](../../reference/types/filter.md).

### Column filters

You can also add a filter directly to a column, which will display the filter in the column header.
To do this, use the `filter` and (optionally) `filter_options` keys in the column definition:

```php
$builder
->addColumn('name', TextColumnType::class, [
'label' => 'Full name',
'sort' => true,
'filter' => StringFilterType::class,
]);
```

You can pass options to the filter using the `filter_options` array:

```php
$builder
->addColumn('name', TextColumnType::class, [
'label' => 'Full name',
'sort' => true,
'filter' => StringFilterType::class,
'filter_options' => [
'operator_selectable' => true,
],
]);
```

- `filter`: The filter type class (e.g. `StringFilterType::class`)
- `filter_options`: An array of options passed to the filter (e.g. `operator_selectable`, `default_operator`, etc.)

Column filters are rendered inside the column header and are scoped to the column they are defined on.

> **Note:**
> A field cannot have both a header filter (via `addFilter()`) and a column filter (via the column's `filter` option) at the same time.
> Attempting to add both types of filters for the same field will result in an `InvalidArgumentException`.

### Case-insensitive filters for Postgres or MySQL with a case sensitive collation

If your database does `LIKE` comparisons in a case-sensitive manner (aka `A` is not the same as `a`), but you want
Expand Down
22 changes: 22 additions & 0 deletions src/Column/Type/ColumnType.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use Kreyu\Bundle\DataTableBundle\Column\ColumnHeaderView;
use Kreyu\Bundle\DataTableBundle\Column\ColumnInterface;
use Kreyu\Bundle\DataTableBundle\Column\ColumnValueView;
use Kreyu\Bundle\DataTableBundle\Filter\Type\AbstractFilterType;
use Kreyu\Bundle\DataTableBundle\Util\StringUtil;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\PropertyAccess\PropertyAccess;
Expand Down Expand Up @@ -73,6 +74,8 @@ public function buildHeaderView(ColumnHeaderView $view, ColumnInterface $column,
'sort_direction' => $sortColumnData?->getDirection(),
'sortable' => $column->getConfig()->isSortable(),
'export' => $column->getConfig()->isExportable(),
'filter' => $options['filter'],
'filter_options' => $options['filter_options'],
]);
}

Expand Down Expand Up @@ -330,6 +333,25 @@ public function configureOptions(OptionsResolver $resolver): void
->allowedTypes('bool')
->info('Defines whether the column can be personalized by the user in personalization feature.')
;

$resolver->define('filter')
->default(null)
->allowedTypes('string', 'null')
->allowedValues(function (?string $value): bool {
if (null === $value) {
return true;
}

return is_subclass_of($value, AbstractFilterType::class);
})
->info('Provide a Filter Type FQCN (extending AbstractFilterType) to render a per-column filter.')
;

$resolver->define('filter_options')
->default([])
->allowedTypes('array')
->info('Options passed to the per-column filter type when rendering the inline filter in the header.')
;
}

public function getBlockPrefix(): string
Expand Down
17 changes: 17 additions & 0 deletions src/DataTable.php
Original file line number Diff line number Diff line change
Expand Up @@ -771,6 +771,23 @@ public function createFiltrationFormBuilder(?DataTableView $view = null): FormBu
);
}

public function createColumnFiltrationFormBuilder(?DataTableView $view = null, array $filters = []): FormBuilderInterface
{
if (!$this->config->isFiltrationEnabled()) {
throw new RuntimeException('The data table has filtration feature disabled.');
}

return $this->config->getFiltrationFormFactory()->createNamedBuilder(
name: $this->config->getColumnFiltrationParameterName(),
type: FiltrationDataType::class,
options: [
'data_table' => $this,
'data_table_view' => $view,
'is_header_form' => false,
],
);
}

public function createPersonalizationFormBuilder(?DataTableView $view = null): FormBuilderInterface
{
if (!$this->config->isPersonalizationEnabled()) {
Expand Down
41 changes: 38 additions & 3 deletions src/DataTableBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -262,13 +262,18 @@ public function getFilter(string $name): FilterBuilderInterface
}

if (isset($this->unresolvedFilters[$name])) {
return $this->resolveFilter($name);
return $this->resolveHeaderFilter($name);
}

if (isset($this->filters[$name])) {
return $this->filters[$name];
}

$filter = $this->resolveColumnFilter($name);
if (null !== $filter) {
return $filter;
}

throw new InvalidArgumentException(sprintf('The filter with the name "%s" does not exist.', $name));
}

Expand Down Expand Up @@ -794,7 +799,7 @@ private function resolveColumns(): void
}
}

private function resolveFilter(string $name): FilterBuilderInterface
private function resolveHeaderFilter(string $name): FilterBuilderInterface
{
[$type, $options] = $this->unresolvedFilters[$name];

Expand All @@ -803,10 +808,40 @@ private function resolveFilter(string $name): FilterBuilderInterface
return $this->filters[$name] = $this->getFilterFactory()->createNamedBuilder($name, $type, $options);
}

private function resolveColumnFilter(string $name): ?FilterBuilderInterface
{
if (isset($this->filters[$name])) {
throw new InvalidArgumentException(sprintf('A filter named "%s" is already registered as a header filter. Only one filter with the same name (column + header) can be active at a time. Remove or replace the existing filter before adding a new one.', $name));
}

$column = $this->columns[$name] ?? null;

if (null === $column) {
return null;
}

$type = $column->getOption('filter');

if (null === $type) {
return null;
}

$options = $column->getOption('filter_options');
$options['is_header_filter'] = false;

return $this->filters[$name] = $this->getFilterFactory()->createNamedBuilder($name, $type, $options);
}

private function resolveFilters(): void
{
foreach (array_keys($this->unresolvedFilters) as $filter) {
$this->resolveFilter($filter);
$this->resolveHeaderFilter($filter);
}

foreach ($this->columns as $column) {
if (null !== $column->getOptions()['filter']) {
$this->resolveColumnFilter($column->getName());
}
}
}

Expand Down
5 changes: 5 additions & 0 deletions src/DataTableConfigBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -845,6 +845,11 @@ public function getFiltrationParameterName(): string
return $this->getParameterName(static::FILTRATION_PARAMETER);
}

public function getColumnFiltrationParameterName(): string
{
return $this->getParameterName(static::COLUMN_FILTRATION_PARAMETER);
}

public function getPersonalizationParameterName(): string
{
return $this->getParameterName(static::PERSONALIZATION_PARAMETER);
Expand Down
3 changes: 3 additions & 0 deletions src/DataTableConfigInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ interface DataTableConfigInterface
public const PER_PAGE_PARAMETER = 'limit';
public const SORT_PARAMETER = 'sort';
public const FILTRATION_PARAMETER = 'filter';
public const COLUMN_FILTRATION_PARAMETER = 'filter_column';
public const PERSONALIZATION_PARAMETER = 'personalization';
public const EXPORT_PARAMETER = 'export';

Expand Down Expand Up @@ -131,6 +132,8 @@ public function getSortParameterName(): string;

public function getFiltrationParameterName(): string;

public function getColumnFiltrationParameterName(): string;

public function getPersonalizationParameterName(): string;

public function getExportParameterName(): string;
Expand Down
2 changes: 2 additions & 0 deletions src/DataTableInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,8 @@ public function getExportData(): ?ExportData;

public function createFiltrationFormBuilder(?DataTableView $view = null): FormBuilderInterface;

public function createColumnFiltrationFormBuilder(?DataTableView $view = null, array $filters = []): FormBuilderInterface;

public function createPersonalizationFormBuilder(?DataTableView $view = null): FormBuilderInterface;

public function createExportFormBuilder(?DataTableView $view = null): FormBuilderInterface;
Expand Down
2 changes: 1 addition & 1 deletion src/Filter/FilterClearUrlGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ private function getFilterClearQueryParameters(FilterView $filterView): array
$dataTableView = $filterView->parent;

return [
$dataTableView->vars['filtration_parameter_name'] => [
$dataTableView->vars[$filterView->vars['is_header_filter'] ? 'filtration_parameter_name' : 'column_filtration_parameter_name'] => [
$filterView->vars['name'] => $parameters,
],
];
Expand Down
17 changes: 17 additions & 0 deletions src/Filter/FilterConfigBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ class FilterConfigBuilder implements FilterConfigBuilderInterface
private array $supportedOperators = [];
private Operator $defaultOperator = Operator::Equals;
private bool $operatorSelectable = false;
private bool $isHeaderFilter = true;
private FilterData $emptyData;

public function __construct(
Expand Down Expand Up @@ -291,6 +292,22 @@ public function getFilterConfig(): FilterConfigInterface
return $config;
}

public function isHeaderFilter(): bool
{
return $this->isHeaderFilter;
}

public function setIsHeaderFilter(bool $isHeaderFilter): self
{
if ($this->locked) {
throw $this->createBuilderLockedException();
}

$this->isHeaderFilter = $isHeaderFilter;

return $this;
}

private function createBuilderLockedException(): BadMethodCallException
{
return new BadMethodCallException('FilterConfigBuilder methods cannot be accessed anymore once the builder is turned into a FilterConfigInterface instance.');
Expand Down
2 changes: 2 additions & 0 deletions src/Filter/FilterConfigBuilderInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,6 @@ public function setDefaultOperator(Operator $defaultOperator): static;
public function setOperatorSelectable(bool $operatorSelectable): static;

public function getFilterConfig(): FilterConfigInterface;

public function setIsHeaderFilter(bool $isHeaderFilter): self;
}
2 changes: 2 additions & 0 deletions src/Filter/FilterConfigInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,6 @@ public function getSupportedOperators(): array;
public function getDefaultOperator(): Operator;

public function isOperatorSelectable(): bool;

public function isHeaderFilter(): bool;
}
6 changes: 6 additions & 0 deletions src/Filter/Form/Type/FiltrationDataType.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ public function buildForm(FormBuilderInterface $builder, array $options): void
$dataTable = $options['data_table'];

foreach ($dataTable->getFilters() as $filter) {
if ($filter->getConfig()->isHeaderFilter() !== $options['is_header_form']) {
continue;
}

$builder->add($filter->getFormName(), FilterDataType::class, $filter->getFormOptions() + [
'getter' => fn (FiltrationData $filtrationData) => $filtrationData->getFilterData($filter),
'setter' => fn (FiltrationData $filtrationData, FilterData $filterData) => $filtrationData->setFilterData($filter, $filterData),
Expand Down Expand Up @@ -83,10 +87,12 @@ public function configureOptions(OptionsResolver $resolver): void
'data_class' => FiltrationData::class,
'csrf_protection' => false,
'data_table_view' => null,
'is_header_form' => true,
])
->setRequired('data_table')
->setAllowedTypes('data_table', DataTableInterface::class)
->setAllowedTypes('data_table_view', ['null', DataTableView::class])
->setAllowedTypes('is_header_form', ['bool'])
;
}

Expand Down
4 changes: 4 additions & 0 deletions src/Filter/Type/FilterType.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ public function buildFilter(FilterBuilderInterface $builder, array $options): vo
'default_operator' => $builder->setDefaultOperator(...),
'supported_operators' => $builder->setSupportedOperators(...),
'operator_selectable' => $builder->setOperatorSelectable(...),
'is_header_filter' => $builder->setIsHeaderFilter(...),
];

foreach ($setters as $option => $setter) {
Expand Down Expand Up @@ -62,6 +63,7 @@ public function buildView(FilterView $view, FilterInterface $filter, FilterData
'operator_selectable' => $filter->getConfig()->isOperatorSelectable(),
'default_operator' => $filter->getConfig()->getDefaultOperator(),
'supported_operators' => $filter->getConfig()->getSupportedOperators(),
'is_header_filter' => $filter->getConfig()->isHeaderFilter(),
'data' => $view->data,
'value' => $view->value,
]);
Expand All @@ -83,6 +85,7 @@ public function configureOptions(OptionsResolver $resolver): void
'supported_operators' => [],
'operator_selectable' => false,
'active_filter_formatter' => null,
'is_header_filter' => true,
])
->setAllowedTypes('label', ['null', 'bool', 'string', TranslatableInterface::class])
->setAllowedTypes('label_translation_parameters', 'array')
Expand All @@ -96,6 +99,7 @@ public function configureOptions(OptionsResolver $resolver): void
->setAllowedTypes('supported_operators', Operator::class.'[]')
->setAllowedTypes('operator_selectable', 'bool')
->setAllowedTypes('active_filter_formatter', ['null', 'callable'])
->setAllowedTypes('is_header_filter', ['bool'])
->setAllowedValues('translation_domain', function (mixed $value): bool {
return is_null($value) || false === $value || is_string($value);
})
Expand Down
10 changes: 10 additions & 0 deletions src/Request/HttpFoundationRequestHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,16 @@ private function filter(DataTableInterface $dataTable, Request $request): void
if ($form->isSubmitted() && $form->isValid()) {
$dataTable->filter($form->getData());
}

$columnForm = $dataTable->createColumnFiltrationFormBuilder()->getForm();

if ($data = $request->get($columnForm->getName())) {
$columnForm->submit($data);
}

if ($columnForm->isSubmitted() && $columnForm->isValid()) {
$dataTable->filter($columnForm->getData());
}
}

private function sort(DataTableInterface $dataTable, Request $request): void
Expand Down
Loading