From e4406907a975a8a7b82c245fcd07f85f37a57868 Mon Sep 17 00:00:00 2001 From: David Maicher Date: Wed, 3 Feb 2021 15:50:16 +0100 Subject: [PATCH] Merge 3.x (#1294) * create CountFilter (#1280) * 3.28 (#1284) * DevKit updates (#1286) * Close API (#1292) * fix CountFilter * fix CountFilter after merge * fix CountFilterTest after merge Co-authored-by: rgrassian <46503877+rgrassian@users.noreply.github.com> Co-authored-by: Vincent Langlet Co-authored-by: Sonata CI --- CHANGELOG.md | 14 ++++ CONTRIBUTING.md | 17 +---- docs/reference/filter_field_definition.rst | 1 + src/Datagrid/ProxyQuery.php | 2 + src/Exporter/DataSource.php | 3 + src/Filter/CountFilter.php | 68 ++++++++++++++++++++ src/Filter/Filter.php | 15 +++++ tests/Filter/CountFilterTest.php | 75 ++++++++++++++++++++++ tests/Filter/FilterTestCase.php | 12 ++++ 9 files changed, 191 insertions(+), 16 deletions(-) create mode 100644 src/Filter/CountFilter.php create mode 100644 tests/Filter/CountFilterTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 3dd78095f..d652d884b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,20 @@ All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). +## [3.28.0](https://github.com/sonata-project/SonataDoctrineORMAdminBundle/compare/3.27.0...3.28.0) - 2021-01-26 +### Added +- [[#1280](https://github.com/sonata-project/SonataDoctrineORMAdminBundle/pull/1280)] Added `CountFilter`. ([@rgrassian](https://github.com/rgrassian)) + +### Changed +- [[#1268](https://github.com/sonata-project/SonataDoctrineORMAdminBundle/pull/1268)] Use Doctrine ORM Paginator to count in Pager. ([@VincentLanglet](https://github.com/VincentLanglet)) + +### Deprecated +- [[#1268](https://github.com/sonata-project/SonataDoctrineORMAdminBundle/pull/1268)] `Pager::CONCAT_SEPARATOR` ([@VincentLanglet](https://github.com/VincentLanglet)) + +### Fixed +- [[#1265](https://github.com/sonata-project/SonataDoctrineORMAdminBundle/pull/1265)] Do not provide a default `null` `field_type` option for Filter ([@VincentLanglet](https://github.com/VincentLanglet)) +- [[#1268](https://github.com/sonata-project/SonataDoctrineORMAdminBundle/pull/1268)] Support of composite key for computeNbResult ([@VincentLanglet](https://github.com/VincentLanglet)) + ## [3.27.0](https://github.com/sonata-project/SonataDoctrineORMAdminBundle/compare/3.26.0...3.27.0) - 2021-01-17 ### Added - [[#1262](https://github.com/sonata-project/SonataDoctrineORMAdminBundle/pull/1262)] Added Pager::getCurrentPageResults() ([@VincentLanglet](https://github.com/VincentLanglet)) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2e9268816..86544a365 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -148,6 +148,7 @@ If your PR contains an addition, a new feature, this one has to be fully covered Some rules have to be respected about the test: +* Prefer [the built-in test doubles implementation](https://phpunit.de/manual/current/en/test-doubles.html) over prophecy. * Annotations about coverage are prohibited. This concerns: * `@covers` * `@coversDefaultClass` @@ -161,22 +162,6 @@ Some rules have to be respected about the test: * Most of the time, the test class SHOULD have the same name as the targeted class, suffixed by `Test`. * The `@expectedException*` annotations are prohibited. Use `PHPUnit_Framework_TestCase::setExpectedException()`. -##### Using test doubles - -Historically, Sonata has been using [the built-in test doubles implementation](https://phpunit.de/manual/current/en/test-doubles.html), -but [started to use Prophecy](https://github.com/sonata-project/dev-kit/issues/89). -This means the current Sonata codebase currently uses both implementations. - -If you want to contribute a test that uses test doubles, please follow these rules : - -1. All new test classes MUST use built-in test double implementation. -2. If you are changing an existing test method, you MUST use the same implementation it already uses, -and focus on the goal of your PR and only on that. -3. If you are changing an existing test class, you MUST use the same implementation it already uses, -to be more consistent. -4. You MAY submit a PR that migrates a test class from Prophecy to the built-in test double implementation, -but it MUST migrate it entirely. The PR SHOULD only be about the migration. - ### Writing a Pull Request #### Subject diff --git a/docs/reference/filter_field_definition.rst b/docs/reference/filter_field_definition.rst index 072420109..deba787d5 100644 --- a/docs/reference/filter_field_definition.rst +++ b/docs/reference/filter_field_definition.rst @@ -35,6 +35,7 @@ For now, only `Doctrine ORM` filters are available: * ``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, +* ``Sonata\DoctrineORMAdminBundle\Filter\CountFilter``: depends on the ``Sonata\AdminBundle\Form\Type\Filter\NumberType`` Form Type, * ``Sonata\DoctrineORMAdminBundle\Filter\NumberFilter``: depends on the ``Sonata\AdminBundle\Form\Type\Filter\NumberType`` Form Type, * ``Sonata\DoctrineORMAdminBundle\Filter\ModelAutocompleteFilter``: uses ``Sonata\AdminBundle\Form\Type\Filter\ModelAutocompleteType`` form type, can be used as replacement of ``Sonata\DoctrineORMAdminBundle\Filter\ModelFilter`` to handle too many items that cannot be loaded into memory. * ``Sonata\DoctrineORMAdminBundle\Filter\StringFilter``: depends on the ``Sonata\AdminBundle\Form\Type\Filter\ChoiceType`` Form Type, diff --git a/src/Datagrid/ProxyQuery.php b/src/Datagrid/ProxyQuery.php index a4e9ab9d3..cf266ea26 100644 --- a/src/Datagrid/ProxyQuery.php +++ b/src/Datagrid/ProxyQuery.php @@ -23,6 +23,8 @@ /** * This class try to unify the query usage with Doctrine. * + * @final since sonata-project/doctrine-orm-admin-bundle 3.x + * * @method Query\Expr expr() * @method QueryBuilder setCacheable($cacheable) * @method bool isCacheable() diff --git a/src/Exporter/DataSource.php b/src/Exporter/DataSource.php index 27399cce7..73a46a843 100644 --- a/src/Exporter/DataSource.php +++ b/src/Exporter/DataSource.php @@ -20,6 +20,9 @@ use Sonata\Exporter\Source\DoctrineORMQuerySourceIterator; use Sonata\Exporter\Source\SourceIteratorInterface; +/** + * @final since sonata-project/doctrine-orm-admin-bundle 3.x + */ class DataSource implements DataSourceInterface { public function createIterator(ProxyQueryInterface $query, array $fields): SourceIteratorInterface diff --git a/src/Filter/CountFilter.php b/src/Filter/CountFilter.php new file mode 100644 index 000000000..b63e7c461 --- /dev/null +++ b/src/Filter/CountFilter.php @@ -0,0 +1,68 @@ + + * + * 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\Form\Type\Filter\NumberType; +use Sonata\AdminBundle\Form\Type\Operator\NumberOperatorType; +use Sonata\DoctrineORMAdminBundle\Datagrid\ProxyQueryInterface; +use Symfony\Component\Form\Extension\Core\Type\NumberType as FormNumberType; + +final class CountFilter extends Filter +{ + public const CHOICES = [ + NumberOperatorType::TYPE_EQUAL => '=', + NumberOperatorType::TYPE_GREATER_EQUAL => '>=', + NumberOperatorType::TYPE_GREATER_THAN => '>', + NumberOperatorType::TYPE_LESS_EQUAL => '<=', + NumberOperatorType::TYPE_LESS_THAN => '<', + ]; + + public function filter(ProxyQueryInterface $query, string $alias, string $field, array $data): void + { + if (!\array_key_exists('value', $data) || !is_numeric($data['value'])) { + return; + } + + $type = $data['type'] ?? NumberOperatorType::TYPE_EQUAL; + $operator = $this->getOperator((int) $type); + + // c.name > '1' => c.name OPERATOR :FIELDNAME + $parameterName = $this->getNewParameterName($query); + $rootAlias = current($query->getQueryBuilder()->getRootAliases()); + $query->getQueryBuilder()->addGroupBy($rootAlias); + $this->applyWhere($query, sprintf('COUNT(%s.%s) %s :%s', $alias, $field, $operator, $parameterName)); + $query->getQueryBuilder()->setParameter($parameterName, $data['value']); + } + + public function getDefaultOptions(): array + { + return [ + 'field_type' => FormNumberType::class, + ]; + } + + public function getRenderSettings(): array + { + return [NumberType::class, [ + 'field_type' => $this->getFieldType(), + 'field_options' => $this->getFieldOptions(), + 'label' => $this->getLabel(), + ]]; + } + + private function getOperator(int $type): string + { + return self::CHOICES[$type] ?? self::CHOICES[NumberOperatorType::TYPE_EQUAL]; + } +} diff --git a/src/Filter/Filter.php b/src/Filter/Filter.php index 7a289ef82..1526f2b62 100644 --- a/src/Filter/Filter.php +++ b/src/Filter/Filter.php @@ -69,6 +69,21 @@ protected function applyWhere(ProxyQueryInterface $query, $parameter): void $this->active = true; } + /** + * @param mixed $parameter + */ + protected function applyHaving(ProxyQueryInterface $query, $parameter): void + { + if (self::CONDITION_OR === $this->getCondition()) { + $query->getQueryBuilder()->orHaving($parameter); + } else { + $query->getQueryBuilder()->andHaving($parameter); + } + + // filter is active since it's added to the queryBuilder + $this->active = true; + } + protected function getNewParameterName(ProxyQueryInterface $query): string { // dots are not accepted in a DQL identifier so replace them diff --git a/tests/Filter/CountFilterTest.php b/tests/Filter/CountFilterTest.php new file mode 100644 index 000000000..d7ba6f136 --- /dev/null +++ b/tests/Filter/CountFilterTest.php @@ -0,0 +1,75 @@ + + * + * 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 Sonata\AdminBundle\Form\Type\Operator\NumberOperatorType; +use Sonata\DoctrineORMAdminBundle\Datagrid\ProxyQuery; +use Sonata\DoctrineORMAdminBundle\Filter\CountFilter; + +class CountFilterTest extends FilterTestCase +{ + public function testFilterEmpty(): void + { + $filter = new CountFilter(); + $filter->initialize('field_name', ['field_options' => ['class' => 'FooBar']]); + + $builder = new ProxyQuery($this->createQueryBuilderStub()); + + $filter->filter($builder, 'alias', 'field', []); + + $this->assertSame([], $builder->query); + $this->assertFalse($filter->isActive()); + } + + public function testFilterInvalidOperator(): void + { + $filter = new CountFilter(); + $filter->initialize('field_name', ['field_options' => ['class' => 'FooBar']]); + + $builder = new ProxyQuery($this->createQueryBuilderStub()); + + $filter->filter($builder, 'alias', 'field', ['type' => 'foo']); + + $this->assertSame([], $builder->query); + $this->assertFalse($filter->isActive()); + } + + /** + * @dataProvider filterDataProvider + */ + public function testFilter(string $expected, ?int $type): void + { + $filter = new CountFilter(); + $filter->initialize('field_name', ['field_options' => ['class' => 'FooBar']]); + + $builder = new ProxyQuery($this->createQueryBuilderStub()); + + $filter->filter($builder, 'alias', 'field', ['type' => $type, 'value' => 42]); + + $this->assertSame(['GROUP BY o', $expected], $builder->query); + $this->assertTrue($filter->isActive()); + } + + public function filterDataProvider(): array + { + return [ + ['COUNT(alias.field) = :field_name_0', NumberOperatorType::TYPE_EQUAL], + ['COUNT(alias.field) >= :field_name_0', NumberOperatorType::TYPE_GREATER_EQUAL], + ['COUNT(alias.field) > :field_name_0', NumberOperatorType::TYPE_GREATER_THAN], + ['COUNT(alias.field) <= :field_name_0', NumberOperatorType::TYPE_LESS_EQUAL], + ['COUNT(alias.field) < :field_name_0', NumberOperatorType::TYPE_LESS_THAN], + ['COUNT(alias.field) = :field_name_0', null], + ]; + } +} diff --git a/tests/Filter/FilterTestCase.php b/tests/Filter/FilterTestCase.php index 0587a515e..3cb1ebb63 100644 --- a/tests/Filter/FilterTestCase.php +++ b/tests/Filter/FilterTestCase.php @@ -42,6 +42,18 @@ static function ($query) use ($queryBuilder): void { } ); + $queryBuilder->method('andHaving')->willReturnCallback( + static function ($query) use ($queryBuilder): void { + $queryBuilder->query[] = (string) $query; + } + ); + + $queryBuilder->method('addGroupBy')->willReturnCallback( + static function (string $groupBy) use ($queryBuilder): void { + $queryBuilder->query[] = sprintf('GROUP BY %s', $groupBy); + } + ); + $queryBuilder->method('expr')->willReturnCallback( static function () use ($testCase): Expr { return $testCase->createExprStub();