Skip to content

Commit 4503275

Browse files
committed
Merge pull request #122 from MisatoTremor/orm_multi_filtration
Add Doctrine ORM and multicolumn filtration
2 parents d0515cf + f1afc79 commit 4503275

File tree

10 files changed

+1159
-32
lines changed

10 files changed

+1159
-32
lines changed

doc/pager/config.md

+7
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ The list of existing options are:
1919
| sortFieldWhitelist | array | [] | SortableSubscriber |
2020
| sortFieldParameterName | string | sort | SortableSubscriber |
2121
| sortDirectionParameterName | string | sort | SortableSubscriber |
22+
| defaultFilterFields | string\|array* | | FiltrationSubscriber |
23+
| filterFieldWhitelist | array | | FiltrationSubscriber |
2224
| filterFieldParameterName | string | filterParam | FiltrationSubscriber |
2325
| filterValueParameterName | string | filterValue | FiltrationSubscriber |
2426

@@ -45,3 +47,8 @@ you have to set `wrap-queries` to `true`. Otherwise you will get an exception wi
4547
Used as default field name for the sorting. It can take an array for sorting by multiple fields.
4648

4749
\* **Attention**: Arrays are only supported for *Doctrine's ORM*.
50+
51+
52+
### `defaultFilterFields`
53+
54+
Used as default field names for the filtration. It can take an array for filtering by multiple fields.

doc/pager/usage.md

+16
Original file line numberDiff line numberDiff line change
@@ -95,3 +95,19 @@ $pagination = $paginator->paginate($query, 1/*page number*/, 20/*limit per page*
9595

9696
The Paginator will add an `ORDER BY` automatically for each attribute for the
9797
`defaultSortFieldName` option.
98+
99+
## Filtering database query results by multiple columns (only Doctrine ORM and Propel)
100+
101+
You can also filter the result of a database query by multiple columns.
102+
For example users should be filtered by lastname or by firstname:
103+
104+
```php
105+
$query = $entityManager->createQuery('SELECT u FROM User');
106+
107+
$pagination = $paginator->paginate($query, 1/*page number*/, 20/*limit per page*/, array(
108+
'defaultFilterFields' => array('u.lastname', 'u.firstname'),
109+
));
110+
```
111+
112+
If the `filterValue` parameter is set, the Paginator will add an `WHERE` condition automatically
113+
for each attribute for the `defaultFilterFields` option. The conditions are `OR`-linked.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
<?php
2+
3+
namespace Knp\Component\Pager\Event\Subscriber\Filtration\Doctrine\ORM\Query;
4+
5+
use Doctrine\ORM\Query\TreeWalkerAdapter;
6+
use Doctrine\ORM\Query\AST\Node;
7+
use Doctrine\ORM\Query\AST\SelectStatement;
8+
use Doctrine\ORM\Query\AST\WhereClause;
9+
use Doctrine\ORM\Query\AST\PathExpression;
10+
use Doctrine\ORM\Query\AST\LikeExpression;
11+
use Doctrine\ORM\Query\AST\ComparisonExpression;
12+
use Doctrine\ORM\Query\AST\Literal;
13+
use Doctrine\ORM\Query\AST\ConditionalExpression;
14+
use Doctrine\ORM\Query\AST\ConditionalFactor;
15+
use Doctrine\ORM\Query\AST\ConditionalPrimary;
16+
use Doctrine\ORM\Query\AST\ConditionalTerm;
17+
18+
/**
19+
* Where Query TreeWalker for Filtration functionality
20+
* in doctrine paginator
21+
*/
22+
class WhereWalker extends TreeWalkerAdapter
23+
{
24+
/**
25+
* Filter key columns hint name
26+
*/
27+
const HINT_PAGINATOR_FILTER_COLUMNS = 'knp_paginator.filter.columns';
28+
29+
/**
30+
* Filter value hint name
31+
*/
32+
const HINT_PAGINATOR_FILTER_VALUE = 'knp_paginator.filter.value';
33+
34+
/**
35+
* Walks down a SelectStatement AST node, modifying it to
36+
* filter the query like requested by url
37+
*
38+
* @param SelectStatement $AST
39+
* @return void
40+
*/
41+
public function walkSelectStatement(SelectStatement $AST)
42+
{
43+
$query = $this->_getQuery();
44+
$queriedValue = $query->getHint(self::HINT_PAGINATOR_FILTER_VALUE);
45+
$columns = $query->getHint(self::HINT_PAGINATOR_FILTER_COLUMNS);
46+
$components = $this->_getQueryComponents();
47+
$filterExpressions = array();
48+
$expressions = array();
49+
foreach ($columns as $column) {
50+
$alias = false;
51+
$parts = explode('.', $column);
52+
$field = end($parts);
53+
if (2 <= count($parts)) {
54+
$alias = reset($parts);
55+
if (!array_key_exists($alias, $components)) {
56+
throw new \UnexpectedValueException("There is no component aliased by [{$alias}] in the given Query");
57+
}
58+
$meta = $components[$alias];
59+
if (!$meta['metadata']->hasField($field)) {
60+
throw new \UnexpectedValueException("There is no such field [{$field}] in the given Query component, aliased by [$alias]");
61+
}
62+
$pathExpression = new PathExpression(PathExpression::TYPE_STATE_FIELD, $alias, $field);
63+
$pathExpression->type = PathExpression::TYPE_STATE_FIELD;
64+
} else {
65+
if (!array_key_exists($field, $components)) {
66+
throw new \UnexpectedValueException("There is no component field [{$field}] in the given Query");
67+
}
68+
$pathExpression = $components[$field]['resultVariable'];
69+
}
70+
$expression = new ConditionalPrimary();
71+
if (isset($meta) && $meta['metadata']->getTypeOfField($field) === 'boolean') {
72+
if (in_array(strtolower($queriedValue), array('true', 'false'))) {
73+
$expression->simpleConditionalExpression = new ComparisonExpression($pathExpression, '=', new Literal(Literal::BOOLEAN, $queriedValue));
74+
} elseif (is_numeric($queriedValue)) {
75+
$expression->simpleConditionalExpression = new ComparisonExpression($pathExpression, '=', new Literal(Literal::BOOLEAN, $queriedValue == '1' ? 'true' : 'false'));
76+
} else {
77+
continue;
78+
}
79+
unset($meta);
80+
} elseif (is_numeric($queriedValue)) {
81+
$expression->simpleConditionalExpression = new ComparisonExpression($pathExpression, '=', new Literal(Literal::NUMERIC, $queriedValue));
82+
} else {
83+
$expression->simpleConditionalExpression = new LikeExpression($pathExpression, new Literal(Literal::STRING, $queriedValue));
84+
}
85+
$filterExpressions[] = $expression->simpleConditionalExpression;
86+
$expressions[] = $expression;
87+
}
88+
if (count($expressions) > 1) {
89+
$conditionalPrimary = new ConditionalExpression($expressions);
90+
} elseif (count($expressions) > 0) {
91+
$conditionalPrimary = reset($expressions);
92+
} else {
93+
return;
94+
}
95+
if ($AST->whereClause) {
96+
if ($AST->whereClause->conditionalExpression instanceof ConditionalTerm) {
97+
if (!$this->termContainsFilter($AST->whereClause->conditionalExpression, $filterExpressions)) {
98+
array_unshift(
99+
$AST->whereClause->conditionalExpression->conditionalFactors,
100+
$this->createPrimaryFromNode($conditionalPrimary)
101+
);
102+
}
103+
} elseif ($AST->whereClause->conditionalExpression instanceof ConditionalPrimary) {
104+
if (!$this->primaryContainsFilter($AST->whereClause->conditionalExpression, $filterExpressions)) {
105+
$AST->whereClause->conditionalExpression = new ConditionalTerm(array(
106+
$this->createPrimaryFromNode($conditionalPrimary),
107+
$AST->whereClause->conditionalExpression,
108+
));
109+
}
110+
} elseif ($AST->whereClause->conditionalExpression instanceof ConditionalExpression) {
111+
if (!$this->expressionContainsFilter($AST->whereClause->conditionalExpression, $filterExpressions)) {
112+
$previousPrimary = new ConditionalPrimary();
113+
$previousPrimary->conditionalExpression = $AST->whereClause->conditionalExpression;
114+
$AST->whereClause->conditionalExpression = new ConditionalTerm(array(
115+
$this->createPrimaryFromNode($conditionalPrimary),
116+
$previousPrimary,
117+
));
118+
}
119+
}
120+
} else {
121+
$AST->whereClause = new WhereClause(
122+
$conditionalPrimary
123+
);
124+
}
125+
}
126+
127+
/**
128+
* @param ConditionalExpression $node
129+
* @param Node[] $filterExpressions
130+
* @return bool
131+
*/
132+
private function expressionContainsFilter(ConditionalExpression $node, $filterExpressions)
133+
{
134+
foreach ($node->conditionalTerms as $conditionalTerm) {
135+
if ($conditionalTerm instanceof ConditionalTerm && $this->termContainsFilter($conditionalTerm, $filterExpressions)) {
136+
return true;
137+
} elseif ($conditionalTerm instanceof ConditionalPrimary && $this->primaryContainsFilter($conditionalTerm, $filterExpressions)) {
138+
return true;
139+
}
140+
}
141+
142+
return false;
143+
}
144+
145+
/**
146+
* @param ConditionalTerm $node
147+
* @param Node[] $filterExpressions
148+
* @return bool|void
149+
*/
150+
private function termContainsFilter(ConditionalTerm $node, $filterExpressions)
151+
{
152+
foreach ($node->conditionalFactors as $conditionalFactor) {
153+
if ($conditionalFactor instanceof ConditionalFactor) {
154+
if ($this->factorContainsFilter($conditionalFactor, $filterExpressions)) {
155+
return true;
156+
}
157+
} elseif ($conditionalFactor instanceof ConditionalPrimary) {
158+
if ($this->primaryContainsFilter($conditionalFactor, $filterExpressions)) {
159+
return true;
160+
}
161+
}
162+
}
163+
164+
return false;
165+
}
166+
167+
/**
168+
* @param ConditionalFactor $node
169+
* @param Node[] $filterExpressions
170+
* @return bool
171+
*/
172+
private function factorContainsFilter(ConditionalFactor $node, $filterExpressions)
173+
{
174+
if ($node->conditionalPrimary instanceof ConditionalPrimary && $node->not === false) {
175+
return $this->primaryContainsFilter($node->conditionalPrimary, $filterExpressions);
176+
}
177+
178+
return false;
179+
}
180+
181+
/**
182+
* @param ConditionalPrimary $node
183+
* @param Node[] $filterExpressions
184+
* @return bool
185+
*/
186+
private function primaryContainsFilter(ConditionalPrimary $node, $filterExpressions)
187+
{
188+
if ($node->isSimpleConditionalExpression() && ($node->simpleConditionalExpression instanceof LikeExpression || $node->simpleConditionalExpression instanceof ComparisonExpression)) {
189+
return $this->isExpressionInFilterExpressions($node->simpleConditionalExpression, $filterExpressions);
190+
}
191+
if ($node->isConditionalExpression()) {
192+
return $this->expressionContainsFilter($node->conditionalExpression, $filterExpressions);
193+
}
194+
195+
return false;
196+
}
197+
198+
/**
199+
* @param Node $node
200+
* @param Node[] $filterExpressions
201+
* @return bool
202+
*/
203+
private function isExpressionInFilterExpressions(Node $node, $filterExpressions)
204+
{
205+
foreach ($filterExpressions as $filterExpression) {
206+
if ((string) $filterExpression === (string) $node) {
207+
return true;
208+
}
209+
}
210+
211+
return false;
212+
}
213+
214+
/**
215+
* @param Node $node
216+
* @return ConditionalPrimary
217+
*/
218+
private function createPrimaryFromNode($node)
219+
{
220+
if ($node instanceof ConditionalPrimary) {
221+
$conditionalPrimary = $node;
222+
} else {
223+
$conditionalPrimary = new ConditionalPrimary();
224+
$conditionalPrimary->conditionalExpression = $node;
225+
}
226+
227+
return $conditionalPrimary;
228+
}
229+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
<?php
2+
3+
namespace Knp\Component\Pager\Event\Subscriber\Filtration\Doctrine\ORM;
4+
5+
use Doctrine\ORM\Query;
6+
use Knp\Component\Pager\Event\ItemsEvent;
7+
use Knp\Component\Pager\Event\Subscriber\Filtration\Doctrine\ORM\Query\WhereWalker;
8+
use Knp\Component\Pager\Event\Subscriber\Paginate\Doctrine\ORM\Query\Helper as QueryHelper;
9+
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
10+
11+
class QuerySubscriber implements EventSubscriberInterface
12+
{
13+
public function items(ItemsEvent $event)
14+
{
15+
if ($event->target instanceof Query) {
16+
if (!isset($_GET[$event->options['filterValueParameterName']]) || (empty($_GET[$event->options['filterValueParameterName']]) && $_GET[$event->options['filterValueParameterName']] !== "0")) {
17+
return;
18+
}
19+
if (!empty($_GET[$event->options['filterFieldParameterName']])) {
20+
$columns = $_GET[$event->options['filterFieldParameterName']];
21+
} elseif (!empty($event->options['defaultFilterFields'])) {
22+
$columns = $event->options['defaultFilterFields'];
23+
} else {
24+
return;
25+
}
26+
$value = $_GET[$event->options['filterValueParameterName']];
27+
if (false !== strpos($value, '*')) {
28+
$value = str_replace('*', '%', $value);
29+
}
30+
if (is_string($columns) && false !== strpos($columns, ',')) {
31+
$columns = explode(',', $columns);
32+
}
33+
$columns = (array) $columns;
34+
if (isset($event->options['filterFieldWhitelist'])) {
35+
foreach ($columns as $column) {
36+
if (!in_array($column, $event->options['filterFieldWhitelist'])) {
37+
throw new \UnexpectedValueException("Cannot filter by: [{$column}] this field is not in whitelist");
38+
}
39+
}
40+
}
41+
$event->target
42+
->setHint(WhereWalker::HINT_PAGINATOR_FILTER_VALUE, $value)
43+
->setHint(WhereWalker::HINT_PAGINATOR_FILTER_COLUMNS, $columns);
44+
QueryHelper::addCustomTreeWalker($event->target, 'Knp\Component\Pager\Event\Subscriber\Filtration\Doctrine\ORM\Query\WhereWalker');
45+
}
46+
}
47+
48+
public static function getSubscribedEvents()
49+
{
50+
return array(
51+
'knp_pager.items' => array('items', 0),
52+
);
53+
}
54+
}

src/Knp/Component/Pager/Event/Subscriber/Filtration/FiltrationSubscriber.php

+3-2
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,15 @@ class FiltrationSubscriber implements EventSubscriberInterface
1010
public function before(BeforeEvent $event)
1111
{
1212
$disp = $event->getEventDispatcher();
13-
// hook all standard sortable subscribers
13+
// hook all standard filtration subscribers
14+
$disp->addSubscriber(new Doctrine\ORM\QuerySubscriber());
1415
$disp->addSubscriber(new PropelQuerySubscriber());
1516
}
1617

1718
public static function getSubscribedEvents()
1819
{
1920
return array(
20-
'knp_pager.before' => array('before', 1)
21+
'knp_pager.before' => array('before', 1),
2122
);
2223
}
2324
}

0 commit comments

Comments
 (0)