0f51fe82bb4cb1dcd0816bc9332ab3f1f72f07f1
[Packages/TYPO3.CMS.git] / typo3 / sysext / extbase / Classes / Persistence / Generic / Storage / Typo3DbQueryParser.php
1 <?php
2 namespace 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
17 use TYPO3\CMS\Backend\Utility\BackendUtility;
18 use TYPO3\CMS\Extbase\Persistence\Generic\Mapper\ColumnMap;
19 use TYPO3\CMS\Extbase\Persistence\Generic\QuerySettingsInterface;
20 use TYPO3\CMS\Extbase\Persistence\QueryInterface;
21 use TYPO3\CMS\Extbase\Persistence\Generic\Qom;
22
23 /**
24 * QueryParser, converting the qom to string representation
25 */
26 class Typo3DbQueryParser implements \TYPO3\CMS\Core\SingletonInterface {
27
28 /**
29 * The TYPO3 database object
30 *
31 * @var \TYPO3\CMS\Core\Database\DatabaseConnection
32 */
33 protected $databaseHandle;
34
35 /**
36 * @var \TYPO3\CMS\Extbase\Persistence\Generic\Mapper\DataMapper
37 */
38 protected $dataMapper;
39
40 /**
41 * The TYPO3 page repository. Used for language and workspace overlay
42 *
43 * @var \TYPO3\CMS\Frontend\Page\PageRepository
44 */
45 protected $pageRepository;
46
47 /**
48 * @var \TYPO3\CMS\Core\Cache\CacheManager
49 */
50 protected $cacheManager;
51
52 /**
53 * @var \TYPO3\CMS\Core\Cache\Frontend\VariableFrontend
54 */
55 protected $tableColumnCache;
56
57 /**
58 * @var \TYPO3\CMS\Extbase\Service\EnvironmentService
59 */
60 protected $environmentService;
61
62 /**
63 * @param \TYPO3\CMS\Extbase\Persistence\Generic\Mapper\DataMapper $dataMapper
64 */
65 public function injectDataMapper(\TYPO3\CMS\Extbase\Persistence\Generic\Mapper\DataMapper $dataMapper) {
66 $this->dataMapper = $dataMapper;
67 }
68
69 /**
70 * @param \TYPO3\CMS\Core\Cache\CacheManager $cacheManager
71 */
72 public function injectCacheManager(\TYPO3\CMS\Core\Cache\CacheManager $cacheManager) {
73 $this->cacheManager = $cacheManager;
74 }
75
76 /**
77 * @param \TYPO3\CMS\Extbase\Service\EnvironmentService $environmentService
78 */
79 public function injectEnvironmentService(\TYPO3\CMS\Extbase\Service\EnvironmentService $environmentService) {
80 $this->environmentService = $environmentService;
81 }
82
83 /**
84 * Maps domain model properties to their corresponding table aliases that are used in the query, e.g.:
85 *
86 * 'property1' => 'tableName',
87 * 'property1.property2' => 'tableName1',
88 *
89 * @var array
90 */
91 protected $tablePropertyMap = array();
92
93 /**
94 * Constructor. takes the database handle from $GLOBALS['TYPO3_DB']
95 */
96 public function __construct() {
97 $this->databaseHandle = $GLOBALS['TYPO3_DB'];
98 }
99
100 /**
101 * Lifecycle method
102 *
103 * @return void
104 */
105 public function initializeObject() {
106 $this->tableColumnCache = $this->cacheManager->getCache('extbase_typo3dbbackend_tablecolumns');
107 }
108
109 /**
110 * Preparses the query and returns the query's hash and the parameters
111 *
112 * @param QueryInterface $query The query
113 * @return array the hash and the parameters
114 */
115 public function preparseQuery(QueryInterface $query) {
116 list($parameters, $operators) = $this->preparseComparison($query->getConstraint());
117 $hashPartials = array(
118 $query->getQuerySettings(),
119 $query->getSource(),
120 array_keys($parameters),
121 $operators,
122 $query->getOrderings(),
123 );
124 $hash = md5(serialize($hashPartials));
125
126 return array($hash, $parameters);
127 }
128
129 /**
130 * Walks through the qom's constraints and extracts the properties and values.
131 *
132 * In the qom the query structure and values are glued together. This walks through the
133 * qom and only extracts the parts necessary for generating the hash and filling the
134 * statement. It leaves out the actual statement generation, as it is the most time
135 * consuming.
136 *
137 * @param Qom\ConstraintInterface $comparison The constraint. Could be And-, Or-, Not- or ComparisonInterface
138 * @param string $qomPath current position of the child in the qom
139 * @return array Array of parameters and operators
140 * @throws \Exception
141 */
142 protected function preparseComparison($comparison, $qomPath = '') {
143 $parameters = array();
144 $operators = array();
145 $objectsToParse = array();
146
147 $delimiter = '';
148 if ($comparison instanceof Qom\AndInterface) {
149 $delimiter = 'AND';
150 $objectsToParse = array($comparison->getConstraint1(), $comparison->getConstraint2());
151 } elseif ($comparison instanceof Qom\OrInterface) {
152 $delimiter = 'OR';
153 $objectsToParse = array($comparison->getConstraint1(), $comparison->getConstraint2());
154 } elseif ($comparison instanceof Qom\NotInterface) {
155 $delimiter = 'NOT';
156 $objectsToParse = array($comparison->getConstraint());
157 } elseif ($comparison instanceof Qom\ComparisonInterface) {
158 $operand1 = $comparison->getOperand1();
159 $parameterIdentifier = $this->normalizeParameterIdentifier($qomPath . $operand1->getPropertyName());
160 $comparison->setParameterIdentifier($parameterIdentifier);
161 $operator = $comparison->getOperator();
162 $operand2 = $comparison->getOperand2();
163 if ($operator === QueryInterface::OPERATOR_IN) {
164 $items = array();
165 foreach ($operand2 as $value) {
166 $value = $this->dataMapper->getPlainValue($value);
167 if ($value !== NULL) {
168 $items[] = $value;
169 }
170 }
171 $parameters[$parameterIdentifier] = $items;
172 } else {
173 $parameters[$parameterIdentifier] = $operand2;
174 }
175 $operators[] = $operator;
176 } elseif (!is_object($comparison)) {
177 $parameters = array(array(), $comparison);
178 return array($parameters, $operators);
179 } else {
180 throw new \Exception('Can not hash Query Component "' . get_class($comparison) . '".', 1392840462);
181 }
182
183 $childObjectIterator = 0;
184 foreach ($objectsToParse as $objectToParse) {
185 list($preparsedParameters, $preparsedOperators) = $this->preparseComparison($objectToParse, $qomPath . $delimiter . $childObjectIterator++);
186 if (!empty($preparsedParameters)) {
187 $parameters = array_merge($parameters, $preparsedParameters);
188 }
189 if (!empty($preparsedOperators)) {
190 $operators = array_merge($operators, $preparsedOperators);
191 }
192 }
193
194 return array($parameters, $operators);
195 }
196
197 /**
198 * normalizes the parameter's identifier
199 *
200 * @param string $identifier
201 * @return string
202 * @todo come on, clean up that method!
203 */
204 public function normalizeParameterIdentifier($identifier) {
205 return ':' . preg_replace('/[^A-Za-z0-9]/', '', $identifier);
206 }
207
208 /**
209 * Parses the query and returns the SQL statement parts.
210 *
211 * @param QueryInterface $query The query
212 * @return array The SQL statement parts
213 */
214 public function parseQuery(QueryInterface $query) {
215 $this->tablePropertyMap = array();
216 $sql = array();
217 $sql['keywords'] = array();
218 $sql['tables'] = array();
219 $sql['unions'] = array();
220 $sql['fields'] = array();
221 $sql['where'] = array();
222 $sql['additionalWhereClause'] = array();
223 $sql['orderings'] = array();
224 $sql['limit'] = ((int)$query->getLimit() ?: NULL);
225 $sql['offset'] = ((int)$query->getOffset() ?: NULL);
226 $sql['tableAliasMap'] = array();
227 $source = $query->getSource();
228 $this->parseSource($source, $sql);
229 $this->parseConstraint($query->getConstraint(), $source, $sql);
230 $this->parseOrderings($query->getOrderings(), $source, $sql);
231
232 foreach ($sql['tableAliasMap'] as $tableAlias => $tableName) {
233 $additionalWhereClause = $this->getAdditionalWhereClause($query->getQuerySettings(), $tableName, $tableAlias);
234 if ($additionalWhereClause !== '') {
235 $additionalWhereClause = $this->addNullConditionToStatementIfRequired($sql, $additionalWhereClause, $tableAlias);
236 $sql['additionalWhereClause'][] = $additionalWhereClause;
237 }
238 }
239
240 return $sql;
241 }
242
243 /**
244 * Add query parts that MUST NOT be cached.
245 * Call this function for any query
246 *
247 * @param QuerySettingsInterface $querySettings
248 * @param array $sql
249 * @throws \InvalidArgumentException
250 * @return void
251 */
252 public function addDynamicQueryParts(QuerySettingsInterface $querySettings, array &$sql) {
253 if (!isset($sql['additionalWhereClause'])) {
254 throw new \InvalidArgumentException('Invalid statement given.', 1399512421);
255 }
256 foreach ($sql['tableAliasMap'] as $tableAlias => $tableName) {
257 $statement = $this->getVisibilityConstraintStatement($querySettings, $tableName, $tableAlias);
258 if ($statement !== '') {
259 $statement = $this->addNullConditionToStatementIfRequired($sql, $statement, $tableAlias);
260 $sql['additionalWhereClause'][] = $statement;
261 }
262 }
263 }
264
265 /**
266 * If the given table alias is used in a UNION statement it is required to
267 * add an additional condition that allows the fields of the joined table
268 * to be NULL. Otherwise the condition would be too strict and filter out
269 * records that are actually valid.
270 *
271 * @param array $sql The current SQL query parts.
272 * @param string $statement The SQL statement to which the NULL condition should be added.
273 * @param string $tableAlias The table alias used in the SQL statement.
274 * @return string The statement including the NULL condition or the original statement.
275 */
276 protected function addNullConditionToStatementIfRequired(array $sql, $statement, $tableAlias) {
277
278 if (isset($sql['unions'][$tableAlias])) {
279 $statement = '((' . $statement . ') OR ' . $tableAlias . '.uid' . ' IS NULL)';
280 }
281
282 return $statement;
283 }
284
285 /**
286 * Transforms a Query Source into SQL and parameter arrays
287 *
288 * @param Qom\SourceInterface $source The source
289 * @param array &$sql
290 * @return void
291 */
292 protected function parseSource(Qom\SourceInterface $source, array &$sql) {
293 if ($source instanceof Qom\SelectorInterface) {
294 $className = $source->getNodeTypeName();
295 $tableName = $this->dataMapper->getDataMap($className)->getTableName();
296 $this->addRecordTypeConstraint($className, $sql);
297 $tableName = $this->getUniqueAlias($sql, $tableName);
298 $sql['fields'][$tableName] = $tableName . '.*';
299 $sql['tables'][$tableName] = $tableName;
300 } elseif ($source instanceof Qom\JoinInterface) {
301 $this->parseJoin($source, $sql);
302 }
303 }
304
305 /**
306 * Transforms a constraint into SQL and parameter arrays
307 *
308 * @param Qom\ConstraintInterface $constraint The constraint
309 * @param Qom\SourceInterface $source The source
310 * @param array &$sql The query parts
311 * @return void
312 */
313 protected function parseConstraint(Qom\ConstraintInterface $constraint = NULL, Qom\SourceInterface $source, array &$sql) {
314 if ($constraint instanceof Qom\AndInterface) {
315 $sql['where'][] = '(';
316 $this->parseConstraint($constraint->getConstraint1(), $source, $sql);
317 $sql['where'][] = ' AND ';
318 $this->parseConstraint($constraint->getConstraint2(), $source, $sql);
319 $sql['where'][] = ')';
320 } elseif ($constraint instanceof Qom\OrInterface) {
321 $sql['where'][] = '(';
322 $this->parseConstraint($constraint->getConstraint1(), $source, $sql);
323 $sql['where'][] = ' OR ';
324 $this->parseConstraint($constraint->getConstraint2(), $source, $sql);
325 $sql['where'][] = ')';
326 } elseif ($constraint instanceof Qom\NotInterface) {
327 $sql['where'][] = 'NOT (';
328 $this->parseConstraint($constraint->getConstraint(), $source, $sql);
329 $sql['where'][] = ')';
330 } elseif ($constraint instanceof Qom\ComparisonInterface) {
331 $this->parseComparison($constraint, $source, $sql);
332 }
333 }
334
335 /**
336 * Transforms orderings into SQL.
337 *
338 * @param array $orderings An array of orderings (Qom\Ordering)
339 * @param Qom\SourceInterface $source The source
340 * @param array &$sql The query parts
341 * @throws \TYPO3\CMS\Extbase\Persistence\Generic\Exception\UnsupportedOrderException
342 * @return void
343 */
344 protected function parseOrderings(array $orderings, Qom\SourceInterface $source, array &$sql) {
345 foreach ($orderings as $propertyName => $order) {
346 switch ($order) {
347 case QueryInterface::ORDER_ASCENDING:
348 $order = 'ASC';
349 break;
350 case QueryInterface::ORDER_DESCENDING:
351 $order = 'DESC';
352 break;
353 default:
354 throw new \TYPO3\CMS\Extbase\Persistence\Generic\Exception\UnsupportedOrderException('Unsupported order encountered.', 1242816074);
355 }
356 $className = '';
357 $tableName = '';
358 if ($source instanceof Qom\SelectorInterface) {
359 $className = $source->getNodeTypeName();
360 $tableName = $this->dataMapper->convertClassNameToTableName($className);
361 $fullPropertyPath = '';
362 while (strpos($propertyName, '.') !== FALSE) {
363 $this->addUnionStatement($className, $tableName, $propertyName, $sql, $fullPropertyPath);
364 }
365 } elseif ($source instanceof Qom\JoinInterface) {
366 $tableName = $source->getLeft()->getSelectorName();
367 }
368 $columnName = $this->dataMapper->convertPropertyNameToColumnName($propertyName, $className);
369 if ($tableName !== '') {
370 $sql['orderings'][] = $tableName . '.' . $columnName . ' ' . $order;
371 } else {
372 $sql['orderings'][] = $columnName . ' ' . $order;
373 }
374 }
375 }
376
377 /**
378 * Parse a Comparison into SQL and parameter arrays.
379 *
380 * @param Qom\ComparisonInterface $comparison The comparison to parse
381 * @param Qom\SourceInterface $source The source
382 * @param array &$sql SQL query parts to add to
383 * @throws \RuntimeException
384 * @throws \TYPO3\CMS\Extbase\Persistence\Generic\Exception\RepositoryException
385 * @return void
386 */
387 protected function parseComparison(Qom\ComparisonInterface $comparison, Qom\SourceInterface $source, array &$sql) {
388 $parameterIdentifier = $this->normalizeParameterIdentifier($comparison->getParameterIdentifier());
389
390 $operator = $comparison->getOperator();
391 $operand2 = $comparison->getOperand2();
392 if ($operator === QueryInterface::OPERATOR_IN) {
393 $hasValue = FALSE;
394 foreach ($operand2 as $value) {
395 $value = $this->dataMapper->getPlainValue($value);
396 if ($value !== NULL) {
397 $parameters[] = $value;
398 $hasValue = TRUE;
399 }
400 }
401 if ($hasValue === FALSE) {
402 $sql['where'][] = '1<>1';
403 } else {
404 $this->parseDynamicOperand($comparison, $source, $sql);
405 }
406 } elseif ($operator === QueryInterface::OPERATOR_CONTAINS) {
407 if ($operand2 === NULL) {
408 $sql['where'][] = '1<>1';
409 } else {
410 if (!$source instanceof Qom\SelectorInterface) {
411 throw new \RuntimeException('Source is not of type "SelectorInterface"', 1395362539);
412 }
413 $className = $source->getNodeTypeName();
414 $tableName = $this->dataMapper->convertClassNameToTableName($className);
415 $operand1 = $comparison->getOperand1();
416 $propertyName = $operand1->getPropertyName();
417 $fullPropertyPath = '';
418 while (strpos($propertyName, '.') !== FALSE) {
419 $this->addUnionStatement($className, $tableName, $propertyName, $sql, $fullPropertyPath);
420 }
421 $columnName = $this->dataMapper->convertPropertyNameToColumnName($propertyName, $className);
422 $dataMap = $this->dataMapper->getDataMap($className);
423 $columnMap = $dataMap->getColumnMap($propertyName);
424 $typeOfRelation = $columnMap instanceof ColumnMap ? $columnMap->getTypeOfRelation() : NULL;
425 if ($typeOfRelation === ColumnMap::RELATION_HAS_AND_BELONGS_TO_MANY) {
426 $relationTableName = $columnMap->getRelationTableName();
427 $additionalWhereForMatchFields = $this->getAdditionalMatchFieldsStatement($columnMap, $relationTableName, $relationTableName);
428 $sql['where'][] = $tableName . '.uid IN (SELECT ' . $columnMap->getParentKeyFieldName() . ' FROM ' . $relationTableName . ' WHERE ' . $columnMap->getChildKeyFieldName() . '=' . $parameterIdentifier . $additionalWhereForMatchFields . ')';
429 } elseif ($typeOfRelation === ColumnMap::RELATION_HAS_MANY) {
430 $parentKeyFieldName = $columnMap->getParentKeyFieldName();
431 if (isset($parentKeyFieldName)) {
432 $childTableName = $columnMap->getChildTableName();
433 $sql['where'][] = $tableName . '.uid=(SELECT ' . $childTableName . '.' . $parentKeyFieldName . ' FROM ' . $childTableName . ' WHERE ' . $childTableName . '.uid=' . $parameterIdentifier . ')';
434 } else {
435 $sql['where'][] = 'FIND_IN_SET(' . $parameterIdentifier . ', ' . $tableName . '.' . $columnName . ')';
436 }
437 } else {
438 throw new \TYPO3\CMS\Extbase\Persistence\Generic\Exception\RepositoryException('Unsupported or non-existing property name "' . $propertyName . '" used in relation matching.', 1327065745);
439 }
440 }
441 } else {
442 $this->parseDynamicOperand($comparison, $source, $sql);
443 }
444 }
445
446 /**
447 * Parse a DynamicOperand into SQL and parameter arrays.
448 *
449 * @param Qom\ComparisonInterface $comparison
450 * @param Qom\SourceInterface $source The source
451 * @param array &$sql The query parts
452 * @return void
453 */
454 protected function parseDynamicOperand(Qom\ComparisonInterface $comparison, Qom\SourceInterface $source, array &$sql) {
455 $operator = $this->resolveOperator($comparison->getOperator());
456 $operand = $comparison->getOperand1();
457
458 $constraintSQL = $this->parseOperand($operand, $source, $sql) . ' ' . $operator . ' ';
459
460 $parameterIdentifier = $this->normalizeParameterIdentifier($comparison->getParameterIdentifier());
461 if ($operator === 'IN') {
462 $parameterIdentifier = '(' . $parameterIdentifier . ')';
463 }
464 $constraintSQL .= $parameterIdentifier;
465
466 $sql['where'][] = $constraintSQL;
467 }
468
469 /**
470 * @param Qom\DynamicOperandInterface $operand
471 * @param Qom\SourceInterface $source The source
472 * @param array &$sql The query parts
473 * @return string
474 * @throws \InvalidArgumentException
475 */
476 protected function parseOperand(Qom\DynamicOperandInterface $operand, Qom\SourceInterface $source, array &$sql) {
477 if ($operand instanceof Qom\LowerCaseInterface) {
478 $constraintSQL = 'LOWER(' . $this->parseOperand($operand->getOperand(), $source, $sql) . ')';
479 } elseif ($operand instanceof Qom\UpperCaseInterface) {
480 $constraintSQL = 'UPPER(' . $this->parseOperand($operand->getOperand(), $source, $sql) . ')';
481 } elseif ($operand instanceof Qom\PropertyValueInterface) {
482 $propertyName = $operand->getPropertyName();
483 $className = '';
484 if ($source instanceof Qom\SelectorInterface) {
485 // @todo Only necessary to differ from Join
486 $className = $source->getNodeTypeName();
487 $tableName = $this->dataMapper->convertClassNameToTableName($className);
488 $fullPropertyPath = '';
489 while (strpos($propertyName, '.') !== FALSE) {
490 $this->addUnionStatement($className, $tableName, $propertyName, $sql, $fullPropertyPath);
491 }
492 } elseif ($source instanceof Qom\JoinInterface) {
493 $tableName = $source->getJoinCondition()->getSelector1Name();
494 }
495 $columnName = $this->dataMapper->convertPropertyNameToColumnName($propertyName, $className);
496 $constraintSQL = (!empty($tableName) ? $tableName . '.' : '') . $columnName;
497 } else {
498 throw new \InvalidArgumentException('Given operand has invalid type "' . get_class($operand) . '".', 1395710211);
499 }
500 return $constraintSQL;
501 }
502
503 /**
504 * Add a constraint to ensure that the record type of the returned tuples is matching the data type of the repository.
505 *
506 * @param string $className The class name
507 * @param array &$sql The query parts
508 * @return void
509 */
510 protected function addRecordTypeConstraint($className, &$sql) {
511 if ($className !== NULL) {
512 $dataMap = $this->dataMapper->getDataMap($className);
513 if ($dataMap->getRecordTypeColumnName() !== NULL) {
514 $recordTypes = array();
515 if ($dataMap->getRecordType() !== NULL) {
516 $recordTypes[] = $dataMap->getRecordType();
517 }
518 foreach ($dataMap->getSubclasses() as $subclassName) {
519 $subclassDataMap = $this->dataMapper->getDataMap($subclassName);
520 if ($subclassDataMap->getRecordType() !== NULL) {
521 $recordTypes[] = $subclassDataMap->getRecordType();
522 }
523 }
524 if (!empty($recordTypes)) {
525 $recordTypeStatements = array();
526 foreach ($recordTypes as $recordType) {
527 $tableName = $dataMap->getTableName();
528 $recordTypeStatements[] = $tableName . '.' . $dataMap->getRecordTypeColumnName() . '=' . $this->databaseHandle->fullQuoteStr($recordType, $tableName);
529 }
530 $sql['additionalWhereClause'][] = '(' . implode(' OR ', $recordTypeStatements) . ')';
531 }
532 }
533 }
534 }
535
536 /**
537 * Builds a condition for filtering records by the configured match field,
538 * e.g. MM_match_fields, foreign_match_fields or foreign_table_field.
539 *
540 * @param ColumnMap $columnMap The column man for which the condition should be build.
541 * @param string $childTableName The real name of the child record table.
542 * @param string $childTableAlias The alias of the child record table used in the query.
543 * @param string $parentTable The real name of the parent table (used for building the foreign_table_field condition).
544 * @return string The match field conditions or an empty string.
545 */
546 protected function getAdditionalMatchFieldsStatement($columnMap, $childTableName, $childTableAlias, $parentTable = NULL) {
547
548 $additionalWhereForMatchFields = '';
549
550 $relationTableMatchFields = $columnMap->getRelationTableMatchFields();
551 if (is_array($relationTableMatchFields) && !empty($relationTableMatchFields)) {
552 $additionalWhere = array();
553 foreach ($relationTableMatchFields as $fieldName => $value) {
554 $additionalWhere[] = $childTableAlias . '.' . $fieldName . ' = ' . $this->databaseHandle->fullQuoteStr($value, $childTableName);
555 }
556 $additionalWhereForMatchFields .= ' AND ' . implode(' AND ', $additionalWhere);
557 }
558
559 if (isset($parentTable)) {
560 $parentTableFieldName = $columnMap->getParentTableFieldName();
561 if (isset($parentTableFieldName) && $parentTableFieldName !== '') {
562 $additionalWhereForMatchFields .= ' AND ' . $childTableAlias . '.' . $parentTableFieldName . ' = ' . $this->databaseHandle->fullQuoteStr($parentTable, $childTableAlias);
563 }
564 }
565
566 return $additionalWhereForMatchFields;
567 }
568
569 /**
570 * Adds additional WHERE statements according to the query settings.
571 *
572 * @param QuerySettingsInterface $querySettings The TYPO3 CMS specific query settings
573 * @param string $tableName The table name to add the additional where clause for
574 * @param string $tableAlias The table alias used in the query.
575 * @return string
576 */
577 protected function getAdditionalWhereClause(QuerySettingsInterface $querySettings, $tableName, $tableAlias = NULL) {
578
579 $sysLanguageStatement = '';
580 if ($querySettings->getRespectSysLanguage()) {
581 $sysLanguageStatement = $this->getSysLanguageStatement($tableName, $tableAlias, $querySettings);
582 }
583
584 $pageIdStatement = '';
585 if ($querySettings->getRespectStoragePage()) {
586 $pageIdStatement = $this->getPageIdStatement($tableName, $tableAlias, $querySettings->getStoragePageIds());
587 }
588
589 if ($sysLanguageStatement !== '' && $pageIdStatement !== '') {
590 $whereClause = $sysLanguageStatement . ' AND ' . $pageIdStatement;
591 } elseif ($sysLanguageStatement !== '') {
592 $whereClause = $sysLanguageStatement;
593 } else {
594 $whereClause = $pageIdStatement;
595 }
596
597 return $whereClause;
598 }
599
600 /**
601 * Adds enableFields and deletedClause to the query if necessary
602 *
603 * @param QuerySettingsInterface $querySettings
604 * @param string $tableName The database table name
605 * @param string $tableAlias
606 * @return string
607 */
608 protected function getVisibilityConstraintStatement(QuerySettingsInterface $querySettings, $tableName, $tableAlias) {
609 $statement = '';
610 if (is_array($GLOBALS['TCA'][$tableName]['ctrl'])) {
611 $ignoreEnableFields = $querySettings->getIgnoreEnableFields();
612 $enableFieldsToBeIgnored = $querySettings->getEnableFieldsToBeIgnored();
613 $includeDeleted = $querySettings->getIncludeDeleted();
614 if ($this->environmentService->isEnvironmentInFrontendMode()) {
615 $statement .= $this->getFrontendConstraintStatement($tableName, $ignoreEnableFields, $enableFieldsToBeIgnored, $includeDeleted);
616 } else {
617 // TYPO3_MODE === 'BE'
618 $statement .= $this->getBackendConstraintStatement($tableName, $ignoreEnableFields, $includeDeleted);
619 }
620 if (!empty($statement)) {
621 $statement = $this->replaceTableNameWithAlias($statement, $tableName, $tableAlias);
622 $statement = strtolower(substr($statement, 1, 3)) === 'and' ? substr($statement, 5) : $statement;
623 }
624 }
625 return $statement;
626 }
627
628 /**
629 * Returns constraint statement for frontend context
630 *
631 * @param string $tableName
632 * @param bool $ignoreEnableFields A flag indicating whether the enable fields should be ignored
633 * @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.
634 * @param bool $includeDeleted A flag indicating whether deleted records should be included
635 * @return string
636 * @throws \TYPO3\CMS\Extbase\Persistence\Generic\Exception\InconsistentQuerySettingsException
637 */
638 protected function getFrontendConstraintStatement($tableName, $ignoreEnableFields, array $enableFieldsToBeIgnored = array(), $includeDeleted) {
639 $statement = '';
640 if ($ignoreEnableFields && !$includeDeleted) {
641 if (!empty($enableFieldsToBeIgnored)) {
642 // array_combine() is necessary because of the way \TYPO3\CMS\Frontend\Page\PageRepository::enableFields() is implemented
643 $statement .= $this->getPageRepository()->enableFields($tableName, -1, array_combine($enableFieldsToBeIgnored, $enableFieldsToBeIgnored));
644 } else {
645 $statement .= $this->getPageRepository()->deleteClause($tableName);
646 }
647 } elseif (!$ignoreEnableFields && !$includeDeleted) {
648 $statement .= $this->getPageRepository()->enableFields($tableName);
649 } elseif (!$ignoreEnableFields && $includeDeleted) {
650 throw new \TYPO3\CMS\Extbase\Persistence\Generic\Exception\InconsistentQuerySettingsException('Query setting "ignoreEnableFields=FALSE" can not be used together with "includeDeleted=TRUE" in frontend context.', 1327678173);
651 }
652 return $statement;
653 }
654
655 /**
656 * Returns constraint statement for backend context
657 *
658 * @param string $tableName
659 * @param bool $ignoreEnableFields A flag indicating whether the enable fields should be ignored
660 * @param bool $includeDeleted A flag indicating whether deleted records should be included
661 * @return string
662 */
663 protected function getBackendConstraintStatement($tableName, $ignoreEnableFields, $includeDeleted) {
664 $statement = '';
665 if (!$ignoreEnableFields) {
666 $statement .= BackendUtility::BEenableFields($tableName);
667 }
668 if (!$includeDeleted) {
669 $statement .= BackendUtility::deleteClause($tableName);
670 }
671 return $statement;
672 }
673
674 /**
675 * Builds the language field statement
676 *
677 * @param string $tableName The database table name
678 * @param string $tableAlias The table alias used in the query.
679 * @param QuerySettingsInterface $querySettings The TYPO3 CMS specific query settings
680 * @return string
681 */
682 protected function getSysLanguageStatement($tableName, $tableAlias, $querySettings) {
683 $sysLanguageStatement = '';
684 if (is_array($GLOBALS['TCA'][$tableName]['ctrl'])) {
685 if (!empty($GLOBALS['TCA'][$tableName]['ctrl']['languageField'])) {
686 // Select all entries for the current language
687 $additionalWhereClause = $tableAlias . '.' . $GLOBALS['TCA'][$tableName]['ctrl']['languageField'] . ' IN (' . (int)$querySettings->getLanguageUid() . ',-1)';
688 // If any language is set -> get those entries which are not translated yet
689 // They will be removed by \TYPO3\CMS\Frontend\Page\PageRepository::getRecordOverlay if not matching overlay mode
690 if (isset($GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField'])
691 && $querySettings->getLanguageUid() > 0
692 ) {
693
694 $mode = $querySettings->getLanguageMode();
695 if ($mode === 'strict') {
696 $additionalWhereClause = $tableAlias . '.' . $GLOBALS['TCA'][$tableName]['ctrl']['languageField'] . '=-1' .
697 ' OR (' . $tableAlias . '.' . $GLOBALS['TCA'][$tableName]['ctrl']['languageField'] . ' = ' . (int)$querySettings->getLanguageUid() .
698 ' AND ' . $tableAlias . '.' . $GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField'] . '=0' .
699 ') OR (' . $tableAlias . '.' . $GLOBALS['TCA'][$tableName]['ctrl']['languageField'] . '=0' .
700 ' AND ' . $tableAlias . '.uid IN (SELECT ' . $tableName . '.' . $GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField'] .
701 ' FROM ' . $tableName .
702 ' WHERE ' . $tableName . '.' . $GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField'] . '>0' .
703 ' AND ' . $tableName . '.' . $GLOBALS['TCA'][$tableName]['ctrl']['languageField'] . '=' . (int)$querySettings->getLanguageUid();
704 } else {
705 $additionalWhereClause .= ' OR (' . $tableAlias . '.' . $GLOBALS['TCA'][$tableName]['ctrl']['languageField'] . '=0' .
706 ' AND ' . $tableAlias . '.uid NOT IN (SELECT ' . $tableAlias . '.' . $GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField'] .
707 ' FROM ' . $tableName .
708 ' WHERE ' . $tableName . '.' . $GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField'] . '>0' .
709 ' AND ' . $tableName . '.' . $GLOBALS['TCA'][$tableName]['ctrl']['languageField'] . '=' . (int)$querySettings->getLanguageUid();
710 }
711
712 // Add delete clause to ensure all entries are loaded
713 if (isset($GLOBALS['TCA'][$tableName]['ctrl']['delete'])) {
714 $additionalWhereClause .= ' AND ' . $tableName . '.' . $GLOBALS['TCA'][$tableName]['ctrl']['delete'] . '=0';
715 }
716 $additionalWhereClause .= '))';
717 }
718 $sysLanguageStatement = '(' . $additionalWhereClause . ')';
719 }
720 }
721 return $sysLanguageStatement;
722 }
723
724 /**
725 * Builds the page ID checking statement
726 *
727 * @param string $tableName The database table name
728 * @param string $tableAlias The table alias used in the query.
729 * @param array $storagePageIds list of storage page ids
730 * @throws \TYPO3\CMS\Extbase\Persistence\Generic\Exception\InconsistentQuerySettingsException
731 * @return string
732 */
733 protected function getPageIdStatement($tableName, $tableAlias, array $storagePageIds) {
734 $pageIdStatement = '';
735 $tableColumns = $this->tableColumnCache->get($tableName);
736 if ($tableColumns === FALSE) {
737 $tableColumns = $this->databaseHandle->admin_get_fields($tableName);
738 $this->tableColumnCache->set($tableName, $tableColumns);
739 }
740 if (is_array($GLOBALS['TCA'][$tableName]['ctrl']) && array_key_exists('pid', $tableColumns)) {
741 $rootLevel = (int)$GLOBALS['TCA'][$tableName]['ctrl']['rootLevel'];
742 if ($rootLevel) {
743 if ($rootLevel === 1) {
744 $pageIdStatement = $tableAlias . '.pid = 0';
745 }
746 } else {
747 if (empty($storagePageIds)) {
748 throw new \TYPO3\CMS\Extbase\Persistence\Generic\Exception\InconsistentQuerySettingsException('Missing storage page ids.', 1365779762);
749 }
750 $pageIdStatement = $tableAlias . '.pid IN (' . implode(', ', $storagePageIds) . ')';
751 }
752 }
753 return $pageIdStatement;
754 }
755
756 /**
757 * Transforms a Join into SQL and parameter arrays
758 *
759 * @param Qom\JoinInterface $join The join
760 * @param array &$sql The query parts
761 * @return void
762 */
763 protected function parseJoin(Qom\JoinInterface $join, array &$sql) {
764 $leftSource = $join->getLeft();
765 $leftClassName = $leftSource->getNodeTypeName();
766 $leftTableName = $leftSource->getSelectorName();
767 $this->addRecordTypeConstraint($leftClassName, $sql);
768 $rightSource = $join->getRight();
769 if ($rightSource instanceof Qom\JoinInterface) {
770 $left = $rightSource->getLeft();
771 $rightClassName = $left->getNodeTypeName();
772 $rightTableName = $left->getSelectorName();
773 } else {
774 $rightClassName = $rightSource->getNodeTypeName();
775 $rightTableName = $rightSource->getSelectorName();
776 $sql['fields'][$leftTableName] = $rightTableName . '.*';
777 }
778 $this->addRecordTypeConstraint($rightClassName, $sql);
779 $leftTableName = $this->getUniqueAlias($sql, $leftTableName);
780 $sql['tables'][$leftTableName] = $leftTableName;
781 $rightTableName = $this->getUniqueAlias($sql, $rightTableName);
782 $sql['unions'][$rightTableName] = 'LEFT JOIN ' . $rightTableName;
783 $joinCondition = $join->getJoinCondition();
784 if ($joinCondition instanceof Qom\EquiJoinCondition) {
785 $column1Name = $this->dataMapper->convertPropertyNameToColumnName($joinCondition->getProperty1Name(), $leftClassName);
786 $column2Name = $this->dataMapper->convertPropertyNameToColumnName($joinCondition->getProperty2Name(), $rightClassName);
787 $sql['unions'][$rightTableName] .= ' ON ' . $leftTableName . '.' . $column1Name . ' = ' . $rightTableName . '.' . $column2Name;
788 }
789 if ($rightSource instanceof Qom\JoinInterface) {
790 $this->parseJoin($rightSource, $sql);
791 }
792 }
793
794 /**
795 * Generates a unique alias for the given table and the given property path.
796 * The property path will be mapped to the generated alias in the tablePropertyMap.
797 *
798 * @param array $sql The SQL satement parts, will be filled with the tableAliasMap.
799 * @param string $tableName The name of the table for which the alias should be generated.
800 * @param string $fullPropertyPath The full property path that is related to the given table.
801 * @return string The generated table alias.
802 */
803 protected function getUniqueAlias(array &$sql, $tableName, $fullPropertyPath = NULL) {
804
805 if (isset($fullPropertyPath) && isset($this->tablePropertyMap[$fullPropertyPath])) {
806 return $this->tablePropertyMap[$fullPropertyPath];
807 }
808
809 $alias = $tableName;
810 $i = 0;
811 while (isset($sql['tableAliasMap'][$alias])) {
812 $alias = $tableName . $i;
813 $i++;
814 }
815
816 $sql['tableAliasMap'][$alias] = $tableName;
817
818 if (isset($fullPropertyPath)) {
819 $this->tablePropertyMap[$fullPropertyPath] = $alias;
820 }
821
822 return $alias;
823 }
824
825 /**
826 * adds a union statement to the query, mostly for tables referenced in the where condition.
827 * The property for which the union statement is generated will be appended.
828 *
829 * @param string &$className The name of the parent class, will be set to the child class after processing.
830 * @param string &$tableName The name of the parent table, will be set to the table alias that is used in the union statement.
831 * @param array &$propertyPath The remaining property path, will be cut of by one part during the process.
832 * @param array &$sql The SQL statement parts, will be filled with the union statements.
833 * @param string $fullPropertyPath The full path the the current property, will be used to make table names unique.
834 * @throws \TYPO3\CMS\Extbase\Persistence\Generic\Exception
835 * @throws \TYPO3\CMS\Extbase\Persistence\Generic\Exception\InvalidRelationConfigurationException
836 * @throws \TYPO3\CMS\Extbase\Persistence\Generic\Exception\MissingColumnMapException
837 */
838 protected function addUnionStatement(&$className, &$tableName, &$propertyPath, array &$sql, &$fullPropertyPath) {
839
840 $explodedPropertyPath = explode('.', $propertyPath, 2);
841 $propertyName = $explodedPropertyPath[0];
842 $columnName = $this->dataMapper->convertPropertyNameToColumnName($propertyName, $className);
843 $realTableName = $this->dataMapper->convertClassNameToTableName($className);
844 $tableName = isset($this->tablePropertyMap[$fullPropertyPath]) ? $this->tablePropertyMap[$fullPropertyPath] : $realTableName;
845 $columnMap = $this->dataMapper->getDataMap($className)->getColumnMap($propertyName);
846
847 if ($columnMap === NULL) {
848 throw new \TYPO3\CMS\Extbase\Persistence\Generic\Exception\MissingColumnMapException('The ColumnMap for property "' . $propertyName . '" of class "' . $className . '" is missing.', 1355142232);
849 }
850
851 $parentKeyFieldName = $columnMap->getParentKeyFieldName();
852 $childTableName = $columnMap->getChildTableName();
853
854 if ($childTableName === NULL) {
855 throw new \TYPO3\CMS\Extbase\Persistence\Generic\Exception\InvalidRelationConfigurationException('The relation information for property "' . $propertyName . '" of class "' . $className . '" is missing.', 1353170925);
856 }
857
858 $fullPropertyPath .= ($fullPropertyPath === '') ? $propertyName : '.' . $propertyName;
859 $childTableAlias = $this->getUniqueAlias($sql, $childTableName, $fullPropertyPath);
860
861 // If there is already exists a union with the current identifier we do not need to build it again and exit early.
862 if (isset($sql['unions'][$childTableAlias])) {
863 $propertyPath = $explodedPropertyPath[1];
864 $tableName = $childTableAlias;
865 $className = $this->dataMapper->getType($className, $propertyName);
866 return;
867 }
868
869 if ($columnMap->getTypeOfRelation() === ColumnMap::RELATION_HAS_ONE) {
870 if (isset($parentKeyFieldName)) {
871 $sql['unions'][$childTableAlias] = 'LEFT JOIN ' . $childTableName . ' AS ' . $childTableAlias . ' ON ' . $tableName . '.uid=' . $childTableAlias . '.' . $parentKeyFieldName;
872 } else {
873 $sql['unions'][$childTableAlias] = 'LEFT JOIN ' . $childTableName . ' AS ' . $childTableAlias . ' ON ' . $tableName . '.' . $columnName . '=' . $childTableAlias . '.uid';
874 }
875 $sql['unions'][$childTableAlias] .= $this->getAdditionalMatchFieldsStatement($columnMap, $childTableName, $childTableAlias, $realTableName);
876 } elseif ($columnMap->getTypeOfRelation() === ColumnMap::RELATION_HAS_MANY) {
877 if (isset($parentKeyFieldName)) {
878 $sql['unions'][$childTableAlias] = 'LEFT JOIN ' . $childTableName . ' AS ' . $childTableAlias . ' ON ' . $tableName . '.uid=' . $childTableAlias . '.' . $parentKeyFieldName;
879 } else {
880 $onStatement = '(FIND_IN_SET(' . $childTableAlias . '.uid, ' . $tableName . '.' . $columnName . '))';
881 $sql['unions'][$childTableAlias] = 'LEFT JOIN ' . $childTableName . ' AS ' . $childTableAlias . ' ON ' . $onStatement;
882 }
883 $sql['unions'][$childTableAlias] .= $this->getAdditionalMatchFieldsStatement($columnMap, $childTableName, $childTableAlias, $realTableName);
884 } elseif ($columnMap->getTypeOfRelation() === ColumnMap::RELATION_HAS_AND_BELONGS_TO_MANY) {
885 $relationTableName = $columnMap->getRelationTableName();
886 $relationTableAlias = $relationTableAlias = $this->getUniqueAlias($sql, $relationTableName, $fullPropertyPath . '_mm');
887 $sql['unions'][$relationTableAlias] = 'LEFT JOIN ' . $relationTableName . ' AS ' . $relationTableAlias . ' ON ' . $tableName . '.uid=' . $relationTableAlias . '.' . $columnMap->getParentKeyFieldName();
888 $sql['unions'][$childTableAlias] = 'LEFT JOIN ' . $childTableName . ' AS ' . $childTableAlias . ' ON ' . $relationTableAlias . '.' . $columnMap->getChildKeyFieldName() . '=' . $childTableAlias . '.uid';
889 $sql['unions'][$childTableAlias] .= $this->getAdditionalMatchFieldsStatement($columnMap, $relationTableName, $relationTableAlias, $realTableName);
890 } else {
891 throw new \TYPO3\CMS\Extbase\Persistence\Generic\Exception('Could not determine type of relation.', 1252502725);
892 }
893 // @todo check if there is another solution for this
894 $sql['keywords']['distinct'] = 'DISTINCT';
895 $propertyPath = $explodedPropertyPath[1];
896 $tableName = $childTableAlias;
897 $className = $this->dataMapper->getType($className, $propertyName);
898 }
899
900 /**
901 * If the table name does not match the table alias all occurrences of
902 * "tableName." are replaced with "tableAlias." in the given SQL statement.
903 *
904 * @param string $statement The SQL statement in which the values are replaced.
905 * @param string $tableName The table name that is replaced.
906 * @param string $tableAlias The table alias that replaced the table name.
907 * @return string The modified SQL statement.
908 */
909 protected function replaceTableNameWithAlias($statement, $tableName, $tableAlias) {
910
911 if ($tableAlias !== $tableName) {
912 $statement = str_replace($tableName . '.', $tableAlias . '.', $statement);
913 }
914
915 return $statement;
916 }
917
918 /**
919 * Returns the SQL operator for the given JCR operator type.
920 *
921 * @param string $operator One of the JCR_OPERATOR_* constants
922 * @throws \TYPO3\CMS\Extbase\Persistence\Generic\Exception
923 * @return string an SQL operator
924 */
925 protected function resolveOperator($operator) {
926 switch ($operator) {
927 case QueryInterface::OPERATOR_IN:
928 $operator = 'IN';
929 break;
930 case QueryInterface::OPERATOR_EQUAL_TO:
931 $operator = '=';
932 break;
933 case QueryInterface::OPERATOR_EQUAL_TO_NULL:
934 $operator = 'IS';
935 break;
936 case QueryInterface::OPERATOR_NOT_EQUAL_TO:
937 $operator = '!=';
938 break;
939 case QueryInterface::OPERATOR_NOT_EQUAL_TO_NULL:
940 $operator = 'IS NOT';
941 break;
942 case QueryInterface::OPERATOR_LESS_THAN:
943 $operator = '<';
944 break;
945 case QueryInterface::OPERATOR_LESS_THAN_OR_EQUAL_TO:
946 $operator = '<=';
947 break;
948 case QueryInterface::OPERATOR_GREATER_THAN:
949 $operator = '>';
950 break;
951 case QueryInterface::OPERATOR_GREATER_THAN_OR_EQUAL_TO:
952 $operator = '>=';
953 break;
954 case QueryInterface::OPERATOR_LIKE:
955 $operator = 'LIKE';
956 break;
957 default:
958 throw new \TYPO3\CMS\Extbase\Persistence\Generic\Exception('Unsupported operator encountered.', 1242816073);
959 }
960 return $operator;
961 }
962
963 /**
964 * @return \TYPO3\CMS\Frontend\Page\PageRepository
965 */
966 protected function getPageRepository() {
967 if (!$this->pageRepository instanceof \TYPO3\CMS\Frontend\Page\PageRepository) {
968 if ($this->environmentService->isEnvironmentInFrontendMode() && is_object($GLOBALS['TSFE'])) {
969 $this->pageRepository = $GLOBALS['TSFE']->sys_page;
970 } else {
971 $this->pageRepository = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(\TYPO3\CMS\Frontend\Page\PageRepository::class);
972 }
973 }
974
975 return $this->pageRepository;
976 }
977
978 }