TYPO3 CMS  TYPO3_8-7
Typo3DatabaseBackend.php
Go to the documentation of this file.
1 <?php
3 
4 /*
5  * This file is part of the TYPO3 CMS project.
6  *
7  * It is free software; you can redistribute it and/or modify it under
8  * the terms of the GNU General Public License, either version 2
9  * of the License, or any later version.
10  *
11  * For the full copyright and license information, please read the
12  * LICENSE.txt file that was distributed with this source code.
13  *
14  * The TYPO3 project - inspiring people to share!
15  */
16 
24 
30 {
34  const FAKED_UNLIMITED_EXPIRE = 2145909600;
38  protected $cacheTable;
39 
43  protected $tagsTable;
44 
48  protected $compression = false;
49 
53  protected $compressionLevel = -1;
54 
58  protected $maximumLifetime;
59 
67  {
68  parent::setCache($cache);
69  $this->cacheTable = 'cf_' . $this->cacheIdentifier;
70  $this->tagsTable = 'cf_' . $this->cacheIdentifier . '_tags';
71  $this->maximumLifetime = self::FAKED_UNLIMITED_EXPIRE - $GLOBALS['EXEC_TIME'];
72  }
73 
84  public function set($entryIdentifier, $data, array $tags = [], $lifetime = null)
85  {
87  if (!is_string($data)) {
88  throw new InvalidDataException(
89  'The specified data is of type "' . gettype($data) . '" but a string is expected.',
90  1236518298
91  );
92  }
93  if (is_null($lifetime)) {
94  $lifetime = $this->defaultLifetime;
95  }
96  if ($lifetime === 0 || $lifetime > $this->maximumLifetime) {
97  $lifetime = $this->maximumLifetime;
98  }
99  $expires = $GLOBALS['EXEC_TIME'] + $lifetime;
100  $this->remove($entryIdentifier);
101  if ($this->compression) {
102  $data = gzcompress($data, $this->compressionLevel);
103  }
104  GeneralUtility::makeInstance(ConnectionPool::class)
105  ->getConnectionForTable($this->cacheTable)
106  ->insert(
107  $this->cacheTable,
108  [
109  'identifier' => $entryIdentifier,
110  'expires' => $expires,
111  'content' => $data,
112  ],
113  [
114  'content' => Connection::PARAM_LOB,
115  ]
116  );
117  if (!empty($tags)) {
118  $tagRows = [];
119  foreach ($tags as $tag) {
120  $tagRows[] = [$entryIdentifier, $tag];
121  }
122  GeneralUtility::makeInstance(ConnectionPool::class)
123  ->getConnectionForTable($this->tagsTable)
124  ->bulkInsert($this->tagsTable, $tagRows, ['identifier', 'tag']);
125  }
126  }
127 
134  public function get($entryIdentifier)
135  {
137  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
138  ->getQueryBuilderForTable($this->cacheTable);
139  $cacheRow = $queryBuilder->select('content')
140  ->from($this->cacheTable)
141  ->where(
142  $queryBuilder->expr()->eq(
143  'identifier',
144  $queryBuilder->createNamedParameter($entryIdentifier, \PDO::PARAM_STR)
145  ),
146  $queryBuilder->expr()->gte(
147  'expires',
148  $queryBuilder->createNamedParameter($GLOBALS['EXEC_TIME'], \PDO::PARAM_INT)
149  )
150  )
151  ->execute()
152  ->fetch();
153  $content = '';
154  if (!empty($cacheRow)) {
155  $content = $cacheRow['content'];
156  }
157  if ($this->compression && (string)$content !== '') {
158  $content = gzuncompress($content);
159  }
160  return !empty($cacheRow) ? $content : false;
161  }
162 
169  public function has($entryIdentifier)
170  {
172  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
173  ->getQueryBuilderForTable($this->cacheTable);
174  $count = $queryBuilder->count('*')
175  ->from($this->cacheTable)
176  ->where(
177  $queryBuilder->expr()->eq(
178  'identifier',
179  $queryBuilder->createNamedParameter($entryIdentifier, \PDO::PARAM_STR)
180  ),
181  $queryBuilder->expr()->gte(
182  'expires',
183  $queryBuilder->createNamedParameter($GLOBALS['EXEC_TIME'], \PDO::PARAM_INT)
184  )
185  )
186  ->execute()
187  ->fetchColumn(0);
188  return (bool)$count;
189  }
190 
198  public function remove($entryIdentifier)
199  {
201  $numberOfRowsRemoved = GeneralUtility::makeInstance(ConnectionPool::class)
202  ->getConnectionForTable($this->cacheTable)
203  ->delete(
204  $this->cacheTable,
205  ['identifier' => $entryIdentifier]
206  );
207  GeneralUtility::makeInstance(ConnectionPool::class)
208  ->getConnectionForTable($this->tagsTable)
209  ->delete(
210  $this->tagsTable,
211  ['identifier' => $entryIdentifier]
212  );
213  return (bool)$numberOfRowsRemoved;
214  }
215 
222  public function findIdentifiersByTag($tag)
223  {
225  $cacheEntryIdentifiers = [];
226  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
227  ->getQueryBuilderForTable($this->tagsTable);
228  $result = $queryBuilder->select($this->cacheTable . '.identifier')
229  ->from($this->cacheTable)
230  ->from($this->tagsTable)
231  ->where(
232  $queryBuilder->expr()->eq($this->cacheTable . '.identifier', $queryBuilder->quoteIdentifier($this->tagsTable . '.identifier')),
233  $queryBuilder->expr()->eq(
234  $this->tagsTable . '.tag',
235  $queryBuilder->createNamedParameter($tag, \PDO::PARAM_STR)
236  ),
237  $queryBuilder->expr()->gte(
238  $this->cacheTable . '.expires',
239  $queryBuilder->createNamedParameter($GLOBALS['EXEC_TIME'], \PDO::PARAM_INT)
240  )
241  )
242  ->execute();
243  while ($row = $result->fetch()) {
244  $cacheEntryIdentifiers[$row['identifier']] = $row['identifier'];
245  }
246  return $cacheEntryIdentifiers;
247  }
248 
252  public function flush()
253  {
255  GeneralUtility::makeInstance(ConnectionPool::class)
256  ->getConnectionForTable($this->cacheTable)
257  ->truncate($this->cacheTable);
258  GeneralUtility::makeInstance(ConnectionPool::class)
259  ->getConnectionForTable($this->tagsTable)
260  ->truncate($this->tagsTable);
261  }
262 
269  public function flushByTags(array $tags)
270  {
272 
273  if (empty($tags)) {
274  return;
275  }
276 
278  $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($this->cacheTable);
279 
280  // A large set of tags was detected. Process it in chunks to guard against exceeding
281  // maximum SQL query limits.
282  if (count($tags) > 100) {
283  array_walk(array_chunk($tags, 100), [$this, 'flushByTags']);
284  return;
285  }
286  // VERY simple quoting of tags is sufficient here for performance. Tags are already
287  // validated to not contain any bad characters, e.g. they are automatically generated
288  // inside this class and suffixed with a pure integer enforced by DB.
289  $quotedTagList = array_map(function ($value) {
290  return '\'' . $value . '\'';
291  }, $tags);
292 
293  if ($this->isConnectionMysql($connection)) {
294  // Use a optimized query on mysql ... don't use on your own
295  // * ansi sql does not know about multi table delete
296  // * doctrine query builder does not support join on delete()
297  $connection->executeQuery(
298  'DELETE tags2, cache1'
299  . ' FROM ' . $this->tagsTable . ' AS tags1'
300  . ' JOIN ' . $this->tagsTable . ' AS tags2 ON tags1.identifier = tags2.identifier'
301  . ' JOIN ' . $this->cacheTable . ' AS cache1 ON tags1.identifier = cache1.identifier'
302  . ' WHERE tags1.tag IN (' . implode(',', $quotedTagList) . ')'
303  );
304  } else {
305  $queryBuilder = $connection->createQueryBuilder();
306  $result = $queryBuilder->select('identifier')
307  ->from($this->tagsTable)
308  ->where('tag IN (' . implode(',', $quotedTagList) . ')')
309  // group by is like DISTINCT and used here to suppress possible duplicate identifiers
310  ->groupBy('identifier')
311  ->execute();
312  $cacheEntryIdentifiers = [];
313  while ($row = $result->fetch()) {
314  $cacheEntryIdentifiers[] = $row['identifier'];
315  }
316  $quotedIdentifiers = $queryBuilder->createNamedParameter($cacheEntryIdentifiers, Connection::PARAM_STR_ARRAY);
317  $queryBuilder->delete($this->cacheTable)
318  ->where($queryBuilder->expr()->in('identifier', $quotedIdentifiers))
319  ->execute();
320  $queryBuilder->delete($this->tagsTable)
321  ->where($queryBuilder->expr()->in('identifier', $quotedIdentifiers))
322  ->execute();
323  }
324  }
325 
331  public function flushByTag($tag)
332  {
334 
335  if (empty($tag)) {
336  return;
337  }
338 
340  $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($this->cacheTable);
341 
342  $quotedTag = '\'' . $tag . '\'';
343 
344  if ($this->isConnectionMysql($connection)) {
345  // Use a optimized query on mysql ... don't use on your own
346  // * ansi sql does not know about multi table delete
347  // * doctrine query builder does not support join on delete()
348  $connection->executeQuery(
349  'DELETE tags2, cache1'
350  . ' FROM ' . $this->tagsTable . ' AS tags1'
351  . ' JOIN ' . $this->tagsTable . ' AS tags2 ON tags1.identifier = tags2.identifier'
352  . ' JOIN ' . $this->cacheTable . ' AS cache1 ON tags1.identifier = cache1.identifier'
353  . ' WHERE tags1.tag = ' . $quotedTag
354  );
355  } else {
356  $queryBuilder = $connection->createQueryBuilder();
357  $result = $queryBuilder->select('identifier')
358  ->from($this->tagsTable)
359  ->where('tag = ' . $quotedTag)
360  // group by is like DISTINCT and used here to suppress possible duplicate identifiers
361  ->groupBy('identifier')
362  ->execute();
363  $cacheEntryIdentifiers = [];
364  while ($row = $result->fetch()) {
365  $cacheEntryIdentifiers[] = $row['identifier'];
366  }
367  $quotedIdentifiers = $queryBuilder->createNamedParameter($cacheEntryIdentifiers, Connection::PARAM_STR_ARRAY);
368  $queryBuilder->delete($this->cacheTable)
369  ->where($queryBuilder->expr()->in('identifier', $quotedIdentifiers))
370  ->execute();
371  $queryBuilder->delete($this->tagsTable)
372  ->where($queryBuilder->expr()->in('identifier', $quotedIdentifiers))
373  ->execute();
374  }
375  }
376 
380  public function collectGarbage()
381  {
383 
384  $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($this->cacheTable);
385  if ($this->isConnectionMysql($connection)) {
386  // Use a optimized query on mysql ... don't use on your own
387  // * ansi sql does not know about multi table delete
388  // * doctrine query builder does not support join on delete()
389  // First delete all expired rows from cache table and their connected tag rows
390  $connection->executeQuery(
391  'DELETE cache, tags'
392  . ' FROM ' . $this->cacheTable . ' AS cache'
393  . ' LEFT OUTER JOIN ' . $this->tagsTable . ' AS tags ON cache.identifier = tags.identifier'
394  . ' WHERE cache.expires < ?',
395  [(int)$GLOBALS['EXEC_TIME']]
396  );
397  // Then delete possible "orphaned" rows from tags table - tags that have no cache row for whatever reason
398  $connection->executeQuery(
399  'DELETE tags'
400  . ' FROM ' . $this->tagsTable . ' AS tags'
401  . ' LEFT OUTER JOIN ' . $this->cacheTable . ' as cache ON tags.identifier = cache.identifier'
402  . ' WHERE cache.identifier IS NULL'
403  );
404  } else {
405  $queryBuilder = $connection->createQueryBuilder();
406  $result = $queryBuilder->select('identifier')
407  ->from($this->cacheTable)
408  ->where($queryBuilder->expr()->lt(
409  'expires',
410  $queryBuilder->createNamedParameter($GLOBALS['EXEC_TIME'], \PDO::PARAM_INT)
411  ))
412  // group by is like DISTINCT and used here to suppress possible duplicate identifiers
413  ->groupBy('identifier')
414  ->execute();
415 
416  // Get identifiers of expired cache entries
417  $cacheEntryIdentifiers = [];
418  while ($row = $result->fetch()) {
419  $cacheEntryIdentifiers[] = $row['identifier'];
420  }
421  if (!empty($cacheEntryIdentifiers)) {
422  // Delete tag rows connected to expired cache entries
423  $quotedIdentifiers = $queryBuilder->createNamedParameter($cacheEntryIdentifiers, Connection::PARAM_STR_ARRAY);
424  $queryBuilder->delete($this->tagsTable)
425  ->where($queryBuilder->expr()->in('identifier', $quotedIdentifiers))
426  ->execute();
427  }
428  $queryBuilder->delete($this->cacheTable)
429  ->where($queryBuilder->expr()->lt(
430  'expires',
431  $queryBuilder->createNamedParameter($GLOBALS['EXEC_TIME'], \PDO::PARAM_INT)
432  ))
433  ->execute();
434 
435  // Find out which "orphaned" tags rows exists that have no cache row and delete those, too.
436  $queryBuilder = $connection->createQueryBuilder();
437  $result = $queryBuilder->select('tags.identifier')
438  ->from($this->tagsTable, 'tags')
439  ->leftJoin(
440  'tags',
441  $this->cacheTable,
442  'cache',
443  $queryBuilder->expr()->eq('tags.identifier', $queryBuilder->quoteIdentifier('cache.identifier'))
444  )
445  ->where($queryBuilder->expr()->isNull('cache.identifier'))
446  ->groupBy('tags.identifier')
447  ->execute();
448  $tagsEntryIdentifiers = [];
449  while ($row = $result->fetch()) {
450  $tagsEntryIdentifiers[] = $row['identifier'];
451  }
452  if (!empty($tagsEntryIdentifiers)) {
453  $quotedIdentifiers = $queryBuilder->createNamedParameter($tagsEntryIdentifiers, Connection::PARAM_STR_ARRAY);
454  $queryBuilder->delete($this->tagsTable)
455  ->where($queryBuilder->expr()->in('identifier', $quotedIdentifiers))
456  ->execute();
457  }
458  }
459  }
460 
466  public function getCacheTable()
467  {
469  return $this->cacheTable;
470  }
471 
477  public function getTagsTable()
478  {
480  return $this->tagsTable;
481  }
482 
488  public function setCompression($compression)
489  {
490  $this->compression = $compression;
491  }
492 
501  {
502  if ($compressionLevel >= -1 && $compressionLevel <= 9) {
503  $this->compressionLevel = $compressionLevel;
504  }
505  }
506 
514  protected function isConnectionMysql(Connection $connection): bool
515  {
516  $serverVersion = $connection->getServerVersion();
517  return (bool)(strpos($serverVersion, 'MySQL') === 0);
518  }
519 
526  {
527  if (!$this->cache instanceof FrontendInterface) {
528  throw new Exception('No cache frontend has been set via setCache() yet.', 1236518288);
529  }
530  }
531 
539  public function getTableDefinitions()
540  {
541  $cacheTableSql = file_get_contents(
543  'Resources/Private/Sql/Cache/Backend/Typo3DatabaseBackendCache.sql'
544  );
545  $requiredTableStructures = str_replace('###CACHE_TABLE###', $this->cacheTable, $cacheTableSql) . LF . LF;
546  $tagsTableSql = file_get_contents(
548  'Resources/Private/Sql/Cache/Backend/Typo3DatabaseBackendTags.sql'
549  );
550  $requiredTableStructures .= str_replace('###TAGS_TABLE###', $this->tagsTable, $tagsTableSql) . LF;
551  return $requiredTableStructures;
552  }
553 }
static makeInstance($className,... $constructorArguments)
if(TYPO3_MODE==='BE') $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tsfebeuserauth.php']['frontendEditingController']['default']