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