From e61e0b9491b812f865a4dc6380094097790c63b8 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Fri, 8 May 2020 13:07:44 +0200 Subject: [PATCH] Add StringListFilter --- docs/reference/filter_field_definition.rst | 20 +++ src/Filter/StringListFilter.php | 62 +++++++++ tests/Filter/StringListFilterTest.php | 151 +++++++++++++++++++++ 3 files changed, 233 insertions(+) create mode 100644 src/Filter/StringListFilter.php create mode 100644 tests/Filter/StringListFilterTest.php diff --git a/docs/reference/filter_field_definition.rst b/docs/reference/filter_field_definition.rst index 14e120563..f97049845 100644 --- a/docs/reference/filter_field_definition.rst +++ b/docs/reference/filter_field_definition.rst @@ -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, @@ -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 ----------------------- diff --git a/src/Filter/StringListFilter.php b/src/Filter/StringListFilter.php new file mode 100644 index 000000000..762255084 --- /dev/null +++ b/src/Filter/StringListFilter.php @@ -0,0 +1,62 @@ + + * + * 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(), + ]]; + } +} diff --git a/tests/Filter/StringListFilterTest.php b/tests/Filter/StringListFilterTest.php new file mode 100644 index 000000000..661046738 --- /dev/null +++ b/tests/Filter/StringListFilterTest.php @@ -0,0 +1,151 @@ + + * + * 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";%', + ], + ]; + } +}