Completed
Push — master ( 40ee82...90af97 )
by Fabien
03:33
created
Classes/Persistence/Storage/VidiDbBackend.php 1 patch
Indentation   +1205 added lines, -1205 removed lines patch added patch discarded remove patch
@@ -32,1209 +32,1209 @@
 block discarded – undo
32 32
 class VidiDbBackend
33 33
 {
34 34
 
35
-    const OPERATOR_EQUAL_TO_NULL = 'operatorEqualToNull';
36
-    const OPERATOR_NOT_EQUAL_TO_NULL = 'operatorNotEqualToNull';
37
-
38
-    /**
39
-     * The TYPO3 database object
40
-     *
41
-     * @var \TYPO3\CMS\Core\Database\DatabaseConnection
42
-     */
43
-    protected $databaseHandle;
44
-
45
-    /**
46
-     * The TYPO3 page repository. Used for language and workspace overlay
47
-     *
48
-     * @var PageRepository
49
-     */
50
-    protected $pageRepository;
51
-
52
-    /**
53
-     * A first-level TypoScript configuration cache
54
-     *
55
-     * @var array
56
-     */
57
-    protected $pageTSConfigCache = array();
58
-
59
-    /**
60
-     * @var \TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface
61
-     * @inject
62
-     */
63
-    protected $configurationManager;
64
-
65
-    /**
66
-     * @var \TYPO3\CMS\Extbase\Service\CacheService
67
-     * @inject
68
-     */
69
-    protected $cacheService;
70
-
71
-    /**
72
-     * @var \TYPO3\CMS\Core\Cache\CacheManager
73
-     * @inject
74
-     */
75
-    protected $cacheManager;
76
-
77
-    /**
78
-     * @var \TYPO3\CMS\Core\Cache\Frontend\VariableFrontend
79
-     */
80
-    protected $tableColumnCache;
81
-
82
-    /**
83
-     * @var \TYPO3\CMS\Extbase\Service\EnvironmentService
84
-     * @inject
85
-     */
86
-    protected $environmentService;
87
-
88
-    /**
89
-     * @var \Fab\Vidi\Persistence\Query
90
-     */
91
-    protected $query;
92
-
93
-    /**
94
-     * Store some info related to table name and its aliases.
95
-     *
96
-     * @var array
97
-     */
98
-    protected $tableNameAliases = array(
99
-        'aliases' => array(),
100
-        'aliasIncrement' => array(),
101
-    );
102
-
103
-    /**
104
-     * Use to store the current foreign table name alias.
105
-     *
106
-     * @var string
107
-     */
108
-    protected $currentChildTableNameAlias = '';
109
-
110
-    /**
111
-     * The default object type being returned.
112
-     *
113
-     * @var string
114
-     */
115
-    protected $objectType = 'Fab\Vidi\Domain\Model\Content';
116
-
117
-    /**
118
-     * Constructor. takes the database handle from $GLOBALS['TYPO3_DB']
119
-     */
120
-    public function __construct(QueryInterface $query)
121
-    {
122
-        $this->query = $query;
123
-        $this->databaseHandle = $GLOBALS['TYPO3_DB'];
124
-    }
125
-
126
-    /**
127
-     * Lifecycle method
128
-     *
129
-     * @return void
130
-     */
131
-    public function initializeObject()
132
-    {
133
-        $this->tableColumnCache = $this->cacheManager->getCache('extbase_typo3dbbackend_tablecolumns');
134
-    }
135
-
136
-    /**
137
-     * @param array $identifier
138
-     * @return string
139
-     */
140
-    protected function parseIdentifier(array $identifier)
141
-    {
142
-        $fieldNames = array_keys($identifier);
143
-        $suffixedFieldNames = array();
144
-        foreach ($fieldNames as $fieldName) {
145
-            $suffixedFieldNames[] = $fieldName . '=?';
146
-        }
147
-        return implode(' AND ', $suffixedFieldNames);
148
-    }
149
-
150
-    /**
151
-     * Returns the result of the query
152
-     */
153
-    public function fetchResult()
154
-    {
155
-
156
-        $parameters = array();
157
-        $statementParts = $this->parseQuery($this->query, $parameters);
158
-        $statementParts = $this->processStatementStructureForRecursiveMMRelation($statementParts); // Mmm... check if that is the right way of doing that.
159
-
160
-        $sql = $this->buildQuery($statementParts);
161
-        $tableName = '';
162
-        if (is_array($statementParts) && !empty(reset($statementParts['tables']))) {
163
-            $tableName = reset($statementParts['tables']);
164
-        }
165
-        $this->replacePlaceholders($sql, $parameters, $tableName);
166
-        #print $sql; exit(); // @debug
167
-
168
-        $result = $this->databaseHandle->sql_query($sql);
169
-        $this->checkSqlErrors($sql);
170
-        $rows = $this->getRowsFromResult($result);
171
-        $this->databaseHandle->sql_free_result($result);
172
-
173
-        return $rows;
174
-    }
175
-
176
-    /**
177
-     * Returns the number of tuples matching the query.
178
-     *
179
-     * @throws \TYPO3\CMS\Extbase\Persistence\Generic\Storage\Exception\BadConstraintException
180
-     * @return int The number of matching tuples
181
-     */
182
-    public function countResult()
183
-    {
184
-
185
-        $parameters = array();
186
-        $statementParts = $this->parseQuery($this->query, $parameters);
187
-        $statementParts = $this->processStatementStructureForRecursiveMMRelation($statementParts); // Mmm... check if that is the right way of doing that.
188
-        // Reset $statementParts for valid table return
189
-        reset($statementParts);
190
-
191
-        // if limit is set, we need to count the rows "manually" as COUNT(*) ignores LIMIT constraints
192
-        if (!empty($statementParts['limit'])) {
193
-            $statement = $this->buildQuery($statementParts);
194
-            $this->replacePlaceholders($statement, $parameters, current($statementParts['tables']));
195
-            #print $statement; exit(); // @debug
196
-            $result = $this->databaseHandle->sql_query($statement);
197
-            $this->checkSqlErrors($statement);
198
-            $count = $this->databaseHandle->sql_num_rows($result);
199
-        } else {
200
-            $statementParts['fields'] = array('COUNT(*)');
201
-            // having orderings without grouping is not compatible with non-MySQL DBMS
202
-            $statementParts['orderings'] = array();
203
-            if (isset($statementParts['keywords']['distinct'])) {
204
-                unset($statementParts['keywords']['distinct']);
205
-                $distinctField = $this->query->getDistinct() ? $this->query->getDistinct() : 'uid';
206
-                $statementParts['fields'] = array('COUNT(DISTINCT ' . reset($statementParts['tables']) . '.' . $distinctField . ')');
207
-            }
208
-
209
-            $statement = $this->buildQuery($statementParts);
210
-            $this->replacePlaceholders($statement, $parameters, current($statementParts['tables']));
211
-
212
-            #print $statement; exit(); // @debug
213
-            $result = $this->databaseHandle->sql_query($statement);
214
-            $this->checkSqlErrors($statement);
215
-            $count = 0;
216
-            if ($result) {
217
-                $row = $this->databaseHandle->sql_fetch_assoc($result);
218
-                $count = current($row);
219
-            }
220
-        }
221
-        $this->databaseHandle->sql_free_result($result);
222
-        return (int)$count;
223
-    }
224
-
225
-    /**
226
-     * Parses the query and returns the SQL statement parts.
227
-     *
228
-     * @param QueryInterface $query The query
229
-     * @param array &$parameters
230
-     * @return array The SQL statement parts
231
-     */
232
-    public function parseQuery(QueryInterface $query, array &$parameters)
233
-    {
234
-        $statementParts = array();
235
-        $statementParts['keywords'] = array();
236
-        $statementParts['tables'] = array();
237
-        $statementParts['unions'] = array();
238
-        $statementParts['fields'] = array();
239
-        $statementParts['where'] = array();
240
-        $statementParts['additionalWhereClause'] = array();
241
-        $statementParts['orderings'] = array();
242
-        $statementParts['limit'] = array();
243
-        $source = $query->getSource();
244
-        $this->parseSource($source, $statementParts);
245
-        $this->parseConstraint($query->getConstraint(), $source, $statementParts, $parameters);
246
-        $this->parseOrderings($query->getOrderings(), $source, $statementParts);
247
-        $this->parseLimitAndOffset($query->getLimit(), $query->getOffset(), $statementParts);
248
-        $tableNames = array_unique(array_keys($statementParts['tables'] + $statementParts['unions']));
249
-        foreach ($tableNames as $tableNameOrAlias) {
250
-            if (is_string($tableNameOrAlias) && strlen($tableNameOrAlias) > 0) {
251
-                $this->addAdditionalWhereClause($query->getQuerySettings(), $tableNameOrAlias, $statementParts);
252
-            }
253
-        }
254
-
255
-        return $statementParts;
256
-    }
257
-
258
-    /**
259
-     * Fiddle with the statement structure to handle recursive MM relations.
260
-     * For the recursive MM query to work, we must invert some values.
261
-     * Let see if that is the best way of doing that...
262
-     *
263
-     * @param array $statementParts
264
-     * @return array
265
-     */
266
-    public function processStatementStructureForRecursiveMMRelation(array $statementParts)
267
-    {
268
-
269
-        if ($this->hasRecursiveMMRelation()) {
270
-            $tableName = $this->query->getType();
271
-
272
-            // In order the MM query to work for a recursive MM query, we must invert some values.
273
-            // tx_domain_model_foo0 (the alias) <--> tx_domain_model_foo (the origin table name)
274
-            $values = array();
275
-            foreach ($statementParts['fields'] as $key => $value) {
276
-                $values[$key] = str_replace($tableName, $tableName . '0', $value);
277
-            }
278
-            $statementParts['fields'] = $values;
279
-
280
-            // Same comment as above.
281
-            $values = array();
282
-            foreach ($statementParts['where'] as $key => $value) {
283
-                $values[$key] = str_replace($tableName . '0', $tableName, $value);
284
-            }
285
-            $statementParts['where'] = $values;
286
-
287
-            // We must be more restrictive by transforming the "left" union by "inner"
288
-            $values = array();
289
-            foreach ($statementParts['unions'] as $key => $value) {
290
-                $values[$key] = str_replace('LEFT JOIN', 'INNER JOIN', $value);
291
-            }
292
-            $statementParts['unions'] = $values;
293
-        }
294
-
295
-        return $statementParts;
296
-    }
297
-
298
-    /**
299
-     * Tell whether there is a recursive MM relation.
300
-     *
301
-     * @return bool
302
-     */
303
-    public function hasRecursiveMMRelation()
304
-    {
305
-        return isset($this->tableNameAliases['aliasIncrement'][$this->query->getType()]);
306
-
307
-    }
308
-
309
-    /**
310
-     * Returns the statement, ready to be executed.
311
-     *
312
-     * @param array $statementParts The SQL statement parts
313
-     * @return string The SQL statement
314
-     */
315
-    public function buildQuery(array $statementParts)
316
-    {
317
-
318
-        // Add more statement to the UNION part.
319
-        if (!empty($statementParts['unions'])) {
320
-            foreach ($statementParts['unions'] as $tableName => $unionPart) {
321
-                if (!empty($statementParts['additionalWhereClause'][$tableName])) {
322
-                    $statementParts['unions'][$tableName] .= ' AND ' . implode(' AND ', $statementParts['additionalWhereClause'][$tableName]);
323
-                }
324
-            }
325
-        }
326
-
327
-        $statement = 'SELECT ' . implode(' ', $statementParts['keywords']) . ' ' . implode(',', $statementParts['fields']) . ' FROM ' . implode(' ', $statementParts['tables']) . ' ' . implode(' ', $statementParts['unions']);
328
-        if (!empty($statementParts['where'])) {
329
-            $statement .= ' WHERE ' . implode('', $statementParts['where']);
330
-            if (!empty($statementParts['additionalWhereClause'][$this->query->getType()])) {
331
-                $statement .= ' AND ' . implode(' AND ', $statementParts['additionalWhereClause'][$this->query->getType()]);
332
-            }
333
-        } elseif (!empty($statementParts['additionalWhereClause'])) {
334
-            $statement .= ' WHERE ' . implode(' AND ', $statementParts['additionalWhereClause'][$this->query->getType()]);
335
-        }
336
-        if (!empty($statementParts['orderings'])) {
337
-            $statement .= ' ORDER BY ' . implode(', ', $statementParts['orderings']);
338
-        }
339
-        if (!empty($statementParts['limit'])) {
340
-            $statement .= ' LIMIT ' . $statementParts['limit'];
341
-        }
342
-
343
-        return $statement;
344
-    }
345
-
346
-    /**
347
-     * Transforms a Query Source into SQL and parameter arrays
348
-     *
349
-     * @param SourceInterface $source The source
350
-     * @param array &$sql
351
-     * @return void
352
-     */
353
-    protected function parseSource(SourceInterface $source, array &$sql)
354
-    {
355
-        if ($source instanceof SelectorInterface) {
356
-            $tableName = $source->getNodeTypeName();
357
-            $sql['fields'][$tableName] = $tableName . '.*';
358
-            $sql['tables'][$tableName] = $tableName;
359
-            if ($this->query->getDistinct()) {
360
-                $sql['fields'][$tableName] = $tableName . '.' . $this->query->getDistinct();
361
-                $sql['keywords']['distinct'] = 'DISTINCT';
362
-            }
363
-        } elseif ($source instanceof JoinInterface) {
364
-            $this->parseJoin($source, $sql);
365
-        }
366
-    }
367
-
368
-    /**
369
-     * Transforms a Join into SQL and parameter arrays
370
-     *
371
-     * @param JoinInterface $join The join
372
-     * @param array &$sql The query parts
373
-     * @return void
374
-     */
375
-    protected function parseJoin(JoinInterface $join, array &$sql)
376
-    {
377
-        $leftSource = $join->getLeft();
378
-        $leftTableName = $leftSource->getSelectorName();
379
-        // $sql['fields'][$leftTableName] = $leftTableName . '.*';
380
-        $rightSource = $join->getRight();
381
-        if ($rightSource instanceof JoinInterface) {
382
-            $rightTableName = $rightSource->getLeft()->getSelectorName();
383
-        } else {
384
-            $rightTableName = $rightSource->getSelectorName();
385
-            $sql['fields'][$leftTableName] = $rightTableName . '.*';
386
-        }
387
-        $sql['tables'][$leftTableName] = $leftTableName;
388
-        $sql['unions'][$rightTableName] = 'LEFT JOIN ' . $rightTableName;
389
-        $joinCondition = $join->getJoinCondition();
390
-        if ($joinCondition instanceof \TYPO3\CMS\Extbase\Persistence\Generic\Qom\EquiJoinCondition) {
391
-            $column1Name = $joinCondition->getProperty1Name();
392
-            $column2Name = $joinCondition->getProperty2Name();
393
-            $sql['unions'][$rightTableName] .= ' ON ' . $joinCondition->getSelector1Name() . '.' . $column1Name . ' = ' . $joinCondition->getSelector2Name() . '.' . $column2Name;
394
-        }
395
-        if ($rightSource instanceof JoinInterface) {
396
-            $this->parseJoin($rightSource, $sql);
397
-        }
398
-    }
399
-
400
-    /**
401
-     * Transforms a constraint into SQL and parameter arrays
402
-     *
403
-     * @param \TYPO3\CMS\Extbase\Persistence\Generic\Qom\ConstraintInterface $constraint The constraint
404
-     * @param SourceInterface $source The source
405
-     * @param array &$sql The query parts
406
-     * @param array &$parameters The parameters that will replace the markers
407
-     * @return void
408
-     */
409
-    protected function parseConstraint(\TYPO3\CMS\Extbase\Persistence\Generic\Qom\ConstraintInterface $constraint = NULL, SourceInterface $source, array &$sql, array &$parameters)
410
-    {
411
-        if ($constraint instanceof \TYPO3\CMS\Extbase\Persistence\Generic\Qom\AndInterface) {
412
-            $sql['where'][] = '(';
413
-            $this->parseConstraint($constraint->getConstraint1(), $source, $sql, $parameters);
414
-            $sql['where'][] = ' AND ';
415
-            $this->parseConstraint($constraint->getConstraint2(), $source, $sql, $parameters);
416
-            $sql['where'][] = ')';
417
-        } elseif ($constraint instanceof \TYPO3\CMS\Extbase\Persistence\Generic\Qom\OrInterface) {
418
-            $sql['where'][] = '(';
419
-            $this->parseConstraint($constraint->getConstraint1(), $source, $sql, $parameters);
420
-            $sql['where'][] = ' OR ';
421
-            $this->parseConstraint($constraint->getConstraint2(), $source, $sql, $parameters);
422
-            $sql['where'][] = ')';
423
-        } elseif ($constraint instanceof \TYPO3\CMS\Extbase\Persistence\Generic\Qom\NotInterface) {
424
-            $sql['where'][] = 'NOT (';
425
-            $this->parseConstraint($constraint->getConstraint(), $source, $sql, $parameters);
426
-            $sql['where'][] = ')';
427
-        } elseif ($constraint instanceof ComparisonInterface) {
428
-            $this->parseComparison($constraint, $source, $sql, $parameters);
429
-        }
430
-    }
431
-
432
-    /**
433
-     * Parse a Comparison into SQL and parameter arrays.
434
-     *
435
-     * @param ComparisonInterface $comparison The comparison to parse
436
-     * @param SourceInterface $source The source
437
-     * @param array &$sql SQL query parts to add to
438
-     * @param array &$parameters Parameters to bind to the SQL
439
-     * @throws Exception\RepositoryException
440
-     * @return void
441
-     */
442
-    protected function parseComparison(ComparisonInterface $comparison, SourceInterface $source, array &$sql, array &$parameters)
443
-    {
444
-        $operand1 = $comparison->getOperand1();
445
-        $operator = $comparison->getOperator();
446
-        $operand2 = $comparison->getOperand2();
447
-        if ($operator === QueryInterface::OPERATOR_IN) {
448
-            $items = array();
449
-            $hasValue = FALSE;
450
-            foreach ($operand2 as $value) {
451
-                $value = $this->getPlainValue($value);
452
-                if ($value !== NULL) {
453
-                    $items[] = $value;
454
-                    $hasValue = TRUE;
455
-                }
456
-            }
457
-            if ($hasValue === FALSE) {
458
-                $sql['where'][] = '1<>1';
459
-            } else {
460
-                $this->parseDynamicOperand($operand1, $operator, $source, $sql, $parameters, NULL);
461
-                $parameters[] = $items;
462
-            }
463
-        } elseif ($operator === QueryInterface::OPERATOR_CONTAINS) {
464
-            if ($operand2 === NULL) {
465
-                $sql['where'][] = '1<>1';
466
-            } else {
467
-                throw new \Exception('Not implemented! Contact extension author.', 1412931227);
468
-                # @todo re-implement me if necessary.
469
-                #$tableName = $this->query->getType();
470
-                #$propertyName = $operand1->getPropertyName();
471
-                #while (strpos($propertyName, '.') !== FALSE) {
472
-                #	$this->addUnionStatement($tableName, $propertyName, $sql);
473
-                #}
474
-                #$columnName = $propertyName;
475
-                #$columnMap = $propertyName;
476
-                #$typeOfRelation = $columnMap instanceof ColumnMap ? $columnMap->getTypeOfRelation() : NULL;
477
-                #if ($typeOfRelation === ColumnMap::RELATION_HAS_AND_BELONGS_TO_MANY) {
478
-                #	$relationTableName = $columnMap->getRelationTableName();
479
-                #	$sql['where'][] = $tableName . '.uid IN (SELECT ' . $columnMap->getParentKeyFieldName() . ' FROM ' . $relationTableName . ' WHERE ' . $columnMap->getChildKeyFieldName() . '=?)';
480
-                #	$parameters[] = intval($this->getPlainValue($operand2));
481
-                #} elseif ($typeOfRelation === ColumnMap::RELATION_HAS_MANY) {
482
-                #	$parentKeyFieldName = $columnMap->getParentKeyFieldName();
483
-                #	if (isset($parentKeyFieldName)) {
484
-                #		$childTableName = $columnMap->getChildTableName();
485
-                #		$sql['where'][] = $tableName . '.uid=(SELECT ' . $childTableName . '.' . $parentKeyFieldName . ' FROM ' . $childTableName . ' WHERE ' . $childTableName . '.uid=?)';
486
-                #		$parameters[] = intval($this->getPlainValue($operand2));
487
-                #	} else {
488
-                #		$sql['where'][] = 'FIND_IN_SET(?,' . $tableName . '.' . $columnName . ')';
489
-                #		$parameters[] = intval($this->getPlainValue($operand2));
490
-                #	}
491
-                #} else {
492
-                #	throw new Exception\RepositoryException('Unsupported or non-existing property name "' . $propertyName . '" used in relation matching.', 1327065745);
493
-                #}
494
-            }
495
-        } else {
496
-            if ($operand2 === NULL) {
497
-                if ($operator === QueryInterface::OPERATOR_EQUAL_TO) {
498
-                    $operator = self::OPERATOR_EQUAL_TO_NULL;
499
-                } elseif ($operator === QueryInterface::OPERATOR_NOT_EQUAL_TO) {
500
-                    $operator = self::OPERATOR_NOT_EQUAL_TO_NULL;
501
-                }
502
-            }
503
-            $this->parseDynamicOperand($operand1, $operator, $source, $sql, $parameters);
504
-            $parameters[] = $this->getPlainValue($operand2);
505
-        }
506
-    }
507
-
508
-    /**
509
-     * Returns a plain value, i.e. objects are flattened out if possible.
510
-     *
511
-     * @param mixed $input
512
-     * @throws \TYPO3\CMS\Extbase\Persistence\Generic\Exception\UnexpectedTypeException
513
-     * @return mixed
514
-     */
515
-    protected function getPlainValue($input)
516
-    {
517
-        if (is_array($input)) {
518
-            throw new \TYPO3\CMS\Extbase\Persistence\Generic\Exception\UnexpectedTypeException('An array could not be converted to a plain value.', 1274799932);
519
-        }
520
-        if ($input instanceof \DateTime) {
521
-            return $input->format('U');
522
-        } elseif (is_object($input)) {
523
-            if ($input instanceof \TYPO3\CMS\Extbase\Persistence\Generic\LazyLoadingProxy) {
524
-                $realInput = $input->_loadRealInstance();
525
-            } else {
526
-                $realInput = $input;
527
-            }
528
-            if ($realInput instanceof \TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface) {
529
-                return $realInput->getUid();
530
-            } else {
531
-                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);
532
-            }
533
-        } elseif (is_bool($input)) {
534
-            return $input === TRUE ? 1 : 0;
535
-        } else {
536
-            return $input;
537
-        }
538
-    }
539
-
540
-    /**
541
-     * Parse a DynamicOperand into SQL and parameter arrays.
542
-     *
543
-     * @param DynamicOperandInterface $operand
544
-     * @param string $operator One of the JCR_OPERATOR_* constants
545
-     * @param SourceInterface $source The source
546
-     * @param array &$sql The query parts
547
-     * @param array &$parameters The parameters that will replace the markers
548
-     * @param string $valueFunction an optional SQL function to apply to the operand value
549
-     * @return void
550
-     */
551
-    protected function parseDynamicOperand(DynamicOperandInterface $operand, $operator, SourceInterface $source, array &$sql, array &$parameters, $valueFunction = NULL)
552
-    {
553
-        if ($operand instanceof LowerCaseInterface) {
554
-            $this->parseDynamicOperand($operand->getOperand(), $operator, $source, $sql, $parameters, 'LOWER');
555
-        } elseif ($operand instanceof UpperCaseInterface) {
556
-            $this->parseDynamicOperand($operand->getOperand(), $operator, $source, $sql, $parameters, 'UPPER');
557
-        } elseif ($operand instanceof PropertyValueInterface) {
558
-            $propertyName = $operand->getPropertyName();
559
-
560
-            // Reset value.
561
-            $this->currentChildTableNameAlias = '';
562
-
563
-            if ($source instanceof SelectorInterface) {
564
-                $tableName = $this->query->getType();
565
-                while (strpos($propertyName, '.') !== FALSE) {
566
-                    $this->addUnionStatement($tableName, $propertyName, $sql);
567
-                }
568
-            } elseif ($source instanceof JoinInterface) {
569
-                $tableName = $source->getJoinCondition()->getSelector1Name();
570
-            }
571
-
572
-            $columnName = $propertyName;
573
-            $operator = $this->resolveOperator($operator);
574
-            $constraintSQL = '';
575
-            if ($valueFunction === NULL) {
576
-                $constraintSQL .= (!empty($tableName) ? $tableName . '.' : '') . $columnName . ' ' . $operator . ' ?';
577
-            } else {
578
-                $constraintSQL .= $valueFunction . '(' . (!empty($tableName) ? $tableName . '.' : '') . $columnName . ') ' . $operator . ' ?';
579
-            }
580
-
581
-            if (isset($tableName) && !empty($this->currentChildTableNameAlias)) {
582
-                $constraintSQL = $this->replaceTableNameByAlias($tableName, $this->currentChildTableNameAlias, $constraintSQL);
583
-            }
584
-            $sql['where'][] = $constraintSQL;
585
-        }
586
-    }
587
-
588
-    /**
589
-     * @param string &$tableName
590
-     * @param array &$propertyPath
591
-     * @param array &$sql
592
-     * @throws Exception
593
-     * @throws Exception\InvalidRelationConfigurationException
594
-     * @throws \TYPO3\CMS\Extbase\Persistence\Generic\Exception\MissingColumnMapException
595
-     */
596
-    protected function addUnionStatement(&$tableName, &$propertyPath, array &$sql)
597
-    {
598
-
599
-        $table = Tca::table($tableName);
600
-
601
-        $explodedPropertyPath = explode('.', $propertyPath, 2);
602
-        $fieldName = $explodedPropertyPath[0];
603
-
604
-        // Field of type "group" are special because property path must contain the table name
605
-        // to determine the relation type. Example for sys_category, property path will look like "items.sys_file"
606
-        if ($table->field($fieldName)->isGroup()) {
607
-            $parts = explode('.', $propertyPath, 3);
608
-            $explodedPropertyPath[0] = $parts[0] . '.' . $parts[1];
609
-            $explodedPropertyPath[1] = $parts[2];
610
-            $fieldName = $explodedPropertyPath[0];
611
-        }
612
-
613
-        $parentKeyFieldName = $table->field($fieldName)->getForeignField();
614
-        $childTableName = $table->field($fieldName)->getForeignTable();
615
-
616
-        if ($childTableName === NULL) {
617
-            throw new Exception\InvalidRelationConfigurationException('The relation information for property "' . $fieldName . '" of class "' . $tableName . '" is missing.', 1353170925);
618
-        }
619
-
620
-        if ($table->field($fieldName)->hasOne()) { // includes relation "one-to-one" and "many-to-one"
621
-            // sometimes the opposite relation is not defined. We don't want to force this config for backward compatibility reasons.
622
-            // $parentKeyFieldName === NULL does the trick somehow. Before condition was if (isset($parentKeyFieldName))
623
-            if ($table->field($fieldName)->hasRelationManyToOne() || $parentKeyFieldName === NULL) {
624
-                $sql['unions'][$childTableName] = 'LEFT JOIN ' . $childTableName . ' ON ' . $tableName . '.' . $fieldName . '=' . $childTableName . '.uid';
625
-            } else {
626
-                $sql['unions'][$childTableName] = 'LEFT JOIN ' . $childTableName . ' ON ' . $tableName . '.uid=' . $childTableName . '.' . $parentKeyFieldName;
627
-            }
628
-        } elseif ($table->field($fieldName)->hasRelationManyToMany()) {
629
-            $relationTableName = $table->field($fieldName)->getManyToManyTable();
630
-
631
-            $parentKeyFieldName = $table->field($fieldName)->isOppositeRelation() ? 'uid_foreign' : 'uid_local';
632
-            $childKeyFieldName = !$table->field($fieldName)->isOppositeRelation() ? 'uid_foreign' : 'uid_local';
633
-
634
-            // MM table e.g sys_category_record_mm
635
-            $relationTableNameAlias = $this->generateAlias($relationTableName);
636
-            $join = sprintf(
637
-                'LEFT JOIN %s AS %s ON %s.uid=%s.%s', $relationTableName,
638
-                $relationTableNameAlias,
639
-                $tableName,
640
-                $relationTableNameAlias,
641
-                $parentKeyFieldName
642
-            );
643
-            $sql['unions'][$relationTableNameAlias] = $join;
644
-
645
-            // Foreign table e.g sys_category
646
-            $childTableNameAlias = $this->generateAlias($childTableName);
647
-            $this->currentChildTableNameAlias = $childTableNameAlias;
648
-            $join = sprintf(
649
-                'LEFT JOIN %s AS %s ON %s.%s=%s.uid',
650
-                $childTableName,
651
-                $childTableNameAlias,
652
-                $relationTableNameAlias,
653
-                $childKeyFieldName,
654
-                $childTableNameAlias
655
-            );
656
-            $sql['unions'][$childTableNameAlias] = $join;
657
-
658
-            // Find a possible table name for a MM condition.
659
-            $tableNameCondition = $table->field($fieldName)->getAdditionalTableNameCondition();
660
-            if ($tableNameCondition) {
661
-
662
-                // If we can find a source file name,  we can then retrieve more MM conditions from the TCA such as a field name.
663
-                $sourceFileName = $this->query->getSourceFieldName();
664
-                if (empty($sourceFileName)) {
665
-                    $additionalMMConditions = array(
666
-                        'tablenames' => $tableNameCondition,
667
-                    );
668
-                } else {
669
-                    $additionalMMConditions = Tca::table($tableNameCondition)->field($sourceFileName)->getAdditionalMMCondition();
670
-                }
671
-
672
-                foreach ($additionalMMConditions as $additionalFieldName => $additionalMMCondition) {
673
-                    $additionalJoin = sprintf(' AND %s.%s = "%s"', $relationTableNameAlias, $additionalFieldName, $additionalMMCondition);
674
-                    $sql['unions'][$relationTableNameAlias] .= $additionalJoin;
675
-
676
-                    $additionalJoin = sprintf(' AND %s.%s = "%s"', $relationTableNameAlias, $additionalFieldName, $additionalMMCondition);
677
-                    $sql['unions'][$childTableNameAlias] .= $additionalJoin;
678
-                }
679
-
680
-            }
681
-
682
-
683
-        } elseif ($table->field($fieldName)->hasMany()) { // includes relations "many-to-one" and "csv" relations
684
-            $childTableNameAlias = $this->generateAlias($childTableName);
685
-            $this->currentChildTableNameAlias = $childTableNameAlias;
686
-
687
-            if (isset($parentKeyFieldName)) {
688
-                $join = sprintf(
689
-                    'LEFT JOIN %s AS %s ON %s.uid=%s.%s',
690
-                    $childTableName,
691
-                    $childTableNameAlias,
692
-                    $tableName,
693
-                    $childTableNameAlias,
694
-                    $parentKeyFieldName
695
-                );
696
-                $sql['unions'][$childTableNameAlias] = $join;
697
-            } else {
698
-                $join = sprintf(
699
-                    'LEFT JOIN %s AS %s ON (FIND_IN_SET(%s.uid, %s.%s))',
700
-                    $childTableName,
701
-                    $childTableNameAlias,
702
-                    $childTableNameAlias,
703
-                    $tableName,
704
-                    $fieldName
705
-                );
706
-                $sql['unions'][$childTableNameAlias] = $join;
707
-            }
708
-        } else {
709
-            throw new Exception('Could not determine type of relation.', 1252502725);
710
-        }
711
-
712
-        // TODO check if there is another solution for this
713
-        $sql['keywords']['distinct'] = 'DISTINCT';
714
-        $propertyPath = $explodedPropertyPath[1];
715
-        $tableName = $childTableName;
716
-    }
717
-
718
-    /**
719
-     * Returns the SQL operator for the given JCR operator type.
720
-     *
721
-     * @param string $operator One of the JCR_OPERATOR_* constants
722
-     * @throws Exception
723
-     * @return string an SQL operator
724
-     */
725
-    protected function resolveOperator($operator)
726
-    {
727
-        switch ($operator) {
728
-            case self::OPERATOR_EQUAL_TO_NULL:
729
-                $operator = 'IS';
730
-                break;
731
-            case self::OPERATOR_NOT_EQUAL_TO_NULL:
732
-                $operator = 'IS NOT';
733
-                break;
734
-            case QueryInterface::OPERATOR_IN:
735
-                $operator = 'IN';
736
-                break;
737
-            case QueryInterface::OPERATOR_EQUAL_TO:
738
-                $operator = '=';
739
-                break;
740
-            case QueryInterface::OPERATOR_NOT_EQUAL_TO:
741
-                $operator = '!=';
742
-                break;
743
-            case QueryInterface::OPERATOR_LESS_THAN:
744
-                $operator = '<';
745
-                break;
746
-            case QueryInterface::OPERATOR_LESS_THAN_OR_EQUAL_TO:
747
-                $operator = '<=';
748
-                break;
749
-            case QueryInterface::OPERATOR_GREATER_THAN:
750
-                $operator = '>';
751
-                break;
752
-            case QueryInterface::OPERATOR_GREATER_THAN_OR_EQUAL_TO:
753
-                $operator = '>=';
754
-                break;
755
-            case QueryInterface::OPERATOR_LIKE:
756
-                $operator = 'LIKE';
757
-                break;
758
-            default:
759
-                throw new Exception('Unsupported operator encountered.', 1242816073);
760
-        }
761
-        return $operator;
762
-    }
763
-
764
-    /**
765
-     * Replace query placeholders in a query part by the given
766
-     * parameters.
767
-     *
768
-     * @param string &$sqlString The query part with placeholders
769
-     * @param array $parameters The parameters
770
-     * @param string $tableName
771
-     *
772
-     * @throws Exception
773
-     */
774
-    protected function replacePlaceholders(&$sqlString, array $parameters, $tableName = 'foo')
775
-    {
776
-        // TODO profile this method again
777
-        if (substr_count($sqlString, '?') !== count($parameters)) {
778
-            throw new Exception('The number of question marks to replace must be equal to the number of parameters.', 1242816074);
779
-        }
780
-        $offset = 0;
781
-        foreach ($parameters as $parameter) {
782
-            $markPosition = strpos($sqlString, '?', $offset);
783
-            if ($markPosition !== FALSE) {
784
-                if ($parameter === NULL) {
785
-                    $parameter = 'NULL';
786
-                } elseif (is_array($parameter) || $parameter instanceof \ArrayAccess || $parameter instanceof \Traversable) {
787
-                    $items = array();
788
-                    foreach ($parameter as $item) {
789
-                        $items[] = $this->databaseHandle->fullQuoteStr($item, $tableName);
790
-                    }
791
-                    $parameter = '(' . implode(',', $items) . ')';
792
-                } else {
793
-                    $parameter = $this->databaseHandle->fullQuoteStr($parameter, $tableName);
794
-                }
795
-                $sqlString = substr($sqlString, 0, $markPosition) . $parameter . substr($sqlString, ($markPosition + 1));
796
-            }
797
-            $offset = $markPosition + strlen($parameter);
798
-        }
799
-    }
800
-
801
-    /**
802
-     * Adds additional WHERE statements according to the query settings.
803
-     *
804
-     * @param QuerySettingsInterface $querySettings The TYPO3 CMS specific query settings
805
-     * @param string $tableNameOrAlias The table name to add the additional where clause for
806
-     * @param array &$statementParts
807
-     * @return void
808
-     */
809
-    protected function addAdditionalWhereClause(QuerySettingsInterface $querySettings, $tableNameOrAlias, &$statementParts)
810
-    {
811
-        $this->addVisibilityConstraintStatement($querySettings, $tableNameOrAlias, $statementParts);
812
-        if ($querySettings->getRespectSysLanguage()) {
813
-            $this->addSysLanguageStatement($tableNameOrAlias, $statementParts, $querySettings);
814
-        }
815
-        if ($querySettings->getRespectStoragePage()) {
816
-            $this->addPageIdStatement($tableNameOrAlias, $statementParts, $querySettings->getStoragePageIds());
817
-        }
818
-    }
819
-
820
-    /**
821
-     * Adds enableFields and deletedClause to the query if necessary
822
-     *
823
-     * @param QuerySettingsInterface $querySettings
824
-     * @param string $tableNameOrAlias The database table name
825
-     * @param array &$statementParts The query parts
826
-     * @return void
827
-     */
828
-    protected function addVisibilityConstraintStatement(QuerySettingsInterface $querySettings, $tableNameOrAlias, array &$statementParts)
829
-    {
830
-        $statement = '';
831
-        $tableName = $this->resolveTableNameAlias($tableNameOrAlias);
832
-        if (is_array($GLOBALS['TCA'][$tableName]['ctrl'])) {
833
-            $ignoreEnableFields = $querySettings->getIgnoreEnableFields();
834
-            $enableFieldsToBeIgnored = $querySettings->getEnableFieldsToBeIgnored();
835
-            $includeDeleted = $querySettings->getIncludeDeleted();
836
-            if ($this->environmentService->isEnvironmentInFrontendMode()) {
837
-                $statement .= $this->getFrontendConstraintStatement($tableNameOrAlias, $ignoreEnableFields, $enableFieldsToBeIgnored, $includeDeleted);
838
-            } else {
839
-                // TYPO3_MODE === 'BE'
840
-                $statement .= $this->getBackendConstraintStatement($tableNameOrAlias, $ignoreEnableFields, $includeDeleted);
841
-            }
842
-
843
-            // Remove the prefixing "AND" if any.
844
-            if (!empty($statement)) {
845
-                $statement = strtolower(substr($statement, 1, 3)) === 'and' ? substr($statement, 5) : $statement;
846
-                $statementParts['additionalWhereClause'][$tableNameOrAlias][] = $statement;
847
-            }
848
-        }
849
-    }
850
-
851
-    /**
852
-     * Returns constraint statement for frontend context
853
-     *
854
-     * @param string $tableNameOrAlias
855
-     * @param boolean $ignoreEnableFields A flag indicating whether the enable fields should be ignored
856
-     * @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.
857
-     * @param boolean $includeDeleted A flag indicating whether deleted records should be included
858
-     * @return string
859
-     * @throws Exception\InconsistentQuerySettingsException
860
-     */
861
-    protected function getFrontendConstraintStatement($tableNameOrAlias, $ignoreEnableFields, $enableFieldsToBeIgnored = array(), $includeDeleted)
862
-    {
863
-        $statement = '';
864
-        $tableName = $this->resolveTableNameAlias($tableNameOrAlias);
865
-        if ($ignoreEnableFields && !$includeDeleted) {
866
-            if (count($enableFieldsToBeIgnored)) {
867
-                // array_combine() is necessary because of the way \TYPO3\CMS\Frontend\Page\PageRepository::enableFields() is implemented
868
-                $statement .= $this->getPageRepository()->enableFields($tableName, -1, array_combine($enableFieldsToBeIgnored, $enableFieldsToBeIgnored));
869
-            } else {
870
-                $statement .= $this->getPageRepository()->deleteClause($tableName);
871
-            }
872
-        } elseif (!$ignoreEnableFields && !$includeDeleted) {
873
-            $statement .= $this->getPageRepository()->enableFields($tableName);
874
-        } elseif (!$ignoreEnableFields && $includeDeleted) {
875
-            throw new Exception\InconsistentQuerySettingsException('Query setting "ignoreEnableFields=FALSE" can not be used together with "includeDeleted=TRUE" in frontend context.', 1327678173);
876
-        }
877
-        return $this->replaceTableNameByAlias($tableName, $tableNameOrAlias, $statement);
878
-    }
879
-
880
-    /**
881
-     * Returns constraint statement for backend context
882
-     *
883
-     * @param string $tableNameOrAlias
884
-     * @param boolean $ignoreEnableFields A flag indicating whether the enable fields should be ignored
885
-     * @param boolean $includeDeleted A flag indicating whether deleted records should be included
886
-     * @return string
887
-     */
888
-    protected function getBackendConstraintStatement($tableNameOrAlias, $ignoreEnableFields, $includeDeleted)
889
-    {
890
-        $tableName = $this->resolveTableNameAlias($tableNameOrAlias);
891
-        $statement = '';
892
-        if (!$ignoreEnableFields) {
893
-            $statement .= BackendUtility::BEenableFields($tableName);
894
-        }
895
-
896
-        // If the table is found to have "workspace" support, add the corresponding fields in the statement.
897
-        if (Tca::table($tableName)->hasWorkspaceSupport()) {
898
-            if ($this->getBackendUser()->workspace === 0) {
899
-                $statement .= ' AND ' . $tableName . '.t3ver_state<=' . new VersionState(VersionState::DEFAULT_STATE);
900
-            } else {
901
-                // Show only records of live and of the current workspace
902
-                // In case we are in a Versioning preview
903
-                $statement .= ' AND (' .
904
-                    $tableName . '.t3ver_wsid=0 OR ' .
905
-                    $tableName . '.t3ver_wsid=' . (int)$this->getBackendUser()->workspace .
906
-                    ')';
907
-            }
908
-
909
-            // Check if this segment make sense here or whether it should be in the "if" part when we have workspace = 0
910
-            $statement .= ' AND ' . $tableName . '.pid<>-1';
911
-        }
912
-
913
-        if (!$includeDeleted) {
914
-            $statement .= BackendUtility::deleteClause($tableName);
915
-        }
916
-
917
-        return $this->replaceTableNameByAlias($tableName, $tableNameOrAlias, $statement);
918
-    }
919
-
920
-    /**
921
-     * Builds the language field statement
922
-     *
923
-     * @param string $tableNameOrAlias The database table name
924
-     * @param array &$statementParts The query parts
925
-     * @param QuerySettingsInterface $querySettings The TYPO3 CMS specific query settings
926
-     * @throws Exception
927
-     * @return void
928
-     */
929
-    protected function addSysLanguageStatement($tableNameOrAlias, array &$statementParts, $querySettings)
930
-    {
931
-
932
-        $tableName = $this->resolveTableNameAlias($tableNameOrAlias);
933
-        if (is_array($GLOBALS['TCA'][$tableName]['ctrl'])) {
934
-            if (!empty($GLOBALS['TCA'][$tableName]['ctrl']['languageField'])) {
935
-                // Select all entries for the current language
936
-                $additionalWhereClause = $tableNameOrAlias . '.' . $GLOBALS['TCA'][$tableName]['ctrl']['languageField'] . ' IN (' . intval($querySettings->getLanguageUid()) . ',-1)';
937
-                // If any language is set -> get those entries which are not translated yet
938
-                // They will be removed by t3lib_page::getRecordOverlay if not matching overlay mode
939
-                if (isset($GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField'])
940
-                    && $querySettings->getLanguageUid() > 0
941
-                ) {
942
-                    $additionalWhereClause .= ' OR (' . $tableNameOrAlias . '.' . $GLOBALS['TCA'][$tableName]['ctrl']['languageField'] . '=0' .
943
-                        ' AND ' . $tableNameOrAlias . '.uid NOT IN (SELECT ' . $tableName . '.' . $GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField'] .
944
-                        ' FROM ' . $tableName .
945
-                        ' WHERE ' . $tableName . '.' . $GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField'] . '>0' .
946
-                        ' AND ' . $tableName . '.' . $GLOBALS['TCA'][$tableName]['ctrl']['languageField'] . '>0';
947
-
948
-                    // Add delete clause to ensure all entries are loaded
949
-                    if (isset($GLOBALS['TCA'][$tableName]['ctrl']['delete'])) {
950
-                        $additionalWhereClause .= ' AND ' . $tableNameOrAlias . '.' . $GLOBALS['TCA'][$tableName]['ctrl']['delete'] . '=0';
951
-                    }
952
-                    $additionalWhereClause .= '))';
953
-                }
954
-                $statementParts['additionalWhereClause'][$tableNameOrAlias][] = '(' . $additionalWhereClause . ')';
955
-            }
956
-        }
957
-    }
958
-
959
-    /**
960
-     * Builds the page ID checking statement
961
-     *
962
-     * @param string $tableNameOrAlias The database table name
963
-     * @param array &$statementParts The query parts
964
-     * @param array $storagePageIds list of storage page ids
965
-     * @throws Exception\InconsistentQuerySettingsException
966
-     * @return void
967
-     */
968
-    protected function addPageIdStatement($tableNameOrAlias, array &$statementParts, array $storagePageIds)
969
-    {
970
-
971
-        $tableName = $this->resolveTableNameAlias($tableNameOrAlias);
972
-        $tableColumns = $this->tableColumnCache->get($tableName);
973
-        if ($tableColumns === FALSE) {
974
-            $tableColumns = $this->databaseHandle->admin_get_fields($tableName);
975
-            $this->tableColumnCache->set($tableName, $tableColumns);
976
-        }
977
-        if (is_array($GLOBALS['TCA'][$tableName]['ctrl']) && array_key_exists('pid', $tableColumns)) {
978
-            $rootLevel = (int)$GLOBALS['TCA'][$tableName]['ctrl']['rootLevel'];
979
-            if ($rootLevel) {
980
-                if ($rootLevel === 1) {
981
-                    $statementParts['additionalWhereClause'][$tableNameOrAlias][] = $tableNameOrAlias . '.pid = 0';
982
-                }
983
-            } else {
984
-                if (empty($storagePageIds)) {
985
-                    throw new Exception\InconsistentQuerySettingsException('Missing storage page ids.', 1365779762);
986
-                }
987
-                $statementParts['additionalWhereClause'][$tableNameOrAlias][] = $tableNameOrAlias . '.pid IN (' . implode(', ', $storagePageIds) . ')';
988
-            }
989
-        }
990
-    }
991
-
992
-    /**
993
-     * Transforms orderings into SQL.
994
-     *
995
-     * @param array $orderings An array of orderings (Tx_Extbase_Persistence_QOM_Ordering)
996
-     * @param SourceInterface $source The source
997
-     * @param array &$sql The query parts
998
-     * @throws Exception\UnsupportedOrderException
999
-     * @return void
1000
-     */
1001
-    protected function parseOrderings(array $orderings, SourceInterface $source, array &$sql)
1002
-    {
1003
-        foreach ($orderings as $fieldNameAndPath => $order) {
1004
-            switch ($order) {
1005
-                case QueryInterface::ORDER_ASCENDING:
1006
-                    $order = 'ASC';
1007
-                    break;
1008
-                case QueryInterface::ORDER_DESCENDING:
1009
-                    $order = 'DESC';
1010
-                    break;
1011
-                default:
1012
-                    throw new Exception\UnsupportedOrderException('Unsupported order encountered.', 1456845126);
1013
-            }
1014
-
1015
-            $tableName = $this->getFieldPathResolver()->getDataType($fieldNameAndPath, $this->query->getType());
1016
-            $fieldName = $this->getFieldPathResolver()->stripFieldPath($fieldNameAndPath, $tableName);
1017
-            $sql['orderings'][] = sprintf('%s.%s %s', $tableName, $fieldName, $order);
1018
-        }
1019
-    }
1020
-
1021
-    /**
1022
-     * Transforms limit and offset into SQL
1023
-     *
1024
-     * @param int $limit
1025
-     * @param int $offset
1026
-     * @param array &$sql
1027
-     * @return void
1028
-     */
1029
-    protected function parseLimitAndOffset($limit, $offset, array &$sql)
1030
-    {
1031
-        if ($limit !== NULL && $offset !== NULL) {
1032
-            $sql['limit'] = intval($offset) . ', ' . intval($limit);
1033
-        } elseif ($limit !== NULL) {
1034
-            $sql['limit'] = intval($limit);
1035
-        }
1036
-    }
1037
-
1038
-    /**
1039
-     * Transforms a Resource from a database query to an array of rows.
1040
-     *
1041
-     * @param resource $result The result
1042
-     * @return array The result as an array of rows (tuples)
1043
-     */
1044
-    protected function getRowsFromResult($result)
1045
-    {
1046
-        $rows = array();
1047
-        while ($row = $this->databaseHandle->sql_fetch_assoc($result)) {
1048
-            if (is_array($row)) {
1049
-
1050
-                // Get language uid from querySettings.
1051
-                // Ensure the backend handling is not broken (fallback to Get parameter 'L' if needed)
1052
-                $overlaidRow = $this->doLanguageAndWorkspaceOverlay($this->query->getSource(), $row, $this->query->getQuerySettings());
1053
-                $contentObject = GeneralUtility::makeInstance($this->objectType, $this->query->getType(), $overlaidRow);
1054
-                $rows[] = $contentObject;
1055
-            }
1056
-        }
1057
-
1058
-        return $rows;
1059
-    }
1060
-
1061
-    /**
1062
-     * Performs workspace and language overlay on the given row array. The language and workspace id is automatically
1063
-     * detected (depending on FE or BE context). You can also explicitly set the language/workspace id.
1064
-     *
1065
-     * @param SourceInterface $source The source (selector od join)
1066
-     * @param array $row
1067
-     * @param QuerySettingsInterface $querySettings The TYPO3 CMS specific query settings
1068
-     * @return array
1069
-     */
1070
-    protected function doLanguageAndWorkspaceOverlay(SourceInterface $source, array $row, $querySettings)
1071
-    {
1072
-
1073
-        /** @var SelectorInterface $source */
1074
-        $tableName = $source->getSelectorName();
1075
-
1076
-        $pageRepository = $this->getPageRepository();
1077
-        if (is_object($GLOBALS['TSFE'])) {
1078
-            $languageMode = $GLOBALS['TSFE']->sys_language_mode;
1079
-            if ($this->isBackendUserLogged() && $this->getBackendUser()->workspace !== 0) {
1080
-                $pageRepository->versioningWorkspaceId = $this->getBackendUser()->workspace;
1081
-            }
1082
-        } else {
1083
-            $languageMode = '';
1084
-            $workspaceUid = $this->getBackendUser()->workspace;
1085
-            $pageRepository->versioningWorkspaceId = $workspaceUid;
1086
-            if ($this->getBackendUser()->workspace !== 0) {
1087
-                $pageRepository->versioningPreview = 1;
1088
-            }
1089
-        }
1090
-
1091
-        // If current row is a translation select its parent
1092
-        if (isset($GLOBALS['TCA'][$tableName]['ctrl']['languageField'])
1093
-            && isset($GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField'])
1094
-        ) {
1095
-            if (isset($row[$GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField']])
1096
-                && $row[$GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField']] > 0
1097
-            ) {
1098
-                $row = $this->databaseHandle->exec_SELECTgetSingleRow(
1099
-                    $tableName . '.*',
1100
-                    $tableName,
1101
-                    $tableName . '.uid=' . (int)$row[$GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField']] .
1102
-                    ' AND ' . $tableName . '.' . $GLOBALS['TCA'][$tableName]['ctrl']['languageField'] . '=0'
1103
-                );
1104
-            }
1105
-        }
1106
-
1107
-        // Retrieve the original uid; Used for Workspaces!
1108
-        $pageRepository->versionOL($tableName, $row, true, true);
1109
-        if ($pageRepository->versioningPreview && isset($row['_ORIG_uid'])) {
1110
-            $row['uid'] = $row['_ORIG_uid'];
1111
-        }
1112
-
1113
-        // Special case for table "pages"
1114
-        if ($tableName == 'pages') {
1115
-            $row = $pageRepository->getPageOverlay($row, $querySettings->getLanguageUid());
1116
-        } elseif (isset($GLOBALS['TCA'][$tableName]['ctrl']['languageField'])
1117
-            && $GLOBALS['TCA'][$tableName]['ctrl']['languageField'] !== ''
1118
-        ) {
1119
-            if (in_array($row[$GLOBALS['TCA'][$tableName]['ctrl']['languageField']], array(-1, 0))) {
1120
-                $overlayMode = $languageMode === 'strict' ? 'hideNonTranslated' : '';
1121
-                $row = $pageRepository->getRecordOverlay($tableName, $row, $querySettings->getLanguageUid(), $overlayMode);
1122
-            }
1123
-        }
1124
-
1125
-        return $row;
1126
-    }
1127
-
1128
-    /**
1129
-     * Return a resolved table name given a possible table name alias.
1130
-     *
1131
-     * @param string $tableNameOrAlias
1132
-     * @return string
1133
-     */
1134
-    protected function resolveTableNameAlias($tableNameOrAlias)
1135
-    {
1136
-        $resolvedTableName = $tableNameOrAlias;
1137
-        if (!empty($this->tableNameAliases['aliases'][$tableNameOrAlias])) {
1138
-            $resolvedTableName = $this->tableNameAliases['aliases'][$tableNameOrAlias];
1139
-        }
1140
-        return $resolvedTableName;
1141
-    }
1142
-
1143
-    /**
1144
-     * Generate a unique table name alias for the given table name.
1145
-     *
1146
-     * @param string $tableName
1147
-     * @return string
1148
-     */
1149
-    protected function generateAlias($tableName)
1150
-    {
1151
-
1152
-        if (!isset($this->tableNameAliases['aliasIncrement'][$tableName])) {
1153
-            $this->tableNameAliases['aliasIncrement'][$tableName] = 0;
1154
-        }
1155
-
1156
-        $numberOfAliases = $this->tableNameAliases['aliasIncrement'][$tableName];
1157
-        $tableNameAlias = $tableName . $numberOfAliases;
1158
-
1159
-        $this->tableNameAliases['aliasIncrement'][$tableName]++;
1160
-        $this->tableNameAliases['aliases'][$tableNameAlias] = $tableName;
1161
-
1162
-        return $tableNameAlias;
1163
-    }
1164
-
1165
-    /**
1166
-     * Replace the table names by its table name alias within the given statement.
1167
-     *
1168
-     * @param string $tableName
1169
-     * @param string $tableNameAlias
1170
-     * @param string $statement
1171
-     * @return string
1172
-     */
1173
-    protected function replaceTableNameByAlias($tableName, $tableNameAlias, $statement)
1174
-    {
1175
-        if ($statement && $tableName !== $tableNameAlias) {
1176
-            $statement = str_replace($tableName, $tableNameAlias, $statement);
1177
-        }
1178
-        return $statement;
1179
-    }
1180
-
1181
-    /**
1182
-     * Returns an instance of the current Backend User.
1183
-     *
1184
-     * @return \TYPO3\CMS\Core\Authentication\BackendUserAuthentication
1185
-     */
1186
-    protected function getBackendUser()
1187
-    {
1188
-        return $GLOBALS['BE_USER'];
1189
-    }
1190
-
1191
-    /**
1192
-     * Tell whether a Backend User is logged in.
1193
-     *
1194
-     * @return bool
1195
-     */
1196
-    protected function isBackendUserLogged()
1197
-    {
1198
-        return is_object($GLOBALS['BE_USER']);
1199
-    }
1200
-
1201
-    /**
1202
-     * @return PageRepository
1203
-     */
1204
-    protected function getPageRepository()
1205
-    {
1206
-        if (!$this->pageRepository instanceof PageRepository) {
1207
-            if ($this->environmentService->isEnvironmentInFrontendMode() && is_object($GLOBALS['TSFE'])) {
1208
-                $this->pageRepository = $GLOBALS['TSFE']->sys_page;
1209
-            } else {
1210
-                $this->pageRepository = GeneralUtility::makeInstance('TYPO3\\CMS\\Frontend\\Page\\PageRepository');
1211
-            }
1212
-        }
1213
-
1214
-        return $this->pageRepository;
1215
-    }
1216
-
1217
-    /**
1218
-     * @return \Fab\Vidi\Resolver\FieldPathResolver
1219
-     */
1220
-    protected function getFieldPathResolver()
1221
-    {
1222
-        return GeneralUtility::makeInstance('Fab\Vidi\Resolver\FieldPathResolver');
1223
-    }
1224
-
1225
-    /**
1226
-     * Checks if there are SQL errors in the last query, and if yes, throw an exception.
1227
-     *
1228
-     * @return void
1229
-     * @param string $sql The SQL statement
1230
-     * @throws \TYPO3\CMS\Extbase\Persistence\Generic\Storage\Exception\SqlErrorException
1231
-     */
1232
-    protected function checkSqlErrors($sql = '')
1233
-    {
1234
-        $error = $this->databaseHandle->sql_error();
1235
-        if ($error !== '') {
1236
-            $error .= $sql ? ': ' . $sql : '';
1237
-            throw new \TYPO3\CMS\Extbase\Persistence\Generic\Storage\Exception\SqlErrorException($error, 1247602160);
1238
-        }
1239
-    }
35
+	const OPERATOR_EQUAL_TO_NULL = 'operatorEqualToNull';
36
+	const OPERATOR_NOT_EQUAL_TO_NULL = 'operatorNotEqualToNull';
37
+
38
+	/**
39
+	 * The TYPO3 database object
40
+	 *
41
+	 * @var \TYPO3\CMS\Core\Database\DatabaseConnection
42
+	 */
43
+	protected $databaseHandle;
44
+
45
+	/**
46
+	 * The TYPO3 page repository. Used for language and workspace overlay
47
+	 *
48
+	 * @var PageRepository
49
+	 */
50
+	protected $pageRepository;
51
+
52
+	/**
53
+	 * A first-level TypoScript configuration cache
54
+	 *
55
+	 * @var array
56
+	 */
57
+	protected $pageTSConfigCache = array();
58
+
59
+	/**
60
+	 * @var \TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface
61
+	 * @inject
62
+	 */
63
+	protected $configurationManager;
64
+
65
+	/**
66
+	 * @var \TYPO3\CMS\Extbase\Service\CacheService
67
+	 * @inject
68
+	 */
69
+	protected $cacheService;
70
+
71
+	/**
72
+	 * @var \TYPO3\CMS\Core\Cache\CacheManager
73
+	 * @inject
74
+	 */
75
+	protected $cacheManager;
76
+
77
+	/**
78
+	 * @var \TYPO3\CMS\Core\Cache\Frontend\VariableFrontend
79
+	 */
80
+	protected $tableColumnCache;
81
+
82
+	/**
83
+	 * @var \TYPO3\CMS\Extbase\Service\EnvironmentService
84
+	 * @inject
85
+	 */
86
+	protected $environmentService;
87
+
88
+	/**
89
+	 * @var \Fab\Vidi\Persistence\Query
90
+	 */
91
+	protected $query;
92
+
93
+	/**
94
+	 * Store some info related to table name and its aliases.
95
+	 *
96
+	 * @var array
97
+	 */
98
+	protected $tableNameAliases = array(
99
+		'aliases' => array(),
100
+		'aliasIncrement' => array(),
101
+	);
102
+
103
+	/**
104
+	 * Use to store the current foreign table name alias.
105
+	 *
106
+	 * @var string
107
+	 */
108
+	protected $currentChildTableNameAlias = '';
109
+
110
+	/**
111
+	 * The default object type being returned.
112
+	 *
113
+	 * @var string
114
+	 */
115
+	protected $objectType = 'Fab\Vidi\Domain\Model\Content';
116
+
117
+	/**
118
+	 * Constructor. takes the database handle from $GLOBALS['TYPO3_DB']
119
+	 */
120
+	public function __construct(QueryInterface $query)
121
+	{
122
+		$this->query = $query;
123
+		$this->databaseHandle = $GLOBALS['TYPO3_DB'];
124
+	}
125
+
126
+	/**
127
+	 * Lifecycle method
128
+	 *
129
+	 * @return void
130
+	 */
131
+	public function initializeObject()
132
+	{
133
+		$this->tableColumnCache = $this->cacheManager->getCache('extbase_typo3dbbackend_tablecolumns');
134
+	}
135
+
136
+	/**
137
+	 * @param array $identifier
138
+	 * @return string
139
+	 */
140
+	protected function parseIdentifier(array $identifier)
141
+	{
142
+		$fieldNames = array_keys($identifier);
143
+		$suffixedFieldNames = array();
144
+		foreach ($fieldNames as $fieldName) {
145
+			$suffixedFieldNames[] = $fieldName . '=?';
146
+		}
147
+		return implode(' AND ', $suffixedFieldNames);
148
+	}
149
+
150
+	/**
151
+	 * Returns the result of the query
152
+	 */
153
+	public function fetchResult()
154
+	{
155
+
156
+		$parameters = array();
157
+		$statementParts = $this->parseQuery($this->query, $parameters);
158
+		$statementParts = $this->processStatementStructureForRecursiveMMRelation($statementParts); // Mmm... check if that is the right way of doing that.
159
+
160
+		$sql = $this->buildQuery($statementParts);
161
+		$tableName = '';
162
+		if (is_array($statementParts) && !empty(reset($statementParts['tables']))) {
163
+			$tableName = reset($statementParts['tables']);
164
+		}
165
+		$this->replacePlaceholders($sql, $parameters, $tableName);
166
+		#print $sql; exit(); // @debug
167
+
168
+		$result = $this->databaseHandle->sql_query($sql);
169
+		$this->checkSqlErrors($sql);
170
+		$rows = $this->getRowsFromResult($result);
171
+		$this->databaseHandle->sql_free_result($result);
172
+
173
+		return $rows;
174
+	}
175
+
176
+	/**
177
+	 * Returns the number of tuples matching the query.
178
+	 *
179
+	 * @throws \TYPO3\CMS\Extbase\Persistence\Generic\Storage\Exception\BadConstraintException
180
+	 * @return int The number of matching tuples
181
+	 */
182
+	public function countResult()
183
+	{
184
+
185
+		$parameters = array();
186
+		$statementParts = $this->parseQuery($this->query, $parameters);
187
+		$statementParts = $this->processStatementStructureForRecursiveMMRelation($statementParts); // Mmm... check if that is the right way of doing that.
188
+		// Reset $statementParts for valid table return
189
+		reset($statementParts);
190
+
191
+		// if limit is set, we need to count the rows "manually" as COUNT(*) ignores LIMIT constraints
192
+		if (!empty($statementParts['limit'])) {
193
+			$statement = $this->buildQuery($statementParts);
194
+			$this->replacePlaceholders($statement, $parameters, current($statementParts['tables']));
195
+			#print $statement; exit(); // @debug
196
+			$result = $this->databaseHandle->sql_query($statement);
197
+			$this->checkSqlErrors($statement);
198
+			$count = $this->databaseHandle->sql_num_rows($result);
199
+		} else {
200
+			$statementParts['fields'] = array('COUNT(*)');
201
+			// having orderings without grouping is not compatible with non-MySQL DBMS
202
+			$statementParts['orderings'] = array();
203
+			if (isset($statementParts['keywords']['distinct'])) {
204
+				unset($statementParts['keywords']['distinct']);
205
+				$distinctField = $this->query->getDistinct() ? $this->query->getDistinct() : 'uid';
206
+				$statementParts['fields'] = array('COUNT(DISTINCT ' . reset($statementParts['tables']) . '.' . $distinctField . ')');
207
+			}
208
+
209
+			$statement = $this->buildQuery($statementParts);
210
+			$this->replacePlaceholders($statement, $parameters, current($statementParts['tables']));
211
+
212
+			#print $statement; exit(); // @debug
213
+			$result = $this->databaseHandle->sql_query($statement);
214
+			$this->checkSqlErrors($statement);
215
+			$count = 0;
216
+			if ($result) {
217
+				$row = $this->databaseHandle->sql_fetch_assoc($result);
218
+				$count = current($row);
219
+			}
220
+		}
221
+		$this->databaseHandle->sql_free_result($result);
222
+		return (int)$count;
223
+	}
224
+
225
+	/**
226
+	 * Parses the query and returns the SQL statement parts.
227
+	 *
228
+	 * @param QueryInterface $query The query
229
+	 * @param array &$parameters
230
+	 * @return array The SQL statement parts
231
+	 */
232
+	public function parseQuery(QueryInterface $query, array &$parameters)
233
+	{
234
+		$statementParts = array();
235
+		$statementParts['keywords'] = array();
236
+		$statementParts['tables'] = array();
237
+		$statementParts['unions'] = array();
238
+		$statementParts['fields'] = array();
239
+		$statementParts['where'] = array();
240
+		$statementParts['additionalWhereClause'] = array();
241
+		$statementParts['orderings'] = array();
242
+		$statementParts['limit'] = array();
243
+		$source = $query->getSource();
244
+		$this->parseSource($source, $statementParts);
245
+		$this->parseConstraint($query->getConstraint(), $source, $statementParts, $parameters);
246
+		$this->parseOrderings($query->getOrderings(), $source, $statementParts);
247
+		$this->parseLimitAndOffset($query->getLimit(), $query->getOffset(), $statementParts);
248
+		$tableNames = array_unique(array_keys($statementParts['tables'] + $statementParts['unions']));
249
+		foreach ($tableNames as $tableNameOrAlias) {
250
+			if (is_string($tableNameOrAlias) && strlen($tableNameOrAlias) > 0) {
251
+				$this->addAdditionalWhereClause($query->getQuerySettings(), $tableNameOrAlias, $statementParts);
252
+			}
253
+		}
254
+
255
+		return $statementParts;
256
+	}
257
+
258
+	/**
259
+	 * Fiddle with the statement structure to handle recursive MM relations.
260
+	 * For the recursive MM query to work, we must invert some values.
261
+	 * Let see if that is the best way of doing that...
262
+	 *
263
+	 * @param array $statementParts
264
+	 * @return array
265
+	 */
266
+	public function processStatementStructureForRecursiveMMRelation(array $statementParts)
267
+	{
268
+
269
+		if ($this->hasRecursiveMMRelation()) {
270
+			$tableName = $this->query->getType();
271
+
272
+			// In order the MM query to work for a recursive MM query, we must invert some values.
273
+			// tx_domain_model_foo0 (the alias) <--> tx_domain_model_foo (the origin table name)
274
+			$values = array();
275
+			foreach ($statementParts['fields'] as $key => $value) {
276
+				$values[$key] = str_replace($tableName, $tableName . '0', $value);
277
+			}
278
+			$statementParts['fields'] = $values;
279
+
280
+			// Same comment as above.
281
+			$values = array();
282
+			foreach ($statementParts['where'] as $key => $value) {
283
+				$values[$key] = str_replace($tableName . '0', $tableName, $value);
284
+			}
285
+			$statementParts['where'] = $values;
286
+
287
+			// We must be more restrictive by transforming the "left" union by "inner"
288
+			$values = array();
289
+			foreach ($statementParts['unions'] as $key => $value) {
290
+				$values[$key] = str_replace('LEFT JOIN', 'INNER JOIN', $value);
291
+			}
292
+			$statementParts['unions'] = $values;
293
+		}
294
+
295
+		return $statementParts;
296
+	}
297
+
298
+	/**
299
+	 * Tell whether there is a recursive MM relation.
300
+	 *
301
+	 * @return bool
302
+	 */
303
+	public function hasRecursiveMMRelation()
304
+	{
305
+		return isset($this->tableNameAliases['aliasIncrement'][$this->query->getType()]);
306
+
307
+	}
308
+
309
+	/**
310
+	 * Returns the statement, ready to be executed.
311
+	 *
312
+	 * @param array $statementParts The SQL statement parts
313
+	 * @return string The SQL statement
314
+	 */
315
+	public function buildQuery(array $statementParts)
316
+	{
317
+
318
+		// Add more statement to the UNION part.
319
+		if (!empty($statementParts['unions'])) {
320
+			foreach ($statementParts['unions'] as $tableName => $unionPart) {
321
+				if (!empty($statementParts['additionalWhereClause'][$tableName])) {
322
+					$statementParts['unions'][$tableName] .= ' AND ' . implode(' AND ', $statementParts['additionalWhereClause'][$tableName]);
323
+				}
324
+			}
325
+		}
326
+
327
+		$statement = 'SELECT ' . implode(' ', $statementParts['keywords']) . ' ' . implode(',', $statementParts['fields']) . ' FROM ' . implode(' ', $statementParts['tables']) . ' ' . implode(' ', $statementParts['unions']);
328
+		if (!empty($statementParts['where'])) {
329
+			$statement .= ' WHERE ' . implode('', $statementParts['where']);
330
+			if (!empty($statementParts['additionalWhereClause'][$this->query->getType()])) {
331
+				$statement .= ' AND ' . implode(' AND ', $statementParts['additionalWhereClause'][$this->query->getType()]);
332
+			}
333
+		} elseif (!empty($statementParts['additionalWhereClause'])) {
334
+			$statement .= ' WHERE ' . implode(' AND ', $statementParts['additionalWhereClause'][$this->query->getType()]);
335
+		}
336
+		if (!empty($statementParts['orderings'])) {
337
+			$statement .= ' ORDER BY ' . implode(', ', $statementParts['orderings']);
338
+		}
339
+		if (!empty($statementParts['limit'])) {
340
+			$statement .= ' LIMIT ' . $statementParts['limit'];
341
+		}
342
+
343
+		return $statement;
344
+	}
345
+
346
+	/**
347
+	 * Transforms a Query Source into SQL and parameter arrays
348
+	 *
349
+	 * @param SourceInterface $source The source
350
+	 * @param array &$sql
351
+	 * @return void
352
+	 */
353
+	protected function parseSource(SourceInterface $source, array &$sql)
354
+	{
355
+		if ($source instanceof SelectorInterface) {
356
+			$tableName = $source->getNodeTypeName();
357
+			$sql['fields'][$tableName] = $tableName . '.*';
358
+			$sql['tables'][$tableName] = $tableName;
359
+			if ($this->query->getDistinct()) {
360
+				$sql['fields'][$tableName] = $tableName . '.' . $this->query->getDistinct();
361
+				$sql['keywords']['distinct'] = 'DISTINCT';
362
+			}
363
+		} elseif ($source instanceof JoinInterface) {
364
+			$this->parseJoin($source, $sql);
365
+		}
366
+	}
367
+
368
+	/**
369
+	 * Transforms a Join into SQL and parameter arrays
370
+	 *
371
+	 * @param JoinInterface $join The join
372
+	 * @param array &$sql The query parts
373
+	 * @return void
374
+	 */
375
+	protected function parseJoin(JoinInterface $join, array &$sql)
376
+	{
377
+		$leftSource = $join->getLeft();
378
+		$leftTableName = $leftSource->getSelectorName();
379
+		// $sql['fields'][$leftTableName] = $leftTableName . '.*';
380
+		$rightSource = $join->getRight();
381
+		if ($rightSource instanceof JoinInterface) {
382
+			$rightTableName = $rightSource->getLeft()->getSelectorName();
383
+		} else {
384
+			$rightTableName = $rightSource->getSelectorName();
385
+			$sql['fields'][$leftTableName] = $rightTableName . '.*';
386
+		}
387
+		$sql['tables'][$leftTableName] = $leftTableName;
388
+		$sql['unions'][$rightTableName] = 'LEFT JOIN ' . $rightTableName;
389
+		$joinCondition = $join->getJoinCondition();
390
+		if ($joinCondition instanceof \TYPO3\CMS\Extbase\Persistence\Generic\Qom\EquiJoinCondition) {
391
+			$column1Name = $joinCondition->getProperty1Name();
392
+			$column2Name = $joinCondition->getProperty2Name();
393
+			$sql['unions'][$rightTableName] .= ' ON ' . $joinCondition->getSelector1Name() . '.' . $column1Name . ' = ' . $joinCondition->getSelector2Name() . '.' . $column2Name;
394
+		}
395
+		if ($rightSource instanceof JoinInterface) {
396
+			$this->parseJoin($rightSource, $sql);
397
+		}
398
+	}
399
+
400
+	/**
401
+	 * Transforms a constraint into SQL and parameter arrays
402
+	 *
403
+	 * @param \TYPO3\CMS\Extbase\Persistence\Generic\Qom\ConstraintInterface $constraint The constraint
404
+	 * @param SourceInterface $source The source
405
+	 * @param array &$sql The query parts
406
+	 * @param array &$parameters The parameters that will replace the markers
407
+	 * @return void
408
+	 */
409
+	protected function parseConstraint(\TYPO3\CMS\Extbase\Persistence\Generic\Qom\ConstraintInterface $constraint = NULL, SourceInterface $source, array &$sql, array &$parameters)
410
+	{
411
+		if ($constraint instanceof \TYPO3\CMS\Extbase\Persistence\Generic\Qom\AndInterface) {
412
+			$sql['where'][] = '(';
413
+			$this->parseConstraint($constraint->getConstraint1(), $source, $sql, $parameters);
414
+			$sql['where'][] = ' AND ';
415
+			$this->parseConstraint($constraint->getConstraint2(), $source, $sql, $parameters);
416
+			$sql['where'][] = ')';
417
+		} elseif ($constraint instanceof \TYPO3\CMS\Extbase\Persistence\Generic\Qom\OrInterface) {
418
+			$sql['where'][] = '(';
419
+			$this->parseConstraint($constraint->getConstraint1(), $source, $sql, $parameters);
420
+			$sql['where'][] = ' OR ';
421
+			$this->parseConstraint($constraint->getConstraint2(), $source, $sql, $parameters);
422
+			$sql['where'][] = ')';
423
+		} elseif ($constraint instanceof \TYPO3\CMS\Extbase\Persistence\Generic\Qom\NotInterface) {
424
+			$sql['where'][] = 'NOT (';
425
+			$this->parseConstraint($constraint->getConstraint(), $source, $sql, $parameters);
426
+			$sql['where'][] = ')';
427
+		} elseif ($constraint instanceof ComparisonInterface) {
428
+			$this->parseComparison($constraint, $source, $sql, $parameters);
429
+		}
430
+	}
431
+
432
+	/**
433
+	 * Parse a Comparison into SQL and parameter arrays.
434
+	 *
435
+	 * @param ComparisonInterface $comparison The comparison to parse
436
+	 * @param SourceInterface $source The source
437
+	 * @param array &$sql SQL query parts to add to
438
+	 * @param array &$parameters Parameters to bind to the SQL
439
+	 * @throws Exception\RepositoryException
440
+	 * @return void
441
+	 */
442
+	protected function parseComparison(ComparisonInterface $comparison, SourceInterface $source, array &$sql, array &$parameters)
443
+	{
444
+		$operand1 = $comparison->getOperand1();
445
+		$operator = $comparison->getOperator();
446
+		$operand2 = $comparison->getOperand2();
447
+		if ($operator === QueryInterface::OPERATOR_IN) {
448
+			$items = array();
449
+			$hasValue = FALSE;
450
+			foreach ($operand2 as $value) {
451
+				$value = $this->getPlainValue($value);
452
+				if ($value !== NULL) {
453
+					$items[] = $value;
454
+					$hasValue = TRUE;
455
+				}
456
+			}
457
+			if ($hasValue === FALSE) {
458
+				$sql['where'][] = '1<>1';
459
+			} else {
460
+				$this->parseDynamicOperand($operand1, $operator, $source, $sql, $parameters, NULL);
461
+				$parameters[] = $items;
462
+			}
463
+		} elseif ($operator === QueryInterface::OPERATOR_CONTAINS) {
464
+			if ($operand2 === NULL) {
465
+				$sql['where'][] = '1<>1';
466
+			} else {
467
+				throw new \Exception('Not implemented! Contact extension author.', 1412931227);
468
+				# @todo re-implement me if necessary.
469
+				#$tableName = $this->query->getType();
470
+				#$propertyName = $operand1->getPropertyName();
471
+				#while (strpos($propertyName, '.') !== FALSE) {
472
+				#	$this->addUnionStatement($tableName, $propertyName, $sql);
473
+				#}
474
+				#$columnName = $propertyName;
475
+				#$columnMap = $propertyName;
476
+				#$typeOfRelation = $columnMap instanceof ColumnMap ? $columnMap->getTypeOfRelation() : NULL;
477
+				#if ($typeOfRelation === ColumnMap::RELATION_HAS_AND_BELONGS_TO_MANY) {
478
+				#	$relationTableName = $columnMap->getRelationTableName();
479
+				#	$sql['where'][] = $tableName . '.uid IN (SELECT ' . $columnMap->getParentKeyFieldName() . ' FROM ' . $relationTableName . ' WHERE ' . $columnMap->getChildKeyFieldName() . '=?)';
480
+				#	$parameters[] = intval($this->getPlainValue($operand2));
481
+				#} elseif ($typeOfRelation === ColumnMap::RELATION_HAS_MANY) {
482
+				#	$parentKeyFieldName = $columnMap->getParentKeyFieldName();
483
+				#	if (isset($parentKeyFieldName)) {
484
+				#		$childTableName = $columnMap->getChildTableName();
485
+				#		$sql['where'][] = $tableName . '.uid=(SELECT ' . $childTableName . '.' . $parentKeyFieldName . ' FROM ' . $childTableName . ' WHERE ' . $childTableName . '.uid=?)';
486
+				#		$parameters[] = intval($this->getPlainValue($operand2));
487
+				#	} else {
488
+				#		$sql['where'][] = 'FIND_IN_SET(?,' . $tableName . '.' . $columnName . ')';
489
+				#		$parameters[] = intval($this->getPlainValue($operand2));
490
+				#	}
491
+				#} else {
492
+				#	throw new Exception\RepositoryException('Unsupported or non-existing property name "' . $propertyName . '" used in relation matching.', 1327065745);
493
+				#}
494
+			}
495
+		} else {
496
+			if ($operand2 === NULL) {
497
+				if ($operator === QueryInterface::OPERATOR_EQUAL_TO) {
498
+					$operator = self::OPERATOR_EQUAL_TO_NULL;
499
+				} elseif ($operator === QueryInterface::OPERATOR_NOT_EQUAL_TO) {
500
+					$operator = self::OPERATOR_NOT_EQUAL_TO_NULL;
501
+				}
502
+			}
503
+			$this->parseDynamicOperand($operand1, $operator, $source, $sql, $parameters);
504
+			$parameters[] = $this->getPlainValue($operand2);
505
+		}
506
+	}
507
+
508
+	/**
509
+	 * Returns a plain value, i.e. objects are flattened out if possible.
510
+	 *
511
+	 * @param mixed $input
512
+	 * @throws \TYPO3\CMS\Extbase\Persistence\Generic\Exception\UnexpectedTypeException
513
+	 * @return mixed
514
+	 */
515
+	protected function getPlainValue($input)
516
+	{
517
+		if (is_array($input)) {
518
+			throw new \TYPO3\CMS\Extbase\Persistence\Generic\Exception\UnexpectedTypeException('An array could not be converted to a plain value.', 1274799932);
519
+		}
520
+		if ($input instanceof \DateTime) {
521
+			return $input->format('U');
522
+		} elseif (is_object($input)) {
523
+			if ($input instanceof \TYPO3\CMS\Extbase\Persistence\Generic\LazyLoadingProxy) {
524
+				$realInput = $input->_loadRealInstance();
525
+			} else {
526
+				$realInput = $input;
527
+			}
528
+			if ($realInput instanceof \TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface) {
529
+				return $realInput->getUid();
530
+			} else {
531
+				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);
532
+			}
533
+		} elseif (is_bool($input)) {
534
+			return $input === TRUE ? 1 : 0;
535
+		} else {
536
+			return $input;
537
+		}
538
+	}
539
+
540
+	/**
541
+	 * Parse a DynamicOperand into SQL and parameter arrays.
542
+	 *
543
+	 * @param DynamicOperandInterface $operand
544
+	 * @param string $operator One of the JCR_OPERATOR_* constants
545
+	 * @param SourceInterface $source The source
546
+	 * @param array &$sql The query parts
547
+	 * @param array &$parameters The parameters that will replace the markers
548
+	 * @param string $valueFunction an optional SQL function to apply to the operand value
549
+	 * @return void
550
+	 */
551
+	protected function parseDynamicOperand(DynamicOperandInterface $operand, $operator, SourceInterface $source, array &$sql, array &$parameters, $valueFunction = NULL)
552
+	{
553
+		if ($operand instanceof LowerCaseInterface) {
554
+			$this->parseDynamicOperand($operand->getOperand(), $operator, $source, $sql, $parameters, 'LOWER');
555
+		} elseif ($operand instanceof UpperCaseInterface) {
556
+			$this->parseDynamicOperand($operand->getOperand(), $operator, $source, $sql, $parameters, 'UPPER');
557
+		} elseif ($operand instanceof PropertyValueInterface) {
558
+			$propertyName = $operand->getPropertyName();
559
+
560
+			// Reset value.
561
+			$this->currentChildTableNameAlias = '';
562
+
563
+			if ($source instanceof SelectorInterface) {
564
+				$tableName = $this->query->getType();
565
+				while (strpos($propertyName, '.') !== FALSE) {
566
+					$this->addUnionStatement($tableName, $propertyName, $sql);
567
+				}
568
+			} elseif ($source instanceof JoinInterface) {
569
+				$tableName = $source->getJoinCondition()->getSelector1Name();
570
+			}
571
+
572
+			$columnName = $propertyName;
573
+			$operator = $this->resolveOperator($operator);
574
+			$constraintSQL = '';
575
+			if ($valueFunction === NULL) {
576
+				$constraintSQL .= (!empty($tableName) ? $tableName . '.' : '') . $columnName . ' ' . $operator . ' ?';
577
+			} else {
578
+				$constraintSQL .= $valueFunction . '(' . (!empty($tableName) ? $tableName . '.' : '') . $columnName . ') ' . $operator . ' ?';
579
+			}
580
+
581
+			if (isset($tableName) && !empty($this->currentChildTableNameAlias)) {
582
+				$constraintSQL = $this->replaceTableNameByAlias($tableName, $this->currentChildTableNameAlias, $constraintSQL);
583
+			}
584
+			$sql['where'][] = $constraintSQL;
585
+		}
586
+	}
587
+
588
+	/**
589
+	 * @param string &$tableName
590
+	 * @param array &$propertyPath
591
+	 * @param array &$sql
592
+	 * @throws Exception
593
+	 * @throws Exception\InvalidRelationConfigurationException
594
+	 * @throws \TYPO3\CMS\Extbase\Persistence\Generic\Exception\MissingColumnMapException
595
+	 */
596
+	protected function addUnionStatement(&$tableName, &$propertyPath, array &$sql)
597
+	{
598
+
599
+		$table = Tca::table($tableName);
600
+
601
+		$explodedPropertyPath = explode('.', $propertyPath, 2);
602
+		$fieldName = $explodedPropertyPath[0];
603
+
604
+		// Field of type "group" are special because property path must contain the table name
605
+		// to determine the relation type. Example for sys_category, property path will look like "items.sys_file"
606
+		if ($table->field($fieldName)->isGroup()) {
607
+			$parts = explode('.', $propertyPath, 3);
608
+			$explodedPropertyPath[0] = $parts[0] . '.' . $parts[1];
609
+			$explodedPropertyPath[1] = $parts[2];
610
+			$fieldName = $explodedPropertyPath[0];
611
+		}
612
+
613
+		$parentKeyFieldName = $table->field($fieldName)->getForeignField();
614
+		$childTableName = $table->field($fieldName)->getForeignTable();
615
+
616
+		if ($childTableName === NULL) {
617
+			throw new Exception\InvalidRelationConfigurationException('The relation information for property "' . $fieldName . '" of class "' . $tableName . '" is missing.', 1353170925);
618
+		}
619
+
620
+		if ($table->field($fieldName)->hasOne()) { // includes relation "one-to-one" and "many-to-one"
621
+			// sometimes the opposite relation is not defined. We don't want to force this config for backward compatibility reasons.
622
+			// $parentKeyFieldName === NULL does the trick somehow. Before condition was if (isset($parentKeyFieldName))
623
+			if ($table->field($fieldName)->hasRelationManyToOne() || $parentKeyFieldName === NULL) {
624
+				$sql['unions'][$childTableName] = 'LEFT JOIN ' . $childTableName . ' ON ' . $tableName . '.' . $fieldName . '=' . $childTableName . '.uid';
625
+			} else {
626
+				$sql['unions'][$childTableName] = 'LEFT JOIN ' . $childTableName . ' ON ' . $tableName . '.uid=' . $childTableName . '.' . $parentKeyFieldName;
627
+			}
628
+		} elseif ($table->field($fieldName)->hasRelationManyToMany()) {
629
+			$relationTableName = $table->field($fieldName)->getManyToManyTable();
630
+
631
+			$parentKeyFieldName = $table->field($fieldName)->isOppositeRelation() ? 'uid_foreign' : 'uid_local';
632
+			$childKeyFieldName = !$table->field($fieldName)->isOppositeRelation() ? 'uid_foreign' : 'uid_local';
633
+
634
+			// MM table e.g sys_category_record_mm
635
+			$relationTableNameAlias = $this->generateAlias($relationTableName);
636
+			$join = sprintf(
637
+				'LEFT JOIN %s AS %s ON %s.uid=%s.%s', $relationTableName,
638
+				$relationTableNameAlias,
639
+				$tableName,
640
+				$relationTableNameAlias,
641
+				$parentKeyFieldName
642
+			);
643
+			$sql['unions'][$relationTableNameAlias] = $join;
644
+
645
+			// Foreign table e.g sys_category
646
+			$childTableNameAlias = $this->generateAlias($childTableName);
647
+			$this->currentChildTableNameAlias = $childTableNameAlias;
648
+			$join = sprintf(
649
+				'LEFT JOIN %s AS %s ON %s.%s=%s.uid',
650
+				$childTableName,
651
+				$childTableNameAlias,
652
+				$relationTableNameAlias,
653
+				$childKeyFieldName,
654
+				$childTableNameAlias
655
+			);
656
+			$sql['unions'][$childTableNameAlias] = $join;
657
+
658
+			// Find a possible table name for a MM condition.
659
+			$tableNameCondition = $table->field($fieldName)->getAdditionalTableNameCondition();
660
+			if ($tableNameCondition) {
661
+
662
+				// If we can find a source file name,  we can then retrieve more MM conditions from the TCA such as a field name.
663
+				$sourceFileName = $this->query->getSourceFieldName();
664
+				if (empty($sourceFileName)) {
665
+					$additionalMMConditions = array(
666
+						'tablenames' => $tableNameCondition,
667
+					);
668
+				} else {
669
+					$additionalMMConditions = Tca::table($tableNameCondition)->field($sourceFileName)->getAdditionalMMCondition();
670
+				}
671
+
672
+				foreach ($additionalMMConditions as $additionalFieldName => $additionalMMCondition) {
673
+					$additionalJoin = sprintf(' AND %s.%s = "%s"', $relationTableNameAlias, $additionalFieldName, $additionalMMCondition);
674
+					$sql['unions'][$relationTableNameAlias] .= $additionalJoin;
675
+
676
+					$additionalJoin = sprintf(' AND %s.%s = "%s"', $relationTableNameAlias, $additionalFieldName, $additionalMMCondition);
677
+					$sql['unions'][$childTableNameAlias] .= $additionalJoin;
678
+				}
679
+
680
+			}
681
+
682
+
683
+		} elseif ($table->field($fieldName)->hasMany()) { // includes relations "many-to-one" and "csv" relations
684
+			$childTableNameAlias = $this->generateAlias($childTableName);
685
+			$this->currentChildTableNameAlias = $childTableNameAlias;
686
+
687
+			if (isset($parentKeyFieldName)) {
688
+				$join = sprintf(
689
+					'LEFT JOIN %s AS %s ON %s.uid=%s.%s',
690
+					$childTableName,
691
+					$childTableNameAlias,
692
+					$tableName,
693
+					$childTableNameAlias,
694
+					$parentKeyFieldName
695
+				);
696
+				$sql['unions'][$childTableNameAlias] = $join;
697
+			} else {
698
+				$join = sprintf(
699
+					'LEFT JOIN %s AS %s ON (FIND_IN_SET(%s.uid, %s.%s))',
700
+					$childTableName,
701
+					$childTableNameAlias,
702
+					$childTableNameAlias,
703
+					$tableName,
704
+					$fieldName
705
+				);
706
+				$sql['unions'][$childTableNameAlias] = $join;
707
+			}
708
+		} else {
709
+			throw new Exception('Could not determine type of relation.', 1252502725);
710
+		}
711
+
712
+		// TODO check if there is another solution for this
713
+		$sql['keywords']['distinct'] = 'DISTINCT';
714
+		$propertyPath = $explodedPropertyPath[1];
715
+		$tableName = $childTableName;
716
+	}
717
+
718
+	/**
719
+	 * Returns the SQL operator for the given JCR operator type.
720
+	 *
721
+	 * @param string $operator One of the JCR_OPERATOR_* constants
722
+	 * @throws Exception
723
+	 * @return string an SQL operator
724
+	 */
725
+	protected function resolveOperator($operator)
726
+	{
727
+		switch ($operator) {
728
+			case self::OPERATOR_EQUAL_TO_NULL:
729
+				$operator = 'IS';
730
+				break;
731
+			case self::OPERATOR_NOT_EQUAL_TO_NULL:
732
+				$operator = 'IS NOT';
733
+				break;
734
+			case QueryInterface::OPERATOR_IN:
735
+				$operator = 'IN';
736
+				break;
737
+			case QueryInterface::OPERATOR_EQUAL_TO:
738
+				$operator = '=';
739
+				break;
740
+			case QueryInterface::OPERATOR_NOT_EQUAL_TO:
741
+				$operator = '!=';
742
+				break;
743
+			case QueryInterface::OPERATOR_LESS_THAN:
744
+				$operator = '<';
745
+				break;
746
+			case QueryInterface::OPERATOR_LESS_THAN_OR_EQUAL_TO:
747
+				$operator = '<=';
748
+				break;
749
+			case QueryInterface::OPERATOR_GREATER_THAN:
750
+				$operator = '>';
751
+				break;
752
+			case QueryInterface::OPERATOR_GREATER_THAN_OR_EQUAL_TO:
753
+				$operator = '>=';
754
+				break;
755
+			case QueryInterface::OPERATOR_LIKE:
756
+				$operator = 'LIKE';
757
+				break;
758
+			default:
759
+				throw new Exception('Unsupported operator encountered.', 1242816073);
760
+		}
761
+		return $operator;
762
+	}
763
+
764
+	/**
765
+	 * Replace query placeholders in a query part by the given
766
+	 * parameters.
767
+	 *
768
+	 * @param string &$sqlString The query part with placeholders
769
+	 * @param array $parameters The parameters
770
+	 * @param string $tableName
771
+	 *
772
+	 * @throws Exception
773
+	 */
774
+	protected function replacePlaceholders(&$sqlString, array $parameters, $tableName = 'foo')
775
+	{
776
+		// TODO profile this method again
777
+		if (substr_count($sqlString, '?') !== count($parameters)) {
778
+			throw new Exception('The number of question marks to replace must be equal to the number of parameters.', 1242816074);
779
+		}
780
+		$offset = 0;
781
+		foreach ($parameters as $parameter) {
782
+			$markPosition = strpos($sqlString, '?', $offset);
783
+			if ($markPosition !== FALSE) {
784
+				if ($parameter === NULL) {
785
+					$parameter = 'NULL';
786
+				} elseif (is_array($parameter) || $parameter instanceof \ArrayAccess || $parameter instanceof \Traversable) {
787
+					$items = array();
788
+					foreach ($parameter as $item) {
789
+						$items[] = $this->databaseHandle->fullQuoteStr($item, $tableName);
790
+					}
791
+					$parameter = '(' . implode(',', $items) . ')';
792
+				} else {
793
+					$parameter = $this->databaseHandle->fullQuoteStr($parameter, $tableName);
794
+				}
795
+				$sqlString = substr($sqlString, 0, $markPosition) . $parameter . substr($sqlString, ($markPosition + 1));
796
+			}
797
+			$offset = $markPosition + strlen($parameter);
798
+		}
799
+	}
800
+
801
+	/**
802
+	 * Adds additional WHERE statements according to the query settings.
803
+	 *
804
+	 * @param QuerySettingsInterface $querySettings The TYPO3 CMS specific query settings
805
+	 * @param string $tableNameOrAlias The table name to add the additional where clause for
806
+	 * @param array &$statementParts
807
+	 * @return void
808
+	 */
809
+	protected function addAdditionalWhereClause(QuerySettingsInterface $querySettings, $tableNameOrAlias, &$statementParts)
810
+	{
811
+		$this->addVisibilityConstraintStatement($querySettings, $tableNameOrAlias, $statementParts);
812
+		if ($querySettings->getRespectSysLanguage()) {
813
+			$this->addSysLanguageStatement($tableNameOrAlias, $statementParts, $querySettings);
814
+		}
815
+		if ($querySettings->getRespectStoragePage()) {
816
+			$this->addPageIdStatement($tableNameOrAlias, $statementParts, $querySettings->getStoragePageIds());
817
+		}
818
+	}
819
+
820
+	/**
821
+	 * Adds enableFields and deletedClause to the query if necessary
822
+	 *
823
+	 * @param QuerySettingsInterface $querySettings
824
+	 * @param string $tableNameOrAlias The database table name
825
+	 * @param array &$statementParts The query parts
826
+	 * @return void
827
+	 */
828
+	protected function addVisibilityConstraintStatement(QuerySettingsInterface $querySettings, $tableNameOrAlias, array &$statementParts)
829
+	{
830
+		$statement = '';
831
+		$tableName = $this->resolveTableNameAlias($tableNameOrAlias);
832
+		if (is_array($GLOBALS['TCA'][$tableName]['ctrl'])) {
833
+			$ignoreEnableFields = $querySettings->getIgnoreEnableFields();
834
+			$enableFieldsToBeIgnored = $querySettings->getEnableFieldsToBeIgnored();
835
+			$includeDeleted = $querySettings->getIncludeDeleted();
836
+			if ($this->environmentService->isEnvironmentInFrontendMode()) {
837
+				$statement .= $this->getFrontendConstraintStatement($tableNameOrAlias, $ignoreEnableFields, $enableFieldsToBeIgnored, $includeDeleted);
838
+			} else {
839
+				// TYPO3_MODE === 'BE'
840
+				$statement .= $this->getBackendConstraintStatement($tableNameOrAlias, $ignoreEnableFields, $includeDeleted);
841
+			}
842
+
843
+			// Remove the prefixing "AND" if any.
844
+			if (!empty($statement)) {
845
+				$statement = strtolower(substr($statement, 1, 3)) === 'and' ? substr($statement, 5) : $statement;
846
+				$statementParts['additionalWhereClause'][$tableNameOrAlias][] = $statement;
847
+			}
848
+		}
849
+	}
850
+
851
+	/**
852
+	 * Returns constraint statement for frontend context
853
+	 *
854
+	 * @param string $tableNameOrAlias
855
+	 * @param boolean $ignoreEnableFields A flag indicating whether the enable fields should be ignored
856
+	 * @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.
857
+	 * @param boolean $includeDeleted A flag indicating whether deleted records should be included
858
+	 * @return string
859
+	 * @throws Exception\InconsistentQuerySettingsException
860
+	 */
861
+	protected function getFrontendConstraintStatement($tableNameOrAlias, $ignoreEnableFields, $enableFieldsToBeIgnored = array(), $includeDeleted)
862
+	{
863
+		$statement = '';
864
+		$tableName = $this->resolveTableNameAlias($tableNameOrAlias);
865
+		if ($ignoreEnableFields && !$includeDeleted) {
866
+			if (count($enableFieldsToBeIgnored)) {
867
+				// array_combine() is necessary because of the way \TYPO3\CMS\Frontend\Page\PageRepository::enableFields() is implemented
868
+				$statement .= $this->getPageRepository()->enableFields($tableName, -1, array_combine($enableFieldsToBeIgnored, $enableFieldsToBeIgnored));
869
+			} else {
870
+				$statement .= $this->getPageRepository()->deleteClause($tableName);
871
+			}
872
+		} elseif (!$ignoreEnableFields && !$includeDeleted) {
873
+			$statement .= $this->getPageRepository()->enableFields($tableName);
874
+		} elseif (!$ignoreEnableFields && $includeDeleted) {
875
+			throw new Exception\InconsistentQuerySettingsException('Query setting "ignoreEnableFields=FALSE" can not be used together with "includeDeleted=TRUE" in frontend context.', 1327678173);
876
+		}
877
+		return $this->replaceTableNameByAlias($tableName, $tableNameOrAlias, $statement);
878
+	}
879
+
880
+	/**
881
+	 * Returns constraint statement for backend context
882
+	 *
883
+	 * @param string $tableNameOrAlias
884
+	 * @param boolean $ignoreEnableFields A flag indicating whether the enable fields should be ignored
885
+	 * @param boolean $includeDeleted A flag indicating whether deleted records should be included
886
+	 * @return string
887
+	 */
888
+	protected function getBackendConstraintStatement($tableNameOrAlias, $ignoreEnableFields, $includeDeleted)
889
+	{
890
+		$tableName = $this->resolveTableNameAlias($tableNameOrAlias);
891
+		$statement = '';
892
+		if (!$ignoreEnableFields) {
893
+			$statement .= BackendUtility::BEenableFields($tableName);
894
+		}
895
+
896
+		// If the table is found to have "workspace" support, add the corresponding fields in the statement.
897
+		if (Tca::table($tableName)->hasWorkspaceSupport()) {
898
+			if ($this->getBackendUser()->workspace === 0) {
899
+				$statement .= ' AND ' . $tableName . '.t3ver_state<=' . new VersionState(VersionState::DEFAULT_STATE);
900
+			} else {
901
+				// Show only records of live and of the current workspace
902
+				// In case we are in a Versioning preview
903
+				$statement .= ' AND (' .
904
+					$tableName . '.t3ver_wsid=0 OR ' .
905
+					$tableName . '.t3ver_wsid=' . (int)$this->getBackendUser()->workspace .
906
+					')';
907
+			}
908
+
909
+			// Check if this segment make sense here or whether it should be in the "if" part when we have workspace = 0
910
+			$statement .= ' AND ' . $tableName . '.pid<>-1';
911
+		}
912
+
913
+		if (!$includeDeleted) {
914
+			$statement .= BackendUtility::deleteClause($tableName);
915
+		}
916
+
917
+		return $this->replaceTableNameByAlias($tableName, $tableNameOrAlias, $statement);
918
+	}
919
+
920
+	/**
921
+	 * Builds the language field statement
922
+	 *
923
+	 * @param string $tableNameOrAlias The database table name
924
+	 * @param array &$statementParts The query parts
925
+	 * @param QuerySettingsInterface $querySettings The TYPO3 CMS specific query settings
926
+	 * @throws Exception
927
+	 * @return void
928
+	 */
929
+	protected function addSysLanguageStatement($tableNameOrAlias, array &$statementParts, $querySettings)
930
+	{
931
+
932
+		$tableName = $this->resolveTableNameAlias($tableNameOrAlias);
933
+		if (is_array($GLOBALS['TCA'][$tableName]['ctrl'])) {
934
+			if (!empty($GLOBALS['TCA'][$tableName]['ctrl']['languageField'])) {
935
+				// Select all entries for the current language
936
+				$additionalWhereClause = $tableNameOrAlias . '.' . $GLOBALS['TCA'][$tableName]['ctrl']['languageField'] . ' IN (' . intval($querySettings->getLanguageUid()) . ',-1)';
937
+				// If any language is set -> get those entries which are not translated yet
938
+				// They will be removed by t3lib_page::getRecordOverlay if not matching overlay mode
939
+				if (isset($GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField'])
940
+					&& $querySettings->getLanguageUid() > 0
941
+				) {
942
+					$additionalWhereClause .= ' OR (' . $tableNameOrAlias . '.' . $GLOBALS['TCA'][$tableName]['ctrl']['languageField'] . '=0' .
943
+						' AND ' . $tableNameOrAlias . '.uid NOT IN (SELECT ' . $tableName . '.' . $GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField'] .
944
+						' FROM ' . $tableName .
945
+						' WHERE ' . $tableName . '.' . $GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField'] . '>0' .
946
+						' AND ' . $tableName . '.' . $GLOBALS['TCA'][$tableName]['ctrl']['languageField'] . '>0';
947
+
948
+					// Add delete clause to ensure all entries are loaded
949
+					if (isset($GLOBALS['TCA'][$tableName]['ctrl']['delete'])) {
950
+						$additionalWhereClause .= ' AND ' . $tableNameOrAlias . '.' . $GLOBALS['TCA'][$tableName]['ctrl']['delete'] . '=0';
951
+					}
952
+					$additionalWhereClause .= '))';
953
+				}
954
+				$statementParts['additionalWhereClause'][$tableNameOrAlias][] = '(' . $additionalWhereClause . ')';
955
+			}
956
+		}
957
+	}
958
+
959
+	/**
960
+	 * Builds the page ID checking statement
961
+	 *
962
+	 * @param string $tableNameOrAlias The database table name
963
+	 * @param array &$statementParts The query parts
964
+	 * @param array $storagePageIds list of storage page ids
965
+	 * @throws Exception\InconsistentQuerySettingsException
966
+	 * @return void
967
+	 */
968
+	protected function addPageIdStatement($tableNameOrAlias, array &$statementParts, array $storagePageIds)
969
+	{
970
+
971
+		$tableName = $this->resolveTableNameAlias($tableNameOrAlias);
972
+		$tableColumns = $this->tableColumnCache->get($tableName);
973
+		if ($tableColumns === FALSE) {
974
+			$tableColumns = $this->databaseHandle->admin_get_fields($tableName);
975
+			$this->tableColumnCache->set($tableName, $tableColumns);
976
+		}
977
+		if (is_array($GLOBALS['TCA'][$tableName]['ctrl']) && array_key_exists('pid', $tableColumns)) {
978
+			$rootLevel = (int)$GLOBALS['TCA'][$tableName]['ctrl']['rootLevel'];
979
+			if ($rootLevel) {
980
+				if ($rootLevel === 1) {
981
+					$statementParts['additionalWhereClause'][$tableNameOrAlias][] = $tableNameOrAlias . '.pid = 0';
982
+				}
983
+			} else {
984
+				if (empty($storagePageIds)) {
985
+					throw new Exception\InconsistentQuerySettingsException('Missing storage page ids.', 1365779762);
986
+				}
987
+				$statementParts['additionalWhereClause'][$tableNameOrAlias][] = $tableNameOrAlias . '.pid IN (' . implode(', ', $storagePageIds) . ')';
988
+			}
989
+		}
990
+	}
991
+
992
+	/**
993
+	 * Transforms orderings into SQL.
994
+	 *
995
+	 * @param array $orderings An array of orderings (Tx_Extbase_Persistence_QOM_Ordering)
996
+	 * @param SourceInterface $source The source
997
+	 * @param array &$sql The query parts
998
+	 * @throws Exception\UnsupportedOrderException
999
+	 * @return void
1000
+	 */
1001
+	protected function parseOrderings(array $orderings, SourceInterface $source, array &$sql)
1002
+	{
1003
+		foreach ($orderings as $fieldNameAndPath => $order) {
1004
+			switch ($order) {
1005
+				case QueryInterface::ORDER_ASCENDING:
1006
+					$order = 'ASC';
1007
+					break;
1008
+				case QueryInterface::ORDER_DESCENDING:
1009
+					$order = 'DESC';
1010
+					break;
1011
+				default:
1012
+					throw new Exception\UnsupportedOrderException('Unsupported order encountered.', 1456845126);
1013
+			}
1014
+
1015
+			$tableName = $this->getFieldPathResolver()->getDataType($fieldNameAndPath, $this->query->getType());
1016
+			$fieldName = $this->getFieldPathResolver()->stripFieldPath($fieldNameAndPath, $tableName);
1017
+			$sql['orderings'][] = sprintf('%s.%s %s', $tableName, $fieldName, $order);
1018
+		}
1019
+	}
1020
+
1021
+	/**
1022
+	 * Transforms limit and offset into SQL
1023
+	 *
1024
+	 * @param int $limit
1025
+	 * @param int $offset
1026
+	 * @param array &$sql
1027
+	 * @return void
1028
+	 */
1029
+	protected function parseLimitAndOffset($limit, $offset, array &$sql)
1030
+	{
1031
+		if ($limit !== NULL && $offset !== NULL) {
1032
+			$sql['limit'] = intval($offset) . ', ' . intval($limit);
1033
+		} elseif ($limit !== NULL) {
1034
+			$sql['limit'] = intval($limit);
1035
+		}
1036
+	}
1037
+
1038
+	/**
1039
+	 * Transforms a Resource from a database query to an array of rows.
1040
+	 *
1041
+	 * @param resource $result The result
1042
+	 * @return array The result as an array of rows (tuples)
1043
+	 */
1044
+	protected function getRowsFromResult($result)
1045
+	{
1046
+		$rows = array();
1047
+		while ($row = $this->databaseHandle->sql_fetch_assoc($result)) {
1048
+			if (is_array($row)) {
1049
+
1050
+				// Get language uid from querySettings.
1051
+				// Ensure the backend handling is not broken (fallback to Get parameter 'L' if needed)
1052
+				$overlaidRow = $this->doLanguageAndWorkspaceOverlay($this->query->getSource(), $row, $this->query->getQuerySettings());
1053
+				$contentObject = GeneralUtility::makeInstance($this->objectType, $this->query->getType(), $overlaidRow);
1054
+				$rows[] = $contentObject;
1055
+			}
1056
+		}
1057
+
1058
+		return $rows;
1059
+	}
1060
+
1061
+	/**
1062
+	 * Performs workspace and language overlay on the given row array. The language and workspace id is automatically
1063
+	 * detected (depending on FE or BE context). You can also explicitly set the language/workspace id.
1064
+	 *
1065
+	 * @param SourceInterface $source The source (selector od join)
1066
+	 * @param array $row
1067
+	 * @param QuerySettingsInterface $querySettings The TYPO3 CMS specific query settings
1068
+	 * @return array
1069
+	 */
1070
+	protected function doLanguageAndWorkspaceOverlay(SourceInterface $source, array $row, $querySettings)
1071
+	{
1072
+
1073
+		/** @var SelectorInterface $source */
1074
+		$tableName = $source->getSelectorName();
1075
+
1076
+		$pageRepository = $this->getPageRepository();
1077
+		if (is_object($GLOBALS['TSFE'])) {
1078
+			$languageMode = $GLOBALS['TSFE']->sys_language_mode;
1079
+			if ($this->isBackendUserLogged() && $this->getBackendUser()->workspace !== 0) {
1080
+				$pageRepository->versioningWorkspaceId = $this->getBackendUser()->workspace;
1081
+			}
1082
+		} else {
1083
+			$languageMode = '';
1084
+			$workspaceUid = $this->getBackendUser()->workspace;
1085
+			$pageRepository->versioningWorkspaceId = $workspaceUid;
1086
+			if ($this->getBackendUser()->workspace !== 0) {
1087
+				$pageRepository->versioningPreview = 1;
1088
+			}
1089
+		}
1090
+
1091
+		// If current row is a translation select its parent
1092
+		if (isset($GLOBALS['TCA'][$tableName]['ctrl']['languageField'])
1093
+			&& isset($GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField'])
1094
+		) {
1095
+			if (isset($row[$GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField']])
1096
+				&& $row[$GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField']] > 0
1097
+			) {
1098
+				$row = $this->databaseHandle->exec_SELECTgetSingleRow(
1099
+					$tableName . '.*',
1100
+					$tableName,
1101
+					$tableName . '.uid=' . (int)$row[$GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField']] .
1102
+					' AND ' . $tableName . '.' . $GLOBALS['TCA'][$tableName]['ctrl']['languageField'] . '=0'
1103
+				);
1104
+			}
1105
+		}
1106
+
1107
+		// Retrieve the original uid; Used for Workspaces!
1108
+		$pageRepository->versionOL($tableName, $row, true, true);
1109
+		if ($pageRepository->versioningPreview && isset($row['_ORIG_uid'])) {
1110
+			$row['uid'] = $row['_ORIG_uid'];
1111
+		}
1112
+
1113
+		// Special case for table "pages"
1114
+		if ($tableName == 'pages') {
1115
+			$row = $pageRepository->getPageOverlay($row, $querySettings->getLanguageUid());
1116
+		} elseif (isset($GLOBALS['TCA'][$tableName]['ctrl']['languageField'])
1117
+			&& $GLOBALS['TCA'][$tableName]['ctrl']['languageField'] !== ''
1118
+		) {
1119
+			if (in_array($row[$GLOBALS['TCA'][$tableName]['ctrl']['languageField']], array(-1, 0))) {
1120
+				$overlayMode = $languageMode === 'strict' ? 'hideNonTranslated' : '';
1121
+				$row = $pageRepository->getRecordOverlay($tableName, $row, $querySettings->getLanguageUid(), $overlayMode);
1122
+			}
1123
+		}
1124
+
1125
+		return $row;
1126
+	}
1127
+
1128
+	/**
1129
+	 * Return a resolved table name given a possible table name alias.
1130
+	 *
1131
+	 * @param string $tableNameOrAlias
1132
+	 * @return string
1133
+	 */
1134
+	protected function resolveTableNameAlias($tableNameOrAlias)
1135
+	{
1136
+		$resolvedTableName = $tableNameOrAlias;
1137
+		if (!empty($this->tableNameAliases['aliases'][$tableNameOrAlias])) {
1138
+			$resolvedTableName = $this->tableNameAliases['aliases'][$tableNameOrAlias];
1139
+		}
1140
+		return $resolvedTableName;
1141
+	}
1142
+
1143
+	/**
1144
+	 * Generate a unique table name alias for the given table name.
1145
+	 *
1146
+	 * @param string $tableName
1147
+	 * @return string
1148
+	 */
1149
+	protected function generateAlias($tableName)
1150
+	{
1151
+
1152
+		if (!isset($this->tableNameAliases['aliasIncrement'][$tableName])) {
1153
+			$this->tableNameAliases['aliasIncrement'][$tableName] = 0;
1154
+		}
1155
+
1156
+		$numberOfAliases = $this->tableNameAliases['aliasIncrement'][$tableName];
1157
+		$tableNameAlias = $tableName . $numberOfAliases;
1158
+
1159
+		$this->tableNameAliases['aliasIncrement'][$tableName]++;
1160
+		$this->tableNameAliases['aliases'][$tableNameAlias] = $tableName;
1161
+
1162
+		return $tableNameAlias;
1163
+	}
1164
+
1165
+	/**
1166
+	 * Replace the table names by its table name alias within the given statement.
1167
+	 *
1168
+	 * @param string $tableName
1169
+	 * @param string $tableNameAlias
1170
+	 * @param string $statement
1171
+	 * @return string
1172
+	 */
1173
+	protected function replaceTableNameByAlias($tableName, $tableNameAlias, $statement)
1174
+	{
1175
+		if ($statement && $tableName !== $tableNameAlias) {
1176
+			$statement = str_replace($tableName, $tableNameAlias, $statement);
1177
+		}
1178
+		return $statement;
1179
+	}
1180
+
1181
+	/**
1182
+	 * Returns an instance of the current Backend User.
1183
+	 *
1184
+	 * @return \TYPO3\CMS\Core\Authentication\BackendUserAuthentication
1185
+	 */
1186
+	protected function getBackendUser()
1187
+	{
1188
+		return $GLOBALS['BE_USER'];
1189
+	}
1190
+
1191
+	/**
1192
+	 * Tell whether a Backend User is logged in.
1193
+	 *
1194
+	 * @return bool
1195
+	 */
1196
+	protected function isBackendUserLogged()
1197
+	{
1198
+		return is_object($GLOBALS['BE_USER']);
1199
+	}
1200
+
1201
+	/**
1202
+	 * @return PageRepository
1203
+	 */
1204
+	protected function getPageRepository()
1205
+	{
1206
+		if (!$this->pageRepository instanceof PageRepository) {
1207
+			if ($this->environmentService->isEnvironmentInFrontendMode() && is_object($GLOBALS['TSFE'])) {
1208
+				$this->pageRepository = $GLOBALS['TSFE']->sys_page;
1209
+			} else {
1210
+				$this->pageRepository = GeneralUtility::makeInstance('TYPO3\\CMS\\Frontend\\Page\\PageRepository');
1211
+			}
1212
+		}
1213
+
1214
+		return $this->pageRepository;
1215
+	}
1216
+
1217
+	/**
1218
+	 * @return \Fab\Vidi\Resolver\FieldPathResolver
1219
+	 */
1220
+	protected function getFieldPathResolver()
1221
+	{
1222
+		return GeneralUtility::makeInstance('Fab\Vidi\Resolver\FieldPathResolver');
1223
+	}
1224
+
1225
+	/**
1226
+	 * Checks if there are SQL errors in the last query, and if yes, throw an exception.
1227
+	 *
1228
+	 * @return void
1229
+	 * @param string $sql The SQL statement
1230
+	 * @throws \TYPO3\CMS\Extbase\Persistence\Generic\Storage\Exception\SqlErrorException
1231
+	 */
1232
+	protected function checkSqlErrors($sql = '')
1233
+	{
1234
+		$error = $this->databaseHandle->sql_error();
1235
+		if ($error !== '') {
1236
+			$error .= $sql ? ': ' . $sql : '';
1237
+			throw new \TYPO3\CMS\Extbase\Persistence\Generic\Storage\Exception\SqlErrorException($error, 1247602160);
1238
+		}
1239
+	}
1240 1240
 }
Please login to merge, or discard this patch.