Skip to content

Commit

Permalink
Merge pull request #122 from MisatoTremor/orm_multi_filtration
Browse files Browse the repository at this point in the history
Add Doctrine ORM and multicolumn filtration
  • Loading branch information
pilot committed Sep 1, 2015
2 parents d0515cf + f1afc79 commit 4503275
Show file tree
Hide file tree
Showing 10 changed files with 1,159 additions and 32 deletions.
7 changes: 7 additions & 0 deletions doc/pager/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ The list of existing options are:
| sortFieldWhitelist | array | [] | SortableSubscriber |
| sortFieldParameterName | string | sort | SortableSubscriber |
| sortDirectionParameterName | string | sort | SortableSubscriber |
| defaultFilterFields | string\|array* | | FiltrationSubscriber |
| filterFieldWhitelist | array | | FiltrationSubscriber |
| filterFieldParameterName | string | filterParam | FiltrationSubscriber |
| filterValueParameterName | string | filterValue | FiltrationSubscriber |

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

\* **Attention**: Arrays are only supported for *Doctrine's ORM*.


### `defaultFilterFields`

Used as default field names for the filtration. It can take an array for filtering by multiple fields.
16 changes: 16 additions & 0 deletions doc/pager/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,3 +95,19 @@ $pagination = $paginator->paginate($query, 1/*page number*/, 20/*limit per page*

The Paginator will add an `ORDER BY` automatically for each attribute for the
`defaultSortFieldName` option.

## Filtering database query results by multiple columns (only Doctrine ORM and Propel)

You can also filter the result of a database query by multiple columns.
For example users should be filtered by lastname or by firstname:

```php
$query = $entityManager->createQuery('SELECT u FROM User');

$pagination = $paginator->paginate($query, 1/*page number*/, 20/*limit per page*/, array(
'defaultFilterFields' => array('u.lastname', 'u.firstname'),
));
```

If the `filterValue` parameter is set, the Paginator will add an `WHERE` condition automatically
for each attribute for the `defaultFilterFields` option. The conditions are `OR`-linked.
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
<?php

namespace Knp\Component\Pager\Event\Subscriber\Filtration\Doctrine\ORM\Query;

use Doctrine\ORM\Query\TreeWalkerAdapter;
use Doctrine\ORM\Query\AST\Node;
use Doctrine\ORM\Query\AST\SelectStatement;
use Doctrine\ORM\Query\AST\WhereClause;
use Doctrine\ORM\Query\AST\PathExpression;
use Doctrine\ORM\Query\AST\LikeExpression;
use Doctrine\ORM\Query\AST\ComparisonExpression;
use Doctrine\ORM\Query\AST\Literal;
use Doctrine\ORM\Query\AST\ConditionalExpression;
use Doctrine\ORM\Query\AST\ConditionalFactor;
use Doctrine\ORM\Query\AST\ConditionalPrimary;
use Doctrine\ORM\Query\AST\ConditionalTerm;

/**
* Where Query TreeWalker for Filtration functionality
* in doctrine paginator
*/
class WhereWalker extends TreeWalkerAdapter
{
/**
* Filter key columns hint name
*/
const HINT_PAGINATOR_FILTER_COLUMNS = 'knp_paginator.filter.columns';

/**
* Filter value hint name
*/
const HINT_PAGINATOR_FILTER_VALUE = 'knp_paginator.filter.value';

/**
* Walks down a SelectStatement AST node, modifying it to
* filter the query like requested by url
*
* @param SelectStatement $AST
* @return void
*/
public function walkSelectStatement(SelectStatement $AST)
{
$query = $this->_getQuery();
$queriedValue = $query->getHint(self::HINT_PAGINATOR_FILTER_VALUE);
$columns = $query->getHint(self::HINT_PAGINATOR_FILTER_COLUMNS);
$components = $this->_getQueryComponents();
$filterExpressions = array();
$expressions = array();
foreach ($columns as $column) {
$alias = false;
$parts = explode('.', $column);
$field = end($parts);
if (2 <= count($parts)) {
$alias = reset($parts);
if (!array_key_exists($alias, $components)) {
throw new \UnexpectedValueException("There is no component aliased by [{$alias}] in the given Query");
}
$meta = $components[$alias];
if (!$meta['metadata']->hasField($field)) {
throw new \UnexpectedValueException("There is no such field [{$field}] in the given Query component, aliased by [$alias]");
}
$pathExpression = new PathExpression(PathExpression::TYPE_STATE_FIELD, $alias, $field);
$pathExpression->type = PathExpression::TYPE_STATE_FIELD;
} else {
if (!array_key_exists($field, $components)) {
throw new \UnexpectedValueException("There is no component field [{$field}] in the given Query");
}
$pathExpression = $components[$field]['resultVariable'];
}
$expression = new ConditionalPrimary();
if (isset($meta) && $meta['metadata']->getTypeOfField($field) === 'boolean') {
if (in_array(strtolower($queriedValue), array('true', 'false'))) {
$expression->simpleConditionalExpression = new ComparisonExpression($pathExpression, '=', new Literal(Literal::BOOLEAN, $queriedValue));
} elseif (is_numeric($queriedValue)) {
$expression->simpleConditionalExpression = new ComparisonExpression($pathExpression, '=', new Literal(Literal::BOOLEAN, $queriedValue == '1' ? 'true' : 'false'));
} else {
continue;
}
unset($meta);
} elseif (is_numeric($queriedValue)) {
$expression->simpleConditionalExpression = new ComparisonExpression($pathExpression, '=', new Literal(Literal::NUMERIC, $queriedValue));
} else {
$expression->simpleConditionalExpression = new LikeExpression($pathExpression, new Literal(Literal::STRING, $queriedValue));
}
$filterExpressions[] = $expression->simpleConditionalExpression;
$expressions[] = $expression;
}
if (count($expressions) > 1) {
$conditionalPrimary = new ConditionalExpression($expressions);
} elseif (count($expressions) > 0) {
$conditionalPrimary = reset($expressions);
} else {
return;
}
if ($AST->whereClause) {
if ($AST->whereClause->conditionalExpression instanceof ConditionalTerm) {
if (!$this->termContainsFilter($AST->whereClause->conditionalExpression, $filterExpressions)) {
array_unshift(
$AST->whereClause->conditionalExpression->conditionalFactors,
$this->createPrimaryFromNode($conditionalPrimary)
);
}
} elseif ($AST->whereClause->conditionalExpression instanceof ConditionalPrimary) {
if (!$this->primaryContainsFilter($AST->whereClause->conditionalExpression, $filterExpressions)) {
$AST->whereClause->conditionalExpression = new ConditionalTerm(array(
$this->createPrimaryFromNode($conditionalPrimary),
$AST->whereClause->conditionalExpression,
));
}
} elseif ($AST->whereClause->conditionalExpression instanceof ConditionalExpression) {
if (!$this->expressionContainsFilter($AST->whereClause->conditionalExpression, $filterExpressions)) {
$previousPrimary = new ConditionalPrimary();
$previousPrimary->conditionalExpression = $AST->whereClause->conditionalExpression;
$AST->whereClause->conditionalExpression = new ConditionalTerm(array(
$this->createPrimaryFromNode($conditionalPrimary),
$previousPrimary,
));
}
}
} else {
$AST->whereClause = new WhereClause(
$conditionalPrimary
);
}
}

/**
* @param ConditionalExpression $node
* @param Node[] $filterExpressions
* @return bool
*/
private function expressionContainsFilter(ConditionalExpression $node, $filterExpressions)
{
foreach ($node->conditionalTerms as $conditionalTerm) {
if ($conditionalTerm instanceof ConditionalTerm && $this->termContainsFilter($conditionalTerm, $filterExpressions)) {
return true;
} elseif ($conditionalTerm instanceof ConditionalPrimary && $this->primaryContainsFilter($conditionalTerm, $filterExpressions)) {
return true;
}
}

return false;
}

/**
* @param ConditionalTerm $node
* @param Node[] $filterExpressions
* @return bool|void
*/
private function termContainsFilter(ConditionalTerm $node, $filterExpressions)
{
foreach ($node->conditionalFactors as $conditionalFactor) {
if ($conditionalFactor instanceof ConditionalFactor) {
if ($this->factorContainsFilter($conditionalFactor, $filterExpressions)) {
return true;
}
} elseif ($conditionalFactor instanceof ConditionalPrimary) {
if ($this->primaryContainsFilter($conditionalFactor, $filterExpressions)) {
return true;
}
}
}

return false;
}

/**
* @param ConditionalFactor $node
* @param Node[] $filterExpressions
* @return bool
*/
private function factorContainsFilter(ConditionalFactor $node, $filterExpressions)
{
if ($node->conditionalPrimary instanceof ConditionalPrimary && $node->not === false) {
return $this->primaryContainsFilter($node->conditionalPrimary, $filterExpressions);
}

return false;
}

/**
* @param ConditionalPrimary $node
* @param Node[] $filterExpressions
* @return bool
*/
private function primaryContainsFilter(ConditionalPrimary $node, $filterExpressions)
{
if ($node->isSimpleConditionalExpression() && ($node->simpleConditionalExpression instanceof LikeExpression || $node->simpleConditionalExpression instanceof ComparisonExpression)) {
return $this->isExpressionInFilterExpressions($node->simpleConditionalExpression, $filterExpressions);
}
if ($node->isConditionalExpression()) {
return $this->expressionContainsFilter($node->conditionalExpression, $filterExpressions);
}

return false;
}

/**
* @param Node $node
* @param Node[] $filterExpressions
* @return bool
*/
private function isExpressionInFilterExpressions(Node $node, $filterExpressions)
{
foreach ($filterExpressions as $filterExpression) {
if ((string) $filterExpression === (string) $node) {
return true;
}
}

return false;
}

/**
* @param Node $node
* @return ConditionalPrimary
*/
private function createPrimaryFromNode($node)
{
if ($node instanceof ConditionalPrimary) {
$conditionalPrimary = $node;
} else {
$conditionalPrimary = new ConditionalPrimary();
$conditionalPrimary->conditionalExpression = $node;
}

return $conditionalPrimary;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?php

namespace Knp\Component\Pager\Event\Subscriber\Filtration\Doctrine\ORM;

use Doctrine\ORM\Query;
use Knp\Component\Pager\Event\ItemsEvent;
use Knp\Component\Pager\Event\Subscriber\Filtration\Doctrine\ORM\Query\WhereWalker;
use Knp\Component\Pager\Event\Subscriber\Paginate\Doctrine\ORM\Query\Helper as QueryHelper;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

class QuerySubscriber implements EventSubscriberInterface
{
public function items(ItemsEvent $event)
{
if ($event->target instanceof Query) {
if (!isset($_GET[$event->options['filterValueParameterName']]) || (empty($_GET[$event->options['filterValueParameterName']]) && $_GET[$event->options['filterValueParameterName']] !== "0")) {
return;
}
if (!empty($_GET[$event->options['filterFieldParameterName']])) {
$columns = $_GET[$event->options['filterFieldParameterName']];
} elseif (!empty($event->options['defaultFilterFields'])) {
$columns = $event->options['defaultFilterFields'];
} else {
return;
}
$value = $_GET[$event->options['filterValueParameterName']];
if (false !== strpos($value, '*')) {
$value = str_replace('*', '%', $value);
}
if (is_string($columns) && false !== strpos($columns, ',')) {
$columns = explode(',', $columns);
}
$columns = (array) $columns;
if (isset($event->options['filterFieldWhitelist'])) {
foreach ($columns as $column) {
if (!in_array($column, $event->options['filterFieldWhitelist'])) {
throw new \UnexpectedValueException("Cannot filter by: [{$column}] this field is not in whitelist");
}
}
}
$event->target
->setHint(WhereWalker::HINT_PAGINATOR_FILTER_VALUE, $value)
->setHint(WhereWalker::HINT_PAGINATOR_FILTER_COLUMNS, $columns);
QueryHelper::addCustomTreeWalker($event->target, 'Knp\Component\Pager\Event\Subscriber\Filtration\Doctrine\ORM\Query\WhereWalker');
}
}

public static function getSubscribedEvents()
{
return array(
'knp_pager.items' => array('items', 0),
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,15 @@ class FiltrationSubscriber implements EventSubscriberInterface
public function before(BeforeEvent $event)
{
$disp = $event->getEventDispatcher();
// hook all standard sortable subscribers
// hook all standard filtration subscribers
$disp->addSubscriber(new Doctrine\ORM\QuerySubscriber());
$disp->addSubscriber(new PropelQuerySubscriber());
}

public static function getSubscribedEvents()
{
return array(
'knp_pager.before' => array('before', 1)
'knp_pager.before' => array('before', 1),
);
}
}
Loading

0 comments on commit 4503275

Please sign in to comment.