[TASK] Use queryCache in generic persistence
[Packages/TYPO3.CMS.git] / typo3 / sysext / extbase / Classes / Persistence / Generic / Storage / Typo3DbBackend.php
1 <?php
2 namespace TYPO3\CMS\Extbase\Persistence\Generic\Storage;
3
4 /***************************************************************
5 * Copyright notice
6 *
7 * (c) 2010-2013 Extbase Team (http://forge.typo3.org/projects/typo3v4-mvc)
8 * Extbase is a backport of TYPO3 Flow. All credits go to the TYPO3 Flow team.
9 * All rights reserved
10 *
11 * This script is part of the TYPO3 project. The TYPO3 project is
12 * free software; you can redistribute it and/or modify
13 * it under the terms of the GNU General Public License as published by
14 * the Free Software Foundation; either version 2 of the License, or
15 * (at your option) any later version.
16 *
17 * The GNU General Public License can be found at
18 * http://www.gnu.org/copyleft/gpl.html.
19 * A copy is found in the text file GPL.txt and important notices to the license
20 * from the author is found in LICENSE.txt distributed with these scripts.
21 *
22 *
23 * This script is distributed in the hope that it will be useful,
24 * but WITHOUT ANY WARRANTY; without even the implied warranty of
25 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
26 * GNU General Public License for more details.
27 *
28 * This copyright notice MUST APPEAR in all copies of the script!
29 ***************************************************************/
30
31 use TYPO3\CMS\Backend\Utility\BackendUtility;
32 use TYPO3\CMS\Extbase\Persistence\QueryInterface;
33
34 /**
35 * A Storage backend
36 */
37 class Typo3DbBackend implements \TYPO3\CMS\Extbase\Persistence\Generic\Storage\BackendInterface, \TYPO3\CMS\Core\SingletonInterface {
38
39 /**
40 * The TYPO3 database object
41 *
42 * @var \TYPO3\CMS\Core\Database\DatabaseConnection
43 */
44 protected $databaseHandle;
45
46 /**
47 * @var \TYPO3\CMS\Extbase\Persistence\Generic\Mapper\DataMapper
48 * @inject
49 */
50 protected $dataMapper;
51
52 /**
53 * The TYPO3 page repository. Used for language and workspace overlay
54 *
55 * @var \TYPO3\CMS\Frontend\Page\PageRepository
56 */
57 protected $pageRepository;
58
59 /**
60 * A first-level TypoScript configuration cache
61 *
62 * @var array
63 */
64 protected $pageTSConfigCache = array();
65
66 /**
67 * @var \TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface
68 * @inject
69 */
70 protected $configurationManager;
71
72 /**
73 * @var \TYPO3\CMS\Extbase\Service\CacheService
74 * @inject
75 */
76 protected $cacheService;
77
78 /**
79 * @var \TYPO3\CMS\Core\Cache\CacheManager
80 * @inject
81 */
82 protected $cacheManager;
83
84 /**
85 * @var \TYPO3\CMS\Core\Cache\Frontend\VariableFrontend
86 */
87 protected $tableColumnCache;
88
89 /**
90 * @var \TYPO3\CMS\Core\Cache\Frontend\VariableFrontend
91 */
92 protected $queryCache;
93
94 /**
95 * @var \TYPO3\CMS\Extbase\Service\EnvironmentService
96 * @inject
97 */
98 protected $environmentService;
99
100 /**
101 * @var \TYPO3\CMS\Extbase\Persistence\Generic\Storage\Typo3DbQueryParser
102 * @inject
103 */
104 protected $queryParser;
105
106 /**
107 * Constructor. takes the database handle from $GLOBALS['TYPO3_DB']
108 */
109 public function __construct() {
110 $this->databaseHandle = $GLOBALS['TYPO3_DB'];
111 }
112
113 /**
114 * Lifecycle method
115 *
116 * @return void
117 */
118 public function initializeObject() {
119 $this->tableColumnCache = $this->cacheManager->getCache('extbase_typo3dbbackend_tablecolumns');
120 $this->queryCache = $this->cacheManager->getCache('extbase_typo3dbbackend_queries');
121 }
122
123 /**
124 * Adds a row to the storage
125 *
126 * @param string $tableName The database table name
127 * @param array $fieldValues The row to be inserted
128 * @param bool $isRelation TRUE if we are currently inserting into a relation table, FALSE by default
129 * @return integer The uid of the inserted row
130 */
131 public function addRow($tableName, array $fieldValues, $isRelation = FALSE) {
132 if (isset($fieldValues['uid'])) {
133 unset($fieldValues['uid']);
134 }
135
136 $this->databaseHandle->exec_INSERTquery($tableName, $fieldValues);
137 $this->checkSqlErrors();
138 $uid = $this->databaseHandle->sql_insert_id();
139
140 if (!$isRelation) {
141 $this->clearPageCache($tableName, $uid);
142 }
143 return (int)$uid;
144 }
145
146 /**
147 * Updates a row in the storage
148 *
149 * @param string $tableName The database table name
150 * @param array $fieldValues The row to be updated
151 * @param bool $isRelation TRUE if we are currently inserting into a relation table, FALSE by default
152 * @throws \InvalidArgumentException
153 * @return bool
154 */
155 public function updateRow($tableName, array $fieldValues, $isRelation = FALSE) {
156 if (!isset($fieldValues['uid'])) {
157 throw new \InvalidArgumentException('The given row must contain a value for "uid".');
158 }
159
160 $uid = (int)$fieldValues['uid'];
161 unset($fieldValues['uid']);
162
163 $updateSuccessful = $this->databaseHandle->exec_UPDATEquery($tableName, 'uid = '. $uid, $fieldValues);
164 $this->checkSqlErrors();
165
166 if (!$isRelation) {
167 $this->clearPageCache($tableName, $uid);
168 }
169
170 return $updateSuccessful;
171 }
172
173 /**
174 * Updates a relation row in the storage.
175 *
176 * @param string $tableName The database relation table name
177 * @param array $fieldValues The row to be updated
178 * @throws \InvalidArgumentException
179 * @return bool
180 */
181 public function updateRelationTableRow($tableName, array $fieldValues) {
182 if (!isset($fieldValues['uid_local']) && !isset($fieldValues['uid_foreign'])) {
183 throw new \InvalidArgumentException(
184 'The given fieldValues must contain a value for "uid_local" and "uid_foreign".', 1360500126
185 );
186 }
187
188 $where['uid_local'] = (int)$fieldValues['uid_local'];
189 $where['uid_foreign'] = (int)$fieldValues['uid_foreign'];
190 unset($fieldValues['uid_local']);
191 unset($fieldValues['uid_foreign']);
192
193 $updateSuccessful = $this->databaseHandle->exec_UPDATEquery(
194 $tableName,
195 $this->resolveWhereStatement($where, $tableName),
196 $fieldValues
197 );
198 $this->checkSqlErrors();
199
200 return $updateSuccessful;
201 }
202
203 /**
204 * Deletes a row in the storage
205 *
206 * @param string $tableName The database table name
207 * @param array $where An array of where array('fieldname' => value).
208 * @param bool $isRelation TRUE if we are currently manipulating a relation table, FALSE by default
209 * @return bool
210 */
211 public function removeRow($tableName, array $where, $isRelation = FALSE) {
212 $deleteSuccessful = $this->databaseHandle->exec_DELETEquery(
213 $tableName,
214 $this->resolveWhereStatement($where, $tableName)
215 );
216 $this->checkSqlErrors();
217
218 if (!$isRelation && isset($where['uid'])) {
219 $this->clearPageCache($tableName, $where['uid']);
220 }
221
222 return $deleteSuccessful;
223 }
224
225 /**
226 * Fetches maximal value for given table column from database.
227 *
228 * @param string $tableName The database table name
229 * @param array $where An array of where array('fieldname' => value).
230 * @param string $columnName column name to get the max value from
231 * @return mixed the max value
232 */
233 public function getMaxValueFromTable($tableName, array $where, $columnName) {
234 $result = $this->databaseHandle->exec_SELECTgetSingleRow(
235 $columnName,
236 $tableName,
237 $this->resolveWhereStatement($where, $tableName),
238 '',
239 $columnName . ' DESC',
240 TRUE
241 );
242 $this->checkSqlErrors();
243
244 return $result[0];
245 }
246
247 /**
248 * Fetches row data from the database
249 *
250 * @param string $tableName
251 * @param array $where An array of where array('fieldname' => value).
252 * @return array|bool
253 */
254 public function getRowByIdentifier($tableName, array $where) {
255 $row = $this->databaseHandle->exec_SELECTgetSingleRow(
256 '*',
257 $tableName,
258 $this->resolveWhereStatement($where, $tableName)
259 );
260 $this->checkSqlErrors();
261
262 return $row ?: FALSE;
263 }
264
265 /**
266 * Converts an array to an AND concatenated where statement
267 *
268 * @param array $where array('fieldName' => 'fieldValue')
269 * @param string $tableName table to use for escaping config
270 *
271 * @return string
272 */
273 protected function resolveWhereStatement(array $where, $tableName = 'foo') {
274 $whereStatement = array();
275
276 foreach ($where as $fieldName => $fieldValue) {
277 $whereStatement[] = $fieldName . ' = ' . $this->databaseHandle->fullQuoteStr($fieldValue, $tableName);
278 }
279
280 return implode(' AND ', $whereStatement);
281 }
282
283 /**
284 * Returns the object data matching the $query.
285 *
286 * @param \TYPO3\CMS\Extbase\Persistence\QueryInterface $query
287 * @return array
288 */
289 public function getObjectDataByQuery(\TYPO3\CMS\Extbase\Persistence\QueryInterface $query) {
290 if ($query->getStatement() instanceof \TYPO3\CMS\Extbase\Persistence\Generic\Qom\Statement) {
291 $rows = $this->getObjectDataByRawQuery($query);
292 } else {
293 $rows = $this->getRowsByStatementParts($query);
294 }
295
296 $rows = $this->doLanguageAndWorkspaceOverlay($query->getSource(), $rows, $query->getQuerySettings());
297 return $rows;
298 }
299
300 /**
301 * Creates the parameters for the query methods of the database methods in the TYPO3 core, from an array
302 * that came from a parsed query.
303 *
304 * @param array $statementParts
305 * @return array
306 */
307 protected function createQueryCommandParametersFromStatementParts(array $statementParts) {
308 return array(
309 'selectFields' => implode(' ', $statementParts['keywords']) . ' ' . implode(',', $statementParts['fields']),
310 'fromTable' => implode(' ', $statementParts['tables']) . ' ' . implode(' ', $statementParts['unions']),
311 'whereClause' => (!empty($statementParts['where']) ? implode('', $statementParts['where']) : '1')
312 . (!empty($statementParts['additionalWhereClause'])
313 ? ' AND ' . implode(' AND ', $statementParts['additionalWhereClause'])
314 : ''
315 ),
316 'orderBy' => (!empty($statementParts['orderings']) ? implode(', ', $statementParts['orderings']) : ''),
317 'limit' => ($statementParts['offset'] ? $statementParts['offset'] . ', ' : '')
318 . ($statementParts['limit'] ? $statementParts['limit'] : '')
319 );
320 }
321
322 /**
323 * Determines whether to use prepared statement or not and returns the rows from the corresponding method
324 *
325 * @param \TYPO3\CMS\Extbase\Persistence\QueryInterface $query
326 * @return array
327 */
328 protected function getRowsByStatementParts(\TYPO3\CMS\Extbase\Persistence\QueryInterface $query) {
329 if ($query->getQuerySettings()->getUsePreparedStatement()) {
330 list($statementParts, $parameters) = $this->getStatementParts($query, FALSE);
331 $rows = $this->getRowsFromPreparedDatabase($statementParts, $parameters);
332 } else {
333 list($statementParts) = $this->getStatementParts($query);
334 $rows = $this->getRowsFromDatabase($statementParts);
335 }
336
337 return $rows;
338 }
339
340 /**
341 * Fetches the rows directly from the database, not using prepared statement
342 *
343 * @param array $statementParts
344 * @return array the result
345 */
346 protected function getRowsFromDatabase(array $statementParts) {
347 $queryCommandParameters = $this->createQueryCommandParametersFromStatementParts($statementParts);
348 $rows = $this->databaseHandle->exec_SELECTgetRows(
349 $queryCommandParameters['selectFields'],
350 $queryCommandParameters['fromTable'],
351 $queryCommandParameters['whereClause'],
352 '',
353 $queryCommandParameters['orderBy'],
354 $queryCommandParameters['limit']
355 );
356 $this->checkSqlErrors();
357
358 return $rows;
359 }
360
361 /**
362 * Fetches the rows from the database, using prepared statement
363 *
364 * @param array $statementParts
365 * @param array $parameters
366 * @return array the result
367 */
368 protected function getRowsFromPreparedDatabase(array $statementParts, array $parameters) {
369 $queryCommandParameters = $this->createQueryCommandParametersFromStatementParts($statementParts);
370 $preparedStatement = $this->databaseHandle->prepare_SELECTquery(
371 $queryCommandParameters['selectFields'],
372 $queryCommandParameters['fromTable'],
373 $queryCommandParameters['whereClause'],
374 '',
375 $queryCommandParameters['orderBy'],
376 $queryCommandParameters['limit']
377 );
378
379 $preparedStatement->execute($parameters);
380 $rows = $preparedStatement->fetchAll();
381
382 $preparedStatement->free();
383 return $rows;
384 }
385
386 /**
387 * Returns the object data using a custom statement
388 *
389 * @param \TYPO3\CMS\Extbase\Persistence\QueryInterface $query
390 * @return array
391 */
392 protected function getObjectDataByRawQuery(QueryInterface $query) {
393 $statement = $query->getStatement();
394 $parameters = $statement->getBoundVariables();
395
396 if ($statement instanceof \TYPO3\CMS\Core\Database\PreparedStatement) {
397 $preparedStatement = $statement->getStatement();
398
399 $preparedStatement->execute($parameters);
400 $rows = $preparedStatement->fetchAll();
401
402 $preparedStatement->free();
403 } else {
404
405 $sqlString = $statement->getStatement();
406 /**
407 * @deprecated since 6.2, this block will be removed in two versions
408 * the deprecation log is in Qom\Statement
409 */
410 if (!empty($parameters)) {
411 $this->replacePlaceholders($sqlString, $parameters);
412 }
413
414 $result = $this->databaseHandle->sql_query($sqlString);
415 $this->checkSqlErrors();
416
417 $rows = array();
418 while ($row = $this->databaseHandle->sql_fetch_assoc($result)) {
419 if (is_array($row)) {
420 $rows[] = $row;
421 }
422 }
423 }
424
425 return $rows;
426 }
427
428 /**
429 * Returns the number of tuples matching the query.
430 *
431 * @param \TYPO3\CMS\Extbase\Persistence\QueryInterface $query
432 * @throws Exception\BadConstraintException
433 * @return integer The number of matching tuples
434 */
435 public function getObjectCountByQuery(\TYPO3\CMS\Extbase\Persistence\QueryInterface $query) {
436 if ($query->getConstraint() instanceof \TYPO3\CMS\Extbase\Persistence\Generic\Qom\Statement) {
437 throw new \TYPO3\CMS\Extbase\Persistence\Generic\Storage\Exception\BadConstraintException('Could not execute count on queries with a constraint of type TYPO3\\CMS\\Extbase\\Persistence\\Generic\\Qom\\StatementInterface', 1256661045);
438 }
439
440 list($statementParts) = $this->getStatementParts($query);
441
442 $fields = '*';
443 if (isset($statementParts['keywords']['distinct'])) {
444 $fields = 'DISTINCT ' . reset($statementParts['tables']) . '.uid';
445 }
446
447 $queryCommandParameters = $this->createQueryCommandParametersFromStatementParts($statementParts);
448 $count = $this->databaseHandle->exec_SELECTcountRows(
449 $fields,
450 $queryCommandParameters['fromTable'],
451 $queryCommandParameters['whereClause']
452 );
453 $this->checkSqlErrors();
454
455 if ($statementParts['offset']) {
456 $count -= $statementParts['offset'];
457 }
458
459 if ($statementParts['limit']) {
460 $count = min($count, $statementParts['limit']);
461 }
462
463 return (int)max(0, $count);
464 }
465
466 /**
467 * Looks for the query in cache or builds it up otherwise
468 *
469 * @param \TYPO3\CMS\Extbase\Persistence\QueryInterface $query
470 * @param bool $resolveParameterPlaceholders whether to resolve the parameters or leave the placeholders
471 * @return array
472 * @throws \Exception
473 */
474 protected function getStatementParts($query, $resolveParameterPlaceholders = TRUE) {
475 /**
476 * The queryParser will preparse the query to get the query's hash and parameters.
477 * If the hash is found in the cache and useQueryCaching is enabled, extbase will
478 * then take the string representation from cache and build a prepared query with
479 * the parameters found.
480 *
481 * Otherwise extbase will parse the complete query, build the string representation
482 * and run a usual query.
483 */
484 list($queryHash, $parameters) = $this->queryParser->preparseQuery($query);
485
486 if ($query->getQuerySettings()->getUseQueryCache()) {
487 $statementParts = $this->queryCache->get($queryHash);
488
489 if ($queryHash && !$statementParts) {
490 $statementParts = $this->queryParser->parseQuery($query);
491 $this->queryCache->set($queryHash, $statementParts, array(), 0);
492 }
493 } else {
494 $statementParts = $this->queryParser->parseQuery($query);
495 }
496
497 if (!$statementParts) {
498 throw new \Exception('Your query could not be built.', 1394453197);
499 }
500
501 // Limit and offset are not cached to allow caching of pagebrowser queries.
502 $statementParts['limit'] = ((int)$query->getLimit() ?: NULL);
503 $statementParts['offset'] = ((int)$query->getOffset() ?: NULL);
504
505 if ($resolveParameterPlaceholders === TRUE) {
506 $statementParts = $this->resolveParameterPlaceholders($statementParts, $parameters);
507 }
508
509 return array($statementParts, $parameters);
510 }
511
512 /**
513 * Replaces the parameters in the queryStructure with given values
514 *
515 * @param string $whereStatement
516 * @param array $parameters
517 * @return string
518 */
519 protected function resolveParameterPlaceholders($statementParts, $parameters = array()) {
520 $tableNameForEscape = (reset($statementParts['tables']) ?: 'foo');
521
522 foreach ($parameters as $parameterPlaceholder => $parameter) {
523 if ($parameter instanceof \TYPO3\CMS\Extbase\Persistence\Generic\LazyLoadingProxy) {
524 $parameter = $parameter->_loadRealInstance();
525 }
526
527 if ($parameter instanceof \DateTime) {
528 $parameter = $parameter->format('U');
529 } elseif ($parameter instanceof \TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface) {
530 $parameter = (int)$parameter->getUid();
531 } elseif (is_array($parameter)) {
532 $subParameters = array();
533 foreach ($parameter as $subParameter) {
534 $subParameters[] = $this->databaseHandle->fullQuoteStr($subParameter, $tableNameForEscape);
535 }
536 $parameter = implode(',', $subParameters);
537 } elseif ($parameter === NULL) {
538 $parameter = 'NULL';
539 } elseif (is_bool($input)) {
540 return ($input === TRUE ? 1 : 0);
541 } else {
542 $parameter = $this->databaseHandle->fullQuoteStr((string)$parameter, $tableNameForEscape);
543 }
544
545 $statementParts['where'] = str_replace($parameterPlaceholder, $parameter, $statementParts['where']);
546 }
547
548 return $statementParts;
549 }
550
551 /**
552 * Checks if a Value Object equal to the given Object exists in the data base
553 *
554 * @param \TYPO3\CMS\Extbase\DomainObject\AbstractValueObject $object The Value Object
555 * @return mixed The matching uid if an object was found, else FALSE
556 * @todo this is the last monster in this persistence series. refactor!
557 */
558 public function getUidOfAlreadyPersistedValueObject(\TYPO3\CMS\Extbase\DomainObject\AbstractValueObject $object) {
559 $fields = array();
560 $parameters = array();
561 $dataMap = $this->dataMapper->getDataMap(get_class($object));
562 $properties = $object->_getProperties();
563 foreach ($properties as $propertyName => $propertyValue) {
564 // FIXME We couple the Backend to the Entity implementation (uid, isClone); changes there breaks this method
565 if ($dataMap->isPersistableProperty($propertyName) && $propertyName !== 'uid' && $propertyName !== 'pid' && $propertyName !== 'isClone') {
566 if ($propertyValue === NULL) {
567 $fields[] = $dataMap->getColumnMap($propertyName)->getColumnName() . ' IS NULL';
568 } else {
569 $fields[] = $dataMap->getColumnMap($propertyName)->getColumnName() . '=?';
570 $parameters[] = $this->getPlainValue($propertyValue);
571 }
572 }
573 }
574 $sql = array();
575 $sql['additionalWhereClause'] = array();
576 $tableName = $dataMap->getTableName();
577 $this->addVisibilityConstraintStatement(new \TYPO3\CMS\Extbase\Persistence\Generic\Typo3QuerySettings(), $tableName, $sql);
578 $statement = 'SELECT * FROM ' . $tableName;
579 $statement .= ' WHERE ' . implode(' AND ', $fields);
580 if (!empty($sql['additionalWhereClause'])) {
581 $statement .= ' AND ' . implode(' AND ', $sql['additionalWhereClause']);
582 }
583 $this->replacePlaceholders($statement, $parameters, $tableName);
584 // debug($statement,-2);
585 $res = $this->databaseHandle->sql_query($statement);
586 $this->checkSqlErrors($statement);
587 $row = $this->databaseHandle->sql_fetch_assoc($res);
588 if ($row !== FALSE) {
589 return (int)$row['uid'];
590 } else {
591 return FALSE;
592 }
593 }
594
595 /**
596 * Returns a plain value, i.e. objects are flattened out if possible.
597 *
598 * @param mixed $input
599 * @throws \TYPO3\CMS\Extbase\Persistence\Generic\Exception\UnexpectedTypeException
600 * @return mixed
601 * @todo remove after getUidOfAlreadyPersistedValueObject is adjusted, this was moved to queryParser
602 */
603 protected function getPlainValue($input) {
604 if (is_array($input)) {
605 throw new \TYPO3\CMS\Extbase\Persistence\Generic\Exception\UnexpectedTypeException('An array could not be converted to a plain value.', 1274799932);
606 }
607 if ($input instanceof \DateTime) {
608 return $input->format('U');
609 } elseif ($input instanceof \TYPO3\CMS\Core\Type\TypeInterface) {
610 return (string) $input;
611 } elseif (is_object($input)) {
612 if ($input instanceof \TYPO3\CMS\Extbase\Persistence\Generic\LazyLoadingProxy) {
613 $realInput = $input->_loadRealInstance();
614 } else {
615 $realInput = $input;
616 }
617 if ($realInput instanceof \TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface) {
618 return $realInput->getUid();
619 } else {
620 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);
621 }
622 } elseif (is_bool($input)) {
623 return $input === TRUE ? 1 : 0;
624 } else {
625 return $input;
626 }
627 }
628
629 /**
630 * Replace query placeholders in a query part by the given
631 * parameters.
632 *
633 * @param string &$sqlString The query part with placeholders
634 * @param array $parameters The parameters
635 * @param string $tableName
636 *
637 * @throws \TYPO3\CMS\Extbase\Persistence\Generic\Exception
638 * @deprecated since 6.2, will be removed two versions later
639 * @todo add deprecation notice after getUidOfAlreadyPersistedValueObject is adjusted
640 */
641 protected function replacePlaceholders(&$sqlString, array $parameters, $tableName = 'foo') {
642 // TODO profile this method again
643 if (substr_count($sqlString, '?') !== count($parameters)) {
644 throw new \TYPO3\CMS\Extbase\Persistence\Generic\Exception('The number of question marks to replace must be equal to the number of parameters.', 1242816074);
645 }
646 $offset = 0;
647 foreach ($parameters as $parameter) {
648 $markPosition = strpos($sqlString, '?', $offset);
649 if ($markPosition !== FALSE) {
650 if ($parameter === NULL) {
651 $parameter = 'NULL';
652 } elseif (is_array($parameter) || $parameter instanceof \ArrayAccess || $parameter instanceof \Traversable) {
653 $items = array();
654 foreach ($parameter as $item) {
655 $items[] = $this->databaseHandle->fullQuoteStr($item, $tableName);
656 }
657 $parameter = '(' . implode(',', $items) . ')';
658 } else {
659 $parameter = $this->databaseHandle->fullQuoteStr($parameter, $tableName);
660 }
661 $sqlString = substr($sqlString, 0, $markPosition) . $parameter . substr($sqlString, ($markPosition + 1));
662 }
663 $offset = $markPosition + strlen($parameter);
664 }
665 }
666
667 /**
668 * Adds enableFields and deletedClause to the query if necessary
669 *
670 * @param \TYPO3\CMS\Extbase\Persistence\Generic\QuerySettingsInterface $querySettings
671 * @param string $tableName The database table name
672 * @param array &$sql The query parts
673 * @return void
674 * @todo remove after getUidOfAlreadyPersistedValueObject is adjusted, this was moved to queryParser
675 */
676 protected function addVisibilityConstraintStatement(\TYPO3\CMS\Extbase\Persistence\Generic\QuerySettingsInterface $querySettings, $tableName, array &$sql) {
677 $statement = '';
678 if (is_array($GLOBALS['TCA'][$tableName]['ctrl'])) {
679 $ignoreEnableFields = $querySettings->getIgnoreEnableFields();
680 $enableFieldsToBeIgnored = $querySettings->getEnableFieldsToBeIgnored();
681 $includeDeleted = $querySettings->getIncludeDeleted();
682 if ($this->environmentService->isEnvironmentInFrontendMode()) {
683 $statement .= $this->getFrontendConstraintStatement($tableName, $ignoreEnableFields, $enableFieldsToBeIgnored, $includeDeleted);
684 } else {
685 // TYPO3_MODE === 'BE'
686 $statement .= $this->getBackendConstraintStatement($tableName, $ignoreEnableFields, $includeDeleted);
687 }
688 if (!empty($statement)) {
689 $statement = strtolower(substr($statement, 1, 3)) === 'and' ? substr($statement, 5) : $statement;
690 $sql['additionalWhereClause'][] = $statement;
691 }
692 }
693 }
694
695 /**
696 * Returns constraint statement for frontend context
697 *
698 * @param string $tableName
699 * @param bool $ignoreEnableFields A flag indicating whether the enable fields should be ignored
700 * @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.
701 * @param bool $includeDeleted A flag indicating whether deleted records should be included
702 * @return string
703 * @throws \TYPO3\CMS\Extbase\Persistence\Generic\Exception\InconsistentQuerySettingsException
704 * @todo remove after getUidOfAlreadyPersistedValueObject is adjusted, this was moved to queryParser
705 */
706 protected function getFrontendConstraintStatement($tableName, $ignoreEnableFields, array $enableFieldsToBeIgnored = array(), $includeDeleted) {
707 $statement = '';
708 if ($ignoreEnableFields && !$includeDeleted) {
709 if (count($enableFieldsToBeIgnored)) {
710 // array_combine() is necessary because of the way \TYPO3\CMS\Frontend\Page\PageRepository::enableFields() is implemented
711 $statement .= $this->getPageRepository()->enableFields($tableName, -1, array_combine($enableFieldsToBeIgnored, $enableFieldsToBeIgnored));
712 } else {
713 $statement .= $this->getPageRepository()->deleteClause($tableName);
714 }
715 } elseif (!$ignoreEnableFields && !$includeDeleted) {
716 $statement .= $this->getPageRepository()->enableFields($tableName);
717 } elseif (!$ignoreEnableFields && $includeDeleted) {
718 throw new \TYPO3\CMS\Extbase\Persistence\Generic\Exception\InconsistentQuerySettingsException('Query setting "ignoreEnableFields=FALSE" can not be used together with "includeDeleted=TRUE" in frontend context.', 1327678173);
719 }
720 return $statement;
721 }
722
723 /**
724 * Returns constraint statement for backend context
725 *
726 * @param string $tableName
727 * @param bool $ignoreEnableFields A flag indicating whether the enable fields should be ignored
728 * @param bool $includeDeleted A flag indicating whether deleted records should be included
729 * @return string
730 * @todo remove after getUidOfAlreadyPersistedValueObject is adjusted, this was moved to queryParser
731 */
732 protected function getBackendConstraintStatement($tableName, $ignoreEnableFields, $includeDeleted) {
733 $statement = '';
734 if (!$ignoreEnableFields) {
735 $statement .= BackendUtility::BEenableFields($tableName);
736 }
737 if (!$includeDeleted) {
738 $statement .= BackendUtility::deleteClause($tableName);
739 }
740 return $statement;
741 }
742
743 /**
744 * Performs workspace and language overlay on the given row array. The language and workspace id is automatically
745 * detected (depending on FE or BE context). You can also explicitly set the language/workspace id.
746 *
747 * @param \TYPO3\CMS\Extbase\Persistence\Generic\Qom\SourceInterface $source The source (selector od join)
748 * @param array $rows
749 * @param \TYPO3\CMS\Extbase\Persistence\Generic\QuerySettingsInterface $querySettings The TYPO3 CMS specific query settings
750 * @param null|integer $workspaceUid
751 * @return array
752 */
753 protected function doLanguageAndWorkspaceOverlay(\TYPO3\CMS\Extbase\Persistence\Generic\Qom\SourceInterface $source, array $rows, $querySettings, $workspaceUid = NULL) {
754 if ($source instanceof \TYPO3\CMS\Extbase\Persistence\Generic\Qom\SelectorInterface) {
755 $tableName = $source->getSelectorName();
756 } elseif ($source instanceof \TYPO3\CMS\Extbase\Persistence\Generic\Qom\JoinInterface) {
757 $tableName = $source->getRight()->getSelectorName();
758 }
759 // If we do not have a table name here, we cannot do an overlay and return the original rows instead.
760 if (isset($tableName)) {
761 $pageRepository = $this->getPageRepository();
762 if (is_object($GLOBALS['TSFE'])) {
763 if ($workspaceUid !== NULL) {
764 $pageRepository->versioningWorkspaceId = $workspaceUid;
765 }
766 } else {
767 if ($workspaceUid === NULL) {
768 $workspaceUid = $GLOBALS['BE_USER']->workspace;
769 }
770 $pageRepository->versioningWorkspaceId = $workspaceUid;
771 }
772
773 $overlayedRows = array();
774 foreach ($rows as $row) {
775 // If current row is a translation select its parent
776 if (isset($tableName) && isset($GLOBALS['TCA'][$tableName])
777 && isset($GLOBALS['TCA'][$tableName]['ctrl']['languageField'])
778 && isset($GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField'])
779 && !isset($GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerTable'])
780 ) {
781 if (isset($row[$GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField']])
782 && $row[$GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField']] > 0
783 ) {
784 $row = $this->databaseHandle->exec_SELECTgetSingleRow(
785 $tableName . '.*',
786 $tableName,
787 $tableName . '.uid=' . (int)$row[$GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField']] .
788 ' AND ' . $tableName . '.' . $GLOBALS['TCA'][$tableName]['ctrl']['languageField'] . '=0'
789 );
790 }
791 }
792 $pageRepository->versionOL($tableName, $row, TRUE);
793 if ($pageRepository->versioningPreview && isset($row['_ORIG_uid'])) {
794 $row['uid'] = $row['_ORIG_uid'];
795 }
796 if ($tableName == 'pages') {
797 $row = $pageRepository->getPageOverlay($row, $querySettings->getLanguageUid());
798 } elseif (isset($GLOBALS['TCA'][$tableName]['ctrl']['languageField'])
799 && $GLOBALS['TCA'][$tableName]['ctrl']['languageField'] !== ''
800 && !isset($GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerTable'])
801 ) {
802 if (in_array($row[$GLOBALS['TCA'][$tableName]['ctrl']['languageField']], array(-1, 0))) {
803 $overlayMode = $querySettings->getLanguageMode() === 'strict' ? 'hideNonTranslated' : '';
804 $row = $pageRepository->getRecordOverlay($tableName, $row, $querySettings->getLanguageUid(), $overlayMode);
805 }
806 }
807 if ($row !== NULL && is_array($row)) {
808 $overlayedRows[] = $row;
809 }
810 }
811 } else {
812 $overlayedRows = $rows;
813 }
814 return $overlayedRows;
815 }
816
817 /**
818 * @return \TYPO3\CMS\Frontend\Page\PageRepository
819 */
820 protected function getPageRepository() {
821 if (!$this->pageRepository instanceof \TYPO3\CMS\Frontend\Page\PageRepository) {
822 if ($this->environmentService->isEnvironmentInFrontendMode() && is_object($GLOBALS['TSFE'])) {
823 $this->pageRepository = $GLOBALS['TSFE']->sys_page;
824 } else {
825 $this->pageRepository = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance('TYPO3\\CMS\\Frontend\\Page\\PageRepository');
826 }
827 }
828
829 return $this->pageRepository;
830 }
831
832 /**
833 * Checks if there are SQL errors in the last query, and if yes, throw an exception.
834 *
835 * @return void
836 * @param string $sql The SQL statement
837 * @throws \TYPO3\CMS\Extbase\Persistence\Generic\Storage\Exception\SqlErrorException
838 */
839 protected function checkSqlErrors($sql = '') {
840 $error = $this->databaseHandle->sql_error();
841 if ($error !== '') {
842 $error .= $sql ? ': ' . $sql : '';
843 throw new \TYPO3\CMS\Extbase\Persistence\Generic\Storage\Exception\SqlErrorException($error, 1247602160);
844 }
845 }
846
847 /**
848 * Clear the TYPO3 page cache for the given record.
849 * If the record lies on a page, then we clear the cache of this page.
850 * If the record has no PID column, we clear the cache of the current page as best-effort.
851 *
852 * Much of this functionality is taken from t3lib_tcemain::clear_cache() which unfortunately only works with logged-in BE user.
853 *
854 * @param string $tableName Table name of the record
855 * @param integer $uid UID of the record
856 * @return void
857 */
858 protected function clearPageCache($tableName, $uid) {
859 $frameworkConfiguration = $this->configurationManager->getConfiguration(\TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface::CONFIGURATION_TYPE_FRAMEWORK);
860 if (isset($frameworkConfiguration['persistence']['enableAutomaticCacheClearing']) && $frameworkConfiguration['persistence']['enableAutomaticCacheClearing'] === '1') {
861 } else {
862 // if disabled, return
863 return;
864 }
865 $pageIdsToClear = array();
866 $storagePage = NULL;
867 $columns = $this->databaseHandle->admin_get_fields($tableName);
868 if (array_key_exists('pid', $columns)) {
869 $result = $this->databaseHandle->exec_SELECTquery('pid', $tableName, 'uid=' . (int)$uid);
870 if ($row = $this->databaseHandle->sql_fetch_assoc($result)) {
871 $storagePage = $row['pid'];
872 $pageIdsToClear[] = $storagePage;
873 }
874 } elseif (isset($GLOBALS['TSFE'])) {
875 // No PID column - we can do a best-effort to clear the cache of the current page if in FE
876 $storagePage = $GLOBALS['TSFE']->id;
877 $pageIdsToClear[] = $storagePage;
878 }
879 if ($storagePage === NULL) {
880 return;
881 }
882 if (!isset($this->pageTSConfigCache[$storagePage])) {
883 $this->pageTSConfigCache[$storagePage] = BackendUtility::getPagesTSconfig($storagePage);
884 }
885 if (isset($this->pageTSConfigCache[$storagePage]['TCEMAIN.']['clearCacheCmd'])) {
886 $clearCacheCommands = \TYPO3\CMS\Core\Utility\GeneralUtility::trimExplode(',', strtolower($this->pageTSConfigCache[$storagePage]['TCEMAIN.']['clearCacheCmd']), TRUE);
887 $clearCacheCommands = array_unique($clearCacheCommands);
888 foreach ($clearCacheCommands as $clearCacheCommand) {
889 if (\TYPO3\CMS\Core\Utility\MathUtility::canBeInterpretedAsInteger($clearCacheCommand)) {
890 $pageIdsToClear[] = $clearCacheCommand;
891 }
892 }
893 }
894
895 foreach ($pageIdsToClear as $pageIdToClear) {
896 $this->cacheService->getPageIdStack()->push($pageIdToClear);
897 }
898 }
899 }