[TASK] Streamline phpdoc annotations in EXT:extbase
[Packages/TYPO3.CMS.git] / typo3 / sysext / extbase / Classes / Persistence / Generic / Storage / Typo3DbQueryParser.php
1 <?php
2 namespace TYPO3\CMS\Extbase\Persistence\Generic\Storage;
3
4 /*
5 * This file is part of the TYPO3 CMS project.
6 *
7 * It is free software; you can redistribute it and/or modify it under
8 * the terms of the GNU General Public License, either version 2
9 * of the License, or any later version.
10 *
11 * For the full copyright and license information, please read the
12 * LICENSE.txt file that was distributed with this source code.
13 *
14 * The TYPO3 project - inspiring people to share!
15 */
16
17 use TYPO3\CMS\Backend\Utility\BackendUtility;
18 use TYPO3\CMS\Core\Database\Connection;
19 use TYPO3\CMS\Core\Database\ConnectionPool;
20 use TYPO3\CMS\Core\Database\Query\Expression\CompositeExpression;
21 use TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder;
22 use TYPO3\CMS\Core\Database\Query\QueryBuilder;
23 use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
24 use TYPO3\CMS\Core\Utility\GeneralUtility;
25 use TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface;
26 use TYPO3\CMS\Extbase\DomainObject\AbstractDomainObject;
27 use TYPO3\CMS\Extbase\Object\ObjectManagerInterface;
28 use TYPO3\CMS\Extbase\Persistence\Generic\Exception;
29 use TYPO3\CMS\Extbase\Persistence\Generic\Exception\InconsistentQuerySettingsException;
30 use TYPO3\CMS\Extbase\Persistence\Generic\Exception\InvalidRelationConfigurationException;
31 use TYPO3\CMS\Extbase\Persistence\Generic\Exception\MissingColumnMapException;
32 use TYPO3\CMS\Extbase\Persistence\Generic\Exception\RepositoryException;
33 use TYPO3\CMS\Extbase\Persistence\Generic\Exception\UnsupportedOrderException;
34 use TYPO3\CMS\Extbase\Persistence\Generic\Mapper\ColumnMap;
35 use TYPO3\CMS\Extbase\Persistence\Generic\Mapper\DataMapper;
36 use TYPO3\CMS\Extbase\Persistence\Generic\Qom;
37 use TYPO3\CMS\Extbase\Persistence\Generic\QuerySettingsInterface;
38 use TYPO3\CMS\Extbase\Persistence\Generic\Storage\Exception\BadConstraintException;
39 use TYPO3\CMS\Extbase\Persistence\QueryInterface;
40 use TYPO3\CMS\Extbase\Service\EnvironmentService;
41 use TYPO3\CMS\Frontend\Page\PageRepository;
42
43 /**
44 * QueryParser, converting the qom to string representation
45 * @internal only to be used within Extbase, not part of TYPO3 Core API.
46 */
47 class Typo3DbQueryParser
48 {
49 /**
50 * @var DataMapper
51 */
52 protected $dataMapper;
53
54 /**
55 * The TYPO3 page repository. Used for language and workspace overlay
56 *
57 * @var PageRepository
58 */
59 protected $pageRepository;
60
61 /**
62 * @var EnvironmentService
63 */
64 protected $environmentService;
65
66 /**
67 * @var ConfigurationManagerInterface
68 */
69 protected $configurationManager;
70
71 /**
72 * Instance of the Doctrine query builder
73 *
74 * @var QueryBuilder
75 */
76 protected $queryBuilder;
77
78 /**
79 * Maps domain model properties to their corresponding table aliases that are used in the query, e.g.:
80 *
81 * 'property1' => 'tableName',
82 * 'property1.property2' => 'tableName1',
83 *
84 * @var array
85 */
86 protected $tablePropertyMap = [];
87
88 /**
89 * Maps tablenames to their aliases to be used in where clauses etc.
90 * Mainly used for joins on the same table etc.
91 *
92 * @var array
93 */
94 protected $tableAliasMap = [];
95
96 /**
97 * Stores all tables used in for SQL joins
98 *
99 * @var array
100 */
101 protected $unionTableAliasCache = [];
102
103 /**
104 * @var string
105 */
106 protected $tableName = '';
107
108 /**
109 * @var bool
110 */
111 protected $suggestDistinctQuery = false;
112
113 /**
114 * @var ObjectManagerInterface
115 */
116 protected $objectManager;
117
118 /**
119 * @param ObjectManagerInterface $objectManager
120 */
121 public function injectObjectManager(ObjectManagerInterface $objectManager)
122 {
123 $this->objectManager = $objectManager;
124 }
125
126 /**
127 * Object initialization called when object is created with ObjectManager, after constructor
128 */
129 public function initializeObject()
130 {
131 $this->dataMapper = $this->objectManager->get(DataMapper::class);
132 }
133
134 /**
135 * @param EnvironmentService $environmentService
136 */
137 public function injectEnvironmentService(EnvironmentService $environmentService)
138 {
139 $this->environmentService = $environmentService;
140 }
141
142 /**
143 * @param ConfigurationManagerInterface $configurationManager
144 */
145 public function injectConfigurationManager(ConfigurationManagerInterface $configurationManager)
146 {
147 $this->configurationManager = $configurationManager;
148 }
149
150 /**
151 * Whether using a distinct query is suggested.
152 * This information is defined during parsing of the current query
153 * for RELATION_HAS_MANY & RELATION_HAS_AND_BELONGS_TO_MANY relations.
154 *
155 * @return bool
156 */
157 public function isDistinctQuerySuggested(): bool
158 {
159 return $this->suggestDistinctQuery;
160 }
161
162 /**
163 * Returns a ready to be executed QueryBuilder object, based on the query
164 *
165 * @param QueryInterface $query
166 * @return QueryBuilder
167 */
168 public function convertQueryToDoctrineQueryBuilder(QueryInterface $query)
169 {
170 // Reset all properties
171 $this->tablePropertyMap = [];
172 $this->tableAliasMap = [];
173 $this->unionTableAliasCache = [];
174 $this->tableName = '';
175
176 if ($query->getStatement() && $query->getStatement()->getStatement() instanceof QueryBuilder) {
177 $this->queryBuilder = clone $query->getStatement()->getStatement();
178 return $this->queryBuilder;
179 }
180 // Find the right table name
181 $source = $query->getSource();
182 $this->initializeQueryBuilder($source);
183
184 $constraint = $query->getConstraint();
185 if ($constraint instanceof Qom\ConstraintInterface) {
186 $wherePredicates = $this->parseConstraint($constraint, $source);
187 if (!empty($wherePredicates)) {
188 $this->queryBuilder->andWhere($wherePredicates);
189 }
190 }
191
192 $this->parseOrderings($query->getOrderings(), $source);
193 $this->addTypo3Constraints($query);
194
195 return $this->queryBuilder;
196 }
197
198 /**
199 * Creates the queryBuilder object whether it is a regular select or a JOIN
200 *
201 * @param Qom\SourceInterface $source The source
202 */
203 protected function initializeQueryBuilder(Qom\SourceInterface $source)
204 {
205 if ($source instanceof Qom\SelectorInterface) {
206 $className = $source->getNodeTypeName();
207 $tableName = $this->dataMapper->getDataMap($className)->getTableName();
208 $this->tableName = $tableName;
209
210 $this->queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
211 ->getQueryBuilderForTable($tableName);
212
213 $this->queryBuilder
214 ->getRestrictions()
215 ->removeAll();
216
217 $tableAlias = $this->getUniqueAlias($tableName);
218
219 $this->queryBuilder
220 ->select($tableAlias . '.*')
221 ->from($tableName, $tableAlias);
222
223 $this->addRecordTypeConstraint($className);
224 } elseif ($source instanceof Qom\JoinInterface) {
225 $leftSource = $source->getLeft();
226 $leftTableName = $leftSource->getSelectorName();
227
228 $this->queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
229 ->getQueryBuilderForTable($leftTableName);
230 $leftTableAlias = $this->getUniqueAlias($leftTableName);
231 $this->queryBuilder
232 ->select($leftTableAlias . '.*')
233 ->from($leftTableName, $leftTableAlias);
234 $this->parseJoin($source, $leftTableAlias);
235 }
236 }
237
238 /**
239 * Transforms a constraint into SQL and parameter arrays
240 *
241 * @param Qom\ConstraintInterface $constraint The constraint
242 * @param Qom\SourceInterface $source The source
243 * @return CompositeExpression|string
244 * @throws \RuntimeException
245 */
246 protected function parseConstraint(Qom\ConstraintInterface $constraint, Qom\SourceInterface $source)
247 {
248 if ($constraint instanceof Qom\AndInterface) {
249 $constraint1 = $constraint->getConstraint1();
250 $constraint2 = $constraint->getConstraint2();
251 if (($constraint1 instanceof Qom\ConstraintInterface)
252 && ($constraint2 instanceof Qom\ConstraintInterface)
253 ) {
254 return $this->queryBuilder->expr()->andX(
255 $this->parseConstraint($constraint1, $source),
256 $this->parseConstraint($constraint2, $source)
257 );
258 }
259 return '';
260 }
261 if ($constraint instanceof Qom\OrInterface) {
262 $constraint1 = $constraint->getConstraint1();
263 $constraint2 = $constraint->getConstraint2();
264 if (($constraint1 instanceof Qom\ConstraintInterface)
265 && ($constraint2 instanceof Qom\ConstraintInterface)
266 ) {
267 return $this->queryBuilder->expr()->orX(
268 $this->parseConstraint($constraint->getConstraint1(), $source),
269 $this->parseConstraint($constraint->getConstraint2(), $source)
270 );
271 }
272 return '';
273 }
274 if ($constraint instanceof Qom\NotInterface) {
275 return ' NOT(' . $this->parseConstraint($constraint->getConstraint(), $source) . ')';
276 }
277 if ($constraint instanceof Qom\ComparisonInterface) {
278 return $this->parseComparison($constraint, $source);
279 }
280 throw new \RuntimeException('not implemented', 1476199898);
281 }
282
283 /**
284 * Transforms orderings into SQL.
285 *
286 * @param array $orderings An array of orderings (Qom\Ordering)
287 * @param Qom\SourceInterface $source The source
288 * @throws UnsupportedOrderException
289 */
290 protected function parseOrderings(array $orderings, Qom\SourceInterface $source)
291 {
292 foreach ($orderings as $propertyName => $order) {
293 if ($order !== QueryInterface::ORDER_ASCENDING && $order !== QueryInterface::ORDER_DESCENDING) {
294 throw new UnsupportedOrderException('Unsupported order encountered.', 1242816074);
295 }
296 $className = null;
297 $tableName = '';
298 if ($source instanceof Qom\SelectorInterface) {
299 $className = $source->getNodeTypeName();
300 $tableName = $this->dataMapper->convertClassNameToTableName($className);
301 $fullPropertyPath = '';
302 while (strpos($propertyName, '.') !== false) {
303 $this->addUnionStatement($className, $tableName, $propertyName, $fullPropertyPath);
304 }
305 } elseif ($source instanceof Qom\JoinInterface) {
306 $tableName = $source->getLeft()->getSelectorName();
307 }
308 $columnName = $this->dataMapper->convertPropertyNameToColumnName($propertyName, $className);
309 if ($tableName !== '') {
310 $this->queryBuilder->addOrderBy($tableName . '.' . $columnName, $order);
311 } else {
312 $this->queryBuilder->addOrderBy($columnName, $order);
313 }
314 }
315 }
316
317 /**
318 * add TYPO3 Constraints for all tables to the queryBuilder
319 *
320 * @param QueryInterface $query
321 */
322 protected function addTypo3Constraints(QueryInterface $query)
323 {
324 $index = 0;
325 foreach ($this->tableAliasMap as $tableAlias => $tableName) {
326 if ($index === 0 || !$this->configurationManager->isFeatureEnabled('consistentTranslationOverlayHandling')) {
327 // With the new behaviour enabled, we only add the pid and language check for the first table (aggregate root).
328 // We know the first table is always the main table for the current query run.
329 $additionalWhereClauses = $this->getAdditionalWhereClause($query->getQuerySettings(), $tableName, $tableAlias);
330 } else {
331 $additionalWhereClauses = [];
332 }
333 $index++;
334 $statement = $this->getVisibilityConstraintStatement($query->getQuerySettings(), $tableName, $tableAlias);
335 if ($statement !== '') {
336 $additionalWhereClauses[] = $statement;
337 }
338 if (!empty($additionalWhereClauses)) {
339 if (in_array($tableAlias, $this->unionTableAliasCache, true)) {
340 $this->queryBuilder->andWhere(
341 $this->queryBuilder->expr()->orX(
342 $this->queryBuilder->expr()->andX(...$additionalWhereClauses),
343 $this->queryBuilder->expr()->isNull($tableAlias . '.uid')
344 )
345 );
346 } else {
347 $this->queryBuilder->andWhere(...$additionalWhereClauses);
348 }
349 }
350 }
351 }
352
353 /**
354 * Parse a Comparison into SQL and parameter arrays.
355 *
356 * @param Qom\ComparisonInterface $comparison The comparison to parse
357 * @param Qom\SourceInterface $source The source
358 * @return string
359 * @throws \RuntimeException
360 * @throws RepositoryException
361 * @throws BadConstraintException
362 */
363 protected function parseComparison(Qom\ComparisonInterface $comparison, Qom\SourceInterface $source)
364 {
365 if ($comparison->getOperator() === QueryInterface::OPERATOR_CONTAINS) {
366 if ($comparison->getOperand2() === null) {
367 throw new BadConstraintException('The value for the CONTAINS operator must not be null.', 1484828468);
368 }
369 $value = $this->dataMapper->getPlainValue($comparison->getOperand2());
370 if (!$source instanceof Qom\SelectorInterface) {
371 throw new \RuntimeException('Source is not of type "SelectorInterface"', 1395362539);
372 }
373 $className = $source->getNodeTypeName();
374 $tableName = $this->dataMapper->convertClassNameToTableName($className);
375 $operand1 = $comparison->getOperand1();
376 $propertyName = $operand1->getPropertyName();
377 $fullPropertyPath = '';
378 while (strpos($propertyName, '.') !== false) {
379 $this->addUnionStatement($className, $tableName, $propertyName, $fullPropertyPath);
380 }
381 $columnName = $this->dataMapper->convertPropertyNameToColumnName($propertyName, $className);
382 $dataMap = $this->dataMapper->getDataMap($className);
383 $columnMap = $dataMap->getColumnMap($propertyName);
384 $typeOfRelation = $columnMap instanceof ColumnMap ? $columnMap->getTypeOfRelation() : null;
385 if ($typeOfRelation === ColumnMap::RELATION_HAS_AND_BELONGS_TO_MANY) {
386 $relationTableName = $columnMap->getRelationTableName();
387 $queryBuilderForSubselect = $this->queryBuilder->getConnection()->createQueryBuilder();
388 $queryBuilderForSubselect
389 ->select($columnMap->getParentKeyFieldName())
390 ->from($relationTableName)
391 ->where(
392 $queryBuilderForSubselect->expr()->eq(
393 $columnMap->getChildKeyFieldName(),
394 $this->queryBuilder->createNamedParameter($value)
395 )
396 );
397 $additionalWhereForMatchFields = $this->getAdditionalMatchFieldsStatement($queryBuilderForSubselect->expr(), $columnMap, $relationTableName, $relationTableName);
398 if ($additionalWhereForMatchFields) {
399 $queryBuilderForSubselect->andWhere($additionalWhereForMatchFields);
400 }
401
402 return $this->queryBuilder->expr()->comparison(
403 $this->queryBuilder->quoteIdentifier($tableName . '.uid'),
404 'IN',
405 '(' . $queryBuilderForSubselect->getSQL() . ')'
406 );
407 }
408 if ($typeOfRelation === ColumnMap::RELATION_HAS_MANY) {
409 $parentKeyFieldName = $columnMap->getParentKeyFieldName();
410 if (isset($parentKeyFieldName)) {
411 $childTableName = $columnMap->getChildTableName();
412
413 // Build the SQL statement of the subselect
414 $queryBuilderForSubselect = $this->queryBuilder->getConnection()->createQueryBuilder();
415 $queryBuilderForSubselect
416 ->select($parentKeyFieldName)
417 ->from($childTableName)
418 ->where(
419 $queryBuilderForSubselect->expr()->eq(
420 'uid',
421 (int)$value
422 )
423 );
424
425 // Add it to the main query
426 return $this->queryBuilder->expr()->eq(
427 $tableName . '.uid',
428 '(' . $queryBuilderForSubselect->getSQL() . ')'
429 );
430 }
431 return $this->queryBuilder->expr()->inSet(
432 $tableName . '.' . $columnName,
433 $this->queryBuilder->quote($value)
434 );
435 }
436 throw new RepositoryException('Unsupported or non-existing property name "' . $propertyName . '" used in relation matching.', 1327065745);
437 }
438 return $this->parseDynamicOperand($comparison, $source);
439 }
440
441 /**
442 * Parse a DynamicOperand into SQL and parameter arrays.
443 *
444 * @param Qom\ComparisonInterface $comparison
445 * @param Qom\SourceInterface $source The source
446 * @return string
447 * @throws Exception
448 * @throws BadConstraintException
449 */
450 protected function parseDynamicOperand(Qom\ComparisonInterface $comparison, Qom\SourceInterface $source)
451 {
452 $value = $comparison->getOperand2();
453 $fieldName = $this->parseOperand($comparison->getOperand1(), $source);
454 $expr = null;
455 $exprBuilder = $this->queryBuilder->expr();
456 switch ($comparison->getOperator()) {
457 case QueryInterface::OPERATOR_IN:
458 $hasValue = false;
459 $plainValues = [];
460 foreach ($value as $singleValue) {
461 $plainValue = $this->dataMapper->getPlainValue($singleValue);
462 if ($plainValue !== null) {
463 $hasValue = true;
464 $plainValues[] = $this->createTypedNamedParameter($singleValue);
465 }
466 }
467 if (!$hasValue) {
468 throw new BadConstraintException(
469 'The IN operator needs a non-empty value list to compare against. ' .
470 'The given value list is empty.',
471 1484828466
472 );
473 }
474 $expr = $exprBuilder->comparison($fieldName, 'IN', '(' . implode(', ', $plainValues) . ')');
475 break;
476 case QueryInterface::OPERATOR_EQUAL_TO:
477 if ($value === null) {
478 $expr = $fieldName . ' IS NULL';
479 } else {
480 $placeHolder = $this->createTypedNamedParameter($value);
481 $expr = $exprBuilder->comparison($fieldName, $exprBuilder::EQ, $placeHolder);
482 }
483 break;
484 case QueryInterface::OPERATOR_EQUAL_TO_NULL:
485 $expr = $fieldName . ' IS NULL';
486 break;
487 case QueryInterface::OPERATOR_NOT_EQUAL_TO:
488 if ($value === null) {
489 $expr = $fieldName . ' IS NOT NULL';
490 } else {
491 $placeHolder = $this->createTypedNamedParameter($value);
492 $expr = $exprBuilder->comparison($fieldName, $exprBuilder::NEQ, $placeHolder);
493 }
494 break;
495 case QueryInterface::OPERATOR_NOT_EQUAL_TO_NULL:
496 $expr = $fieldName . ' IS NOT NULL';
497 break;
498 case QueryInterface::OPERATOR_LESS_THAN:
499 $placeHolder = $this->createTypedNamedParameter($value);
500 $expr = $exprBuilder->comparison($fieldName, $exprBuilder::LT, $placeHolder);
501 break;
502 case QueryInterface::OPERATOR_LESS_THAN_OR_EQUAL_TO:
503 $placeHolder = $this->createTypedNamedParameter($value);
504 $expr = $exprBuilder->comparison($fieldName, $exprBuilder::LTE, $placeHolder);
505 break;
506 case QueryInterface::OPERATOR_GREATER_THAN:
507 $placeHolder = $this->createTypedNamedParameter($value);
508 $expr = $exprBuilder->comparison($fieldName, $exprBuilder::GT, $placeHolder);
509 break;
510 case QueryInterface::OPERATOR_GREATER_THAN_OR_EQUAL_TO:
511 $placeHolder = $this->createTypedNamedParameter($value);
512 $expr = $exprBuilder->comparison($fieldName, $exprBuilder::GTE, $placeHolder);
513 break;
514 case QueryInterface::OPERATOR_LIKE:
515 $placeHolder = $this->createTypedNamedParameter($value, \PDO::PARAM_STR);
516 $expr = $exprBuilder->comparison($fieldName, 'LIKE', $placeHolder);
517 break;
518 default:
519 throw new Exception(
520 'Unsupported operator encountered.',
521 1242816073
522 );
523 }
524 return $expr;
525 }
526
527 /**
528 * Maps plain value of operand to PDO types to help Doctrine and/or the database driver process the value
529 * correctly when building the query.
530 *
531 * @param mixed $value The parameter value
532 * @return int
533 * @throws \InvalidArgumentException
534 */
535 protected function getParameterType($value): int
536 {
537 $parameterType = gettype($value);
538 switch ($parameterType) {
539 case 'integer':
540 return \PDO::PARAM_INT;
541 case 'string':
542 return \PDO::PARAM_STR;
543 default:
544 throw new \InvalidArgumentException(
545 'Unsupported parameter type encountered. Expected integer or string, ' . $parameterType . ' given.',
546 1494878863
547 );
548 }
549 }
550
551 /**
552 * Create a named parameter for the QueryBuilder and guess the parameter type based on the
553 * output of DataMapper::getPlainValue(). The type of the named parameter can be forced to
554 * one of the \PDO::PARAM_* types by specifying the $forceType argument.
555 *
556 * @param mixed $value The input value that should be sent to the database
557 * @param int|null $forceType The \PDO::PARAM_* type that should be forced
558 * @return string The placeholder string to be used in the query
559 * @see \TYPO3\CMS\Extbase\Persistence\Generic\Mapper\DataMapper::getPlainValue()
560 */
561 protected function createTypedNamedParameter($value, int $forceType = null): string
562 {
563 $consistentHandlingEnabled = $this->configurationManager->isFeatureEnabled('consistentTranslationOverlayHandling');
564 if ($consistentHandlingEnabled
565 && $value instanceof AbstractDomainObject
566 && $value->_hasProperty('_localizedUid')
567 && $value->_getProperty('_localizedUid') > 0
568 ) {
569 $plainValue = (int)$value->_getProperty('_localizedUid');
570 } else {
571 $plainValue = $this->dataMapper->getPlainValue($value);
572 }
573 $parameterType = $forceType ?? $this->getParameterType($plainValue);
574 $placeholder = $this->queryBuilder->createNamedParameter($plainValue, $parameterType);
575
576 return $placeholder;
577 }
578
579 /**
580 * @param Qom\DynamicOperandInterface $operand
581 * @param Qom\SourceInterface $source The source
582 * @return string
583 * @throws \InvalidArgumentException
584 */
585 protected function parseOperand(Qom\DynamicOperandInterface $operand, Qom\SourceInterface $source)
586 {
587 if ($operand instanceof Qom\LowerCaseInterface) {
588 $constraintSQL = 'LOWER(' . $this->parseOperand($operand->getOperand(), $source) . ')';
589 } elseif ($operand instanceof Qom\UpperCaseInterface) {
590 $constraintSQL = 'UPPER(' . $this->parseOperand($operand->getOperand(), $source) . ')';
591 } elseif ($operand instanceof Qom\PropertyValueInterface) {
592 $propertyName = $operand->getPropertyName();
593 $className = '';
594 if ($source instanceof Qom\SelectorInterface) {
595 $className = $source->getNodeTypeName();
596 $tableName = $this->dataMapper->convertClassNameToTableName($className);
597 $fullPropertyPath = '';
598 while (strpos($propertyName, '.') !== false) {
599 $this->addUnionStatement($className, $tableName, $propertyName, $fullPropertyPath);
600 }
601 } elseif ($source instanceof Qom\JoinInterface) {
602 $tableName = $source->getJoinCondition()->getSelector1Name();
603 }
604 $columnName = $this->dataMapper->convertPropertyNameToColumnName($propertyName, $className);
605 $constraintSQL = (!empty($tableName) ? $tableName . '.' : '') . $columnName;
606 $constraintSQL = $this->queryBuilder->getConnection()->quoteIdentifier($constraintSQL);
607 } else {
608 throw new \InvalidArgumentException('Given operand has invalid type "' . get_class($operand) . '".', 1395710211);
609 }
610 return $constraintSQL;
611 }
612
613 /**
614 * Add a constraint to ensure that the record type of the returned tuples is matching the data type of the repository.
615 *
616 * @param string $className The class name
617 */
618 protected function addRecordTypeConstraint($className)
619 {
620 if ($className !== null) {
621 $dataMap = $this->dataMapper->getDataMap($className);
622 if ($dataMap->getRecordTypeColumnName() !== null) {
623 $recordTypes = [];
624 if ($dataMap->getRecordType() !== null) {
625 $recordTypes[] = $dataMap->getRecordType();
626 }
627 foreach ($dataMap->getSubclasses() as $subclassName) {
628 $subclassDataMap = $this->dataMapper->getDataMap($subclassName);
629 if ($subclassDataMap->getRecordType() !== null) {
630 $recordTypes[] = $subclassDataMap->getRecordType();
631 }
632 }
633 if (!empty($recordTypes)) {
634 $recordTypeStatements = [];
635 foreach ($recordTypes as $recordType) {
636 $tableName = $dataMap->getTableName();
637 $recordTypeStatements[] = $this->queryBuilder->expr()->eq(
638 $tableName . '.' . $dataMap->getRecordTypeColumnName(),
639 $this->queryBuilder->createNamedParameter($recordType)
640 );
641 }
642 $this->queryBuilder->andWhere(
643 $this->queryBuilder->expr()->orX(...$recordTypeStatements)
644 );
645 }
646 }
647 }
648 }
649
650 /**
651 * Builds a condition for filtering records by the configured match field,
652 * e.g. MM_match_fields, foreign_match_fields or foreign_table_field.
653 *
654 * @param ExpressionBuilder $exprBuilder
655 * @param ColumnMap $columnMap The column man for which the condition should be build.
656 * @param string $childTableAlias The alias of the child record table used in the query.
657 * @param string $parentTable The real name of the parent table (used for building the foreign_table_field condition).
658 * @return string The match field conditions or an empty string.
659 */
660 protected function getAdditionalMatchFieldsStatement($exprBuilder, $columnMap, $childTableAlias, $parentTable = null)
661 {
662 $additionalWhereForMatchFields = [];
663 $relationTableMatchFields = $columnMap->getRelationTableMatchFields();
664 if (is_array($relationTableMatchFields) && !empty($relationTableMatchFields)) {
665 foreach ($relationTableMatchFields as $fieldName => $value) {
666 $additionalWhereForMatchFields[] = $exprBuilder->eq($childTableAlias . '.' . $fieldName, $this->queryBuilder->createNamedParameter($value));
667 }
668 }
669
670 if (isset($parentTable)) {
671 $parentTableFieldName = $columnMap->getParentTableFieldName();
672 if (!empty($parentTableFieldName)) {
673 $additionalWhereForMatchFields[] = $exprBuilder->eq($childTableAlias . '.' . $parentTableFieldName, $this->queryBuilder->createNamedParameter($parentTable));
674 }
675 }
676
677 if (!empty($additionalWhereForMatchFields)) {
678 return $exprBuilder->andX(...$additionalWhereForMatchFields);
679 }
680 return '';
681 }
682
683 /**
684 * Adds additional WHERE statements according to the query settings.
685 *
686 * @param QuerySettingsInterface $querySettings The TYPO3 CMS specific query settings
687 * @param string $tableName The table name to add the additional where clause for
688 * @param string $tableAlias The table alias used in the query.
689 * @return array
690 */
691 protected function getAdditionalWhereClause(QuerySettingsInterface $querySettings, $tableName, $tableAlias = null)
692 {
693 $whereClause = [];
694 if ($querySettings->getRespectSysLanguage()) {
695 if ($this->configurationManager->isFeatureEnabled('consistentTranslationOverlayHandling')) {
696 $systemLanguageStatement = $this->getLanguageStatement($tableName, $tableAlias, $querySettings);
697 } else {
698 $systemLanguageStatement = $this->getSysLanguageStatement($tableName, $tableAlias, $querySettings);
699 }
700
701 if (!empty($systemLanguageStatement)) {
702 $whereClause[] = $systemLanguageStatement;
703 }
704 }
705
706 if ($querySettings->getRespectStoragePage()) {
707 $pageIdStatement = $this->getPageIdStatement($tableName, $tableAlias, $querySettings->getStoragePageIds());
708 if (!empty($pageIdStatement)) {
709 $whereClause[] = $pageIdStatement;
710 }
711 }
712
713 return $whereClause;
714 }
715
716 /**
717 * Adds enableFields and deletedClause to the query if necessary
718 *
719 * @param QuerySettingsInterface $querySettings
720 * @param string $tableName The database table name
721 * @param string $tableAlias
722 * @return string
723 */
724 protected function getVisibilityConstraintStatement(QuerySettingsInterface $querySettings, $tableName, $tableAlias)
725 {
726 $statement = '';
727 if (is_array($GLOBALS['TCA'][$tableName]['ctrl'] ?? null)) {
728 $ignoreEnableFields = $querySettings->getIgnoreEnableFields();
729 $enableFieldsToBeIgnored = $querySettings->getEnableFieldsToBeIgnored();
730 $includeDeleted = $querySettings->getIncludeDeleted();
731 if ($this->environmentService->isEnvironmentInFrontendMode()) {
732 $statement .= $this->getFrontendConstraintStatement($tableName, $ignoreEnableFields, $enableFieldsToBeIgnored, $includeDeleted);
733 } else {
734 // TYPO3_MODE === 'BE'
735 $statement .= $this->getBackendConstraintStatement($tableName, $ignoreEnableFields, $includeDeleted);
736 }
737 if (!empty($statement)) {
738 $statement = $this->replaceTableNameWithAlias($statement, $tableName, $tableAlias);
739 $statement = strtolower(substr($statement, 1, 3)) === 'and' ? substr($statement, 5) : $statement;
740 }
741 }
742 return $statement;
743 }
744
745 /**
746 * Returns constraint statement for frontend context
747 *
748 * @param string $tableName
749 * @param bool $ignoreEnableFields A flag indicating whether the enable fields should be ignored
750 * @param array $enableFieldsToBeIgnored If $ignoreEnableFields is true, this array specifies enable fields to be ignored. If it is NULL or an empty array (default) all enable fields are ignored.
751 * @param bool $includeDeleted A flag indicating whether deleted records should be included
752 * @return string
753 * @throws InconsistentQuerySettingsException
754 */
755 protected function getFrontendConstraintStatement($tableName, $ignoreEnableFields, array $enableFieldsToBeIgnored = [], $includeDeleted)
756 {
757 $statement = '';
758 if ($ignoreEnableFields && !$includeDeleted) {
759 if (!empty($enableFieldsToBeIgnored)) {
760 // array_combine() is necessary because of the way \TYPO3\CMS\Frontend\Page\PageRepository::enableFields() is implemented
761 $statement .= $this->getPageRepository()->enableFields($tableName, -1, array_combine($enableFieldsToBeIgnored, $enableFieldsToBeIgnored));
762 } elseif (!empty($GLOBALS['TCA'][$tableName]['ctrl']['delete'])) {
763 $statement .= ' AND ' . $tableName . '.' . $GLOBALS['TCA'][$tableName]['ctrl']['delete'] . '=0';
764 }
765 } elseif (!$ignoreEnableFields && !$includeDeleted) {
766 $statement .= $this->getPageRepository()->enableFields($tableName);
767 } elseif (!$ignoreEnableFields && $includeDeleted) {
768 throw new InconsistentQuerySettingsException('Query setting "ignoreEnableFields=FALSE" can not be used together with "includeDeleted=TRUE" in frontend context.', 1460975922);
769 }
770 return $statement;
771 }
772
773 /**
774 * Returns constraint statement for backend context
775 *
776 * @param string $tableName
777 * @param bool $ignoreEnableFields A flag indicating whether the enable fields should be ignored
778 * @param bool $includeDeleted A flag indicating whether deleted records should be included
779 * @return string
780 */
781 protected function getBackendConstraintStatement($tableName, $ignoreEnableFields, $includeDeleted)
782 {
783 $statement = '';
784 if (!$ignoreEnableFields) {
785 $statement .= BackendUtility::BEenableFields($tableName);
786 }
787 if (!$includeDeleted && !empty($GLOBALS['TCA'][$tableName]['ctrl']['delete'])) {
788 $statement .= ' AND ' . $tableName . '.' . $GLOBALS['TCA'][$tableName]['ctrl']['delete'] . '=0';
789 }
790 return $statement;
791 }
792
793 /**
794 * Builds the language field statement in a legacy way (when consistentTranslationOverlayHandling flag is disabled)
795 *
796 * @param string $tableName The database table name
797 * @param string $tableAlias The table alias used in the query.
798 * @param QuerySettingsInterface $querySettings The TYPO3 CMS specific query settings
799 * @return string
800 */
801 protected function getSysLanguageStatement($tableName, $tableAlias, $querySettings)
802 {
803 if (is_array($GLOBALS['TCA'][$tableName]['ctrl'])) {
804 if (!empty($GLOBALS['TCA'][$tableName]['ctrl']['languageField'])) {
805 // Select all entries for the current language
806 // If any language is set -> get those entries which are not translated yet
807 // They will be removed by \TYPO3\CMS\Frontend\Page\PageRepository::getRecordOverlay if not matching overlay mode
808 $languageField = $GLOBALS['TCA'][$tableName]['ctrl']['languageField'];
809
810 if (isset($GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField'])
811 && $querySettings->getLanguageUid() > 0
812 ) {
813 $mode = $querySettings->getLanguageMode();
814
815 if ($mode === 'strict') {
816 $queryBuilderForSubselect = $this->queryBuilder->getConnection()->createQueryBuilder();
817 $queryBuilderForSubselect->getRestrictions()->removeAll()->add(new DeletedRestriction());
818 $queryBuilderForSubselect
819 ->select($tableName . '.' . $GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField'])
820 ->from($tableName)
821 ->where(
822 $queryBuilderForSubselect->expr()->andX(
823 $queryBuilderForSubselect->expr()->gt($tableName . '.' . $GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField'], 0),
824 $queryBuilderForSubselect->expr()->eq($tableName . '.' . $languageField, (int)$querySettings->getLanguageUid())
825 )
826 );
827 return $this->queryBuilder->expr()->orX(
828 $this->queryBuilder->expr()->eq($tableAlias . '.' . $languageField, -1),
829 $this->queryBuilder->expr()->andX(
830 $this->queryBuilder->expr()->eq($tableAlias . '.' . $languageField, (int)$querySettings->getLanguageUid()),
831 $this->queryBuilder->expr()->eq($tableAlias . '.' . $GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField'], 0)
832 ),
833 $this->queryBuilder->expr()->andX(
834 $this->queryBuilder->expr()->eq($tableAlias . '.' . $languageField, 0),
835 $this->queryBuilder->expr()->in(
836 $tableAlias . '.uid',
837 $queryBuilderForSubselect->getSQL()
838
839 )
840 )
841 );
842 }
843 $queryBuilderForSubselect = $this->queryBuilder->getConnection()->createQueryBuilder();
844 $queryBuilderForSubselect->getRestrictions()->removeAll()->add(new DeletedRestriction());
845 $queryBuilderForSubselect
846 ->select($tableAlias . '.' . $GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField'])
847 ->from($tableName)
848 ->where(
849 $queryBuilderForSubselect->expr()->andX(
850 $queryBuilderForSubselect->expr()->gt($tableName . '.' . $GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField'], 0),
851 $queryBuilderForSubselect->expr()->eq($tableName . '.' . $languageField, (int)$querySettings->getLanguageUid())
852 )
853 );
854 return $this->queryBuilder->expr()->orX(
855 $this->queryBuilder->expr()->in($tableAlias . '.' . $languageField, [(int)$querySettings->getLanguageUid(), -1]),
856 $this->queryBuilder->expr()->andX(
857 $this->queryBuilder->expr()->eq($tableAlias . '.' . $languageField, 0),
858 $this->queryBuilder->expr()->notIn(
859 $tableAlias . '.uid',
860 $queryBuilderForSubselect->getSQL()
861
862 )
863 )
864 );
865 }
866 return $this->queryBuilder->expr()->in(
867 $tableAlias . '.' . $languageField,
868 [(int)$querySettings->getLanguageUid(), -1]
869 );
870 }
871 }
872 return '';
873 }
874
875 /**
876 * Builds the language field statement
877 *
878 * @param string $tableName The database table name
879 * @param string $tableAlias The table alias used in the query.
880 * @param QuerySettingsInterface $querySettings The TYPO3 CMS specific query settings
881 * @return string
882 */
883 protected function getLanguageStatement($tableName, $tableAlias, QuerySettingsInterface $querySettings)
884 {
885 if (empty($GLOBALS['TCA'][$tableName]['ctrl']['languageField'])) {
886 return '';
887 }
888
889 // Select all entries for the current language
890 // If any language is set -> get those entries which are not translated yet
891 // They will be removed by \TYPO3\CMS\Frontend\Page\PageRepository::getRecordOverlay if not matching overlay mode
892 $languageField = $GLOBALS['TCA'][$tableName]['ctrl']['languageField'];
893
894 $transOrigPointerField = $GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField'] ?? '';
895 if (!$transOrigPointerField || !$querySettings->getLanguageUid()) {
896 return $this->queryBuilder->expr()->in(
897 $tableAlias . '.' . $languageField,
898 [(int)$querySettings->getLanguageUid(), -1]
899 );
900 }
901
902 $mode = $querySettings->getLanguageOverlayMode();
903 if (!$mode) {
904 return $this->queryBuilder->expr()->in(
905 $tableAlias . '.' . $languageField,
906 [(int)$querySettings->getLanguageUid(), -1]
907 );
908 }
909
910 $defLangTableAlias = $tableAlias . '_dl';
911 $defaultLanguageRecordsSubSelect = $this->queryBuilder->getConnection()->createQueryBuilder();
912 $defaultLanguageRecordsSubSelect
913 ->select($defLangTableAlias . '.uid')
914 ->from($tableName, $defLangTableAlias)
915 ->where(
916 $defaultLanguageRecordsSubSelect->expr()->andX(
917 $defaultLanguageRecordsSubSelect->expr()->eq($defLangTableAlias . '.' . $transOrigPointerField, 0),
918 $defaultLanguageRecordsSubSelect->expr()->eq($defLangTableAlias . '.' . $languageField, 0)
919 )
920 );
921
922 $andConditions = [];
923 // records in language 'all'
924 $andConditions[] = $this->queryBuilder->expr()->eq($tableAlias . '.' . $languageField, -1);
925 // translated records where a default translation exists
926 $andConditions[] = $this->queryBuilder->expr()->andX(
927 $this->queryBuilder->expr()->eq($tableAlias . '.' . $languageField, (int)$querySettings->getLanguageUid()),
928 $this->queryBuilder->expr()->in(
929 $tableAlias . '.' . $transOrigPointerField,
930 $defaultLanguageRecordsSubSelect->getSQL()
931 )
932 );
933 if ($mode !== 'hideNonTranslated') {
934 // $mode = TRUE
935 // returns records from current language which have default translation
936 // together with not translated default language records
937 $translatedOnlyTableAlias = $tableAlias . '_to';
938 $queryBuilderForSubselect = $this->queryBuilder->getConnection()->createQueryBuilder();
939 $queryBuilderForSubselect
940 ->select($translatedOnlyTableAlias . '.' . $transOrigPointerField)
941 ->from($tableName, $translatedOnlyTableAlias)
942 ->where(
943 $queryBuilderForSubselect->expr()->andX(
944 $queryBuilderForSubselect->expr()->gt($translatedOnlyTableAlias . '.' . $transOrigPointerField, 0),
945 $queryBuilderForSubselect->expr()->eq($translatedOnlyTableAlias . '.' . $languageField, (int)$querySettings->getLanguageUid())
946 )
947 );
948 // records in default language, which do not have a translation
949 $andConditions[] = $this->queryBuilder->expr()->andX(
950 $this->queryBuilder->expr()->eq($tableAlias . '.' . $languageField, 0),
951 $this->queryBuilder->expr()->notIn(
952 $tableAlias . '.uid',
953 $queryBuilderForSubselect->getSQL()
954 )
955 );
956 }
957
958 return $this->queryBuilder->expr()->orX(...$andConditions);
959 }
960
961 /**
962 * Builds the page ID checking statement
963 *
964 * @param string $tableName The database table name
965 * @param string $tableAlias The table alias used in the query.
966 * @param array $storagePageIds list of storage page ids
967 * @return string
968 * @throws InconsistentQuerySettingsException
969 */
970 protected function getPageIdStatement($tableName, $tableAlias, array $storagePageIds)
971 {
972 if (!is_array($GLOBALS['TCA'][$tableName]['ctrl'])) {
973 return '';
974 }
975
976 $rootLevel = (int)$GLOBALS['TCA'][$tableName]['ctrl']['rootLevel'];
977 switch ($rootLevel) {
978 // Only in pid 0
979 case 1:
980 $storagePageIds = [0];
981 break;
982 // Pid 0 and pagetree
983 case -1:
984 if (empty($storagePageIds)) {
985 $storagePageIds = [0];
986 } else {
987 $storagePageIds[] = 0;
988 }
989 break;
990 // Only pagetree or not set
991 case 0:
992 if (empty($storagePageIds)) {
993 throw new InconsistentQuerySettingsException('Missing storage page ids.', 1365779762);
994 }
995 break;
996 // Invalid configuration
997 default:
998 return '';
999 }
1000 $storagePageIds = array_map('intval', $storagePageIds);
1001 if (count($storagePageIds) === 1) {
1002 return $this->queryBuilder->expr()->eq($tableAlias . '.pid', reset($storagePageIds));
1003 }
1004 return $this->queryBuilder->expr()->in($tableAlias . '.pid', $storagePageIds);
1005 }
1006
1007 /**
1008 * Transforms a Join into SQL and parameter arrays
1009 *
1010 * @param Qom\JoinInterface $join The join
1011 * @param string $leftTableAlias The alias from the table to main
1012 */
1013 protected function parseJoin(Qom\JoinInterface $join, $leftTableAlias)
1014 {
1015 $leftSource = $join->getLeft();
1016 $leftClassName = $leftSource->getNodeTypeName();
1017 $this->addRecordTypeConstraint($leftClassName);
1018 $rightSource = $join->getRight();
1019 if ($rightSource instanceof Qom\JoinInterface) {
1020 $left = $rightSource->getLeft();
1021 $rightClassName = $left->getNodeTypeName();
1022 $rightTableName = $left->getSelectorName();
1023 } else {
1024 $rightClassName = $rightSource->getNodeTypeName();
1025 $rightTableName = $rightSource->getSelectorName();
1026 $this->queryBuilder->addSelect($rightTableName . '.*');
1027 }
1028 $this->addRecordTypeConstraint($rightClassName);
1029 $rightTableAlias = $this->getUniqueAlias($rightTableName);
1030 $joinCondition = $join->getJoinCondition();
1031 $joinConditionExpression = null;
1032 $this->unionTableAliasCache[] = $rightTableAlias;
1033 if ($joinCondition instanceof Qom\EquiJoinCondition) {
1034 $column1Name = $this->dataMapper->convertPropertyNameToColumnName($joinCondition->getProperty1Name(), $leftClassName);
1035 $column2Name = $this->dataMapper->convertPropertyNameToColumnName($joinCondition->getProperty2Name(), $rightClassName);
1036
1037 $joinConditionExpression = $this->queryBuilder->expr()->eq(
1038 $leftTableAlias . '.' . $column1Name,
1039 $this->queryBuilder->quoteIdentifier($rightTableAlias . '.' . $column2Name)
1040 );
1041 }
1042 $this->queryBuilder->leftJoin($leftTableAlias, $rightTableName, $rightTableAlias, $joinConditionExpression);
1043 if ($rightSource instanceof Qom\JoinInterface) {
1044 $this->parseJoin($rightSource, $rightTableAlias);
1045 }
1046 }
1047
1048 /**
1049 * Generates a unique alias for the given table and the given property path.
1050 * The property path will be mapped to the generated alias in the tablePropertyMap.
1051 *
1052 * @param string $tableName The name of the table for which the alias should be generated.
1053 * @param string $fullPropertyPath The full property path that is related to the given table.
1054 * @return string The generated table alias.
1055 */
1056 protected function getUniqueAlias($tableName, $fullPropertyPath = null)
1057 {
1058 if (isset($fullPropertyPath) && isset($this->tablePropertyMap[$fullPropertyPath])) {
1059 return $this->tablePropertyMap[$fullPropertyPath];
1060 }
1061
1062 $alias = $tableName;
1063 $i = 0;
1064 while (isset($this->tableAliasMap[$alias])) {
1065 $alias = $tableName . $i;
1066 $i++;
1067 }
1068
1069 $this->tableAliasMap[$alias] = $tableName;
1070
1071 if (isset($fullPropertyPath)) {
1072 $this->tablePropertyMap[$fullPropertyPath] = $alias;
1073 }
1074
1075 return $alias;
1076 }
1077
1078 /**
1079 * adds a union statement to the query, mostly for tables referenced in the where condition.
1080 * The property for which the union statement is generated will be appended.
1081 *
1082 * @param string &$className The name of the parent class, will be set to the child class after processing.
1083 * @param string &$tableName The name of the parent table, will be set to the table alias that is used in the union statement.
1084 * @param string &$propertyPath The remaining property path, will be cut of by one part during the process.
1085 * @param string $fullPropertyPath The full path the the current property, will be used to make table names unique.
1086 * @throws Exception
1087 * @throws InvalidRelationConfigurationException
1088 * @throws MissingColumnMapException
1089 */
1090 protected function addUnionStatement(&$className, &$tableName, &$propertyPath, &$fullPropertyPath)
1091 {
1092 $explodedPropertyPath = explode('.', $propertyPath, 2);
1093 $propertyName = $explodedPropertyPath[0];
1094 $columnName = $this->dataMapper->convertPropertyNameToColumnName($propertyName, $className);
1095 $realTableName = $this->dataMapper->convertClassNameToTableName($className);
1096 $tableName = $this->tablePropertyMap[$fullPropertyPath] ?? $realTableName;
1097 $columnMap = $this->dataMapper->getDataMap($className)->getColumnMap($propertyName);
1098
1099 if ($columnMap === null) {
1100 throw new MissingColumnMapException('The ColumnMap for property "' . $propertyName . '" of class "' . $className . '" is missing.', 1355142232);
1101 }
1102
1103 $parentKeyFieldName = $columnMap->getParentKeyFieldName();
1104 $childTableName = $columnMap->getChildTableName();
1105
1106 if ($childTableName === null) {
1107 throw new InvalidRelationConfigurationException('The relation information for property "' . $propertyName . '" of class "' . $className . '" is missing.', 1353170925);
1108 }
1109
1110 $fullPropertyPath .= ($fullPropertyPath === '') ? $propertyName : '.' . $propertyName;
1111 $childTableAlias = $this->getUniqueAlias($childTableName, $fullPropertyPath);
1112
1113 // If there is already a union with the current identifier we do not need to build it again and exit early.
1114 if (in_array($childTableAlias, $this->unionTableAliasCache, true)) {
1115 $propertyPath = $explodedPropertyPath[1];
1116 $tableName = $childTableAlias;
1117 $className = $this->dataMapper->getType($className, $propertyName);
1118 return;
1119 }
1120
1121 if ($columnMap->getTypeOfRelation() === ColumnMap::RELATION_HAS_ONE) {
1122 if (isset($parentKeyFieldName)) {
1123 // @todo: no test for this part yet
1124 $joinConditionExpression = $this->queryBuilder->expr()->eq(
1125 $tableName . '.uid',
1126 $this->queryBuilder->quoteIdentifier($childTableAlias . '.' . $parentKeyFieldName)
1127 );
1128 } else {
1129 $joinConditionExpression = $this->queryBuilder->expr()->eq(
1130 $tableName . '.' . $columnName,
1131 $this->queryBuilder->quoteIdentifier($childTableAlias . '.uid')
1132 );
1133 }
1134 $this->queryBuilder->leftJoin($tableName, $childTableName, $childTableAlias, $joinConditionExpression);
1135 $this->unionTableAliasCache[] = $childTableAlias;
1136 $this->queryBuilder->andWhere(
1137 $this->getAdditionalMatchFieldsStatement($this->queryBuilder->expr(), $columnMap, $childTableAlias, $realTableName)
1138 );
1139 } elseif ($columnMap->getTypeOfRelation() === ColumnMap::RELATION_HAS_MANY) {
1140 // @todo: no tests for this part yet
1141 if (isset($parentKeyFieldName)) {
1142 $joinConditionExpression = $this->queryBuilder->expr()->eq(
1143 $tableName . '.uid',
1144 $this->queryBuilder->quoteIdentifier($childTableAlias . '.' . $parentKeyFieldName)
1145 );
1146 } else {
1147 $joinConditionExpression = $this->queryBuilder->expr()->inSet(
1148 $tableName . '.' . $columnName,
1149 $this->queryBuilder->quoteIdentifier($childTableAlias . '.uid'),
1150 true
1151 );
1152 }
1153 $this->queryBuilder->leftJoin($tableName, $childTableName, $childTableAlias, $joinConditionExpression);
1154 $this->unionTableAliasCache[] = $childTableAlias;
1155 $this->queryBuilder->andWhere(
1156 $this->getAdditionalMatchFieldsStatement($this->queryBuilder->expr(), $columnMap, $childTableAlias, $realTableName)
1157 );
1158 $this->suggestDistinctQuery = true;
1159 } elseif ($columnMap->getTypeOfRelation() === ColumnMap::RELATION_HAS_AND_BELONGS_TO_MANY) {
1160 $relationTableName = $columnMap->getRelationTableName();
1161 $relationTableAlias = $this->getUniqueAlias($relationTableName, $fullPropertyPath . '_mm');
1162
1163 $joinConditionExpression = $this->queryBuilder->expr()->andX(
1164 $this->queryBuilder->expr()->eq(
1165 $tableName . '.uid',
1166 $this->queryBuilder->quoteIdentifier(
1167 $relationTableAlias . '.' . $columnMap->getParentKeyFieldName()
1168 )
1169 ),
1170 $this->getAdditionalMatchFieldsStatement($this->queryBuilder->expr(), $columnMap, $relationTableAlias, $realTableName)
1171 );
1172 $this->queryBuilder->leftJoin($tableName, $relationTableName, $relationTableAlias, $joinConditionExpression);
1173 $joinConditionExpression = $this->queryBuilder->expr()->eq(
1174 $relationTableAlias . '.' . $columnMap->getChildKeyFieldName(),
1175 $this->queryBuilder->quoteIdentifier($childTableAlias . '.uid')
1176 );
1177 $this->queryBuilder->leftJoin($relationTableAlias, $childTableName, $childTableAlias, $joinConditionExpression);
1178 $this->unionTableAliasCache[] = $childTableAlias;
1179 $this->suggestDistinctQuery = true;
1180 } else {
1181 throw new Exception('Could not determine type of relation.', 1252502725);
1182 }
1183 $propertyPath = $explodedPropertyPath[1];
1184 $tableName = $childTableAlias;
1185 $className = $this->dataMapper->getType($className, $propertyName);
1186 }
1187
1188 /**
1189 * If the table name does not match the table alias all occurrences of
1190 * "tableName." are replaced with "tableAlias." in the given SQL statement.
1191 *
1192 * @param string $statement The SQL statement in which the values are replaced.
1193 * @param string $tableName The table name that is replaced.
1194 * @param string $tableAlias The table alias that replaced the table name.
1195 * @return string The modified SQL statement.
1196 */
1197 protected function replaceTableNameWithAlias($statement, $tableName, $tableAlias)
1198 {
1199 if ($tableAlias !== $tableName) {
1200 /** @var Connection $connection */
1201 $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($tableName);
1202 $quotedTableName = $connection->quoteIdentifier($tableName);
1203 $quotedTableAlias = $connection->quoteIdentifier($tableAlias);
1204 $statement = str_replace(
1205 [$tableName . '.', $quotedTableName . '.'],
1206 [$tableAlias . '.', $quotedTableAlias . '.'],
1207 $statement
1208 );
1209 }
1210
1211 return $statement;
1212 }
1213
1214 /**
1215 * @return PageRepository
1216 */
1217 protected function getPageRepository()
1218 {
1219 if (!$this->pageRepository instanceof PageRepository) {
1220 $this->pageRepository = GeneralUtility::makeInstance(PageRepository::class);
1221 }
1222 return $this->pageRepository;
1223 }
1224 }