Skip to content

Commit

Permalink
Add StringListFilter
Browse files Browse the repository at this point in the history
  • Loading branch information
VincentLanglet committed May 9, 2020
1 parent e490ef9 commit e61e0b9
Show file tree
Hide file tree
Showing 3 changed files with 233 additions and 0 deletions.
20 changes: 20 additions & 0 deletions docs/reference/filter_field_definition.rst
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ Available filter types

For now, only `Doctrine ORM` filters are available:

* ``Sonata\DoctrineORMAdminBundle\Filter\ArrayFilter``: depends on the ``Sonata\AdminBundle\Form\Type\Filter\ChoiceType`` Form Type,
* ``Sonata\DoctrineORMAdminBundle\Filter\BooleanFilter``: depends on the ``Sonata\AdminBundle\Form\Type\Filter\DefaultType`` Form Type, renders yes or no field,
* ``Sonata\DoctrineORMAdminBundle\Filter\CallbackFilter``: depends on the ``Sonata\AdminBundle\Form\Type\Filter\DefaultType`` Form Type, types can be configured as needed,
* ``Sonata\DoctrineORMAdminBundle\Filter\ChoiceFilter``: depends on the ``Sonata\AdminBundle\Form\Type\Filter\ChoiceType`` Form Type,
Expand Down Expand Up @@ -65,6 +66,25 @@ Example
}
}
ArrayFilter
-----------

This filter is made for filtering on values saved in databases as serialized arrays of strings with the
``@ORM\Column(type="array")`` annotation. It is recommended to use another table and ``OneToMany`` relations
if you want to make complex ``SQL`` queries or if your table is too big and you get performance issues but
this filter can provide some basic queries::

protected function configureDatagridFilters(DatagridMapper $datagridMapper): void
{
$datagridMapper
->add('keywords', ArrayFilter::class);
}

.. note::

The filter can give bad results with associative arrays since it is not easy to distinguish between keys
and values for a serialized associative array.

ModelAutocompleteFilter
-----------------------

Expand Down
62 changes: 62 additions & 0 deletions src/Filter/StringListFilter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<?php

declare(strict_types=1);

/*
* This file is part of the Sonata Project package.
*
* (c) Thomas Rabaix <thomas.rabaix@sonata-project.org>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Sonata\DoctrineORMAdminBundle\Filter;

use Sonata\AdminBundle\Datagrid\ProxyQueryInterface;
use Sonata\AdminBundle\Form\Type\Filter\ChoiceType;
use Sonata\AdminBundle\Form\Type\Operator\ContainsOperatorType;

final class StringListFilter extends Filter
{
public function filter(ProxyQueryInterface $queryBuilder, $alias, $field, $data): void
{
if (!$data || !\is_array($data) || !\array_key_exists('type', $data) || !\array_key_exists('value', $data)) {
return;
}

if (!\is_array($data['value'])) {
$data['value'] = [$data['value']];
}

$operator = ContainsOperatorType::TYPE_NOT_CONTAINS === $data['type'] ? 'NOT LIKE' : 'LIKE';

$andConditions = $queryBuilder->expr()->andX();
foreach ($data['value'] as $value) {
$parameterName = $this->getNewParameterName($queryBuilder);
$andConditions->add(sprintf('%s.%s %s :%s', $alias, $field, $operator, $parameterName));

$queryBuilder->setParameter($parameterName, '%'.serialize($value).'%');
}

if (ContainsOperatorType::TYPE_EQUAL === $data['type']) {
$andConditions->add(sprintf("%s.%s LIKE 'a:%s:%%'", $alias, $field, \count($data['value'])));
}

$this->applyWhere($queryBuilder, $andConditions);
}

public function getDefaultOptions(): array
{
return [];
}

public function getRenderSettings(): array
{
return [ChoiceType::class, [
'field_type' => $this->getFieldType(),
'field_options' => $this->getFieldOptions(),
'label' => $this->getLabel(),
]];
}
}
151 changes: 151 additions & 0 deletions tests/Filter/StringListFilterTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
<?php

declare(strict_types=1);

/*
* This file is part of the Sonata Project package.
*
* (c) Thomas Rabaix <thomas.rabaix@sonata-project.org>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Sonata\DoctrineORMAdminBundle\Tests\Filter;

use PHPUnit\Framework\TestCase;
use Sonata\AdminBundle\Form\Type\Operator\ContainsOperatorType;
use Sonata\DoctrineORMAdminBundle\Datagrid\ProxyQuery;
use Sonata\DoctrineORMAdminBundle\Filter\StringListFilter;

class StringListFilterTest extends TestCase
{
public function testItStaysDisabledWhenFilteringWithAnEmptyValue(): void
{
$filter = new StringListFilter();
$filter->initialize('field_name', ['field_options' => ['class' => 'FooBar']]);

$builder = new ProxyQuery(new QueryBuilder());

$filter->filter($builder, 'alias', 'field', null);
$filter->filter($builder, 'alias', 'field', '');

$this->assertSame([], $builder->query);
$this->assertFalse($filter->isActive());
}

public function testFilteringWithNullReturnsArraysThatContainNull(): void
{
$filter = new StringListFilter();
$filter->initialize('field_name');

$builder = new ProxyQuery(new QueryBuilder());
$this->assertSame([], $builder->query);

$filter->filter($builder, 'alias', 'field', ['value' => null, 'type' => null]);
$this->assertSame(['alias.field LIKE :field_name_0'], $builder->query);
$this->assertSame(['field_name_0' => '%N;%'], $builder->parameters);
$this->assertTrue($filter->isActive());
}

/**
* @dataProvider containsDataProvider
*/
public function testContains(?int $type): void
{
$filter = new StringListFilter();
$filter->initialize('field_name');

$builder = new ProxyQuery(new QueryBuilder());
$this->assertSame([], $builder->query);

$filter->filter($builder, 'alias', 'field', ['value' => 'asd', 'type' => $type]);
$this->assertSame(['alias.field LIKE :field_name_0'], $builder->query);
$this->assertSame(['field_name_0' => '%s:3:"asd";%'], $builder->parameters);
$this->assertTrue($filter->isActive());
}

public function containsDataProvider(): iterable
{
yield 'explicit contains' => [ContainsOperatorType::TYPE_CONTAINS];
yield 'implicit contains' => [null];
}

public function testNotContains(): void
{
$filter = new StringListFilter();
$filter->initialize('field_name');

$builder = new ProxyQuery(new QueryBuilder());
$this->assertSame([], $builder->query);

$filter->filter($builder, 'alias', 'field', ['value' => 'asd', 'type' => ContainsOperatorType::TYPE_NOT_CONTAINS]);
$this->assertSame(['alias.field NOT LIKE :field_name_0'], $builder->query);
$this->assertSame(['field_name_0' => '%s:3:"asd";%'], $builder->parameters);
$this->assertTrue($filter->isActive());
}

public function testEquals(): void
{
$filter = new StringListFilter();
$filter->initialize('field_name');

$builder = new ProxyQuery(new QueryBuilder());
$this->assertSame([], $builder->query);

$filter->filter($builder, 'alias', 'field', ['value' => 'asd', 'type' => ContainsOperatorType::TYPE_EQUAL]);
$this->assertSame(['alias.field LIKE :field_name_0 AND alias.field LIKE \'a:1:%\''], $builder->query);
$this->assertSame(['field_name_0' => '%s:3:"asd";%'], $builder->parameters);
$this->assertTrue($filter->isActive());
}

/**
* @dataProvider multipleValuesDataProvider
*/
public function testMultipleValues(array $value, ?int $type, array $query, array $parameters): void
{
$filter = new StringListFilter();
$filter->initialize('field_name');

$builder = new ProxyQuery(new QueryBuilder());
$this->assertSame([], $builder->query);

$filter->filter($builder, 'alias', 'field', ['value' => $value, 'type' => $type]);
$this->assertSame($query, $builder->query);
$this->assertSame($parameters, $builder->parameters);
$this->assertTrue($filter->isActive());
}

public function multipleValuesDataProvider(): iterable
{
yield 'equal choice' => [
['asd', 'qwe'],
ContainsOperatorType::TYPE_EQUAL,
["alias.field LIKE :field_name_0 AND alias.field LIKE :field_name_1 AND alias.field LIKE 'a:2:%'"],
[
'field_name_0' => '%s:3:"asd";%',
'field_name_1' => '%s:3:"qwe";%',
],
];

yield 'contains choice' => [
['asd', 'qwe'],
ContainsOperatorType::TYPE_CONTAINS,
['alias.field LIKE :field_name_0 AND alias.field LIKE :field_name_1'],
[
'field_name_0' => '%s:3:"asd";%',
'field_name_1' => '%s:3:"qwe";%',
],
];

yield 'not contains choice' => [
['asd', 'qwe'],
ContainsOperatorType::TYPE_NOT_CONTAINS,
['alias.field NOT LIKE :field_name_0 AND alias.field NOT LIKE :field_name_1'],
[
'field_name_0' => '%s:3:"asd";%',
'field_name_1' => '%s:3:"qwe";%',
],
];
}
}

0 comments on commit e61e0b9

Please sign in to comment.