‪TYPO3CMS  ‪main
ExpressionBuilder.php
Go to the documentation of this file.
1 <?php
2 
3 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 
19 
20 use Doctrine\DBAL\Platforms\PostgreSQLPlatform;
21 use Doctrine\DBAL\Platforms\TrimMode;
23 
36 {
37  public const ‪EQ = '=';
38  public const ‪NEQ = '<>';
39  public const ‪LT = '<';
40  public const ‪LTE = '<=';
41  public const ‪GT = '>';
42  public const ‪GTE = '>=';
43 
44  public const ‪QUOTE_NOTHING = 0;
45  public const ‪QUOTE_IDENTIFIER = 1;
46  public const ‪QUOTE_PARAMETER = 2;
47 
53  protected ‪$connection;
54 
59  {
60  $this->connection = ‪$connection;
61  }
62 
66  public function ‪and(‪CompositeExpression|string|null ...$expressions): ‪CompositeExpression
67  {
68  return ‪CompositeExpression::and(...$expressions);
69  }
70 
74  public function ‪or(CompositeExpression|string|null ...$expressions): CompositeExpression
75  {
76  return ‪CompositeExpression::or(...$expressions);
77  }
78 
86  public function ‪comparison($leftExpression, string $operator, $rightExpression): string
87  {
88  return $leftExpression . ' ' . $operator . ' ' . $rightExpression;
89  }
90 
97  public function ‪eq(string $fieldName, $value): string
98  {
99  return $this->‪comparison($this->connection->quoteIdentifier($fieldName), static::EQ, $value);
100  }
101 
114  public function ‪neq(string $fieldName, $value): string
115  {
116  return $this->‪comparison($this->connection->quoteIdentifier($fieldName), static::NEQ, $value);
117  }
118 
125  public function ‪lt($fieldName, $value): string
126  {
127  return $this->‪comparison($this->connection->quoteIdentifier($fieldName), static::LT, $value);
128  }
129 
136  public function ‪lte(string $fieldName, $value): string
137  {
138  return $this->‪comparison($this->connection->quoteIdentifier($fieldName), static::LTE, $value);
139  }
140 
147  public function ‪gt(string $fieldName, $value): string
148  {
149  return $this->‪comparison($this->connection->quoteIdentifier($fieldName), static::GT, $value);
150  }
151 
158  public function ‪gte(string $fieldName, $value): string
159  {
160  return $this->‪comparison($this->connection->quoteIdentifier($fieldName), static::GTE, $value);
161  }
162 
168  public function ‪isNull(string $fieldName): string
169  {
170  return $this->connection->quoteIdentifier($fieldName) . ' IS NULL';
171  }
172 
178  public function ‪isNotNull(string $fieldName): string
179  {
180  return $this->connection->quoteIdentifier($fieldName) . ' IS NOT NULL';
181  }
182 
189  public function ‪like(string $fieldName, $value): string
190  {
191  $platform = $this->connection->getDatabasePlatform();
192  if ($platform instanceof PostgreSQLPlatform) {
193  // Use ILIKE to mimic case-insensitive search like most people are trained from MySQL/MariaDB.
194  return $this->‪comparison($this->connection->quoteIdentifier($fieldName), 'ILIKE', $value);
195  }
196  // Note: SQLite does not properly work with non-ascii letters as search word for case-insensitive
197  // matching, UPPER() and LOWER() have the same issue, it only works with ascii letters.
198  // See: https://www.sqlite.org/src/doc/trunk/ext/icu/README.txt
199  return $this->‪comparison($this->connection->quoteIdentifier($fieldName), 'LIKE', $value)
200  . sprintf(' ESCAPE %s', $this->connection->quote('\\'));
201  }
202 
209  public function ‪notLike(string $fieldName, $value): string
210  {
211  $platform = $this->connection->getDatabasePlatform();
212  if ($platform instanceof PostgreSQLPlatform) {
213  // Use ILIKE to mimic case-insensitive search like most people are trained from MySQL/MariaDB.
214  return $this->‪comparison($this->connection->quoteIdentifier($fieldName), 'NOT ILIKE', $value);
215  }
216  // Note: SQLite does not properly work with non-ascii letters as search word for case-insensitive
217  // matching, UPPER() and LOWER() have the same issue, it only works with ascii letters.
218  // See: https://www.sqlite.org/src/doc/trunk/ext/icu/README.txt
219  return $this->‪comparison($this->connection->quoteIdentifier($fieldName), 'NOT LIKE', $value)
220  . sprintf(' ESCAPE %s', $this->connection->quote('\\'));
221  }
222 
230  public function ‪in(string $fieldName, $value): string
231  {
232  return $this->‪comparison(
233  $this->connection->quoteIdentifier($fieldName),
234  'IN',
235  '(' . implode(', ', (array)$value) . ')'
236  );
237  }
238 
246  public function ‪notIn(string $fieldName, $value): string
247  {
248  return $this->‪comparison(
249  $this->connection->quoteIdentifier($fieldName),
250  'NOT IN',
251  '(' . implode(', ', (array)$value) . ')'
252  );
253  }
254 
264  public function ‪inSet(string $fieldName, string $value, bool $isColumn = false): string
265  {
266  if ($value === '') {
267  throw new \InvalidArgumentException(
268  'ExpressionBuilder::inSet() can not be used with an empty string value.',
269  1459696089
270  );
271  }
272 
273  if (str_contains($value, ',')) {
274  throw new \InvalidArgumentException(
275  'ExpressionBuilder::inSet() can not be used with values that contain a comma (",").',
276  1459696090
277  );
278  }
279 
280  switch ($this->connection->getDatabasePlatform()->getName()) {
281  case 'postgresql':
282  case 'pdo_postgresql':
283  return $this->‪comparison(
284  $isColumn ? $value . '::text' : $this->‪literal($this->‪unquoteLiteral((string)$value)),
285  self::EQ,
286  sprintf(
287  'ANY(string_to_array(%s, %s))',
288  $this->connection->quoteIdentifier($fieldName) . '::text',
289  $this->literal(',')
290  )
291  );
292  case 'oci8':
293  case 'pdo_oracle':
294  throw new \RuntimeException(
295  'FIND_IN_SET support for database platform "Oracle" not yet implemented.',
296  1459696680
297  );
298  case 'sqlite':
299  case 'sqlite3':
300  case 'pdo_sqlite':
301  if (str_starts_with($value, ':') || $value === '?') {
302  throw new \InvalidArgumentException(
303  'ExpressionBuilder::inSet() for SQLite can not be used with placeholder arguments.',
304  1476029421
305  );
306  }
307  $comparison = sprintf(
308  'instr(%s, %s)',
309  implode(
310  '||',
311  [
312  $this->‪literal(','),
313  $this->connection->quoteIdentifier($fieldName),
314  $this->literal(','),
315  ]
316  ),
317  $isColumn ?
318  implode(
319  '||',
320  [
321  $this->‪literal(','),
322  // do not explicitly quote value as it is expected to be
323  // quoted by the caller
324  'cast(' . $value . ' as text)',
325  $this->‪literal(','),
326  ]
327  )
328  : $this->‪literal(
329  ',' . $this->‪unquoteLiteral($value) . ','
330  )
331  );
332  return $comparison;
333  default:
334  return sprintf(
335  'FIND_IN_SET(%s, %s)',
336  $value,
337  $this->connection->quoteIdentifier($fieldName)
338  );
339  }
340  }
341 
351  public function ‪notInSet(string $fieldName, string $value, bool $isColumn = false): string
352  {
353  if ($value === '') {
354  throw new \InvalidArgumentException(
355  'ExpressionBuilder::notInSet() can not be used with an empty string value.',
356  1627573099
357  );
358  }
359 
360  if (str_contains($value, ',')) {
361  throw new \InvalidArgumentException(
362  'ExpressionBuilder::notInSet() can not be used with values that contain a comma (",").',
363  1627573100
364  );
365  }
366 
367  switch ($this->connection->getDatabasePlatform()->getName()) {
368  case 'postgresql':
369  case 'pdo_postgresql':
370  return $this->‪comparison(
371  $isColumn ? $value . '::text' : $this->‪literal($this->‪unquoteLiteral((string)$value)),
372  self::NEQ,
373  sprintf(
374  'ALL(string_to_array(%s, %s))',
375  $this->connection->quoteIdentifier($fieldName) . '::text',
376  $this->literal(',')
377  )
378  );
379  case 'oci8':
380  case 'pdo_oracle':
381  throw new \RuntimeException(
382  'negative FIND_IN_SET support for database platform "Oracle" not yet implemented.',
383  1627573101
384  );
385  case 'sqlite':
386  case 'sqlite3':
387  case 'pdo_sqlite':
388  if (str_starts_with($value, ':') || $value === '?') {
389  throw new \InvalidArgumentException(
390  'ExpressionBuilder::inSet() for SQLite can not be used with placeholder arguments.',
391  1627573103
392  );
393  }
394  $comparison = sprintf(
395  'instr(%s, %s) = 0',
396  implode(
397  '||',
398  [
399  $this->‪literal(','),
400  $this->connection->quoteIdentifier($fieldName),
401  $this->literal(','),
402  ]
403  ),
404  $isColumn ?
405  implode(
406  '||',
407  [
408  $this->‪literal(','),
409  // do not explicitly quote value as it is expected to be
410  // quoted by the caller
411  'cast(' . $value . ' as text)',
412  $this->‪literal(','),
413  ]
414  )
415  : $this->‪literal(
416  ',' . $this->‪unquoteLiteral($value) . ','
417  )
418  );
419  return $comparison;
420  default:
421  return sprintf(
422  'NOT FIND_IN_SET(%s, %s)',
423  $value,
424  $this->connection->quoteIdentifier($fieldName)
425  );
426  }
427  }
428 
435  public function ‪bitAnd(string $fieldName, int $value): string
436  {
437  switch ($this->connection->getDatabasePlatform()->getName()) {
438  case 'oci8':
439  case 'pdo_oracle':
440  return sprintf(
441  'BITAND(%s, %s)',
442  $this->connection->quoteIdentifier($fieldName),
443  $value
444  );
445  default:
446  return $this->‪comparison(
447  $this->connection->quoteIdentifier($fieldName),
448  '&',
449  $value
450  );
451  }
452  }
453 
459  public function ‪min(string $fieldName, string $alias = null): string
460  {
461  return $this->‪calculation('MIN', $fieldName, $alias);
462  }
463 
469  public function ‪max(string $fieldName, string $alias = null): string
470  {
471  return $this->‪calculation('MAX', $fieldName, $alias);
472  }
473 
479  public function ‪avg(string $fieldName, string $alias = null): string
480  {
481  return $this->‪calculation('AVG', $fieldName, $alias);
482  }
483 
489  public function ‪sum(string $fieldName, string $alias = null): string
490  {
491  return $this->‪calculation('SUM', $fieldName, $alias);
492  }
493 
499  public function ‪count(string $fieldName, string $alias = null): string
500  {
501  return $this->‪calculation('COUNT', $fieldName, $alias);
502  }
503 
509  public function ‪length(string $fieldName, string $alias = null): string
510  {
511  return $this->‪calculation('LENGTH', $fieldName, $alias);
512  }
513 
519  protected function ‪calculation(string $aggregateName, string $fieldName, string $alias = null): string
520  {
521  $aggregateSQL = sprintf(
522  '%s(%s)',
523  $aggregateName,
524  $this->connection->quoteIdentifier($fieldName)
525  );
526 
527  if (!empty($alias)) {
528  $aggregateSQL .= ' AS ' . $this->connection->quoteIdentifier($alias);
529  }
530 
531  return $aggregateSQL;
532  }
533 
542  public function ‪trim(string $fieldName, int $position = TrimMode::UNSPECIFIED, string $char = null)
543  {
544  return $this->connection->getDatabasePlatform()->getTrimExpression(
545  $this->connection->quoteIdentifier($fieldName),
546  $position,
547  ($char === null ? false : $this->literal($char))
548  );
549  }
550 
558  public function ‪literal($input, int $type = ‪Connection::PARAM_STR)
559  {
560  return $this->connection->quote($input, $type);
561  }
562 
569  protected function ‪unquoteLiteral(string $value): string
570  {
571  $quoteChar = $this->connection
572  ->getDatabasePlatform()
573  ->getStringLiteralQuoteCharacter();
574 
575  $isQuoted = str_starts_with($value, $quoteChar) && str_ends_with($value, $quoteChar);
576 
577  if ($isQuoted) {
578  return str_replace($quoteChar . $quoteChar, $quoteChar, substr($value, 1, -1));
579  }
580 
581  return $value;
582  }
583 }
‪TYPO3\CMS\Core\Database\Query\Expression\CompositeExpression\or
‪static or($part=null,... $parts)
Definition: CompositeExpression.php:75
‪TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder\QUOTE_NOTHING
‪const QUOTE_NOTHING
Definition: ExpressionBuilder.php:44
‪TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder\unquoteLiteral
‪string unquoteLiteral(string $value)
Definition: ExpressionBuilder.php:568
‪TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder\in
‪in(string $fieldName, $value)
Definition: ExpressionBuilder.php:229
‪TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder
Definition: ExpressionBuilder.php:36
‪TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder\EQ
‪const EQ
Definition: ExpressionBuilder.php:37
‪TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder\avg
‪avg(string $fieldName, string $alias=null)
Definition: ExpressionBuilder.php:478
‪TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder\LTE
‪const LTE
Definition: ExpressionBuilder.php:40
‪TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder\trim
‪string trim(string $fieldName, int $position=TrimMode::UNSPECIFIED, string $char=null)
Definition: ExpressionBuilder.php:541
‪TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder\comparison
‪comparison($leftExpression, string $operator, $rightExpression)
Definition: ExpressionBuilder.php:85
‪TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder\gte
‪gte(string $fieldName, $value)
Definition: ExpressionBuilder.php:157
‪TYPO3\CMS\Core\Database\Query\Expression
Definition: CompositeExpression.php:18
‪TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder\eq
‪eq(string $fieldName, $value)
Definition: ExpressionBuilder.php:96
‪TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder\or
‪or(CompositeExpression|string|null ... $expressions)
Definition: ExpressionBuilder.php:73
‪TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder\isNotNull
‪isNotNull(string $fieldName)
Definition: ExpressionBuilder.php:177
‪TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder\min
‪min(string $fieldName, string $alias=null)
Definition: ExpressionBuilder.php:458
‪TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder\gt
‪gt(string $fieldName, $value)
Definition: ExpressionBuilder.php:146
‪TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder\length
‪length(string $fieldName, string $alias=null)
Definition: ExpressionBuilder.php:508
‪TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder\calculation
‪calculation(string $aggregateName, string $fieldName, string $alias=null)
Definition: ExpressionBuilder.php:518
‪TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder\lte
‪lte(string $fieldName, $value)
Definition: ExpressionBuilder.php:135
‪TYPO3\CMS\Core\Database\Connection\PARAM_STR
‪const PARAM_STR
Definition: Connection.php:51
‪TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder\and
‪and(CompositeExpression|string|null ... $expressions)
Definition: ExpressionBuilder.php:65
‪TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder\QUOTE_IDENTIFIER
‪const QUOTE_IDENTIFIER
Definition: ExpressionBuilder.php:45
‪TYPO3\CMS\Core\Database\Query\Expression\CompositeExpression\and
‪static and($part=null,... $parts)
Definition: CompositeExpression.php:64
‪TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder\GTE
‪const GTE
Definition: ExpressionBuilder.php:42
‪TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder\lt
‪lt($fieldName, $value)
Definition: ExpressionBuilder.php:124
‪TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder\__construct
‪__construct(Connection $connection)
Definition: ExpressionBuilder.php:57
‪TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder\count
‪count(string $fieldName, string $alias=null)
Definition: ExpressionBuilder.php:498
‪TYPO3\CMS\Core\Database\Query\Expression\CompositeExpression
Definition: CompositeExpression.php:27
‪TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder\isNull
‪isNull(string $fieldName)
Definition: ExpressionBuilder.php:167
‪TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder\literal
‪mixed literal($input, int $type=Connection::PARAM_STR)
Definition: ExpressionBuilder.php:557
‪TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder\sum
‪sum(string $fieldName, string $alias=null)
Definition: ExpressionBuilder.php:488
‪TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder\GT
‪const GT
Definition: ExpressionBuilder.php:41
‪TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder\NEQ
‪const NEQ
Definition: ExpressionBuilder.php:38
‪TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder\bitAnd
‪bitAnd(string $fieldName, int $value)
Definition: ExpressionBuilder.php:434
‪TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder\notIn
‪notIn(string $fieldName, $value)
Definition: ExpressionBuilder.php:245
‪TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder\LT
‪const LT
Definition: ExpressionBuilder.php:39
‪TYPO3\CMS\Core\Database\Connection
Definition: Connection.php:35
‪TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder\$connection
‪Connection $connection
Definition: ExpressionBuilder.php:52
‪TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder\inSet
‪inSet(string $fieldName, string $value, bool $isColumn=false)
Definition: ExpressionBuilder.php:263
‪TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder\like
‪like(string $fieldName, $value)
Definition: ExpressionBuilder.php:188
‪TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder\max
‪max(string $fieldName, string $alias=null)
Definition: ExpressionBuilder.php:468
‪TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder\QUOTE_PARAMETER
‪const QUOTE_PARAMETER
Definition: ExpressionBuilder.php:46
‪TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder\notInSet
‪notInSet(string $fieldName, string $value, bool $isColumn=false)
Definition: ExpressionBuilder.php:350
‪TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder\notLike
‪notLike(string $fieldName, $value)
Definition: ExpressionBuilder.php:208
‪TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder\neq
‪neq(string $fieldName, $value)
Definition: ExpressionBuilder.php:113