Passed
Push — master ( 1fba08...8fefaa )
by
unknown
13:03
created

Typo3DbQueryParser::initializeQueryBuilder()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 32
Code Lines 24

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 24
nc 3
nop 1
dl 0
loc 32
rs 9.536
c 0
b 0
f 0
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 TYPO3\CMS\Backend\Utility\BackendUtility;
19
use TYPO3\CMS\Core\Context\Context;
20
use TYPO3\CMS\Core\Database\Connection;
21
use TYPO3\CMS\Core\Database\ConnectionPool;
22
use TYPO3\CMS\Core\Database\Query\Expression\CompositeExpression;
23
use TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder;
24
use TYPO3\CMS\Core\Database\Query\QueryBuilder;
25
use TYPO3\CMS\Core\Domain\Repository\PageRepository;
26
use TYPO3\CMS\Core\Utility\GeneralUtility;
27
use TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface;
28
use TYPO3\CMS\Extbase\DomainObject\AbstractDomainObject;
29
use TYPO3\CMS\Extbase\Object\ObjectManagerInterface;
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
use TYPO3\CMS\Extbase\Service\EnvironmentService;
56
57
/**
58
 * QueryParser, converting the qom to string representation
59
 * @internal only to be used within Extbase, not part of TYPO3 Core API.
60
 */
61
class Typo3DbQueryParser
62
{
63
    /**
64
     * @var DataMapper
65
     */
66
    protected $dataMapper;
67
68
    /**
69
     * The TYPO3 page repository. Used for language and workspace overlay
70
     *
71
     * @var PageRepository
72
     */
73
    protected $pageRepository;
74
75
    /**
76
     * @var EnvironmentService
77
     */
78
    protected $environmentService;
79
80
    /**
81
     * @var ConfigurationManagerInterface
82
     */
83
    protected $configurationManager;
84
85
    /**
86
     * Instance of the Doctrine query builder
87
     *
88
     * @var QueryBuilder
89
     */
90
    protected $queryBuilder;
91
92
    /**
93
     * Maps domain model properties to their corresponding table aliases that are used in the query, e.g.:
94
     *
95
     * 'property1' => 'tableName',
96
     * 'property1.property2' => 'tableName1',
97
     *
98
     * @var array
99
     */
100
    protected $tablePropertyMap = [];
101
102
    /**
103
     * Maps tablenames to their aliases to be used in where clauses etc.
104
     * Mainly used for joins on the same table etc.
105
     *
106
     * @var array<string, string>
107
     */
108
    protected $tableAliasMap = [];
109
110
    /**
111
     * Stores all tables used in for SQL joins
112
     *
113
     * @var array
114
     */
115
    protected $unionTableAliasCache = [];
116
117
    /**
118
     * @var string
119
     */
120
    protected $tableName = '';
121
122
    /**
123
     * @var bool
124
     */
125
    protected $suggestDistinctQuery = false;
126
127
    /**
128
     * @var ObjectManagerInterface
129
     */
130
    protected $objectManager;
131
132
    /**
133
     * @param ObjectManagerInterface $objectManager
134
     */
135
    public function injectObjectManager(ObjectManagerInterface $objectManager)
136
    {
137
        $this->objectManager = $objectManager;
138
    }
139
140
    /**
141
     * Object initialization called when object is created with ObjectManager, after constructor
142
     */
143
    public function initializeObject()
144
    {
145
        $this->dataMapper = $this->objectManager->get(DataMapper::class);
146
    }
147
148
    /**
149
     * @param EnvironmentService $environmentService
150
     */
151
    public function injectEnvironmentService(EnvironmentService $environmentService)
152
    {
153
        $this->environmentService = $environmentService;
154
    }
155
156
    /**
157
     * @param ConfigurationManagerInterface $configurationManager
158
     */
159
    public function injectConfigurationManager(ConfigurationManagerInterface $configurationManager)
160
    {
161
        $this->configurationManager = $configurationManager;
162
    }
163
164
    /**
165
     * Whether using a distinct query is suggested.
166
     * This information is defined during parsing of the current query
167
     * for RELATION_HAS_MANY & RELATION_HAS_AND_BELONGS_TO_MANY relations.
168
     *
169
     * @return bool
170
     */
171
    public function isDistinctQuerySuggested(): bool
172
    {
173
        return $this->suggestDistinctQuery;
174
    }
175
176
    /**
177
     * Returns a ready to be executed QueryBuilder object, based on the query
178
     *
179
     * @param QueryInterface $query
180
     * @return QueryBuilder
181
     */
182
    public function convertQueryToDoctrineQueryBuilder(QueryInterface $query)
183
    {
184
        // Reset all properties
185
        $this->tablePropertyMap = [];
186
        $this->tableAliasMap = [];
187
        $this->unionTableAliasCache = [];
188
        $this->tableName = '';
189
190
        if ($query->getStatement() && $query->getStatement()->getStatement() instanceof QueryBuilder) {
191
            $this->queryBuilder = clone $query->getStatement()->getStatement();
192
            return $this->queryBuilder;
193
        }
194
        // Find the right table name
195
        $source = $query->getSource();
196
        $this->initializeQueryBuilder($source);
197
198
        $constraint = $query->getConstraint();
199
        if ($constraint instanceof ConstraintInterface) {
200
            $wherePredicates = $this->parseConstraint($constraint, $source);
201
            if (!empty($wherePredicates)) {
202
                $this->queryBuilder->andWhere($wherePredicates);
203
            }
204
        }
205
206
        $this->parseOrderings($query->getOrderings(), $source);
207
        $this->addTypo3Constraints($query);
208
209
        return $this->queryBuilder;
210
    }
211
212
    /**
213
     * Creates the queryBuilder object whether it is a regular select or a JOIN
214
     *
215
     * @param Qom\SourceInterface $source The source
216
     */
217
    protected function initializeQueryBuilder(SourceInterface $source)
218
    {
219
        if ($source instanceof SelectorInterface) {
220
            $className = $source->getNodeTypeName();
221
            $tableName = $this->dataMapper->getDataMap($className)->getTableName();
222
            $this->tableName = $tableName;
223
224
            $this->queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
225
                ->getQueryBuilderForTable($tableName);
226
227
            $this->queryBuilder
228
                ->getRestrictions()
229
                ->removeAll();
230
231
            $tableAlias = $this->getUniqueAlias($tableName);
232
233
            $this->queryBuilder
234
                ->select($tableAlias . '.*')
235
                ->from($tableName, $tableAlias);
236
237
            $this->addRecordTypeConstraint($className);
238
        } elseif ($source instanceof JoinInterface) {
239
            $leftSource = $source->getLeft();
240
            $leftTableName = $leftSource->getSelectorName();
241
242
            $this->queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
243
                ->getQueryBuilderForTable($leftTableName);
244
            $leftTableAlias = $this->getUniqueAlias($leftTableName);
245
            $this->queryBuilder
246
                ->select($leftTableAlias . '.*')
247
                ->from($leftTableName, $leftTableAlias);
248
            $this->parseJoin($source, $leftTableAlias);
249
        }
250
    }
251
252
    /**
253
     * Transforms a constraint into SQL and parameter arrays
254
     *
255
     * @param Qom\ConstraintInterface $constraint The constraint
256
     * @param Qom\SourceInterface $source The source
257
     * @return CompositeExpression|string
258
     * @throws \RuntimeException
259
     */
260
    protected function parseConstraint(ConstraintInterface $constraint, SourceInterface $source)
261
    {
262
        if ($constraint instanceof AndInterface) {
263
            return $this->queryBuilder->expr()->andX(
264
                $this->parseConstraint($constraint->getConstraint1(), $source),
265
                $this->parseConstraint($constraint->getConstraint2(), $source)
266
            );
267
        }
268
        if ($constraint instanceof OrInterface) {
269
            return $this->queryBuilder->expr()->orX(
270
                $this->parseConstraint($constraint->getConstraint1(), $source),
271
                $this->parseConstraint($constraint->getConstraint2(), $source)
272
            );
273
        }
274
        if ($constraint instanceof NotInterface) {
275
            return ' NOT(' . $this->parseConstraint($constraint->getConstraint(), $source) . ')';
276
        }
277
        if ($constraint instanceof 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, 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 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 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) {
327
                // 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(ComparisonInterface $comparison, SourceInterface $source)
364
    {
365
        if ($comparison->getOperator() === QueryInterface::OPERATOR_CONTAINS) {
0 ignored issues
show
introduced by
The condition $comparison->getOperator...face::OPERATOR_CONTAINS is always false.
Loading history...
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 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
                /** @var ColumnMap $columnMap */
387
                $relationTableName = (string)$columnMap->getRelationTableName();
388
                $queryBuilderForSubselect = $this->queryBuilder->getConnection()->createQueryBuilder();
389
                $queryBuilderForSubselect
390
                        ->select($columnMap->getParentKeyFieldName())
391
                        ->from($relationTableName)
392
                        ->where(
393
                            $queryBuilderForSubselect->expr()->eq(
394
                                $columnMap->getChildKeyFieldName(),
395
                                $this->queryBuilder->createNamedParameter($value)
396
                            )
397
                        );
398
                $additionalWhereForMatchFields = $this->getAdditionalMatchFieldsStatement($queryBuilderForSubselect->expr(), $columnMap, $relationTableName, $relationTableName);
399
                if ($additionalWhereForMatchFields) {
400
                    $queryBuilderForSubselect->andWhere($additionalWhereForMatchFields);
401
                }
402
403
                return $this->queryBuilder->expr()->comparison(
404
                    $this->queryBuilder->quoteIdentifier($tableName . '.uid'),
405
                    'IN',
406
                    '(' . $queryBuilderForSubselect->getSQL() . ')'
407
                );
408
            }
409
            if ($typeOfRelation === ColumnMap::RELATION_HAS_MANY) {
410
                $parentKeyFieldName = $columnMap->getParentKeyFieldName();
411
                if (isset($parentKeyFieldName)) {
412
                    $childTableName = $columnMap->getChildTableName();
413
414
                    // Build the SQL statement of the subselect
415
                    $queryBuilderForSubselect = $this->queryBuilder->getConnection()->createQueryBuilder();
416
                    $queryBuilderForSubselect
417
                            ->select($parentKeyFieldName)
418
                            ->from($childTableName)
419
                            ->where(
420
                                $queryBuilderForSubselect->expr()->eq(
421
                                    'uid',
422
                                    (int)$value
423
                                )
424
                            );
425
426
                    // Add it to the main query
427
                    return $this->queryBuilder->expr()->eq(
428
                        $tableName . '.uid',
429
                        '(' . $queryBuilderForSubselect->getSQL() . ')'
430
                    );
431
                }
432
                return $this->queryBuilder->expr()->inSet(
433
                    $tableName . '.' . $columnName,
434
                    $this->queryBuilder->quote($value)
435
                );
436
            }
437
            throw new RepositoryException('Unsupported or non-existing property name "' . $propertyName . '" used in relation matching.', 1327065745);
438
        }
439
        return $this->parseDynamicOperand($comparison, $source);
440
    }
441
442
    /**
443
     * Parse a DynamicOperand into SQL and parameter arrays.
444
     *
445
     * @param Qom\ComparisonInterface $comparison
446
     * @param Qom\SourceInterface $source The source
447
     * @return string
448
     * @throws Exception
449
     * @throws BadConstraintException
450
     */
451
    protected function parseDynamicOperand(ComparisonInterface $comparison, SourceInterface $source)
452
    {
453
        $value = $comparison->getOperand2();
454
        $fieldName = $this->parseOperand($comparison->getOperand1(), $source);
455
        $expr = null;
456
        $exprBuilder = $this->queryBuilder->expr();
457
        switch ($comparison->getOperator()) {
458
            case QueryInterface::OPERATOR_IN:
459
                $hasValue = false;
460
                $plainValues = [];
461
                foreach ($value as $singleValue) {
462
                    $plainValue = $this->dataMapper->getPlainValue($singleValue);
463
                    if ($plainValue !== null) {
464
                        $hasValue = true;
465
                        $plainValues[] = $this->createTypedNamedParameter($singleValue);
466
                    }
467
                }
468
                if (!$hasValue) {
469
                    throw new BadConstraintException(
470
                        'The IN operator needs a non-empty value list to compare against. ' .
471
                        'The given value list is empty.',
472
                        1484828466
473
                    );
474
                }
475
                $expr = $exprBuilder->comparison($fieldName, 'IN', '(' . implode(', ', $plainValues) . ')');
476
                break;
477
            case QueryInterface::OPERATOR_EQUAL_TO:
478
                if ($value === null) {
479
                    $expr = $fieldName . ' IS NULL';
480
                } else {
481
                    $placeHolder = $this->createTypedNamedParameter($value);
482
                    $expr = $exprBuilder->comparison($fieldName, $exprBuilder::EQ, $placeHolder);
483
                }
484
                break;
485
            case QueryInterface::OPERATOR_EQUAL_TO_NULL:
486
                $expr = $fieldName . ' IS NULL';
487
                break;
488
            case QueryInterface::OPERATOR_NOT_EQUAL_TO:
489
                if ($value === null) {
490
                    $expr = $fieldName . ' IS NOT NULL';
491
                } else {
492
                    $placeHolder = $this->createTypedNamedParameter($value);
493
                    $expr = $exprBuilder->comparison($fieldName, $exprBuilder::NEQ, $placeHolder);
494
                }
495
                break;
496
            case QueryInterface::OPERATOR_NOT_EQUAL_TO_NULL:
497
                $expr = $fieldName . ' IS NOT NULL';
498
                break;
499
            case QueryInterface::OPERATOR_LESS_THAN:
500
                $placeHolder = $this->createTypedNamedParameter($value);
501
                $expr = $exprBuilder->comparison($fieldName, $exprBuilder::LT, $placeHolder);
502
                break;
503
            case QueryInterface::OPERATOR_LESS_THAN_OR_EQUAL_TO:
504
                $placeHolder = $this->createTypedNamedParameter($value);
505
                $expr = $exprBuilder->comparison($fieldName, $exprBuilder::LTE, $placeHolder);
506
                break;
507
            case QueryInterface::OPERATOR_GREATER_THAN:
508
                $placeHolder = $this->createTypedNamedParameter($value);
509
                $expr = $exprBuilder->comparison($fieldName, $exprBuilder::GT, $placeHolder);
510
                break;
511
            case QueryInterface::OPERATOR_GREATER_THAN_OR_EQUAL_TO:
512
                $placeHolder = $this->createTypedNamedParameter($value);
513
                $expr = $exprBuilder->comparison($fieldName, $exprBuilder::GTE, $placeHolder);
514
                break;
515
            case QueryInterface::OPERATOR_LIKE:
516
                $placeHolder = $this->createTypedNamedParameter($value, \PDO::PARAM_STR);
517
                $expr = $exprBuilder->comparison($fieldName, 'LIKE', $placeHolder);
518
                break;
519
            default:
520
                throw new Exception(
521
                    'Unsupported operator encountered.',
522
                    1242816073
523
                );
524
        }
525
        return $expr;
526
    }
527
528
    /**
529
     * Maps plain value of operand to PDO types to help Doctrine and/or the database driver process the value
530
     * correctly when building the query.
531
     *
532
     * @param mixed $value The parameter value
533
     * @return int
534
     * @throws \InvalidArgumentException
535
     */
536
    protected function getParameterType($value): int
537
    {
538
        $parameterType = gettype($value);
539
        switch ($parameterType) {
540
            case 'integer':
541
                return \PDO::PARAM_INT;
542
            case 'string':
543
                return \PDO::PARAM_STR;
544
            default:
545
                throw new \InvalidArgumentException(
546
                    'Unsupported parameter type encountered. Expected integer or string, ' . $parameterType . ' given.',
547
                    1494878863
548
                );
549
        }
550
    }
551
552
    /**
553
     * Create a named parameter for the QueryBuilder and guess the parameter type based on the
554
     * output of DataMapper::getPlainValue(). The type of the named parameter can be forced to
555
     * one of the \PDO::PARAM_* types by specifying the $forceType argument.
556
     *
557
     * @param mixed $value The input value that should be sent to the database
558
     * @param int|null $forceType The \PDO::PARAM_* type that should be forced
559
     * @return string The placeholder string to be used in the query
560
     * @see \TYPO3\CMS\Extbase\Persistence\Generic\Mapper\DataMapper::getPlainValue()
561
     */
562
    protected function createTypedNamedParameter($value, int $forceType = null): string
563
    {
564
        if ($value instanceof AbstractDomainObject
565
            && $value->_hasProperty('_localizedUid')
566
            && $value->_getProperty('_localizedUid') > 0
567
        ) {
568
            $plainValue = (int)$value->_getProperty('_localizedUid');
569
        } else {
570
            $plainValue = $this->dataMapper->getPlainValue($value);
571
        }
572
        $parameterType = $forceType ?? $this->getParameterType($plainValue);
573
        $placeholder = $this->queryBuilder->createNamedParameter($plainValue, $parameterType);
574
575
        return $placeholder;
576
    }
577
578
    /**
579
     * @param Qom\DynamicOperandInterface $operand
580
     * @param Qom\SourceInterface $source The source
581
     * @return string
582
     * @throws \InvalidArgumentException
583
     */
584
    protected function parseOperand(DynamicOperandInterface $operand, SourceInterface $source)
585
    {
586
        $tableName = null;
587
        if ($operand instanceof LowerCaseInterface) {
588
            $constraintSQL = 'LOWER(' . $this->parseOperand($operand->getOperand(), $source) . ')';
589
        } elseif ($operand instanceof UpperCaseInterface) {
590
            $constraintSQL = 'UPPER(' . $this->parseOperand($operand->getOperand(), $source) . ')';
591
        } elseif ($operand instanceof PropertyValueInterface) {
592
            $propertyName = $operand->getPropertyName();
593
            $className = '';
594
            if ($source instanceof 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 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) {
0 ignored issues
show
introduced by
The condition $className !== null is always true.
Loading history...
621
            $dataMap = $this->dataMapper->getDataMap($className);
622
            if ($dataMap->getRecordTypeColumnName() !== null) {
0 ignored issues
show
introduced by
The condition $dataMap->getRecordTypeColumnName() !== null is always true.
Loading history...
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
        $tableAlias = (string)$tableAlias;
694
        // todo: $tableAlias must not be null
695
696
        $whereClause = [];
697
        if ($querySettings->getRespectSysLanguage()) {
698
            $systemLanguageStatement = $this->getLanguageStatement($tableName, $tableAlias, $querySettings);
699
            if (!empty($systemLanguageStatement)) {
700
                $whereClause[] = $systemLanguageStatement;
701
            }
702
        }
703
704
        if ($querySettings->getRespectStoragePage()) {
705
            $pageIdStatement = $this->getPageIdStatement($tableName, $tableAlias, $querySettings->getStoragePageIds());
706
            if (!empty($pageIdStatement)) {
707
                $whereClause[] = $pageIdStatement;
708
            }
709
        } elseif (!empty($GLOBALS['TCA'][$tableName]['ctrl']['versioningWS'])) {
710
            // Always prevent workspace records from being returned
711
            $whereClause[] = $this->queryBuilder->expr()->eq($tableAlias . '.t3ver_oid', 0);
712
        }
713
714
        return $whereClause;
715
    }
716
717
    /**
718
     * Adds enableFields and deletedClause to the query if necessary
719
     *
720
     * @param QuerySettingsInterface $querySettings
721
     * @param string $tableName The database table name
722
     * @param string $tableAlias
723
     * @return string
724
     */
725
    protected function getVisibilityConstraintStatement(QuerySettingsInterface $querySettings, $tableName, $tableAlias)
726
    {
727
        $statement = '';
728
        if (is_array($GLOBALS['TCA'][$tableName]['ctrl'] ?? null)) {
729
            $ignoreEnableFields = $querySettings->getIgnoreEnableFields();
730
            $enableFieldsToBeIgnored = $querySettings->getEnableFieldsToBeIgnored();
731
            $includeDeleted = $querySettings->getIncludeDeleted();
732
            if ($this->environmentService->isEnvironmentInFrontendMode()) {
733
                $statement .= $this->getFrontendConstraintStatement($tableName, $ignoreEnableFields, $enableFieldsToBeIgnored, $includeDeleted);
734
            } else {
735
                // TYPO3_MODE === 'BE'
736
                $statement .= $this->getBackendConstraintStatement($tableName, $ignoreEnableFields, $includeDeleted);
737
            }
738
            if (!empty($statement)) {
739
                $statement = $this->replaceTableNameWithAlias($statement, $tableName, $tableAlias);
740
                $statement = strtolower(substr($statement, 1, 3)) === 'and' ? substr($statement, 5) : $statement;
741
            }
742
        }
743
        return $statement;
744
    }
745
746
    /**
747
     * Returns constraint statement for frontend context
748
     *
749
     * @param string $tableName
750
     * @param bool $ignoreEnableFields A flag indicating whether the enable fields should be ignored
751
     * @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.
752
     * @param bool $includeDeleted A flag indicating whether deleted records should be included
753
     * @return string
754
     * @throws InconsistentQuerySettingsException
755
     */
756
    protected function getFrontendConstraintStatement($tableName, $ignoreEnableFields, array $enableFieldsToBeIgnored, $includeDeleted)
757
    {
758
        $statement = '';
759
        if ($ignoreEnableFields && !$includeDeleted) {
760
            if (!empty($enableFieldsToBeIgnored)) {
761
                // array_combine() is necessary because of the way \TYPO3\CMS\Core\Domain\Repository\PageRepository::enableFields() is implemented
762
                $statement .= $this->getPageRepository()->enableFields($tableName, -1, array_combine($enableFieldsToBeIgnored, $enableFieldsToBeIgnored));
0 ignored issues
show
Bug introduced by
It seems like array_combine($enableFie...nableFieldsToBeIgnored) can also be of type false; however, parameter $ignore_array of TYPO3\CMS\Core\Domain\Re...ository::enableFields() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

762
                $statement .= $this->getPageRepository()->enableFields($tableName, -1, /** @scrutinizer ignore-type */ array_combine($enableFieldsToBeIgnored, $enableFieldsToBeIgnored));
Loading history...
763
            } elseif (!empty($GLOBALS['TCA'][$tableName]['ctrl']['delete'])) {
764
                $statement .= ' AND ' . $tableName . '.' . $GLOBALS['TCA'][$tableName]['ctrl']['delete'] . '=0';
765
            }
766
        } elseif (!$ignoreEnableFields && !$includeDeleted) {
767
            $statement .= $this->getPageRepository()->enableFields($tableName);
768
        } elseif (!$ignoreEnableFields && $includeDeleted) {
769
            throw new InconsistentQuerySettingsException('Query setting "ignoreEnableFields=FALSE" can not be used together with "includeDeleted=TRUE" in frontend context.', 1460975922);
770
        }
771
        return $statement;
772
    }
773
774
    /**
775
     * Returns constraint statement for backend context
776
     *
777
     * @param string $tableName
778
     * @param bool $ignoreEnableFields A flag indicating whether the enable fields should be ignored
779
     * @param bool $includeDeleted A flag indicating whether deleted records should be included
780
     * @return string
781
     */
782
    protected function getBackendConstraintStatement($tableName, $ignoreEnableFields, $includeDeleted)
783
    {
784
        $statement = '';
785
        // In case of versioning-preview, enableFields are ignored (checked in Typo3DbBackend::doLanguageAndWorkspaceOverlay)
786
        $isUserInWorkspace = GeneralUtility::makeInstance(Context::class)->getPropertyFromAspect('workspace', 'isOffline');
787
        if (!$ignoreEnableFields && !$isUserInWorkspace) {
788
            $statement .= BackendUtility::BEenableFields($tableName);
789
        }
790
        if (!$includeDeleted && !empty($GLOBALS['TCA'][$tableName]['ctrl']['delete'])) {
791
            $statement .= ' AND ' . $tableName . '.' . $GLOBALS['TCA'][$tableName]['ctrl']['delete'] . '=0';
792
        }
793
        return $statement;
794
    }
795
796
    /**
797
     * Builds the language field statement
798
     *
799
     * @param string $tableName The database table name
800
     * @param string $tableAlias The table alias used in the query.
801
     * @param QuerySettingsInterface $querySettings The TYPO3 CMS specific query settings
802
     * @return string
803
     */
804
    protected function getLanguageStatement($tableName, $tableAlias, QuerySettingsInterface $querySettings)
805
    {
806
        if (empty($GLOBALS['TCA'][$tableName]['ctrl']['languageField'])) {
807
            return '';
808
        }
809
810
        // Select all entries for the current language
811
        // If any language is set -> get those entries which are not translated yet
812
        // They will be removed by \TYPO3\CMS\Core\Domain\Repository\PageRepository::getRecordOverlay if not matching overlay mode
813
        $languageField = $GLOBALS['TCA'][$tableName]['ctrl']['languageField'];
814
815
        $transOrigPointerField = $GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField'] ?? '';
816
        if (!$transOrigPointerField || !$querySettings->getLanguageUid()) {
817
            return $this->queryBuilder->expr()->in(
818
                $tableAlias . '.' . $languageField,
819
                [(int)$querySettings->getLanguageUid(), -1]
820
            );
821
        }
822
823
        $mode = $querySettings->getLanguageOverlayMode();
824
        if (!$mode) {
825
            return $this->queryBuilder->expr()->in(
826
                $tableAlias . '.' . $languageField,
827
                [(int)$querySettings->getLanguageUid(), -1]
828
            );
829
        }
830
831
        $defLangTableAlias = $tableAlias . '_dl';
832
        $defaultLanguageRecordsSubSelect = $this->queryBuilder->getConnection()->createQueryBuilder();
833
        $defaultLanguageRecordsSubSelect
834
            ->select($defLangTableAlias . '.uid')
835
            ->from($tableName, $defLangTableAlias)
836
            ->where(
837
                $defaultLanguageRecordsSubSelect->expr()->andX(
838
                    $defaultLanguageRecordsSubSelect->expr()->eq($defLangTableAlias . '.' . $transOrigPointerField, 0),
839
                    $defaultLanguageRecordsSubSelect->expr()->eq($defLangTableAlias . '.' . $languageField, 0)
840
                )
841
            );
842
843
        $andConditions = [];
844
        // records in language 'all'
845
        $andConditions[] = $this->queryBuilder->expr()->eq($tableAlias . '.' . $languageField, -1);
846
        // translated records where a default language exists
847
        $andConditions[] = $this->queryBuilder->expr()->andX(
848
            $this->queryBuilder->expr()->eq($tableAlias . '.' . $languageField, (int)$querySettings->getLanguageUid()),
849
            $this->queryBuilder->expr()->in(
850
                $tableAlias . '.' . $transOrigPointerField,
851
                $defaultLanguageRecordsSubSelect->getSQL()
852
            )
853
        );
854
        if ($mode !== 'hideNonTranslated') {
855
            // $mode = TRUE
856
            // returns records from current language which have default language
857
            // together with not translated default language records
858
            $translatedOnlyTableAlias = $tableAlias . '_to';
859
            $queryBuilderForSubselect = $this->queryBuilder->getConnection()->createQueryBuilder();
860
            $queryBuilderForSubselect
861
                ->select($translatedOnlyTableAlias . '.' . $transOrigPointerField)
862
                ->from($tableName, $translatedOnlyTableAlias)
863
                ->where(
864
                    $queryBuilderForSubselect->expr()->andX(
865
                        $queryBuilderForSubselect->expr()->gt($translatedOnlyTableAlias . '.' . $transOrigPointerField, 0),
866
                        $queryBuilderForSubselect->expr()->eq($translatedOnlyTableAlias . '.' . $languageField, (int)$querySettings->getLanguageUid())
867
                    )
868
                );
869
            // records in default language, which do not have a translation
870
            $andConditions[] = $this->queryBuilder->expr()->andX(
871
                $this->queryBuilder->expr()->eq($tableAlias . '.' . $languageField, 0),
872
                $this->queryBuilder->expr()->notIn(
873
                    $tableAlias . '.uid',
874
                    $queryBuilderForSubselect->getSQL()
875
                )
876
            );
877
        }
878
879
        return $this->queryBuilder->expr()->orX(...$andConditions);
880
    }
881
882
    /**
883
     * Builds the page ID checking statement
884
     *
885
     * @param string $tableName The database table name
886
     * @param string $tableAlias The table alias used in the query.
887
     * @param array $storagePageIds list of storage page ids
888
     * @return string
889
     * @throws InconsistentQuerySettingsException
890
     */
891
    protected function getPageIdStatement($tableName, $tableAlias, array $storagePageIds)
892
    {
893
        if (!is_array($GLOBALS['TCA'][$tableName]['ctrl'])) {
894
            return '';
895
        }
896
897
        $rootLevel = (int)$GLOBALS['TCA'][$tableName]['ctrl']['rootLevel'];
898
        switch ($rootLevel) {
899
            // Only in pid 0
900
            case 1:
901
                $storagePageIds = [0];
902
                break;
903
            // Pid 0 and pagetree
904
            case -1:
905
                if (empty($storagePageIds)) {
906
                    $storagePageIds = [0];
907
                } else {
908
                    $storagePageIds[] = 0;
909
                }
910
                break;
911
            // Only pagetree or not set
912
            case 0:
913
                if (empty($storagePageIds)) {
914
                    throw new InconsistentQuerySettingsException('Missing storage page ids.', 1365779762);
915
                }
916
                break;
917
            // Invalid configuration
918
            default:
919
                return '';
920
        }
921
        $storagePageIds = array_map('intval', $storagePageIds);
922
        if (count($storagePageIds) === 1) {
923
            return $this->queryBuilder->expr()->eq($tableAlias . '.pid', reset($storagePageIds));
924
        }
925
        return $this->queryBuilder->expr()->in($tableAlias . '.pid', $storagePageIds);
926
    }
927
928
    /**
929
     * Transforms a Join into SQL and parameter arrays
930
     *
931
     * @param Qom\JoinInterface $join The join
932
     * @param string $leftTableAlias The alias from the table to main
933
     */
934
    protected function parseJoin(JoinInterface $join, $leftTableAlias)
935
    {
936
        $leftSource = $join->getLeft();
937
        $leftClassName = $leftSource->getNodeTypeName();
938
        $this->addRecordTypeConstraint($leftClassName);
939
        $rightSource = $join->getRight();
940
        if ($rightSource instanceof JoinInterface) {
941
            $left = $rightSource->getLeft();
942
            $rightClassName = $left->getNodeTypeName();
943
            $rightTableName = $left->getSelectorName();
944
        } else {
945
            $rightClassName = $rightSource->getNodeTypeName();
946
            $rightTableName = $rightSource->getSelectorName();
947
            $this->queryBuilder->addSelect($rightTableName . '.*');
948
        }
949
        $this->addRecordTypeConstraint($rightClassName);
950
        $rightTableAlias = $this->getUniqueAlias($rightTableName);
951
        $joinCondition = $join->getJoinCondition();
952
        $joinConditionExpression = null;
953
        $this->unionTableAliasCache[] = $rightTableAlias;
954
        if ($joinCondition instanceof EquiJoinCondition) {
955
            $column1Name = $this->dataMapper->convertPropertyNameToColumnName($joinCondition->getProperty1Name(), $leftClassName);
956
            $column2Name = $this->dataMapper->convertPropertyNameToColumnName($joinCondition->getProperty2Name(), $rightClassName);
957
958
            $joinConditionExpression = $this->queryBuilder->expr()->eq(
959
                $leftTableAlias . '.' . $column1Name,
960
                $this->queryBuilder->quoteIdentifier($rightTableAlias . '.' . $column2Name)
961
            );
962
        }
963
        $this->queryBuilder->leftJoin($leftTableAlias, $rightTableName, $rightTableAlias, $joinConditionExpression);
964
        if ($rightSource instanceof JoinInterface) {
965
            $this->parseJoin($rightSource, $rightTableAlias);
966
        }
967
    }
968
969
    /**
970
     * Generates a unique alias for the given table and the given property path.
971
     * The property path will be mapped to the generated alias in the tablePropertyMap.
972
     *
973
     * @param string $tableName The name of the table for which the alias should be generated.
974
     * @param string $fullPropertyPath The full property path that is related to the given table.
975
     * @return string The generated table alias.
976
     */
977
    protected function getUniqueAlias($tableName, $fullPropertyPath = null)
978
    {
979
        if (isset($fullPropertyPath) && isset($this->tablePropertyMap[$fullPropertyPath])) {
980
            return $this->tablePropertyMap[$fullPropertyPath];
981
        }
982
983
        $alias = $tableName;
984
        $i = 0;
985
        while (isset($this->tableAliasMap[$alias])) {
986
            $alias = $tableName . $i;
987
            $i++;
988
        }
989
990
        $this->tableAliasMap[$alias] = $tableName;
991
992
        if (isset($fullPropertyPath)) {
993
            $this->tablePropertyMap[$fullPropertyPath] = $alias;
994
        }
995
996
        return $alias;
997
    }
998
999
    /**
1000
     * adds a union statement to the query, mostly for tables referenced in the where condition.
1001
     * The property for which the union statement is generated will be appended.
1002
     *
1003
     * @param string $className The name of the parent class, will be set to the child class after processing.
1004
     * @param string $tableName The name of the parent table, will be set to the table alias that is used in the union statement.
1005
     * @param string $propertyPath The remaining property path, will be cut of by one part during the process.
1006
     * @param string $fullPropertyPath The full path the the current property, will be used to make table names unique.
1007
     * @throws Exception
1008
     * @throws InvalidRelationConfigurationException
1009
     * @throws MissingColumnMapException
1010
     */
1011
    protected function addUnionStatement(&$className, &$tableName, &$propertyPath, &$fullPropertyPath)
1012
    {
1013
        $explodedPropertyPath = explode('.', $propertyPath, 2);
1014
        $propertyName = $explodedPropertyPath[0];
1015
        $columnName = $this->dataMapper->convertPropertyNameToColumnName($propertyName, $className);
1016
        $realTableName = $this->dataMapper->convertClassNameToTableName($className);
1017
        $tableName = $this->tablePropertyMap[$fullPropertyPath] ?? $realTableName;
1018
        $columnMap = $this->dataMapper->getDataMap($className)->getColumnMap($propertyName);
1019
1020
        if ($columnMap === null) {
1021
            throw new MissingColumnMapException('The ColumnMap for property "' . $propertyName . '" of class "' . $className . '" is missing.', 1355142232);
1022
        }
1023
1024
        $parentKeyFieldName = $columnMap->getParentKeyFieldName();
1025
        $childTableName = $columnMap->getChildTableName();
1026
1027
        if ($childTableName === null) {
1028
            throw new InvalidRelationConfigurationException('The relation information for property "' . $propertyName . '" of class "' . $className . '" is missing.', 1353170925);
1029
        }
1030
1031
        $fullPropertyPath .= ($fullPropertyPath === '') ? $propertyName : '.' . $propertyName;
1032
        $childTableAlias = $this->getUniqueAlias($childTableName, $fullPropertyPath);
1033
1034
        // If there is already a union with the current identifier we do not need to build it again and exit early.
1035
        if (in_array($childTableAlias, $this->unionTableAliasCache, true)) {
1036
            $propertyPath = $explodedPropertyPath[1];
1037
            $tableName = $childTableAlias;
1038
            $className = $this->dataMapper->getType($className, $propertyName);
1039
            return;
1040
        }
1041
1042
        if ($columnMap->getTypeOfRelation() === ColumnMap::RELATION_HAS_ONE) {
1043
            if (isset($parentKeyFieldName)) {
1044
                // @todo: no test for this part yet
1045
                $joinConditionExpression = $this->queryBuilder->expr()->eq(
1046
                    $tableName . '.uid',
1047
                    $this->queryBuilder->quoteIdentifier($childTableAlias . '.' . $parentKeyFieldName)
1048
                );
1049
            } else {
1050
                $joinConditionExpression = $this->queryBuilder->expr()->eq(
1051
                    $tableName . '.' . $columnName,
1052
                    $this->queryBuilder->quoteIdentifier($childTableAlias . '.uid')
1053
                );
1054
            }
1055
            $this->queryBuilder->leftJoin($tableName, $childTableName, $childTableAlias, $joinConditionExpression);
1056
            $this->unionTableAliasCache[] = $childTableAlias;
1057
            $this->queryBuilder->andWhere(
1058
                $this->getAdditionalMatchFieldsStatement($this->queryBuilder->expr(), $columnMap, $childTableAlias, $realTableName)
1059
            );
1060
        } elseif ($columnMap->getTypeOfRelation() === ColumnMap::RELATION_HAS_MANY) {
1061
            // @todo: no tests for this part yet
1062
            if (isset($parentKeyFieldName)) {
1063
                $joinConditionExpression = $this->queryBuilder->expr()->eq(
1064
                    $tableName . '.uid',
1065
                    $this->queryBuilder->quoteIdentifier($childTableAlias . '.' . $parentKeyFieldName)
1066
                );
1067
            } else {
1068
                $joinConditionExpression = $this->queryBuilder->expr()->inSet(
1069
                    $tableName . '.' . $columnName,
1070
                    $this->queryBuilder->quoteIdentifier($childTableAlias . '.uid'),
1071
                    true
1072
                );
1073
            }
1074
            $this->queryBuilder->leftJoin($tableName, $childTableName, $childTableAlias, $joinConditionExpression);
1075
            $this->unionTableAliasCache[] = $childTableAlias;
1076
            $this->queryBuilder->andWhere(
1077
                $this->getAdditionalMatchFieldsStatement($this->queryBuilder->expr(), $columnMap, $childTableAlias, $realTableName)
1078
            );
1079
            $this->suggestDistinctQuery = true;
1080
        } elseif ($columnMap->getTypeOfRelation() === ColumnMap::RELATION_HAS_AND_BELONGS_TO_MANY) {
1081
            $relationTableName = (string)$columnMap->getRelationTableName();
1082
            $relationTableAlias = $this->getUniqueAlias($relationTableName, $fullPropertyPath . '_mm');
1083
1084
            $joinConditionExpression = $this->queryBuilder->expr()->andX(
1085
                $this->queryBuilder->expr()->eq(
1086
                    $tableName . '.uid',
1087
                    $this->queryBuilder->quoteIdentifier(
1088
                        $relationTableAlias . '.' . $columnMap->getParentKeyFieldName()
1089
                    )
1090
                ),
1091
                $this->getAdditionalMatchFieldsStatement($this->queryBuilder->expr(), $columnMap, $relationTableAlias, $realTableName)
1092
            );
1093
            $this->queryBuilder->leftJoin($tableName, $relationTableName, $relationTableAlias, $joinConditionExpression);
1094
            $joinConditionExpression = $this->queryBuilder->expr()->eq(
1095
                $relationTableAlias . '.' . $columnMap->getChildKeyFieldName(),
1096
                $this->queryBuilder->quoteIdentifier($childTableAlias . '.uid')
1097
            );
1098
            $this->queryBuilder->leftJoin($relationTableAlias, $childTableName, $childTableAlias, $joinConditionExpression);
1099
            $this->unionTableAliasCache[] = $childTableAlias;
1100
            $this->suggestDistinctQuery = true;
1101
        } else {
1102
            throw new Exception('Could not determine type of relation.', 1252502725);
1103
        }
1104
        $propertyPath = $explodedPropertyPath[1];
1105
        $tableName = $childTableAlias;
1106
        $className = $this->dataMapper->getType($className, $propertyName);
1107
    }
1108
1109
    /**
1110
     * If the table name does not match the table alias all occurrences of
1111
     * "tableName." are replaced with "tableAlias." in the given SQL statement.
1112
     *
1113
     * @param string $statement The SQL statement in which the values are replaced.
1114
     * @param string $tableName The table name that is replaced.
1115
     * @param string $tableAlias The table alias that replaced the table name.
1116
     * @return string The modified SQL statement.
1117
     */
1118
    protected function replaceTableNameWithAlias($statement, $tableName, $tableAlias)
1119
    {
1120
        if ($tableAlias !== $tableName) {
1121
            /** @var Connection $connection */
1122
            $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($tableName);
1123
            $quotedTableName = $connection->quoteIdentifier($tableName);
1124
            $quotedTableAlias = $connection->quoteIdentifier($tableAlias);
1125
            $statement = str_replace(
1126
                [$tableName . '.', $quotedTableName . '.'],
1127
                [$tableAlias . '.', $quotedTableAlias . '.'],
1128
                $statement
1129
            );
1130
        }
1131
1132
        return $statement;
1133
    }
1134
1135
    /**
1136
     * @return PageRepository
1137
     */
1138
    protected function getPageRepository()
1139
    {
1140
        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...
1141
            $this->pageRepository = GeneralUtility::makeInstance(PageRepository::class);
1142
        }
1143
        return $this->pageRepository;
1144
    }
1145
}
1146