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