QueryExpression.php 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520
  1. <?php
  2. /**
  3. * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
  4. * Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
  5. *
  6. * Licensed under The MIT License
  7. * For full copyright and license information, please see the LICENSE.txt
  8. * Redistributions of files must retain the above copyright notice.
  9. *
  10. * @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
  11. * @link http://cakephp.org CakePHP(tm) Project
  12. * @since 3.0.0
  13. * @license http://www.opensource.org/licenses/mit-license.php MIT License
  14. */
  15. namespace Cake\Database\Expression;
  16. use Cake\Database\ExpressionInterface;
  17. use Cake\Database\Expression\IdentifierExpression;
  18. use Cake\Database\TypeMapTrait;
  19. use Cake\Database\ValueBinder;
  20. use \Countable;
  21. /**
  22. * Represents a SQL Query expression. Internally it stores a tree of
  23. * expressions that can be compiled by converting this object to string
  24. * and will contain a correctly parenthesized and nested expression.
  25. *
  26. * @internal
  27. */
  28. class QueryExpression implements ExpressionInterface, Countable {
  29. use TypeMapTrait;
  30. /**
  31. * String to be used for joining each of the internal expressions
  32. * this object internally stores for example "AND", "OR", etc.
  33. *
  34. * @var string
  35. */
  36. protected $_conjunction;
  37. /**
  38. * A list of strings or other expression objects that represent the "branches" of
  39. * the expression tree. For example one key of the array might look like "sum > :value"
  40. *
  41. * @var array
  42. */
  43. protected $_conditions = [];
  44. /**
  45. * Constructor. A new expression object can be created without any params and
  46. * be built dynamically. Otherwise it is possible to pass an array of conditions
  47. * containing either a tree-like array structure to be parsed and/or other
  48. * expression objects. Optionally, you can set the conjunction keyword to be used
  49. * for joining each part of this level of the expression tree.
  50. *
  51. * @param array $conditions tree-like array structure containing all the conditions
  52. * to be added or nested inside this expression object.
  53. * @param array|TypeMap $types associative array of types to be associated with the values
  54. * passed in $conditions.
  55. * @param string $conjunction the glue that will join all the string conditions at this
  56. * level of the expression tree. For example "AND", "OR", "XOR"...
  57. * @param TypeMap $typeMap contains default and call specific type mapping
  58. * @see QueryExpression::add() for more details on $conditions and $types
  59. */
  60. public function __construct($conditions = [], $types = [], $conjunction = 'AND') {
  61. $this->typeMap($types);
  62. $this->type(strtoupper($conjunction));
  63. if (!empty($conditions)) {
  64. $this->add($conditions, $this->typeMap()->types());
  65. }
  66. }
  67. /**
  68. * Changes the conjunction for the conditions at this level of the expression tree.
  69. * If called with no arguments it will return the currently configured value.
  70. *
  71. * @param string $conjunction value to be used for joining conditions. If null it
  72. * will not set any value, but return the currently stored one
  73. * @return string|QueryExpression
  74. */
  75. public function type($conjunction = null) {
  76. if ($conjunction === null) {
  77. return $this->_conjunction;
  78. }
  79. $this->_conjunction = strtoupper($conjunction);
  80. return $this;
  81. }
  82. /**
  83. * Adds one or more conditions to this expression object. Conditions can be
  84. * expressed in a one dimensional array, that will cause all conditions to
  85. * be added directly at this level of the tree or they can be nested arbitrarily
  86. * making it create more expression objects that will be nested inside and
  87. * configured to use the specified conjunction.
  88. *
  89. * If the type passed for any of the fields is expressed "type[]" (note braces)
  90. * then it will cause the placeholder to be re-written dynamically so if the
  91. * value is an array, it will create as many placeholders as values are in it.
  92. *
  93. * @param string|array|QueryExpression $conditions single or multiple conditions to
  94. * be added. When using and array and the key is 'OR' or 'AND' a new expression
  95. * object will be created with that conjunction and internal array value passed
  96. * as conditions.
  97. * @param array $types associative array of fields pointing to the type of the
  98. * values that are being passed. Used for correctly binding values to statements.
  99. * @see \Cake\Database\Query::where() for examples on conditions
  100. * @return QueryExpression
  101. */
  102. public function add($conditions, $types = []) {
  103. if (is_string($conditions)) {
  104. $this->_conditions[] = $conditions;
  105. return $this;
  106. }
  107. if ($conditions instanceof ExpressionInterface) {
  108. $this->_conditions[] = $conditions;
  109. return $this;
  110. }
  111. $this->_addConditions($conditions, $types);
  112. return $this;
  113. }
  114. /**
  115. * Adds a new condition to the expression object in the form "field = value".
  116. *
  117. * @param string $field database field to be compared against value
  118. * @param mixed $value The value to be bound to $field for comparison
  119. * @param string $type the type name for $value as configured using the Type map.
  120. * If it is suffixed with "[]" and the value is an array then multiple placeholders
  121. * will be created, one per each value in the array.
  122. * @return QueryExpression
  123. */
  124. public function eq($field, $value, $type = null) {
  125. return $this->add(new Comparison($field, $value, $type, '='));
  126. }
  127. /**
  128. * Adds a new condition to the expression object in the form "field != value".
  129. *
  130. * @param string $field database field to be compared against value
  131. * @param mixed $value The value to be bound to $field for comparison
  132. * @param string $type the type name for $value as configured using the Type map.
  133. * If it is suffixed with "[]" and the value is an array then multiple placeholders
  134. * will be created, one per each value in the array.
  135. * @return QueryExpression
  136. */
  137. public function notEq($field, $value, $type = null) {
  138. return $this->add(new Comparison($field, $value, $type, '!='));
  139. }
  140. /**
  141. * Adds a new condition to the expression object in the form "field > value".
  142. *
  143. * @param string $field database field to be compared against value
  144. * @param mixed $value The value to be bound to $field for comparison
  145. * @param string $type the type name for $value as configured using the Type map.
  146. * @return QueryExpression
  147. */
  148. public function gt($field, $value, $type = null) {
  149. return $this->add(new Comparison($field, $value, $type, '>'));
  150. }
  151. /**
  152. * Adds a new condition to the expression object in the form "field < value".
  153. *
  154. * @param string $field database field to be compared against value
  155. * @param mixed $value The value to be bound to $field for comparison
  156. * @param string $type the type name for $value as configured using the Type map.
  157. * @return QueryExpression
  158. */
  159. public function lt($field, $value, $type = null) {
  160. return $this->add(new Comparison($field, $value, $type, '<'));
  161. }
  162. /**
  163. * Adds a new condition to the expression object in the form "field >= value".
  164. *
  165. * @param string $field database field to be compared against value
  166. * @param mixed $value The value to be bound to $field for comparison
  167. * @param string $type the type name for $value as configured using the Type map.
  168. * @return QueryExpression
  169. */
  170. public function gte($field, $value, $type = null) {
  171. return $this->add(new Comparison($field, $value, $type, '>='));
  172. }
  173. /**
  174. * Adds a new condition to the expression object in the form "field <= value".
  175. *
  176. * @param string $field database field to be compared against value
  177. * @param mixed $value The value to be bound to $field for comparison
  178. * @param string $type the type name for $value as configured using the Type map.
  179. * @return QueryExpression
  180. */
  181. public function lte($field, $value, $type = null) {
  182. return $this->add(new Comparison($field, $value, $type, '<='));
  183. }
  184. /**
  185. * Adds a new condition to the expression object in the form "field IS NULL".
  186. *
  187. * @param string|ExpressionInterface $field database field to be tested for null
  188. * @return QueryExpression
  189. */
  190. public function isNull($field) {
  191. if (!($field instanceof ExpressionInterface)) {
  192. $field = new IdentifierExpression($field);
  193. }
  194. return $this->add(new UnaryExpression('IS NULL', $field, UnaryExpression::POSTFIX));
  195. }
  196. /**
  197. * Adds a new condition to the expression object in the form "field IS NOT NULL".
  198. *
  199. * @param string $field database field to be tested for not null
  200. * @return QueryExpression
  201. */
  202. public function isNotNull($field) {
  203. if (!($field instanceof ExpressionInterface)) {
  204. $field = new IdentifierExpression($field);
  205. }
  206. return $this->add(new UnaryExpression('IS NOT NULL', $field, UnaryExpression::POSTFIX));
  207. }
  208. /**
  209. * Adds a new condition to the expression object in the form "field LIKE value".
  210. *
  211. * @param string $field database field to be compared against value
  212. * @param mixed $value The value to be bound to $field for comparison
  213. * @param string $type the type name for $value as configured using the Type map.
  214. * @return QueryExpression
  215. */
  216. public function like($field, $value, $type = null) {
  217. return $this->add(new Comparison($field, $value, $type, 'LIKE'));
  218. }
  219. /**
  220. * Adds a new condition to the expression object in the form "field NOT LIKE value".
  221. *
  222. * @param string $field database field to be compared against value
  223. * @param mixed $value The value to be bound to $field for comparison
  224. * @param string $type the type name for $value as configured using the Type map.
  225. * @return QueryExpression
  226. */
  227. public function notLike($field, $value, $type = null) {
  228. return $this->add(new Comparison($field, $value, $type, 'NOT LIKE'));
  229. }
  230. /**
  231. * Adds a new condition to the expression object in the form
  232. * "field IN (value1, value2)".
  233. *
  234. * @param string $field database field to be compared against value
  235. * @param array $values the value to be bound to $field for comparison
  236. * @param string $type the type name for $value as configured using the Type map.
  237. * @return QueryExpression
  238. */
  239. public function in($field, $values, $type = null) {
  240. $type = $type ?: 'string';
  241. $type .= '[]';
  242. $values = $values instanceof ExpressionInterface ? $values : (array)$values;
  243. return $this->add(new Comparison($field, $values, $type, 'IN'));
  244. }
  245. /**
  246. * Adds a new condition to the expression object in the form
  247. * "field NOT IN (value1, value2)".
  248. *
  249. * @param string $field database field to be compared against value
  250. * @param array $values the value to be bound to $field for comparison
  251. * @param string $type the type name for $value as configured using the Type map.
  252. * @return QueryExpression
  253. */
  254. public function notIn($field, $values, $type = null) {
  255. $type = $type ?: 'string';
  256. $type .= '[]';
  257. $values = $values instanceof ExpressionInterface ? $values : (array)$values;
  258. return $this->add(new Comparison($field, $values, $type, 'NOT IN'));
  259. }
  260. // @codingStandardsIgnoreStart
  261. /**
  262. * Returns a new QueryExpresion object containing all the conditions passed
  263. * and set up the conjunction to be "AND"
  264. *
  265. * @param string|array|QueryExpression $conditions to be joined with AND
  266. * @param array $types associative array of fields pointing to the type of the
  267. * values that are being passed. Used for correctly binding values to statements.
  268. * @return \Cake\Database\Expression\QueryExpression
  269. */
  270. public function and_($conditions, $types = []) {
  271. if (is_callable($conditions)) {
  272. return $conditions(new self([], $this->typeMap()->types($types)));
  273. }
  274. return new self($conditions, $this->typeMap()->types($types));
  275. }
  276. /**
  277. * Returns a new QueryExpresion object containing all the conditions passed
  278. * and set up the conjunction to be "OR"
  279. *
  280. * @param string|array|QueryExpression $conditions to be joined with OR
  281. * @param array $types associative array of fields pointing to the type of the
  282. * values that are being passed. Used for correctly binding values to statements.
  283. * @return \Cake\Database\Expression\QueryExpression
  284. */
  285. public function or_($conditions, $types = []) {
  286. if (is_callable($conditions)) {
  287. return $conditions(new self([], $this->typeMap()->types($types), 'OR'));
  288. }
  289. return new self($conditions, $this->typeMap()->types($types), 'OR');
  290. }
  291. // @codingStandardsIgnoreEnd
  292. /**
  293. * Adds a new set of conditions to this level of the tree and negates
  294. * the final result by prepending a NOT, it will look like
  295. * "NOT ( (condition1) AND (conditions2) )" conjunction depends on the one
  296. * currently configured for this object.
  297. *
  298. * @param string|array|QueryExpression $conditions to be added and negated
  299. * @param array $types associative array of fields pointing to the type of the
  300. * values that are being passed. Used for correctly binding values to statements.
  301. * @return QueryExpression
  302. */
  303. public function not($conditions, $types = []) {
  304. return $this->add(['NOT' => $conditions], $types);
  305. }
  306. /**
  307. * Returns the number of internal conditions that are stored in this expression.
  308. * Useful to determine if this expression object is void or it will generate
  309. * a non-empty string when compiled
  310. *
  311. * @return int
  312. */
  313. public function count() {
  314. return count($this->_conditions);
  315. }
  316. /**
  317. * Returns the string representation of this object so that it can be used in a
  318. * SQL query. Note that values condition values are not included in the string,
  319. * in their place placeholders are put and can be replaced by the quoted values
  320. * accordingly.
  321. *
  322. * @param \Cake\Database\ValueBinder $generator Placeholder generator object
  323. * @return string
  324. */
  325. public function sql(ValueBinder $generator) {
  326. $conjunction = $this->_conjunction;
  327. $template = ($this->count() === 1) ? '%s' : '(%s)';
  328. $parts = [];
  329. foreach ($this->_conditions as $part) {
  330. if ($part instanceof ExpressionInterface) {
  331. $part = $part->sql($generator);
  332. }
  333. $parts[] = $part;
  334. }
  335. return sprintf($template, implode(" $conjunction ", $parts));
  336. }
  337. /**
  338. * Traverses the tree structure of this query expression by executing a callback
  339. * function for each of the conditions that are included in this object.
  340. * Useful for compiling the final expression, or doing
  341. * introspection in the structure.
  342. *
  343. * Callback function receives as only argument an instance of a QueryExpression
  344. *
  345. * @param callable $callable
  346. * @return void
  347. */
  348. public function traverse(callable $callable) {
  349. foreach ($this->_conditions as $c) {
  350. if ($c instanceof ExpressionInterface) {
  351. $callable($c);
  352. $c->traverse($callable);
  353. }
  354. }
  355. }
  356. /**
  357. * Executes a callable function for each of the parts that form this expression
  358. * Callable function is required to return a value, which will the one with
  359. * which the currently visited part will be replaced. If the callable function
  360. * returns null then the part will be discarded completely from this expression
  361. *
  362. * The callback function will receive each of the conditions as first param and
  363. * the key as second param. It is possible to declare the second parameter as
  364. * passed by reference, this will enable you to change the key under which the
  365. * modified part is stored.
  366. *
  367. * @param callable $callable
  368. * @return QueryExpression
  369. */
  370. public function iterateParts(callable $callable) {
  371. $parts = [];
  372. foreach ($this->_conditions as $k => $c) {
  373. $key =& $k;
  374. $part = $callable($c, $key);
  375. if ($part !== null) {
  376. $parts[$key] = $part;
  377. }
  378. }
  379. $this->_conditions = $parts;
  380. return $this;
  381. }
  382. /**
  383. * Auxiliary function used for decomposing a nested array of conditions and build
  384. * a tree structure inside this object to represent the full SQL expression.
  385. * String conditions are stored directly in the conditions, while any other
  386. * representation is wrapped around an adequate instance or of this class.
  387. *
  388. * @param array $conditions list of conditions to be stored in this object
  389. * @param array $types list of types associated on fields referenced in $conditions
  390. * @return void
  391. */
  392. protected function _addConditions(array $conditions, array $types) {
  393. $operators = ['and', 'or', 'xor'];
  394. $typeMap = $this->typeMap()->types($types);
  395. foreach ($conditions as $k => $c) {
  396. $numericKey = is_numeric($k);
  397. if ($numericKey && empty($c)) {
  398. continue;
  399. }
  400. if ($numericKey && is_string($c)) {
  401. $this->_conditions[] = $c;
  402. continue;
  403. }
  404. if ($numericKey && is_array($c) || in_array(strtolower($k), $operators)) {
  405. $this->_conditions[] = new self($c, $typeMap, $numericKey ? 'AND' : $k);
  406. continue;
  407. }
  408. if (strtolower($k) === 'not') {
  409. $this->_conditions[] = new UnaryExpression('NOT', new self($c, $typeMap));
  410. continue;
  411. }
  412. if ($c instanceof self && count($c) > 0) {
  413. $this->_conditions[] = $c;
  414. continue;
  415. }
  416. if (!$numericKey) {
  417. $this->_conditions[] = $this->_parseCondition($k, $c);
  418. }
  419. }
  420. }
  421. /**
  422. * Parses a string conditions by trying to extract the operator inside it if any
  423. * and finally returning either an adequate QueryExpression object or a plain
  424. * string representation of the condition. This function is responsible for
  425. * generating the placeholders and replacing the values by them, while storing
  426. * the value elsewhere for future binding.
  427. *
  428. * @param string $field The value from with the actual field and operator will
  429. * be extracted.
  430. * @param mixed $value The value to be bound to a placeholder for the field
  431. * @return string|QueryExpression
  432. */
  433. protected function _parseCondition($field, $value) {
  434. $operator = '=';
  435. $expression = $field;
  436. $parts = explode(' ', trim($field), 2);
  437. if (count($parts) > 1) {
  438. list($expression, $operator) = $parts;
  439. }
  440. $type = $this->typeMap()->type($expression);
  441. $multi = false;
  442. $typeMultiple = strpos($type, '[]') !== false;
  443. if (in_array(strtolower(trim($operator)), ['in', 'not in']) || $typeMultiple) {
  444. $type = $type ?: 'string';
  445. $type .= $typeMultiple ? null : '[]';
  446. $operator = $operator === '=' ? 'IN' : $operator;
  447. $operator = $operator === '!=' ? 'NOT IN' : $operator;
  448. $typeMultiple = true;
  449. }
  450. if ($typeMultiple) {
  451. $value = $value instanceof ExpressionInterface ? $value : (array)$value;
  452. }
  453. return new Comparison($expression, $value, $type, $operator);
  454. }
  455. /**
  456. * Returns an array of placeholders that will have a bound value corresponding
  457. * to each value in the first argument.
  458. *
  459. * @param string $field database field to be used to bind values
  460. * @param array $values
  461. * @param string $type the type to be used to bind the values
  462. * @return array
  463. */
  464. protected function _bindMultiplePlaceholders($field, $values, $type) {
  465. $type = str_replace('[]', '', $type);
  466. $params = [];
  467. foreach ($values as $value) {
  468. $params[] = $this->_bindValue($field, $value, $type);
  469. }
  470. return implode(', ', $params);
  471. }
  472. }