diff --git a/src/Filter/Filter.php b/src/Filter/Filter.php index e78b2f289..6fc718910 100644 --- a/src/Filter/Filter.php +++ b/src/Filter/Filter.php @@ -26,9 +26,13 @@ abstract class Filter extends BaseFilter protected $active = false; /** - * @var Orx + * Holds an array of `orX` expressions used by each admin when the condition + * equals the value on `FilterInterface::CONDITION_OR`, using the admin code + * as index. + * + * @var array */ - private $orExpression; + private static $orExpressionsByAdmin = []; /** * NEXT_MAJOR change $query type for Sonata\DoctrineORMAdminBundle\Datagrid\ProxyQueryInterface. @@ -58,11 +62,6 @@ public function isActive() return $this->active; } - final public function setOrExpression(Orx $orx): void - { - $this->orExpression = $orx; - } - /** * @param mixed[] $data * @@ -107,8 +106,7 @@ protected function applyWhere(BaseProxyQueryInterface $query, $parameter) } if (self::CONDITION_OR === $this->getCondition()) { - $condition = $this->getOrExpression($query); - $condition->add($parameter); + $this->addOrParameter($query, $parameter); } else { $query->getQueryBuilder()->andWhere($parameter); } @@ -155,26 +153,38 @@ protected function getNewParameterName(BaseProxyQueryInterface $query) } /** - * Returns the reference to the `Orx` expression used in the `where` clause. + * Adds the parameter to the corresponding `Orx` expression used in the `where` clause. * If it doesn't exist, a new one is created. - * This method uses a marker (":sonata_admin_datagrid_filter_query_marker") in + * This method groups the filter "OR" conditions based on the "admin_code" option. It this + * option is not set, it uses a marker (":sonata_admin_datagrid_filter_query_marker") in * the resulting DQL in order to identify the corresponding "WHERE (...)" condition * group each time it is required. * It allows to get queries like "WHERE previous_condition = previous_value AND (filter_1 = value OR filter_2 = value OR ...)", * where the logical "OR" operators added by the filters are grouped inside a condition, * instead of having unfolded "WHERE ..." clauses like "WHERE previous_condition = previous_value OR filter_1 = value OR filter_2 = value OR ...", * which will produce undesired results. + * + * TODO: Remove the logic related to the ":sonata_admin_datagrid_filter_query_marker" marker when + * the constraint for "sonata-project/admin-bundle" guarantees that the "admin_code" option is set. + * + * @param mixed $parameter */ - private function getOrExpression(BaseProxyQueryInterface $query): Orx + private function addOrParameter(BaseProxyQueryInterface $query, $parameter): void { - if (null !== $this->orExpression) { - return $this->orExpression; + $adminCode = $this->getOption('admin_code'); + $orExpression = self::$orExpressionsByAdmin[$adminCode] ?? null; + if ($orExpression instanceof Orx) { + $orExpression->add($parameter); + + return; } $qb = $query->getQueryBuilder(); $where = $qb->getDQLPart('where'); - if ($where) { + // Search for the ":sonata_admin_datagrid_filter_query_marker" marker in order to + // get the `Orx` expression. + if (null === $adminCode && null !== $where) { foreach ($where->getParts() as $expression) { if (!$expression instanceof Orx) { continue; @@ -182,25 +192,30 @@ private function getOrExpression(BaseProxyQueryInterface $query): Orx $expressionParts = $expression->getParts(); - // Search for the ":sonata_admin_datagrid_filter_query_marker" marker in order to - // get the `Orx` expression. if (isset($expressionParts[0]) && \is_string($expressionParts[0]) && 0 === strpos($expressionParts[0], ':sonata_admin_datagrid_filter_query_marker') ) { - $this->orExpression = $expression; + $expression->add($parameter); - return $this->orExpression; + return; } } } - // Create a new `Orx` expression, adding it to the `where` clause. - $this->orExpression = $qb->expr()->orX(); - // Place ":sonata_admin_datagrid_filter_query_marker" parameter as marker for the `Orx` expression. - $this->orExpression->add($qb->expr()->isNull(':sonata_admin_datagrid_filter_query_marker')); - $qb->setParameter('sonata_admin_datagrid_filter_query_marker', 'sonata_admin.datagrid.filter_query.marker'); - $qb->andWhere($this->orExpression); + // Create a new `Orx` expression. + $orExpression = $qb->expr()->orX(); + + if (null === $adminCode) { + // Add the ":sonata_admin_datagrid_filter_query_marker" parameter as marker for the `Orx` expression. + $orExpression->add($qb->expr()->isNull(':sonata_admin_datagrid_filter_query_marker')); + $qb->setParameter('sonata_admin_datagrid_filter_query_marker', 'sonata_admin.datagrid.filter_query.marker'); + } else { + self::$orExpressionsByAdmin[$adminCode] = $orExpression; + } + + $orExpression->add($parameter); - return $this->orExpression; + // Add the `Orx` expression to the `where` clause. + $qb->andWhere($orExpression); } } diff --git a/tests/Filter/FilterTest.php b/tests/Filter/FilterTest.php index c895ca4ff..c4ce7bd8a 100644 --- a/tests/Filter/FilterTest.php +++ b/tests/Filter/FilterTest.php @@ -13,11 +13,15 @@ namespace Sonata\DoctrineORMAdminBundle\Tests\Filter; -use PHPUnit\Framework\TestCase; +use Doctrine\ORM\EntityManagerInterface; +use Doctrine\ORM\Query\Expr; use Sonata\AdminBundle\Datagrid\ProxyQueryInterface; +use Sonata\DoctrineORMAdminBundle\Datagrid\ProxyQuery; use Sonata\DoctrineORMAdminBundle\Filter\Filter; +use Sonata\DoctrineORMAdminBundle\Filter\StringFilter; +use Sonata\DoctrineORMAdminBundle\Tests\Filter\FilterTestCase; -final class FilterTest extends TestCase +final class FilterTest extends FilterTestCase { /** * @var Filter @@ -58,6 +62,65 @@ public function testIsActive(): void $this->assertFalse($this->filter->isActive()); } + /** + * @dataProvider orExpressionProvider + */ + public function testOrExpression(string $expected, array $filterOptions = []): void + { + $allowEmpty = false; + $value = 'sonata'; + + $expressionBuilder = new Expr(); + $entityManager = $this->createMock(EntityManagerInterface::class); + $entityManager->method('getExpressionBuilder')->willReturn($expressionBuilder); + $queryBuilder = new TestQueryBuilder($entityManager); + + $queryBuilder->select('e') + ->from('MyEntity', 'e') + ->where($queryBuilder->expr()->eq(1, 2)) + ->andWhere( + $queryBuilder->expr()->orX( + $queryBuilder->expr()->eq(3, 4), + $queryBuilder->expr()->eq(5, 6) + ) + ); + + $proxyQuery = new ProxyQuery($queryBuilder); + $this->assertSameQuery([], $proxyQuery); + + $filter1 = new StringFilter(); + $filter1->setCondition(Filter::CONDITION_OR); + $filter1->initialize('field_name', $filterOptions); + + $filter1->filter($proxyQuery, 'alias', 'project', ['value' => $value]); + + $filter2 = new StringFilter(); + $filter2->setCondition(Filter::CONDITION_OR); + $filter2->initialize('field_name', $filterOptions); + + $filter2->filter($proxyQuery, 'alias', 'version', ['value' => '3.x']); + + $this->assertSame($expected, $queryBuilder->getDQL()); + } + + public function orExpressionProvider(): iterable + { + yield 'Using "admin_code" option' => [ + 'SELECT e FROM MyEntity e WHERE 1 = 2 AND (3 = 4 OR 5 = 6) AND (alias.project LIKE :field_name_0 OR alias.version LIKE :field_name_1)', + [ + 'allow_empty' => false, + 'admin_code' => 'my_admin', + ], + ]; + + yield 'Missing "admin_code" option, fallback to DQL marker' => [ + 'SELECT e FROM MyEntity e WHERE 1 = 2 AND (3 = 4 OR 5 = 6) AND (:sonata_admin_datagrid_filter_query_marker IS NULL OR alias.project LIKE :field_name_0 OR alias.version LIKE :field_name_1)', + [ + 'allow_empty' => false, + ], + ]; + } + private function createFilter(): Filter { return new class() extends Filter {