‪TYPO3CMS  ‪main
QueryBuilder.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\ArrayParameterType;
21 use Doctrine\DBAL\Driver\Statement as DriverStatement;
22 use Doctrine\DBAL\ParameterType;
23 use Doctrine\DBAL\Platforms\MariaDBPlatform as DoctrineMariaDBPlatform;
24 use Doctrine\DBAL\Platforms\MySQLPlatform as DoctrineMySQLPlatform;
25 use Doctrine\DBAL\Platforms\PostgreSQLPlatform as DoctrinePostgreSQLPlatform;
26 use Doctrine\DBAL\Platforms\SQLitePlatform as DoctrineSQLitePlatform;
27 use Doctrine\DBAL\Query\Expression\CompositeExpression;
28 use Doctrine\DBAL\Query\From;
29 use Doctrine\DBAL\Query\Join;
30 use Doctrine\DBAL\Query\QueryType;
31 use Doctrine\DBAL\Result;
32 use Doctrine\DBAL\Statement;
33 use Doctrine\DBAL\Types\StringType;
34 use Doctrine\DBAL\Types\Type;
47 
64 class QueryBuilder extends ConcreteQueryBuilder
65 {
66  protected ConcreteQueryBuilder $concreteQueryBuilder;
67 
68  protected QueryRestrictionContainerInterface $restrictionContainer;
69 
70  protected array $additionalRestrictions;
71 
81  private array $restrictionsAppliedInJoinCondition = [];
82 
91  public function __construct(
92  Connection $connection,
93  QueryRestrictionContainerInterface $restrictionContainer = null,
94  ConcreteQueryBuilder $concreteQueryBuilder = null,
95  array $additionalRestrictions = null
96  ) {
97  parent::__construct($connection);
98  $this->additionalRestrictions = $additionalRestrictions ?: ‪$GLOBALS['TYPO3_CONF_VARS']['DB']['additionalQueryRestrictions'] ?? [];
99  $this->setRestrictions($restrictionContainer ?: GeneralUtility::makeInstance(DefaultRestrictionContainer::class));
100  $this->concreteQueryBuilder = $concreteQueryBuilder ?: GeneralUtility::makeInstance(ConcreteQueryBuilder::class, $connection);
101  }
102 
107  public function __clone()
108  {
109  $this->concreteQueryBuilder = clone $this->concreteQueryBuilder;
110  $this->restrictionContainer = clone $this->restrictionContainer;
111  }
112 
119  public function __toString(): string
120  {
121  return $this->getSQL();
122  }
123 
124  public function getRestrictions(): QueryRestrictionContainerInterface
125  {
126  return $this->restrictionContainer;
127  }
128 
129  public function setRestrictions(QueryRestrictionContainerInterface $restrictionContainer): void
130  {
131  foreach ($this->additionalRestrictions as $restrictionClass => $options) {
132  if (empty($options['disabled'])) {
134  $restriction = GeneralUtility::makeInstance($restrictionClass);
135  $restrictionContainer->add($restriction);
136  }
137  }
138  $this->restrictionContainer = $restrictionContainer;
139  }
140 
144  public function limitRestrictionsToTables(array $tableAliases): void
145  {
146  $this->restrictionContainer = GeneralUtility::makeInstance(LimitToTablesRestrictionContainer::class)->addForTables($this->restrictionContainer, $tableAliases);
147  }
148 
152  public function resetRestrictions(): void
153  {
154  $this->setRestrictions(GeneralUtility::makeInstance(DefaultRestrictionContainer::class));
155  }
156 
164  public function expr(): ExpressionBuilder
165  {
166  return $this->connection->getExpressionBuilder();
167  }
168 
172  public function getConnection(): Connection
173  {
174  return $this->connection;
175  }
176 
182  public function getConcreteQueryBuilder(): \Doctrine\DBAL\Query\QueryBuilder
183  {
184  return $this->concreteQueryBuilder;
185  }
186 
204  public function prepare(): Statement
205  {
206  $connection = $this->getConnection();
207  $concreteQueryBuilder = $this->concreteQueryBuilder;
208  $originalWhereConditions = null;
209  try {
210  if ($concreteQueryBuilder->type === QueryType::SELECT) {
211  $originalWhereConditions = $this->addAdditionalWhereConditions();
212  }
213  $sql = $concreteQueryBuilder->getSQL();
214  $params = $concreteQueryBuilder->getParameters();
215  $types = $concreteQueryBuilder->getParameterTypes();
216  $this->throwExceptionOnInvalidPreparedStatementParamArrayType($types);
217  $this->throwExceptionOnNamedParameterForPreparedStatement($params);
218  $statement = $connection->prepare($sql)->getWrappedStatement();
219  $this->bindTypedValues($statement, $params, $types);
220  } finally {
221  if ($concreteQueryBuilder->type === QueryType::SELECT) {
222  $concreteQueryBuilder->resetWhere();
223  if ($originalWhereConditions !== null) {
224  $concreteQueryBuilder->where($originalWhereConditions);
225  }
226  }
227  }
228  return new Statement($connection, $statement, $sql);
229  }
230 
242  public function executeQuery(): Result
243  {
244  // Set additional query restrictions
245  $originalWhereConditions = $this->addAdditionalWhereConditions();
246  $concreteQueryBuilder = $this->concreteQueryBuilder;
247  try {
248  return $concreteQueryBuilder->executeQuery();
249  } finally {
250  $concreteQueryBuilder->resetWhere();
251  if ($originalWhereConditions !== null) {
252  $concreteQueryBuilder->where($originalWhereConditions);
253  }
254  }
255  }
256 
271  public function executeStatement(): int
272  {
273  $concreteQueryBuilder = $this->concreteQueryBuilder;
274  return $concreteQueryBuilder->executeStatement();
275  }
276 
285  public function getSQL(): string
286  {
287  $concreteQueryBuilder = $this->concreteQueryBuilder;
288  if ($concreteQueryBuilder->type !== QueryType::SELECT) {
289  return $concreteQueryBuilder->getSQL();
290  }
291  // Set additional query restrictions
292  $originalWhereConditions = $this->addAdditionalWhereConditions();
293  try {
294  $sql = $concreteQueryBuilder->getSQL();
295  } finally {
296  $concreteQueryBuilder->resetWhere();
297  if ($originalWhereConditions !== null) {
298  $concreteQueryBuilder->where($originalWhereConditions);
299  }
300  }
301  return $sql;
302  }
303 
315  public function setParameter(
316  int|string $key,
317  mixed $value,
318  string|ParameterType|Type|ArrayParameterType $type = ParameterType::STRING,
319  ): QueryBuilder {
320  $concreteQueryBuilder = $this->concreteQueryBuilder;
321  $concreteQueryBuilder->setParameter($key, $value, $type);
322  return $this;
323  }
324 
333  public function setParameters(array $params, array $types = []): QueryBuilder
334  {
335  $concreteQueryBuilder = $this->concreteQueryBuilder;
336  $concreteQueryBuilder->setParameters($params, $types);
337  return $this;
338  }
339 
345  public function getParameters(): array
346  {
347  $concreteQueryBuilder = $this->concreteQueryBuilder;
348  return $concreteQueryBuilder->getParameters();
349  }
350 
358  public function getParameter(string|int $key): mixed
359  {
360  $concreteQueryBuilder = $this->concreteQueryBuilder;
361  return $concreteQueryBuilder->getParameter($key);
362  }
363 
369  public function getParameterTypes(): array
370  {
371  $concreteQueryBuilder = $this->concreteQueryBuilder;
372  return $concreteQueryBuilder->getParameterTypes();
373  }
374 
382  public function getParameterType(string|int $key): string|ParameterType|Type|ArrayParameterType
383  {
384  $concreteQueryBuilder = $this->concreteQueryBuilder;
385  return $concreteQueryBuilder->getParameterType($key);
386  }
387 
395  public function setFirstResult(int $firstResult): QueryBuilder
396  {
397  $concreteQueryBuilder = $this->concreteQueryBuilder;
398  $concreteQueryBuilder->setFirstResult($firstResult);
399  return $this;
400  }
401 
408  public function getFirstResult(): int
409  {
410  $concreteQueryBuilder = $this->concreteQueryBuilder;
411  return $concreteQueryBuilder->getFirstResult();
412  }
413 
421  public function setMaxResults(int|null $maxResults = null): QueryBuilder
422  {
423  $concreteQueryBuilder = $this->concreteQueryBuilder;
424  $concreteQueryBuilder->setMaxResults($maxResults);
425  return $this;
426  }
427 
434  public function getMaxResults(): int|null
435  {
436  $concreteQueryBuilder = $this->concreteQueryBuilder;
437  return $concreteQueryBuilder->getMaxResults();
438  }
439 
447  public function count(string $item): QueryBuilder
448  {
449  $countExpr = $this->getCountExpression(
450  $item === '*' ? $item : $this->quoteIdentifier($item)
451  );
452  $concreteQueryBuilder = $this->concreteQueryBuilder;
453  $concreteQueryBuilder->select($countExpr);
454 
455  return $this;
456  }
457 
458  protected function getCountExpression(string $column): string
459  {
460  return 'COUNT(' . $column . ')';
461  }
462 
467  public function select(string ...$selects): QueryBuilder
468  {
469  $concreteQueryBuilder = $this->concreteQueryBuilder;
470  $concreteQueryBuilder->select(...$this->quoteIdentifiersForSelect($selects));
471  return $this;
472  }
473 
477  public function distinct(bool $distinct = true): QueryBuilder
478  {
479  $concreteQueryBuilder = $this->concreteQueryBuilder;
480  $concreteQueryBuilder->distinct($distinct);
481  return $this;
482  }
483 
487  public function addSelect(string ...$selects): QueryBuilder
488  {
489  $concreteQueryBuilder = $this->concreteQueryBuilder;
490  $concreteQueryBuilder->addSelect(...$this->quoteIdentifiersForSelect($selects));
491  return $this;
492  }
493 
502  public function selectLiteral(string ...$selects): QueryBuilder
503  {
504  $concreteQueryBuilder = $this->concreteQueryBuilder;
505  $concreteQueryBuilder->select(...$selects);
506  return $this;
507  }
508 
516  public function addSelectLiteral(string ...$selects): QueryBuilder
517  {
518  $concreteQueryBuilder = $this->concreteQueryBuilder;
519  $concreteQueryBuilder->addSelect(...$selects);
520  return $this;
521  }
522 
530  public function delete(string $table): QueryBuilder
531  {
532  $concreteQueryBuilder = $this->concreteQueryBuilder;
533  $concreteQueryBuilder->delete($this->quoteIdentifier($table));
534  return $this;
535  }
536 
543  public function update(string $table): QueryBuilder
544  {
545  $concreteQueryBuilder = $this->concreteQueryBuilder;
546  $concreteQueryBuilder->update($this->quoteIdentifier($table));
547  return $this;
548  }
549 
556  public function insert(string $table): QueryBuilder
557  {
558  $concreteQueryBuilder = $this->concreteQueryBuilder;
559  $concreteQueryBuilder->insert($this->quoteIdentifier($table));
560  return $this;
561  }
562 
570  public function from(string $table, string $alias = null): QueryBuilder
571  {
572  $concreteQueryBuilder = $this->concreteQueryBuilder;
573  $concreteQueryBuilder->from(
574  $this->quoteIdentifier($table),
575  empty($alias) ? $alias : $this->quoteIdentifier($alias)
576  );
577  return $this;
578  }
579 
588  public function join(string $fromAlias, string $join, string $alias, string $condition = null): QueryBuilder
589  {
590  $concreteQueryBuilder = $this->concreteQueryBuilder;
591  $concreteQueryBuilder->innerJoin(
592  $this->quoteIdentifier($fromAlias),
593  $this->quoteIdentifier($join),
594  $this->quoteIdentifier($alias),
595  $condition
596  );
597  return $this;
598  }
599 
608  public function innerJoin(string $fromAlias, string $join, string $alias, string $condition = null): QueryBuilder
609  {
610  $concreteQueryBuilder = $this->concreteQueryBuilder;
611  $concreteQueryBuilder->innerJoin(
612  $this->quoteIdentifier($fromAlias),
613  $this->quoteIdentifier($join),
614  $this->quoteIdentifier($alias),
615  $condition
616  );
617  return $this;
618  }
619 
628  public function leftJoin(string $fromAlias, string $join, string $alias, CompositeExpression|string $condition = null): QueryBuilder
629  {
630  $conditionExpression = (string)$this->expr()->and(
631  $condition,
632  $this->restrictionContainer->buildExpression([$alias ?? $join => $join], $this->expr())
633  );
634  $this->restrictionsAppliedInJoinCondition[] = $alias ?? $join;
635  $concreteQueryBuilder = $this->concreteQueryBuilder;
636  $concreteQueryBuilder->leftJoin(
637  $this->quoteIdentifier($fromAlias),
638  $this->quoteIdentifier($join),
639  $this->quoteIdentifier($alias),
640  $conditionExpression
641  );
642  return $this;
643  }
644 
653  public function rightJoin(string $fromAlias, string $join, string $alias, string $condition = null): QueryBuilder
654  {
655  $fromTable = $fromAlias;
656  // find the table belonging to the $fromAlias, if it's an alias at all
657  foreach ($this->concreteQueryBuilder->from as $from) {
658  if (is_string($from->alias) && $from->alias !== '' && $this->unquoteSingleIdentifier($from->alias) === $fromAlias) {
659  $fromTable = $this->unquoteSingleIdentifier($from->alias);
660  break;
661  }
662  }
663  $conditionExpression = (string)$this->expr()->and(
664  $condition,
665  $this->restrictionContainer->buildExpression([$fromAlias => $fromTable], $this->expr())
666  );
667  $this->restrictionsAppliedInJoinCondition[] = $fromAlias;
668  $concreteQueryBuilder = $this->concreteQueryBuilder;
669  $concreteQueryBuilder->rightJoin(
670  $this->quoteIdentifier($fromAlias),
671  $this->quoteIdentifier($join),
672  $this->quoteIdentifier($alias),
673  $conditionExpression
674  );
675  return $this;
676  }
677 
685  public function set(string $key, $value, bool $createNamedParameter = true, ParameterType|ArrayParameterType $type = ‪Connection::PARAM_STR): QueryBuilder
686  {
687  $concreteQueryBuilder = $this->concreteQueryBuilder;
688  $concreteQueryBuilder->set(
689  $this->quoteIdentifier($key),
690  $createNamedParameter ? $this->createNamedParameter($value, $type) : $value
691  );
692  return $this;
693  }
694 
701  public function where(...$predicates): QueryBuilder
702  {
703  // Doctrine DBAL 3.x requires a non-empty $predicate, however TYPO3 uses static values
704  // such as PageRepository->$where_hid_del which could be empty
705  $predicates = array_filter($predicates, static fn(CompositeExpression|string|null $value): bool => !self::isEmptyPart($value));
706  if (empty($predicates)) {
707  $this->resetWhere();
708  return $this;
709  }
710  $concreteQueryBuilder = $this->concreteQueryBuilder;
711  $concreteQueryBuilder->where(...$predicates);
712  return $this;
713  }
714 
722  public function andWhere(...$predicates): QueryBuilder
723  {
724  // Doctrine DBAL 3.x requires a non-empty $predicate, however TYPO3 uses static values
725  // such as PageRepository->$where_hid_del which could be empty
726  $predicates = array_filter($predicates, static fn(CompositeExpression|string|null $value): bool => !self::isEmptyPart($value));
727  if (empty($predicates)) {
728  return $this;
729  }
730  $concreteQueryBuilder = $this->concreteQueryBuilder;
731  $concreteQueryBuilder->andWhere(...$predicates);
732  return $this;
733  }
734 
742  public function orWhere(...$predicates): QueryBuilder
743  {
744  // Doctrine DBAL 3.x requires a non-empty $predicate, however TYPO3 uses static values
745  // such as PageRepository->$where_hid_del which could be empty
746  $predicates = array_filter($predicates, static fn(CompositeExpression|string|null $value): bool => !self::isEmptyPart($value));
747  if (empty($predicates)) {
748  return $this;
749  }
750  $concreteQueryBuilder = $this->concreteQueryBuilder;
751  $concreteQueryBuilder->orWhere(...$predicates);
752  return $this;
753  }
754 
761  public function groupBy(...$groupBy): QueryBuilder
762  {
763  $concreteQueryBuilder = $this->concreteQueryBuilder;
764  $concreteQueryBuilder->groupBy(...$this->quoteIdentifiers($groupBy));
765  return $this;
766  }
767 
773  public function addGroupBy(...$groupBy): QueryBuilder
774  {
775  $concreteQueryBuilder = $this->concreteQueryBuilder;
776  $concreteQueryBuilder->addGroupBy(...$this->quoteIdentifiers($groupBy));
777  return $this;
778  }
779 
787  public function setValue(string $column, $value, bool $createNamedParameter = true): QueryBuilder
788  {
789  $concreteQueryBuilder = $this->concreteQueryBuilder;
790  $concreteQueryBuilder->setValue(
791  $this->quoteIdentifier($column),
792  $createNamedParameter ? $this->createNamedParameter($value) : $value
793  );
794  return $this;
795  }
796 
804  public function values(array $values, bool $createNamedParameters = true): QueryBuilder
805  {
806  if ($createNamedParameters === true) {
807  foreach ($values as &$value) {
808  $value = $this->createNamedParameter($value);
809  }
810  }
811  $concreteQueryBuilder = $this->concreteQueryBuilder;
812  $concreteQueryBuilder->values($this->quoteColumnValuePairs($values));
813  return $this;
814  }
815 
822  public function having(...$predicates): QueryBuilder
823  {
824  $predicates = array_filter($predicates, static fn(CompositeExpression|string|null $value): bool => !self::isEmptyPart($value));
825  if (empty($predicates)) {
826  $this->resetHaving();
827  return $this;
828  }
829  $concreteQueryBuilder = $this->concreteQueryBuilder;
830  $concreteQueryBuilder->having(...$predicates);
831  return $this;
832  }
833 
840  public function andHaving(...$predicates): QueryBuilder
841  {
842  $predicates = array_filter($predicates, static fn(CompositeExpression|string|null $value): bool => !self::isEmptyPart($value));
843  if (empty($predicates)) {
844  return $this;
845  }
846  $concreteQueryBuilder = $this->concreteQueryBuilder;
847  $concreteQueryBuilder->andHaving(...$predicates);
848  return $this;
849  }
850 
857  public function orHaving(...$predicates): QueryBuilder
858  {
859  $predicates = array_filter($predicates, static fn(CompositeExpression|string|null $value): bool => !self::isEmptyPart($value));
860  if (empty($predicates)) {
861  return $this;
862  }
863  $concreteQueryBuilder = $this->concreteQueryBuilder;
864  $concreteQueryBuilder->orHaving(...$predicates);
865  return $this;
866  }
867 
875  public function orderBy(string $fieldName, string $order = null): QueryBuilder
876  {
877  $concreteQueryBuilder = $this->concreteQueryBuilder;
878  $concreteQueryBuilder->orderBy($this->connection->quoteIdentifier($fieldName), $order);
879  return $this;
880  }
881 
888  public function addOrderBy(string $fieldName, string $order = null): QueryBuilder
889  {
890  $concreteQueryBuilder = $this->concreteQueryBuilder;
891  $concreteQueryBuilder->addOrderBy($this->connection->quoteIdentifier($fieldName), $order);
892  return $this;
893  }
894 
898  public function resetWhere(): self
899  {
900  $concreteQueryBuilder = $this->concreteQueryBuilder;
901  $concreteQueryBuilder->resetWhere();
902  return $this;
903  }
904 
908  public function resetGroupBy(): self
909  {
910  $concreteQueryBuilder = $this->concreteQueryBuilder;
911  $concreteQueryBuilder->resetGroupBy();
912  return $this;
913  }
914 
918  public function resetHaving(): self
919  {
920  $concreteQueryBuilder = $this->concreteQueryBuilder;
921  $concreteQueryBuilder->resetHaving();
922  return $this;
923  }
924 
928  public function resetOrderBy(): self
929  {
930  $concreteQueryBuilder = $this->concreteQueryBuilder;
931  $concreteQueryBuilder->resetOrderBy();
932  return $this;
933  }
934 
957  public function createNamedParameter(
958  mixed $value,
959  string|ParameterType|Type|ArrayParameterType $type = ParameterType::STRING,
960  string $placeHolder = null
961  ): string {
962  $concreteQueryBuilder = $this->concreteQueryBuilder;
963  return $concreteQueryBuilder->createNamedParameter($value, $type, $placeHolder);
964  }
965 
983  public function createPositionalParameter(
984  mixed $value,
985  string|ParameterType|Type|ArrayParameterType $type = ParameterType::STRING,
986  ): string {
987  $concreteQueryBuilder = $this->concreteQueryBuilder;
988  return $concreteQueryBuilder->createPositionalParameter($value, $type);
989  }
990 
997  public function escapeLikeWildcards(string $value): string
998  {
999  return $this->connection->escapeLikeWildcards($value);
1000  }
1001 
1008  public function quote(string $input): string
1009  {
1010  return $this->getConnection()->quote($input);
1011  }
1012 
1022  public function quoteIdentifier(string ‪$identifier): string
1023  {
1024  return $this->getConnection()->quoteIdentifier(‪$identifier);
1025  }
1026 
1032  public function quoteIdentifiers(array $input): array
1033  {
1034  return $this->getConnection()->quoteIdentifiers($input);
1035  }
1036 
1046  public function quoteIdentifiersForSelect(array $input): array
1047  {
1048  foreach ($input as &$select) {
1049  [$fieldName, $alias, $suffix] = array_pad(
1051  ' AS ',
1052  str_ireplace(' as ', ' AS ', $select),
1053  true,
1054  3
1055  ),
1056  3,
1057  null
1058  );
1059  if (!empty($suffix)) {
1060  throw new \InvalidArgumentException(
1061  'QueryBuilder::quoteIdentifiersForSelect() could not parse the select ' . $select . '.',
1062  1461170686
1063  );
1064  }
1065 
1066  // The SQL * operator must not be quoted. As it can only occur either by itself
1067  // or preceded by a tablename (tablename.*) check if the last character of a select
1068  // expression is the * and quote only prepended table name. In all other cases the
1069  // full expression is being quoted.
1070  if (substr($fieldName, -2) === '.*') {
1071  $select = $this->quoteIdentifier(substr($fieldName, 0, -2)) . '.*';
1072  } elseif ($fieldName !== '*') {
1073  $select = $this->quoteIdentifier($fieldName);
1074  }
1075 
1076  // Quote the alias for the current fieldName, if given
1077  if (!empty($alias)) {
1078  $select .= ' AS ' . $this->quoteIdentifier($alias);
1079  }
1080  }
1081  return $input;
1082  }
1083 
1090  public function quoteColumnValuePairs(array $input): array
1091  {
1092  return $this->getConnection()->quoteColumnValuePairs($input);
1093  }
1094 
1111  public function quoteArrayBasedValueListToIntegerList(array $values): string
1112  {
1113  if (empty($values)) {
1114  return 'NULL';
1115  }
1116  // Ensure values are all integer
1117  $values = ‪GeneralUtility::intExplode(',', implode(',', $values));
1118  // Ensure all values are quoted as int for used dbms
1119  $connection = $this;
1120  array_walk($values, static function (mixed &$value) use ($connection): void {
1121  $value = $connection->quote((string)$value);
1122  });
1123  return implode(',', $values);
1124  }
1125 
1142  public function quoteArrayBasedValueListToStringList(array $values): string
1143  {
1144  if (empty($values)) {
1145  return 'NULL';
1146  }
1147  // Ensure values are all strings
1148  $values = ‪GeneralUtility::trimExplode(',', implode(',', $values));
1149  // Ensure all values are quoted as string values for used dbmns
1150  $connection = $this;
1151  array_walk($values, static function (mixed &$value) use ($connection): void {
1152  $value = $connection->quote((string)$value);
1153  });
1154  return implode(',', $values);
1155  }
1156 
1164  public function castFieldToTextType(string $fieldName): string
1165  {
1166  $databasePlatform = $this->connection->getDatabasePlatform();
1167  // https://dev.mysql.com/doc/refman/5.7/en/cast-functions.html#function_convert
1168  if ($databasePlatform instanceof DoctrineMariaDBPlatform || $databasePlatform instanceof DoctrineMySQLPlatform) {
1169  return sprintf('CONVERT(%s, CHAR)', $this->connection->quoteIdentifier($fieldName));
1170  }
1171  // https://www.postgresql.org/docs/current/sql-createcast.html
1172  if ($databasePlatform instanceof DoctrinePostgreSQLPlatform) {
1173  return sprintf('%s::text', $this->connection->quoteIdentifier($fieldName));
1174  }
1175  // https://www.sqlite.org/lang_expr.html#castexpr
1176  if ($databasePlatform instanceof DoctrineSQLitePlatform) {
1177  return sprintf('CAST(%s as TEXT)', $this->connection->quoteIdentifier($fieldName));
1178  }
1179  throw new \RuntimeException(
1180  sprintf(
1181  '%s is not implemented for the used database platform "%s", yet!',
1182  __METHOD__,
1183  get_class($this->connection->getDatabasePlatform())
1184  ),
1185  1584637096
1186  );
1187  }
1188 
1196  protected function unquoteSingleIdentifier(string ‪$identifier): string
1197  {
1199  $quoteChar = GeneralUtility::makeInstance(PlatformHelper::class)
1200  ->getIdentifierQuoteCharacter($this->getConnection()->getDatabasePlatform());
1201  ‪$identifier = trim(‪$identifier, $quoteChar);
1202  ‪$identifier = str_replace($quoteChar . $quoteChar, $quoteChar, ‪$identifier);
1203  return ‪$identifier;
1204  }
1205 
1211  protected function getIdentifierQuoteCharacter(): string
1212  {
1213  return substr($this->connection->getDatabasePlatform()->quoteSingleIdentifier('fake'), 0, 1);
1214  }
1215 
1222  public function getSelect(): array
1223  {
1224  $concreteQueryBuilder = $this->concreteQueryBuilder;
1225  return $concreteQueryBuilder->select;
1226  }
1227 
1236  public function getFrom()
1237  {
1238  $concreteQueryBuilder = $this->concreteQueryBuilder;
1239  return $concreteQueryBuilder->from;
1240  }
1241 
1249  public function getWhere(): CompositeExpression|string|null
1250  {
1251  $concreteQueryBuilder = $this->concreteQueryBuilder;
1252  return $concreteQueryBuilder->where;
1253  }
1254 
1261  public function getHaving(): CompositeExpression|string|null
1262  {
1263  $concreteQueryBuilder = $this->concreteQueryBuilder;
1264  return $concreteQueryBuilder->having;
1265  }
1266 
1275  public function getOrderBy(): array
1276  {
1277  $concreteQueryBuilder = $this->concreteQueryBuilder;
1278  return $concreteQueryBuilder->orderBy;
1279  }
1280 
1288  public function getGroupBy(): array
1289  {
1290  $concreteQueryBuilder = $this->concreteQueryBuilder;
1291  return $concreteQueryBuilder->groupBy;
1292  }
1293 
1300  public function getJoin(): array
1301  {
1302  $concreteQueryBuilder = $this->concreteQueryBuilder;
1303  return $concreteQueryBuilder->join;
1304  }
1305 
1314  protected function getQueriedTables(): array
1315  {
1317  $queriedTables = [];
1318  // Loop through all FROM tables
1319  foreach ($this->concreteQueryBuilder->from as $from) {
1320  $tableName = $this->unquoteSingleIdentifier($from->table);
1321  $tableAlias = is_string($from->alias) && $from->alias !== '' ? $this->unquoteSingleIdentifier($from->alias) : $tableName;
1322  if (!in_array($tableAlias, $this->restrictionsAppliedInJoinCondition, true)) {
1323  $queriedTables[$tableAlias] = $tableName;
1324  }
1325  }
1326 
1327  // Loop through all JOIN tables
1328  foreach ($this->concreteQueryBuilder->join as $fromTable => $joins) {
1329  foreach ($joins as $join) {
1330  $tableName = $this->unquoteSingleIdentifier($join->table);
1331  $tableAlias = is_string($join->alias) && $join->alias !== '' ? $this->unquoteSingleIdentifier($join->alias) : $tableName;
1332  if (!in_array($tableAlias, $this->restrictionsAppliedInJoinCondition, true)) {
1333  $queriedTables[$tableAlias] = $tableName;
1334  }
1335  }
1336  }
1337  return $queriedTables;
1338  }
1339 
1345  protected function addAdditionalWhereConditions(): CompositeExpression|string|null
1346  {
1347  $originalWhereConditions = $this->getWhere();
1348  $expression = $this->restrictionContainer->buildExpression($this->getQueriedTables(), $this->expr());
1349  // This check would be obsolete, as the composite expression would not add empty expressions anyway
1350  // But we keep it here to only clone the previous state, in case we really will change it.
1351  // Once we remove this state preserving functionality, we can remove the count check here
1352  // and just add the expression to the query builder.
1353  if ($expression->count() > 0) {
1354  $this->concreteQueryBuilder->andWhere($expression);
1355  }
1356  return $originalWhereConditions;
1357  }
1358 
1359  private function throwExceptionOnInvalidPreparedStatementParamArrayType(array $types): void
1360  {
1361  foreach ($types as $type) {
1362  $invalidTypeLabel = match ($type) {
1363  ‪Connection::PARAM_INT_ARRAY => 'PARAM_INT_ARRAY',
1364  ‪Connection::PARAM_STR_ARRAY => 'PARAM_STR_ARRAY',
1365  default => false,
1366  };
1367  if ($invalidTypeLabel !== false) {
1369  }
1370  }
1371  }
1372 
1373  private function throwExceptionOnNamedParameterForPreparedStatement(array $params): void
1374  {
1375  foreach ($params as $key => $value) {
1376  if (is_string($key) && !‪MathUtility::canBeInterpretedAsInteger($key)) {
1378  }
1379  }
1380  }
1381 
1397  private function bindTypedValues(DriverStatement $stmt, array $params, array $types): void
1398  {
1399  // Check whether parameters are positional or named. Mixing is not allowed.
1400  $stringType = new StringType();
1401  if (is_int(key($params))) {
1402  $bindIndex = 1;
1403 
1404  foreach ($params as $key => $value) {
1405  $type = (isset($types[$key])) ? $types[$key] : $stringType;
1406  [$value, $bindingType] = $this->getBindingInfo($value, $type);
1407  $stmt->bindValue($bindIndex, $value, $bindingType);
1408 
1409  ++$bindIndex;
1410  }
1411  } else {
1412  // Named parameters
1413  foreach ($params as $name => $value) {
1414  $type = (isset($types[$name])) ? $types[$name] : $stringType;
1415  [$value, $bindingType] = $this->getBindingInfo($value, $type);
1416  $stmt->bindValue($name, $value, $bindingType);
1417  }
1418  }
1419  }
1420 
1435  private function getBindingInfo(mixed $value, string|ParameterType|Type $type): array
1436  {
1437  if (is_string($type)) {
1438  $type = Type::getType($type);
1439  }
1440  if ($type instanceof Type) {
1441  $value = $type->convertToDatabaseValue($value, $this->connection->getDatabasePlatform());
1442  $bindingType = $type->getBindingType();
1443  } else {
1444  $bindingType = $type;
1445  }
1446  return [$value, $bindingType];
1447  }
1448 }
‪TYPO3\CMS\Core\Database\Query\Restriction\LimitToTablesRestrictionContainer
Definition: LimitToTablesRestrictionContainer.php:28
‪TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder
Definition: ExpressionBuilder.php:40
‪TYPO3\CMS\Extbase\Persistence\Generic\Storage\Typo3DbBackend
Definition: Typo3DbBackend.php:56
‪TYPO3\CMS\Core\Database\Query\Restriction\QueryRestrictionInterface
Definition: QueryRestrictionInterface.php:27
‪TYPO3\CMS\Core\Database\RelationHandler
Definition: RelationHandler.php:36
‪TYPO3\CMS\Core\Database\Query\Restriction\QueryRestrictionContainerInterface
Definition: QueryRestrictionContainerInterface.php:25
‪TYPO3\CMS\Core\Database\Connection\PARAM_STR
‪const PARAM_STR
Definition: Connection.php:57
‪TYPO3\CMS\Core\Utility\MathUtility\canBeInterpretedAsInteger
‪static bool canBeInterpretedAsInteger(mixed $var)
Definition: MathUtility.php:69
‪TYPO3\CMS\Core\Database\Query\UnsupportedPreparedStatementParameterTypeException\new
‪static new(string $parameterType)
Definition: UnsupportedPreparedStatementParameterTypeException.php:22
‪TYPO3\CMS\Core\Database\Connection\PARAM_STR_ARRAY
‪const PARAM_STR_ARRAY
Definition: Connection.php:77
‪TYPO3\CMS\Core\Database\Connection
Definition: Connection.php:41
‪$GLOBALS
‪$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['adminpanel']['modules']
Definition: ext_localconf.php:25
‪TYPO3\CMS\Core\Database\Query
Definition: BulkInsertQuery.php:18
‪TYPO3\CMS\Core\Utility\MathUtility
Definition: MathUtility.php:24
‪TYPO3\CMS\Extbase\Tests\Functional\Persistence\Generic\Storage\Typo3DbQueryParserTest
Definition: Typo3DbQueryParserTest.php:43
‪TYPO3\CMS\Core\Utility\GeneralUtility
Definition: GeneralUtility.php:52
‪TYPO3\CMS\Core\Utility\GeneralUtility\intExplode
‪static list< int > intExplode(string $delimiter, string $string, bool $removeEmptyValues=false)
Definition: GeneralUtility.php:756
‪TYPO3\CMS\Core\Database\Connection\PARAM_INT_ARRAY
‪const PARAM_INT_ARRAY
Definition: Connection.php:72
‪TYPO3\CMS\Core\Database\Query\NamedParameterNotSupportedForPreparedStatementException\new
‪static new(string $placeholderName)
Definition: NamedParameterNotSupportedForPreparedStatementException.php:22
‪TYPO3\CMS\Core\Utility\GeneralUtility\trimExplode
‪static list< string > trimExplode(string $delim, string $string, bool $removeEmptyValues=false, int $limit=0)
Definition: GeneralUtility.php:822
‪TYPO3\CMS\Webhooks\Message\$identifier
‪identifier readonly string $identifier
Definition: FileAddedMessage.php:37
‪TYPO3\CMS\Core\Database\Query\Restriction\DefaultRestrictionContainer
Definition: DefaultRestrictionContainer.php:24
‪TYPO3\CMS\Core\Database\Platform\PlatformHelper
Definition: PlatformHelper.php:26