TYPO3 CMS  TYPO3_8-7
ExpressionBuilder.php
Go to the documentation of this file.
1 <?php
2 declare(strict_types = 1);
4 
5 /*
6  * This file is part of the TYPO3 CMS project.
7  *
8  * It is free software; you can redistribute it and/or modify it under
9  * the terms of the GNU General Public License, either version 2
10  * of the License, or any later version.
11  *
12  * For the full copyright and license information, please read the
13  * LICENSE.txt file that was distributed with this source code.
14  *
15  * The TYPO3 project - inspiring people to share!
16  */
17 
20 
33 {
34  const EQ = '=';
35  const NEQ = '<>';
36  const LT = '<';
37  const LTE = '<=';
38  const GT = '>';
39  const GTE = '>=';
40 
41  const QUOTE_NOTHING = 0;
42  const QUOTE_IDENTIFIER = 1;
43  const QUOTE_PARAMETER = 2;
44 
50  protected $connection;
51 
58  {
59  $this->connection = $connection;
60  }
61 
69  public function andX(...$expressions): CompositeExpression
70  {
71  return new CompositeExpression(CompositeExpression::TYPE_AND, $expressions);
72  }
73 
81  public function orX(...$expressions): CompositeExpression
82  {
83  return new CompositeExpression(CompositeExpression::TYPE_OR, $expressions);
84  }
85 
95  public function comparison($leftExpression, string $operator, $rightExpression): string
96  {
97  return $leftExpression . ' ' . $operator . ' ' . $rightExpression;
98  }
99 
108  public function eq(string $fieldName, $value): string
109  {
110  return $this->comparison($this->connection->quoteIdentifier($fieldName), static::EQ, $value);
111  }
112 
127  public function neq(string $fieldName, $value): string
128  {
129  return $this->comparison($this->connection->quoteIdentifier($fieldName), static::NEQ, $value);
130  }
131 
140  public function lt($fieldName, $value): string
141  {
142  return $this->comparison($this->connection->quoteIdentifier($fieldName), static::LT, $value);
143  }
144 
153  public function lte(string $fieldName, $value): string
154  {
155  return $this->comparison($this->connection->quoteIdentifier($fieldName), static::LTE, $value);
156  }
157 
166  public function gt(string $fieldName, $value): string
167  {
168  return $this->comparison($this->connection->quoteIdentifier($fieldName), static::GT, $value);
169  }
170 
179  public function gte(string $fieldName, $value): string
180  {
181  return $this->comparison($this->connection->quoteIdentifier($fieldName), static::GTE, $value);
182  }
183 
191  public function isNull(string $fieldName): string
192  {
193  return $this->connection->quoteIdentifier($fieldName) . ' IS NULL';
194  }
195 
203  public function isNotNull(string $fieldName): string
204  {
205  return $this->connection->quoteIdentifier($fieldName) . ' IS NOT NULL';
206  }
207 
216  public function like(string $fieldName, $value): string
217  {
218  return $this->comparison($this->connection->quoteIdentifier($fieldName), 'LIKE', $value);
219  }
220 
229  public function notLike(string $fieldName, $value): string
230  {
231  return $this->comparison($this->connection->quoteIdentifier($fieldName), 'NOT LIKE', $value);
232  }
233 
243  public function in(string $fieldName, $value): string
244  {
245  return $this->comparison(
246  $this->connection->quoteIdentifier($fieldName),
247  'IN',
248  '(' . implode(', ', (array)$value) . ')'
249  );
250  }
251 
261  public function notIn(string $fieldName, $value): string
262  {
263  return $this->comparison(
264  $this->connection->quoteIdentifier($fieldName),
265  'NOT IN',
266  '(' . implode(', ', (array)$value) . ')'
267  );
268  }
269 
280  public function inSet(string $fieldName, string $value, bool $isColumn = false): string
281  {
282  if ($value === '') {
283  throw new \InvalidArgumentException(
284  'ExpressionBuilder::inSet() can not be used with an empty string value.',
285  1459696089
286  );
287  }
288 
289  if (strpos($value, ',') !== false) {
290  throw new \InvalidArgumentException(
291  'ExpressionBuilder::inSet() can not be used with values that contain a comma (",").',
292  1459696090
293  );
294  }
295 
296  switch ($this->connection->getDatabasePlatform()->getName()) {
297  case 'postgresql':
298  case 'pdo_postgresql':
299  return $this->comparison(
300  $isColumn ? $value . '::text' : $this->literal($this->unquoteLiteral((string)$value)),
301  self::EQ,
302  sprintf(
303  'ANY(string_to_array(%s, %s))',
304  $this->connection->quoteIdentifier($fieldName) . '::text',
305  $this->literal(',')
306  )
307  );
308  break;
309  case 'oci8':
310  case 'pdo_oracle':
311  throw new \RuntimeException(
312  'FIND_IN_SET support for database platform "Oracle" not yet implemented.',
313  1459696680
314  );
315  break;
316  case 'sqlsrv':
317  case 'pdo_sqlsrv':
318  case 'mssql':
319  // See unit and functional tests for details
320  if ($isColumn) {
321  $expression = $this->orX(
322  $this->eq($fieldName, $value),
323  $this->like($fieldName, $value . ' + \',%\''),
324  $this->like($fieldName, '\'%,\' + ' . $value),
325  $this->like($fieldName, '\'%,\' + ' . $value . ' + \',%\'')
326  );
327  } else {
328  $likeEscapedValue = str_replace(
329  ['[', '%'],
330  ['[[]', '[%]'],
331  $this->unquoteLiteral($value)
332  );
333  $expression = $this->orX(
334  $this->eq($fieldName, $this->literal($this->unquoteLiteral((string)$value))),
335  $this->like($fieldName, $this->literal($likeEscapedValue . ',%')),
336  $this->like($fieldName, $this->literal('%,' . $likeEscapedValue)),
337  $this->like($fieldName, $this->literal('%,' . $likeEscapedValue . ',%'))
338  );
339  }
340  return (string)$expression;
341  case 'sqlite':
342  case 'sqlite3':
343  case 'pdo_sqlite':
344  if (strpos($value, ':') === 0 || $value === '?') {
345  throw new \InvalidArgumentException(
346  'ExpressionBuilder::inSet() for SQLite can not be used with placeholder arguments.',
347  1476029421
348  );
349  }
350 
351  return $this->comparison(
352  implode('||', [
353  $this->literal(','),
354  $this->connection->quoteIdentifier($fieldName),
355  $this->literal(','),
356  ]),
357  'LIKE',
358  $this->literal(
359  '%,' . $this->unquoteLiteral($value) . ',%'
360  )
361  );
362  break;
363  default:
364  return sprintf(
365  'FIND_IN_SET(%s, %s)',
366  $value,
367  $this->connection->quoteIdentifier($fieldName)
368  );
369  }
370  }
371 
379  public function bitAnd(string $fieldName, int $value): string
380  {
381  switch ($this->connection->getDatabasePlatform()->getName()) {
382  case 'oci8':
383  case 'pdo_oracle':
384  return sprintf(
385  'BITAND(%s, %s)',
386  $this->connection->quoteIdentifier($fieldName),
387  $value
388  );
389  default:
390  return $this->comparison(
391  $this->connection->quoteIdentifier($fieldName),
392  '&',
393  $value
394  );
395  }
396  }
397 
405  public function min(string $fieldName, string $alias = null): string
406  {
407  return $this->calculation('MIN', $fieldName, $alias);
408  }
409 
417  public function max(string $fieldName, string $alias = null): string
418  {
419  return $this->calculation('MAX', $fieldName, $alias);
420  }
421 
429  public function avg(string $fieldName, string $alias = null): string
430  {
431  return $this->calculation('AVG', $fieldName, $alias);
432  }
433 
441  public function sum(string $fieldName, string $alias = null): string
442  {
443  return $this->calculation('SUM', $fieldName, $alias);
444  }
445 
453  public function count(string $fieldName, string $alias = null): string
454  {
455  return $this->calculation('COUNT', $fieldName, $alias);
456  }
457 
465  public function length(string $fieldName, string $alias = null): string
466  {
467  return $this->calculation('LENGTH', $fieldName, $alias);
468  }
469 
478  protected function calculation(string $aggregateName, string $fieldName, string $alias = null): string
479  {
480  $aggregateSQL = sprintf(
481  '%s(%s)',
482  $aggregateName,
483  $this->connection->quoteIdentifier($fieldName)
484  );
485 
486  if (!empty($alias)) {
487  $aggregateSQL .= ' AS ' . $this->connection->quoteIdentifier($alias);
488  }
489 
490  return $aggregateSQL;
491  }
492 
501  public function trim(string $fieldName, int $position = AbstractPlatform::TRIM_UNSPECIFIED, string $char = null)
502  {
503  return $this->connection->getDatabasePlatform()->getTrimExpression(
504  $this->connection->quoteIdentifier($fieldName),
505  $position,
506  ($char === null ? false : $this->literal($char))
507  );
508  }
509 
518  public function literal($input, string $type = null)
519  {
520  return $this->connection->quote($input, $type);
521  }
522 
529  protected function unquoteLiteral(string $value): string
530  {
531  $quoteChar = $this->connection
532  ->getDatabasePlatform()
533  ->getStringLiteralQuoteCharacter();
534 
535  $isQuoted = strpos($value, $quoteChar) === 0 && strpos(strrev($value), $quoteChar) === 0;
536 
537  if ($isQuoted) {
538  return str_replace($quoteChar . $quoteChar, $quoteChar, substr($value, 1, -1));
539  }
540 
541  return $value;
542  }
543 }
calculation(string $aggregateName, string $fieldName, string $alias=null)
comparison($leftExpression, string $operator, $rightExpression)
inSet(string $fieldName, string $value, bool $isColumn=false)
trim(string $fieldName, int $position=AbstractPlatform::TRIM_UNSPECIFIED, string $char=null)