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