1<?php 2declare(strict_types = 1); 3namespace TYPO3\CMS\Core\Database\Query\Expression; 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 18use Doctrine\DBAL\Platforms\AbstractPlatform; 19use TYPO3\CMS\Core\Database\Connection; 20 21/** 22 * ExpressionBuilder class is responsible to dynamically create SQL query parts. 23 * 24 * It takes care building query conditions while ensuring table and column names 25 * are quoted within the created expressions / SQL fragments. It is a facade to 26 * the actual Doctrine ExpressionBuilder. 27 * 28 * The ExpressionBuilder is used within the context of the QueryBuilder to ensure 29 * queries are being build based on the requirements of the database platform in 30 * use. 31 */ 32class ExpressionBuilder 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 45 /** 46 * The DBAL Connection. 47 * 48 * @var Connection 49 */ 50 protected $connection; 51 52 /** 53 * Initializes a new ExpressionBuilder 54 * 55 * @param Connection $connection 56 */ 57 public function __construct(Connection $connection) 58 { 59 $this->connection = $connection; 60 } 61 62 /** 63 * Creates a conjunction of the given boolean expressions 64 * 65 * @param mixed,... $expressions Optional clause. Requires at least one defined when converting to string. 66 * 67 * @return CompositeExpression 68 */ 69 public function andX(...$expressions): CompositeExpression 70 { 71 return new CompositeExpression(CompositeExpression::TYPE_AND, $expressions); 72 } 73 74 /** 75 * Creates a disjunction of the given boolean expressions. 76 * 77 * @param mixed,... $expressions Optional clause. Requires at least one defined when converting to string. 78 * 79 * @return CompositeExpression 80 */ 81 public function orX(...$expressions): CompositeExpression 82 { 83 return new CompositeExpression(CompositeExpression::TYPE_OR, $expressions); 84 } 85 86 /** 87 * Creates a comparison expression. 88 * 89 * @param mixed $leftExpression The left expression. 90 * @param string $operator One of the ExpressionBuilder::* constants. 91 * @param mixed $rightExpression The right expression. 92 * 93 * @return string 94 */ 95 public function comparison($leftExpression, string $operator, $rightExpression): string 96 { 97 return $leftExpression . ' ' . $operator . ' ' . $rightExpression; 98 } 99 100 /** 101 * Creates an equality comparison expression with the given arguments. 102 * 103 * @param string $fieldName The fieldname. Will be quoted according to database platform automatically. 104 * @param mixed $value The value. No automatic quoting/escaping is done. 105 * 106 * @return string 107 */ 108 public function eq(string $fieldName, $value): string 109 { 110 return $this->comparison($this->connection->quoteIdentifier($fieldName), static::EQ, $value); 111 } 112 113 /** 114 * Creates a non equality comparison expression with the given arguments. 115 * First argument is considered the left expression and the second is the right expression. 116 * When converted to string, it will generated a <left expr> <> <right expr>. Example: 117 * 118 * [php] 119 * // u.id <> 1 120 * $q->where($q->expr()->neq('u.id', '1')); 121 * 122 * @param string $fieldName The fieldname. Will be quoted according to database platform automatically. 123 * @param mixed $value The value. No automatic quoting/escaping is done. 124 * 125 * @return string 126 */ 127 public function neq(string $fieldName, $value): string 128 { 129 return $this->comparison($this->connection->quoteIdentifier($fieldName), static::NEQ, $value); 130 } 131 132 /** 133 * Creates a lower-than comparison expression with the given arguments. 134 * 135 * @param string $fieldName The fieldname. Will be quoted according to database platform automatically. 136 * @param mixed $value The value. No automatic quoting/escaping is done. 137 * 138 * @return string 139 */ 140 public function lt($fieldName, $value): string 141 { 142 return $this->comparison($this->connection->quoteIdentifier($fieldName), static::LT, $value); 143 } 144 145 /** 146 * Creates a lower-than-equal comparison expression with the given arguments. 147 * 148 * @param string $fieldName The fieldname. Will be quoted according to database platform automatically. 149 * @param mixed $value The value. No automatic quoting/escaping is done. 150 * 151 * @return string 152 */ 153 public function lte(string $fieldName, $value): string 154 { 155 return $this->comparison($this->connection->quoteIdentifier($fieldName), static::LTE, $value); 156 } 157 158 /** 159 * Creates a greater-than comparison expression with the given arguments. 160 * 161 * @param string $fieldName The fieldname. Will be quoted according to database platform automatically. 162 * @param mixed $value The value. No automatic quoting/escaping is done. 163 * 164 * @return string 165 */ 166 public function gt(string $fieldName, $value): string 167 { 168 return $this->comparison($this->connection->quoteIdentifier($fieldName), static::GT, $value); 169 } 170 171 /** 172 * Creates a greater-than-equal comparison expression with the given arguments. 173 * 174 * @param string $fieldName The fieldname. Will be quoted according to database platform automatically. 175 * @param mixed $value The value. No automatic quoting/escaping is done. 176 * 177 * @return string 178 */ 179 public function gte(string $fieldName, $value): string 180 { 181 return $this->comparison($this->connection->quoteIdentifier($fieldName), static::GTE, $value); 182 } 183 184 /** 185 * Creates an IS NULL expression with the given arguments. 186 * 187 * @param string $fieldName The fieldname. Will be quoted according to database platform automatically. 188 * 189 * @return string 190 */ 191 public function isNull(string $fieldName): string 192 { 193 return $this->connection->quoteIdentifier($fieldName) . ' IS NULL'; 194 } 195 196 /** 197 * Creates an IS NOT NULL expression with the given arguments. 198 * 199 * @param string $fieldName The fieldname. Will be quoted according to database platform automatically. 200 * 201 * @return string 202 */ 203 public function isNotNull(string $fieldName): string 204 { 205 return $this->connection->quoteIdentifier($fieldName) . ' IS NOT NULL'; 206 } 207 208 /** 209 * Creates a LIKE() comparison expression with the given arguments. 210 * 211 * @param string $fieldName The fieldname. Will be quoted according to database platform automatically. 212 * @param mixed $value Argument to be used in LIKE() comparison. No automatic quoting/escaping is done. 213 * 214 * @return string 215 */ 216 public function like(string $fieldName, $value): string 217 { 218 return $this->comparison($this->connection->quoteIdentifier($fieldName), 'LIKE', $value); 219 } 220 221 /** 222 * Creates a NOT LIKE() comparison expression with the given arguments. 223 * 224 * @param string $fieldName The fieldname. Will be quoted according to database platform automatically. 225 * @param mixed $value Argument to be used in NOT LIKE() comparison. No automatic quoting/escaping is done. 226 * 227 * @return string 228 */ 229 public function notLike(string $fieldName, $value): string 230 { 231 return $this->comparison($this->connection->quoteIdentifier($fieldName), 'NOT LIKE', $value); 232 } 233 234 /** 235 * Creates a IN () comparison expression with the given arguments. 236 * 237 * @param string $fieldName The fieldname. Will be quoted according to database platform automatically. 238 * @param string|array $value The placeholder or the array of values to be used by IN() comparison. 239 * No automatic quoting/escaping is done. 240 * 241 * @return string 242 */ 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 252 /** 253 * Creates a NOT IN () comparison expression with the given arguments. 254 * 255 * @param string $fieldName The fieldname. Will be quoted according to database platform automatically. 256 * @param string|array $value The placeholder or the array of values to be used by NOT IN() comparison. 257 * No automatic quoting/escaping is done. 258 * 259 * @return string 260 */ 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 270 /** 271 * Returns a comparison that can find a value in a list field (CSV). 272 * 273 * @param string $fieldName The field name. Will be quoted according to database platform automatically. 274 * @param string $value Argument to be used in FIND_IN_SET() comparison. No automatic quoting/escaping is done. 275 * @param bool $isColumn Set when the value to compare is a column on a table to activate casting 276 * @return string 277 * @throws \InvalidArgumentException 278 * @throws \RuntimeException 279 */ 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 case 'oci8': 309 case 'pdo_oracle': 310 throw new \RuntimeException( 311 'FIND_IN_SET support for database platform "Oracle" not yet implemented.', 312 1459696680 313 ); 314 case 'sqlsrv': 315 case 'pdo_sqlsrv': 316 case 'mssql': 317 // See unit and functional tests for details 318 if ($isColumn) { 319 $expression = $this->orX( 320 $this->eq($fieldName, $value), 321 $this->like($fieldName, $value . ' + \',%\''), 322 $this->like($fieldName, '\'%,\' + ' . $value), 323 $this->like($fieldName, '\'%,\' + ' . $value . ' + \',%\'') 324 ); 325 } else { 326 $likeEscapedValue = str_replace( 327 ['[', '%'], 328 ['[[]', '[%]'], 329 $this->unquoteLiteral($value) 330 ); 331 $expression = $this->orX( 332 $this->eq($fieldName, $this->literal($this->unquoteLiteral((string)$value))), 333 $this->like($fieldName, $this->literal($likeEscapedValue . ',%')), 334 $this->like($fieldName, $this->literal('%,' . $likeEscapedValue)), 335 $this->like($fieldName, $this->literal('%,' . $likeEscapedValue . ',%')) 336 ); 337 } 338 return (string)$expression; 339 case 'sqlite': 340 case 'sqlite3': 341 case 'pdo_sqlite': 342 if (strpos($value, ':') === 0 || $value === '?') { 343 throw new \InvalidArgumentException( 344 'ExpressionBuilder::inSet() for SQLite can not be used with placeholder arguments.', 345 1476029421 346 ); 347 } 348 $comparison = sprintf( 349 'instr(%s, %s)', 350 implode( 351 '||', 352 [ 353 $this->literal(','), 354 $this->connection->quoteIdentifier($fieldName), 355 $this->literal(','), 356 ] 357 ), 358 $isColumn ? 359 implode( 360 '||', 361 [ 362 $this->literal(','), 363 // do not explicitly quote value as it is expected to be 364 // quoted by the caller 365 'cast(' . $value . ' as text)', 366 $this->literal(','), 367 ] 368 ) 369 : $this->literal( 370 ',' . $this->unquoteLiteral($value) . ',' 371 ) 372 ); 373 return $comparison; 374 default: 375 return sprintf( 376 'FIND_IN_SET(%s, %s)', 377 $value, 378 $this->connection->quoteIdentifier($fieldName) 379 ); 380 } 381 } 382 383 /** 384 * Creates a bitwise AND expression with the given arguments. 385 * 386 * @param string $fieldName The fieldname. Will be quoted according to database platform automatically. 387 * @param int $value Argument to be used in the bitwise AND operation 388 * @return string 389 */ 390 public function bitAnd(string $fieldName, int $value): string 391 { 392 switch ($this->connection->getDatabasePlatform()->getName()) { 393 case 'oci8': 394 case 'pdo_oracle': 395 return sprintf( 396 'BITAND(%s, %s)', 397 $this->connection->quoteIdentifier($fieldName), 398 $value 399 ); 400 default: 401 return $this->comparison( 402 $this->connection->quoteIdentifier($fieldName), 403 '&', 404 $value 405 ); 406 } 407 } 408 409 /** 410 * Creates a MIN expression for the given field/alias. 411 * 412 * @param string $fieldName 413 * @param string|null $alias 414 * @return string 415 */ 416 public function min(string $fieldName, string $alias = null): string 417 { 418 return $this->calculation('MIN', $fieldName, $alias); 419 } 420 421 /** 422 * Creates a MAX expression for the given field/alias. 423 * 424 * @param string $fieldName 425 * @param string|null $alias 426 * @return string 427 */ 428 public function max(string $fieldName, string $alias = null): string 429 { 430 return $this->calculation('MAX', $fieldName, $alias); 431 } 432 433 /** 434 * Creates a AVG expression for the given field/alias. 435 * 436 * @param string $fieldName 437 * @param string|null $alias 438 * @return string 439 */ 440 public function avg(string $fieldName, string $alias = null): string 441 { 442 return $this->calculation('AVG', $fieldName, $alias); 443 } 444 445 /** 446 * Creates a SUM expression for the given field/alias. 447 * 448 * @param string $fieldName 449 * @param string|null $alias 450 * @return string 451 */ 452 public function sum(string $fieldName, string $alias = null): string 453 { 454 return $this->calculation('SUM', $fieldName, $alias); 455 } 456 457 /** 458 * Creates a COUNT expression for the given field/alias. 459 * 460 * @param string $fieldName 461 * @param string|null $alias 462 * @return string 463 */ 464 public function count(string $fieldName, string $alias = null): string 465 { 466 return $this->calculation('COUNT', $fieldName, $alias); 467 } 468 469 /** 470 * Creates a LENGTH expression for the given field/alias. 471 * 472 * @param string $fieldName 473 * @param string|null $alias 474 * @return string 475 */ 476 public function length(string $fieldName, string $alias = null): string 477 { 478 return $this->calculation('LENGTH', $fieldName, $alias); 479 } 480 481 /** 482 * Create a SQL aggregate function. 483 * 484 * @param string $aggregateName 485 * @param string $fieldName 486 * @param string|null $alias 487 * @return string 488 */ 489 protected function calculation(string $aggregateName, string $fieldName, string $alias = null): string 490 { 491 $aggregateSQL = sprintf( 492 '%s(%s)', 493 $aggregateName, 494 $this->connection->quoteIdentifier($fieldName) 495 ); 496 497 if (!empty($alias)) { 498 $aggregateSQL .= ' AS ' . $this->connection->quoteIdentifier($alias); 499 } 500 501 return $aggregateSQL; 502 } 503 504 /** 505 * Creates a TRIM expression for the given field. 506 * 507 * @param string $fieldName Field name to build expression for 508 * @param int $position Either constant out of LEADING, TRAILING, BOTH 509 * @param string $char Character to be trimmed (defaults to space) 510 * @return string 511 */ 512 public function trim(string $fieldName, int $position = AbstractPlatform::TRIM_UNSPECIFIED, string $char = null) 513 { 514 return $this->connection->getDatabasePlatform()->getTrimExpression( 515 $this->connection->quoteIdentifier($fieldName), 516 $position, 517 ($char === null ? false : $this->literal($char)) 518 ); 519 } 520 521 /** 522 * Quotes a given input parameter. 523 * 524 * @param mixed $input The parameter to be quoted. 525 * @param string|null $type The type of the parameter. 526 * 527 * @return mixed Often string, but also int or float or similar depending on $input and platform 528 */ 529 public function literal($input, string $type = null) 530 { 531 return $this->connection->quote($input, $type); 532 } 533 534 /** 535 * Unquote a string literal. Used to unquote values for internal platform adjustments. 536 * 537 * @param string $value The value to be unquoted 538 * @return string The unquoted value 539 */ 540 protected function unquoteLiteral(string $value): string 541 { 542 $quoteChar = $this->connection 543 ->getDatabasePlatform() 544 ->getStringLiteralQuoteCharacter(); 545 546 $isQuoted = strpos($value, $quoteChar) === 0 && strpos(strrev($value), $quoteChar) === 0; 547 548 if ($isQuoted) { 549 return str_replace($quoteChar . $quoteChar, $quoteChar, substr($value, 1, -1)); 550 } 551 552 return $value; 553 } 554} 555