Passed
Push — master ( a7d46b...0a8eff )
by
unknown
14:11
created

Typo3DbQueryParser::injectEnvironmentService()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

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