Typo3DbQueryParser   F
last analyzed

Complexity

Total Complexity 148

Size/Duplication

Total Lines 1038
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 148
eloc 520
dl 0
loc 1038
rs 2
c 0
b 0
f 0

25 Methods

Rating   Name   Duplication   Size   Complexity  
B getAdditionalMatchFieldsStatement() 0 21 7
B addRecordTypeConstraint() 0 26 8
B getPageIdStatement() 0 35 8
A isDistinctQuerySuggested() 0 3 1
A convertQueryToDoctrineQueryBuilder() 0 28 5
A __construct() 0 3 1
A addTypo3Constraints() 0 26 6
A parseConstraint() 0 21 5
B parseComparison() 0 77 10
A initializeQueryBuilder() 0 32 3
B parseOrderings() 0 23 8
B parseOperand() 0 27 8
A createTypedNamedParameter() 0 14 4
A getAdditionalWhereClause() 0 24 6
A getBackendConstraintStatement() 0 12 5
A parseJoin() 0 31 4
C addUnionStatement() 0 96 10
A getUniqueAlias() 0 20 5
B getLanguageStatement() 0 76 6
A getVisibilityConstraintStatement() 0 21 6
C parseDynamicOperand() 0 74 16
A getParameterType() 0 12 3
A replaceTableNameWithAlias() 0 15 2
B getFrontendConstraintStatement() 0 16 9
A getPageRepository() 0 6 2

How to fix   Complexity   

Complex Class

Complex classes like Typo3DbQueryParser often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Typo3DbQueryParser, and based on these observations, apply Extract Interface, too.

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