/src/Knp/Component/Pager/Event/Subscriber/Filtration/Doctrine/ORM/Query/WhereWalker.php

https://github.com/KnpLabs/knp-components · PHP · 261 lines · 192 code · 20 blank · 49 comment · 37 complexity · 9a28518ac81f0884a721a74fce4f70a7 MD5 · raw file

  1. <?php
  2. namespace Knp\Component\Pager\Event\Subscriber\Filtration\Doctrine\ORM\Query;
  3. use Doctrine\DBAL\Types\Types as Type;
  4. use Doctrine\ORM\Query\AST\Functions\LowerFunction;
  5. use Doctrine\ORM\Query\AST\ComparisonExpression;
  6. use Doctrine\ORM\Query\AST\ConditionalExpression;
  7. use Doctrine\ORM\Query\AST\ConditionalFactor;
  8. use Doctrine\ORM\Query\AST\ConditionalPrimary;
  9. use Doctrine\ORM\Query\AST\ConditionalTerm;
  10. use Doctrine\ORM\Query\AST\LikeExpression;
  11. use Doctrine\ORM\Query\AST\Literal;
  12. use Doctrine\ORM\Query\AST\Node;
  13. use Doctrine\ORM\Query\AST\PathExpression;
  14. use Doctrine\ORM\Query\AST\SelectStatement;
  15. use Doctrine\ORM\Query\AST\WhereClause;
  16. use Doctrine\ORM\Query\TreeWalkerAdapter;
  17. /**
  18. * Where Query TreeWalker for Filtration functionality
  19. * in doctrine paginator
  20. */
  21. class WhereWalker extends TreeWalkerAdapter
  22. {
  23. /**
  24. * Filter key columns hint name
  25. */
  26. public const HINT_PAGINATOR_FILTER_COLUMNS = 'knp_paginator.filter.columns';
  27. /**
  28. * Filter value hint name
  29. */
  30. public const HINT_PAGINATOR_FILTER_VALUE = 'knp_paginator.filter.value';
  31. /**
  32. * Filter strings in a case insensitive way
  33. */
  34. const HINT_PAGINATOR_FILTER_CASE_INSENSITIVE = 'knp_paginator.filter.case_insensitive';
  35. /**
  36. * Walks down a SelectStatement AST node, modifying it to
  37. * filter the query like requested by url
  38. *
  39. * @param SelectStatement $AST
  40. * @return void|string
  41. */
  42. public function walkSelectStatement(SelectStatement $AST)
  43. {
  44. $query = $this->_getQuery();
  45. $queriedValue = $query->getHint(self::HINT_PAGINATOR_FILTER_VALUE);
  46. $columns = $query->getHint(self::HINT_PAGINATOR_FILTER_COLUMNS);
  47. $filterCaseInsensitive = $query->getHint(self::HINT_PAGINATOR_FILTER_CASE_INSENSITIVE);
  48. $components = $this->_getQueryComponents();
  49. $filterExpressions = [];
  50. $expressions = [];
  51. foreach ($columns as $column) {
  52. $alias = false;
  53. $parts = explode('.', $column, 2);
  54. $field = end($parts);
  55. if (2 <= count($parts)) {
  56. $alias = trim(reset($parts));
  57. if (!array_key_exists($alias, $components)) {
  58. throw new \UnexpectedValueException("There is no component aliased by [{$alias}] in the given Query");
  59. }
  60. $meta = $components[$alias];
  61. if (!$meta['metadata']->hasField($field)) {
  62. throw new \UnexpectedValueException("There is no such field [{$field}] in the given Query component, aliased by [$alias]");
  63. }
  64. $pathExpression = new PathExpression(PathExpression::TYPE_STATE_FIELD, $alias, $field);
  65. $pathExpression->type = PathExpression::TYPE_STATE_FIELD;
  66. } else {
  67. if (!array_key_exists($field, $components)) {
  68. throw new \UnexpectedValueException("There is no component field [{$field}] in the given Query");
  69. }
  70. $pathExpression = $components[$field]['resultVariable'];
  71. }
  72. $expression = new ConditionalPrimary();
  73. if (isset($meta) && $meta['metadata']->getTypeOfField($field) === 'boolean') {
  74. if (in_array(strtolower($queriedValue), ['true', 'false'])) {
  75. $expression->simpleConditionalExpression = new ComparisonExpression($pathExpression, '=', new Literal(Literal::BOOLEAN, $queriedValue));
  76. } elseif (is_numeric($queriedValue)) {
  77. $expression->simpleConditionalExpression = new ComparisonExpression($pathExpression, '=', new Literal(Literal::BOOLEAN, $queriedValue == '1' ? 'true' : 'false'));
  78. } else {
  79. continue;
  80. }
  81. unset($meta);
  82. } elseif (is_numeric($queriedValue)
  83. && (
  84. !isset($meta)
  85. || in_array(
  86. $meta['metadata']->getTypeOfField($field),
  87. [
  88. Type::SMALLINT,
  89. Type::INTEGER,
  90. Type::BIGINT,
  91. Type::FLOAT,
  92. Type::DECIMAL,
  93. ]
  94. )
  95. )
  96. ) {
  97. $expression->simpleConditionalExpression = new ComparisonExpression($pathExpression, '=', new Literal(Literal::NUMERIC, $queriedValue));
  98. } else {
  99. $likePathExpression = $pathExpression;
  100. $likeQueriedValue = $queriedValue;
  101. if ($filterCaseInsensitive) {
  102. $lower = new LowerFunction('lower');
  103. $lower->stringPrimary = $pathExpression;
  104. $likePathExpression = $lower;
  105. $likeQueriedValue = strtolower($queriedValue);
  106. }
  107. $expression->simpleConditionalExpression = new LikeExpression($likePathExpression, new Literal(Literal::STRING, $likeQueriedValue));
  108. }
  109. $filterExpressions[] = $expression->simpleConditionalExpression;
  110. $expressions[] = $expression;
  111. }
  112. if (count($expressions) > 1) {
  113. $conditionalPrimary = new ConditionalExpression($expressions);
  114. } elseif (count($expressions) > 0) {
  115. $conditionalPrimary = reset($expressions);
  116. } else {
  117. return;
  118. }
  119. if ($AST->whereClause) {
  120. if ($AST->whereClause->conditionalExpression instanceof ConditionalTerm) {
  121. if (!$this->termContainsFilter($AST->whereClause->conditionalExpression, $filterExpressions)) {
  122. array_unshift(
  123. $AST->whereClause->conditionalExpression->conditionalFactors,
  124. $this->createPrimaryFromNode($conditionalPrimary)
  125. );
  126. }
  127. } elseif ($AST->whereClause->conditionalExpression instanceof ConditionalPrimary) {
  128. if (!$this->primaryContainsFilter($AST->whereClause->conditionalExpression, $filterExpressions)) {
  129. $AST->whereClause->conditionalExpression = new ConditionalTerm([
  130. $this->createPrimaryFromNode($conditionalPrimary),
  131. $AST->whereClause->conditionalExpression,
  132. ]);
  133. }
  134. } elseif ($AST->whereClause->conditionalExpression instanceof ConditionalExpression) {
  135. if (!$this->expressionContainsFilter($AST->whereClause->conditionalExpression, $filterExpressions)) {
  136. $previousPrimary = new ConditionalPrimary();
  137. $previousPrimary->conditionalExpression = $AST->whereClause->conditionalExpression;
  138. $AST->whereClause->conditionalExpression = new ConditionalTerm([
  139. $this->createPrimaryFromNode($conditionalPrimary),
  140. $previousPrimary,
  141. ]);
  142. }
  143. }
  144. } else {
  145. $AST->whereClause = new WhereClause(
  146. $conditionalPrimary
  147. );
  148. }
  149. }
  150. /**
  151. * @param ConditionalExpression $node
  152. * @param Node[] $filterExpressions
  153. * @return bool
  154. */
  155. private function expressionContainsFilter(ConditionalExpression $node, $filterExpressions): bool
  156. {
  157. foreach ($node->conditionalTerms as $conditionalTerm) {
  158. if ($conditionalTerm instanceof ConditionalTerm && $this->termContainsFilter($conditionalTerm, $filterExpressions)) {
  159. return true;
  160. } elseif ($conditionalTerm instanceof ConditionalPrimary && $this->primaryContainsFilter($conditionalTerm, $filterExpressions)) {
  161. return true;
  162. }
  163. }
  164. return false;
  165. }
  166. /**
  167. * @param ConditionalTerm $node
  168. * @param Node[] $filterExpressions
  169. * @return bool
  170. */
  171. private function termContainsFilter(ConditionalTerm $node, $filterExpressions): bool
  172. {
  173. foreach ($node->conditionalFactors as $conditionalFactor) {
  174. if ($conditionalFactor instanceof ConditionalFactor) {
  175. if ($this->factorContainsFilter($conditionalFactor, $filterExpressions)) {
  176. return true;
  177. }
  178. } elseif ($conditionalFactor instanceof ConditionalPrimary) {
  179. if ($this->primaryContainsFilter($conditionalFactor, $filterExpressions)) {
  180. return true;
  181. }
  182. }
  183. }
  184. return false;
  185. }
  186. /**
  187. * @param ConditionalFactor $node
  188. * @param Node[] $filterExpressions
  189. * @return bool
  190. */
  191. private function factorContainsFilter(ConditionalFactor $node, $filterExpressions): bool
  192. {
  193. if ($node->conditionalPrimary instanceof ConditionalPrimary && $node->not === false) {
  194. return $this->primaryContainsFilter($node->conditionalPrimary, $filterExpressions);
  195. }
  196. return false;
  197. }
  198. /**
  199. * @param ConditionalPrimary $node
  200. * @param Node[] $filterExpressions
  201. * @return bool
  202. */
  203. private function primaryContainsFilter(ConditionalPrimary $node, $filterExpressions): bool
  204. {
  205. if ($node->isSimpleConditionalExpression() && ($node->simpleConditionalExpression instanceof LikeExpression || $node->simpleConditionalExpression instanceof ComparisonExpression)) {
  206. return $this->isExpressionInFilterExpressions($node->simpleConditionalExpression, $filterExpressions);
  207. }
  208. if ($node->isConditionalExpression()) {
  209. return $this->expressionContainsFilter($node->conditionalExpression, $filterExpressions);
  210. }
  211. return false;
  212. }
  213. /**
  214. * @param Node $node
  215. * @param Node[] $filterExpressions
  216. * @return bool
  217. */
  218. private function isExpressionInFilterExpressions(Node $node, $filterExpressions): bool
  219. {
  220. foreach ($filterExpressions as $filterExpression) {
  221. if ((string) $filterExpression === (string) $node) {
  222. return true;
  223. }
  224. }
  225. return false;
  226. }
  227. /**
  228. * @param ConditionalPrimary|ConditionalExpression $node
  229. * @return ConditionalPrimary
  230. */
  231. private function createPrimaryFromNode($node): ConditionalPrimary
  232. {
  233. if ($node instanceof ConditionalPrimary) {
  234. $conditionalPrimary = $node;
  235. } else {
  236. $conditionalPrimary = new ConditionalPrimary();
  237. $conditionalPrimary->conditionalExpression = $node;
  238. }
  239. return $conditionalPrimary;
  240. }
  241. }