‪TYPO3CMS  11.5
Typo3DatabaseBackend.php
Go to the documentation of this file.
1 <?php
2 
3 /*
4  * This file is part of the TYPO3 CMS project.
5  *
6  * It is free software; you can redistribute it and/or modify it under
7  * the terms of the GNU General Public License, either version 2
8  * of the License, or any later version.
9  *
10  * For the full copyright and license information, please read the
11  * LICENSE.txt file that was distributed with this source code.
12  *
13  * The TYPO3 project - inspiring people to share!
14  */
15 
17 
25 
30 {
34  public 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 
66  {
67  parent::setCache(‪$cache);
68  $this->cacheTable = 'cache_' . ‪$this->cacheIdentifier;
69  $this->tagsTable = 'cache_' . $this->cacheIdentifier . '_tags';
70  $this->maximumLifetime = self::FAKED_UNLIMITED_EXPIRE - ‪$GLOBALS['EXEC_TIME'];
71  }
72 
83  public function set($entryIdentifier, $data, array $tags = [], $lifetime = null)
84  {
86  if (!is_string($data)) {
87  throw new ‪InvalidDataException(
88  'The specified data is of type "' . gettype($data) . '" but a string is expected.',
89  1236518298
90  );
91  }
92  if ($lifetime === null) {
93  $lifetime = ‪$this->defaultLifetime;
94  }
95  if ($lifetime === 0 || $lifetime > $this->maximumLifetime) {
96  $lifetime = ‪$this->maximumLifetime;
97  }
98  $expires = ‪$GLOBALS['EXEC_TIME'] + $lifetime;
99  $this->remove($entryIdentifier);
100  if ($this->compression) {
101  $data = gzcompress($data, $this->compressionLevel);
102  }
103  GeneralUtility::makeInstance(ConnectionPool::class)
104  ->getConnectionForTable($this->cacheTable)
105  ->insert(
106  $this->cacheTable,
107  [
108  'identifier' => $entryIdentifier,
109  'expires' => $expires,
110  'content' => $data,
111  ],
112  [
113  'content' => ‪Connection::PARAM_LOB,
114  ]
115  );
116  if (!empty($tags)) {
117  $tagRows = [];
118  foreach ($tags as $tag) {
119  $tagRows[] = [$entryIdentifier, $tag];
120  }
121  GeneralUtility::makeInstance(ConnectionPool::class)
122  ->getConnectionForTable($this->tagsTable)
123  ->bulkInsert($this->tagsTable, $tagRows, ['identifier', 'tag']);
124  }
125  }
126 
133  public function get($entryIdentifier)
134  {
136  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
137  ->getQueryBuilderForTable($this->cacheTable);
138  $cacheRow = $queryBuilder->select('content')
139  ->from($this->cacheTable)
140  ->where(
141  $queryBuilder->expr()->eq(
142  'identifier',
143  $queryBuilder->createNamedParameter($entryIdentifier, ‪Connection::PARAM_STR)
144  ),
145  $queryBuilder->expr()->gte(
146  'expires',
147  $queryBuilder->createNamedParameter(‪$GLOBALS['EXEC_TIME'], ‪Connection::PARAM_INT)
148  )
149  )
150  ->executeQuery()
151  ->fetchAssociative();
152  $content = '';
153  if (!empty($cacheRow)) {
154  $content = $cacheRow['content'];
155  }
156  if ($this->compression && (string)$content !== '') {
157  $content = gzuncompress($content);
158  }
159  return !empty($cacheRow) ? $content : false;
160  }
161 
168  public function ‪has($entryIdentifier)
169  {
171  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
172  ->getQueryBuilderForTable($this->cacheTable);
173  $count = $queryBuilder->count('*')
174  ->from($this->cacheTable)
175  ->where(
176  $queryBuilder->expr()->eq(
177  'identifier',
178  $queryBuilder->createNamedParameter($entryIdentifier, ‪Connection::PARAM_STR)
179  ),
180  $queryBuilder->expr()->gte(
181  'expires',
182  $queryBuilder->createNamedParameter(‪$GLOBALS['EXEC_TIME'], ‪Connection::PARAM_INT)
183  )
184  )
185  ->executeQuery()
186  ->fetchOne();
187  return (bool)$count;
188  }
189 
197  public function remove($entryIdentifier)
198  {
200  $numberOfRowsRemoved = GeneralUtility::makeInstance(ConnectionPool::class)
201  ->getConnectionForTable($this->cacheTable)
202  ->delete(
203  $this->cacheTable,
204  ['identifier' => $entryIdentifier]
205  );
206  GeneralUtility::makeInstance(ConnectionPool::class)
207  ->getConnectionForTable($this->tagsTable)
208  ->delete(
209  $this->tagsTable,
210  ['identifier' => $entryIdentifier]
211  );
212  return (bool)$numberOfRowsRemoved;
213  }
214 
221  public function ‪findIdentifiersByTag($tag)
222  {
224  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
225  ->getQueryBuilderForTable($this->tagsTable);
226  $result = $queryBuilder->select($this->cacheTable . '.identifier')
227  ->from($this->cacheTable)
228  ->from($this->tagsTable)
229  ->where(
230  $queryBuilder->expr()->eq($this->cacheTable . '.identifier', $queryBuilder->quoteIdentifier($this->tagsTable . '.identifier')),
231  $queryBuilder->expr()->eq(
232  $this->tagsTable . '.tag',
233  $queryBuilder->createNamedParameter($tag, ‪Connection::PARAM_STR)
234  ),
235  $queryBuilder->expr()->gte(
236  $this->cacheTable . '.expires',
237  $queryBuilder->createNamedParameter(‪$GLOBALS['EXEC_TIME'], ‪Connection::PARAM_INT)
238  )
239  )
240  ->groupBy($this->cacheTable . '.identifier')
241  ->executeQuery();
242  $identifiers = $result->fetchFirstColumn();
243  return array_combine($identifiers, $identifiers);
244  }
245 
249  public function ‪flush()
250  {
252  GeneralUtility::makeInstance(ConnectionPool::class)
253  ->getConnectionForTable($this->cacheTable)
254  ->truncate($this->cacheTable);
255  GeneralUtility::makeInstance(ConnectionPool::class)
256  ->getConnectionForTable($this->tagsTable)
257  ->truncate($this->tagsTable);
258  }
259 
266  public function ‪flushByTags(array $tags)
267  {
269 
270  if (empty($tags)) {
271  return;
272  }
273 
274  $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($this->cacheTable);
275 
276  // A large set of tags was detected. Process it in chunks to guard against exceeding
277  // maximum SQL query limits.
278  if (count($tags) > 100) {
279  $chunks = array_chunk($tags, 100);
280  array_walk($chunks, [$this, 'flushByTags']);
281  return;
282  }
283  // VERY simple quoting of tags is sufficient here for performance. Tags are already
284  // validated to not contain any bad characters, e.g. they are automatically generated
285  // inside this class and suffixed with a pure integer enforced by DB.
286  $quotedTagList = array_map(static function ($value) {
287  return '\'' . $value . '\'';
288  }, $tags);
289 
290  if ($this->‪isConnectionMysql($connection)) {
291  // Use an optimized query on mysql ... don't use on your own
292  // * ansi sql does not know about multi table delete
293  // * doctrine query builder does not support join on delete()
294  $connection->executeQuery(
295  'DELETE tags2, cache1'
296  . ' FROM ' . $this->tagsTable . ' AS tags1'
297  . ' JOIN ' . $this->tagsTable . ' AS tags2 ON tags1.identifier = tags2.identifier'
298  . ' JOIN ' . $this->cacheTable . ' AS cache1 ON tags1.identifier = cache1.identifier'
299  . ' WHERE tags1.tag IN (' . implode(',', $quotedTagList) . ')'
300  );
301  } else {
302  $queryBuilder = $connection->createQueryBuilder();
303  $result = $queryBuilder->select('identifier')
304  ->from($this->tagsTable)
305  ->where('tag IN (' . implode(',', $quotedTagList) . ')')
306  // group by is like DISTINCT and used here to suppress possible duplicate identifiers
307  ->groupBy('identifier')
308  ->executeQuery();
309  $cacheEntryIdentifiers = $result->fetchFirstColumn();
310  $quotedIdentifiers = $queryBuilder->createNamedParameter($cacheEntryIdentifiers, Connection::PARAM_STR_ARRAY);
311  $queryBuilder->delete($this->cacheTable)
312  ->where($queryBuilder->expr()->in('identifier', $quotedIdentifiers))
313  ->executeStatement();
314  $queryBuilder->delete($this->tagsTable)
315  ->where($queryBuilder->expr()->in('identifier', $quotedIdentifiers))
316  ->executeStatement();
317  }
318  }
319 
325  public function ‪flushByTag($tag)
326  {
328 
329  if (empty($tag)) {
330  return;
331  }
332 
333  $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($this->cacheTable);
334 
335  $quotedTag = '\'' . $tag . '\'';
336 
337  if ($this->‪isConnectionMysql($connection)) {
338  // Use an optimized query on mysql ... don't use on your own
339  // * ansi sql does not know about multi table delete
340  // * doctrine query builder does not support join on delete()
341  $connection->executeQuery(
342  'DELETE tags2, cache1'
343  . ' FROM ' . $this->tagsTable . ' AS tags1'
344  . ' JOIN ' . $this->tagsTable . ' AS tags2 ON tags1.identifier = tags2.identifier'
345  . ' JOIN ' . $this->cacheTable . ' AS cache1 ON tags1.identifier = cache1.identifier'
346  . ' WHERE tags1.tag = ' . $quotedTag
347  );
348  } else {
349  $queryBuilder = $connection->createQueryBuilder();
350  $result = $queryBuilder->select('identifier')
351  ->from($this->tagsTable)
352  ->where('tag = ' . $quotedTag)
353  // group by is like DISTINCT and used here to suppress possible duplicate identifiers
354  ->groupBy('identifier')
355  ->executeQuery();
356  $cacheEntryIdentifiers = $result->fetchFirstColumn();
357  $quotedIdentifiers = $queryBuilder->createNamedParameter($cacheEntryIdentifiers, Connection::PARAM_STR_ARRAY);
358  $queryBuilder->delete($this->cacheTable)
359  ->where($queryBuilder->expr()->in('identifier', $quotedIdentifiers))
360  ->executeStatement();
361  $queryBuilder->delete($this->tagsTable)
362  ->where($queryBuilder->expr()->in('identifier', $quotedIdentifiers))
363  ->executeStatement();
364  }
365  }
366 
370  public function ‪collectGarbage()
371  {
373 
374  $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($this->cacheTable);
375  if ($this->‪isConnectionMysql($connection)) {
376  // Use an optimized query on mysql ... don't use on your own
377  // * ansi sql does not know about multi table delete
378  // * doctrine query builder does not support join on delete()
379  // First delete all expired rows from cache table and their connected tag rows
380  $connection->executeQuery(
381  'DELETE cache, tags'
382  . ' FROM ' . $this->cacheTable . ' AS cache'
383  . ' LEFT OUTER JOIN ' . $this->tagsTable . ' AS tags ON cache.identifier = tags.identifier'
384  . ' WHERE cache.expires < ?',
385  [(int)‪$GLOBALS['EXEC_TIME']]
386  );
387  // Then delete possible "orphaned" rows from tags table - tags that have no cache row for whatever reason
388  $connection->executeQuery(
389  'DELETE tags'
390  . ' FROM ' . $this->tagsTable . ' AS tags'
391  . ' LEFT OUTER JOIN ' . $this->cacheTable . ' as cache ON tags.identifier = cache.identifier'
392  . ' WHERE cache.identifier IS NULL'
393  );
394  } else {
395  $queryBuilder = $connection->createQueryBuilder();
396  $result = $queryBuilder->select('identifier')
397  ->from($this->cacheTable)
398  ->where($queryBuilder->expr()->lt(
399  'expires',
400  $queryBuilder->createNamedParameter(‪$GLOBALS['EXEC_TIME'], ‪Connection::PARAM_INT)
401  ))
402  // group by is like DISTINCT and used here to suppress possible duplicate identifiers
403  ->groupBy('identifier')
404  ->executeQuery();
405 
406  // Get identifiers of expired cache entries
407  $cacheEntryIdentifiers = $result->fetchFirstColumn();
408  if (!empty($cacheEntryIdentifiers)) {
409  // Delete tag rows connected to expired cache entries
410  $quotedIdentifiers = $queryBuilder->createNamedParameter($cacheEntryIdentifiers, Connection::PARAM_STR_ARRAY);
411  $queryBuilder->delete($this->tagsTable)
412  ->where($queryBuilder->expr()->in('identifier', $quotedIdentifiers))
413  ->executeStatement();
414  }
415  $queryBuilder->delete($this->cacheTable)
416  ->where($queryBuilder->expr()->lt(
417  'expires',
418  $queryBuilder->createNamedParameter(‪$GLOBALS['EXEC_TIME'], ‪Connection::PARAM_INT)
419  ))
420  ->executeStatement();
421 
422  // Find out which "orphaned" tags rows exists that have no cache row and delete those, too.
423  $queryBuilder = $connection->createQueryBuilder();
424  $result = $queryBuilder->select('tags.identifier')
425  ->from($this->tagsTable, 'tags')
426  ->leftJoin(
427  'tags',
428  $this->cacheTable,
429  'cache',
430  $queryBuilder->expr()->eq('tags.identifier', $queryBuilder->quoteIdentifier('cache.identifier'))
431  )
432  ->where($queryBuilder->expr()->isNull('cache.identifier'))
433  ->groupBy('tags.identifier')
434  ->executeQuery();
435  $tagsEntryIdentifiers = $result->fetchFirstColumn();
436 
437  if (!empty($tagsEntryIdentifiers)) {
438  $quotedIdentifiers = $queryBuilder->createNamedParameter($tagsEntryIdentifiers, Connection::PARAM_STR_ARRAY);
439  $queryBuilder->delete($this->tagsTable)
440  ->where($queryBuilder->expr()->in('identifier', $quotedIdentifiers))
441  ->executeStatement();
442  }
443  }
444  }
445 
451  public function ‪getCacheTable()
452  {
454  return ‪$this->cacheTable;
455  }
456 
462  public function ‪getTagsTable()
463  {
465  return ‪$this->tagsTable;
466  }
467 
473  public function ‪setCompression(‪$compression)
474  {
475  $this->compression = ‪$compression;
476  }
477 
486  {
487  if (‪$compressionLevel >= -1 && ‪$compressionLevel <= 9) {
488  $this->compressionLevel = ‪$compressionLevel;
489  }
490  }
491 
499  protected function ‪isConnectionMysql(Connection $connection): bool
500  {
501  $serverVersion = $connection->getServerVersion();
502  return (bool)(strpos($serverVersion, 'MySQL') === 0);
503  }
504 
510  protected function ‪throwExceptionIfFrontendDoesNotExist()
511  {
512  if (!$this->cache instanceof FrontendInterface) {
513  throw new Exception('No cache frontend has been set via setCache() yet.', 1236518288);
514  }
515  }
516 
524  public function ‪getTableDefinitions()
525  {
526  $cacheTableSql = (string)file_get_contents(
528  'Resources/Private/Sql/Cache/Backend/Typo3DatabaseBackendCache.sql'
529  );
530  $requiredTableStructures = str_replace('###CACHE_TABLE###', $this->cacheTable, $cacheTableSql) . LF . LF;
531  $tagsTableSql = (string)file_get_contents(
533  'Resources/Private/Sql/Cache/Backend/Typo3DatabaseBackendTags.sql'
534  );
535  $requiredTableStructures .= str_replace('###TAGS_TABLE###', $this->tagsTable, $tagsTableSql) . LF;
536  return $requiredTableStructures;
537  }
538 }
‪TYPO3\CMS\Core\Cache\Backend\Typo3DatabaseBackend\flush
‪flush()
Definition: Typo3DatabaseBackend.php:244
‪TYPO3\CMS\Core\Cache\Backend\Typo3DatabaseBackend\$cacheTable
‪string $cacheTable
Definition: Typo3DatabaseBackend.php:37
‪TYPO3\CMS\Core\Cache\Backend\Typo3DatabaseBackend\isConnectionMysql
‪bool isConnectionMysql(Connection $connection)
Definition: Typo3DatabaseBackend.php:494
‪TYPO3\CMS\Core\Database\Connection\PARAM_INT
‪const PARAM_INT
Definition: Connection.php:49
‪TYPO3\CMS\Core\Cache\Backend\Typo3DatabaseBackend\findIdentifiersByTag
‪array findIdentifiersByTag($tag)
Definition: Typo3DatabaseBackend.php:216
‪TYPO3\CMS\Core\Cache\Backend\Typo3DatabaseBackend\has
‪bool has($entryIdentifier)
Definition: Typo3DatabaseBackend.php:163
‪TYPO3\CMS\Core\Cache\Backend\AbstractBackend\$cache
‪FrontendInterface $cache
Definition: AbstractBackend.php:38
‪TYPO3\CMS\Core\Cache\Backend\Typo3DatabaseBackend\$compression
‪bool $compression
Definition: Typo3DatabaseBackend.php:45
‪TYPO3\CMS\Core\Cache\Backend\Typo3DatabaseBackend\setCompression
‪setCompression($compression)
Definition: Typo3DatabaseBackend.php:468
‪TYPO3\CMS\Core\Cache\Backend\Typo3DatabaseBackend\getTableDefinitions
‪string getTableDefinitions()
Definition: Typo3DatabaseBackend.php:519
‪TYPO3\CMS\Core\Cache\Backend\Typo3DatabaseBackend\collectGarbage
‪collectGarbage()
Definition: Typo3DatabaseBackend.php:365
‪TYPO3\CMS\Core\Cache\Backend\Typo3DatabaseBackend\flushByTag
‪flushByTag($tag)
Definition: Typo3DatabaseBackend.php:320
‪TYPO3\CMS\Core\Cache\Backend\TaggableBackendInterface
Definition: TaggableBackendInterface.php:22
‪TYPO3\CMS\Core\Cache\Backend\Typo3DatabaseBackend\setCache
‪setCache(FrontendInterface $cache)
Definition: Typo3DatabaseBackend.php:60
‪TYPO3\CMS\Core\Cache\Backend\Typo3DatabaseBackend
Definition: Typo3DatabaseBackend.php:30
‪TYPO3\CMS\Core\Cache\Backend\AbstractBackend\$defaultLifetime
‪int $defaultLifetime
Definition: AbstractBackend.php:57
‪TYPO3\CMS\Core\Database\Connection\PARAM_STR
‪const PARAM_STR
Definition: Connection.php:54
‪TYPO3\CMS\Core\Cache\Backend\Typo3DatabaseBackend\$maximumLifetime
‪int $maximumLifetime
Definition: Typo3DatabaseBackend.php:53
‪TYPO3\CMS\Core\Cache\Backend\Typo3DatabaseBackend\flushByTags
‪flushByTags(array $tags)
Definition: Typo3DatabaseBackend.php:261
‪TYPO3\CMS\Core\Utility\ExtensionManagementUtility
Definition: ExtensionManagementUtility.php:43
‪TYPO3\CMS\Core\Cache\Backend\Typo3DatabaseBackend\throwExceptionIfFrontendDoesNotExist
‪throwExceptionIfFrontendDoesNotExist()
Definition: Typo3DatabaseBackend.php:505
‪TYPO3\CMS\Core\Cache\Exception
Definition: DuplicateIdentifierException.php:16
‪TYPO3\CMS\Core\Database\Connection\getServerVersion
‪string getServerVersion()
Definition: Connection.php:383
‪TYPO3\CMS\Core\Cache\Exception\InvalidDataException
Definition: InvalidDataException.php:23
‪TYPO3\CMS\Core\Cache\Backend\Typo3DatabaseBackend\getCacheTable
‪string getCacheTable()
Definition: Typo3DatabaseBackend.php:446
‪TYPO3\CMS\Core\Database\Connection
Definition: Connection.php:38
‪TYPO3\CMS\Core\Cache\Frontend\FrontendInterface
Definition: FrontendInterface.php:22
‪TYPO3\CMS\Core\Cache\Backend\AbstractBackend\$cacheIdentifier
‪string $cacheIdentifier
Definition: AbstractBackend.php:42
‪TYPO3\CMS\Core\Cache\Backend\AbstractBackend
Definition: AbstractBackend.php:28
‪$GLOBALS
‪$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['adminpanel']['modules']
Definition: ext_localconf.php:25
‪TYPO3\CMS\Core\Cache\Backend\Typo3DatabaseBackend\FAKED_UNLIMITED_EXPIRE
‪const FAKED_UNLIMITED_EXPIRE
Definition: Typo3DatabaseBackend.php:34
‪TYPO3\CMS\Core\Cache\Backend\Typo3DatabaseBackend\setCompressionLevel
‪setCompressionLevel($compressionLevel)
Definition: Typo3DatabaseBackend.php:480
‪TYPO3\CMS\Core\Utility\ExtensionManagementUtility\extPath
‪static string extPath($key, $script='')
Definition: ExtensionManagementUtility.php:142
‪TYPO3\CMS\Core\Cache\Backend\Typo3DatabaseBackend\$compressionLevel
‪int $compressionLevel
Definition: Typo3DatabaseBackend.php:49
‪TYPO3\CMS\Core\Cache\Backend
Definition: AbstractBackend.php:16
‪TYPO3\CMS\Core\Cache\Backend\Typo3DatabaseBackend\getTagsTable
‪string getTagsTable()
Definition: Typo3DatabaseBackend.php:457
‪TYPO3\CMS\Core\Database\ConnectionPool
Definition: ConnectionPool.php:46
‪TYPO3\CMS\Core\Utility\GeneralUtility
Definition: GeneralUtility.php:50
‪TYPO3\CMS\Core\Cache\Backend\Typo3DatabaseBackend\$tagsTable
‪string $tagsTable
Definition: Typo3DatabaseBackend.php:41
‪TYPO3\CMS\Core\Cache\Exception
Definition: Exception.php:21
‪TYPO3\CMS\Core\Database\Connection\PARAM_LOB
‪const PARAM_LOB
Definition: Connection.php:59