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