‪TYPO3CMS  9.5
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 
17 use Doctrine\DBAL\FetchMode;
25 
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 
66  {
67  parent::setCache(‪$cache);
68  $this->cacheTable = 'cf_' . ‪$this->cacheIdentifier;
69  $this->tagsTable = 'cf_' . $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, \PDO::PARAM_STR)
144  ),
145  $queryBuilder->expr()->gte(
146  'expires',
147  $queryBuilder->createNamedParameter(‪$GLOBALS['EXEC_TIME'], \PDO::PARAM_INT)
148  )
149  )
150  ->execute()
151  ->fetch();
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, \PDO::PARAM_STR)
179  ),
180  $queryBuilder->expr()->gte(
181  'expires',
182  $queryBuilder->createNamedParameter(‪$GLOBALS['EXEC_TIME'], \PDO::PARAM_INT)
183  )
184  )
185  ->execute()
186  ->fetchColumn(0);
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, \PDO::PARAM_STR)
234  ),
235  $queryBuilder->expr()->gte(
236  $this->cacheTable . '.expires',
237  $queryBuilder->createNamedParameter(‪$GLOBALS['EXEC_TIME'], \PDO::PARAM_INT)
238  )
239  )
240  ->groupBy($this->cacheTable . '.identifier')
241  ->execute();
242  $identifiers = $result->fetchAll(FetchMode::COLUMN, 0);
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 
275  $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($this->cacheTable);
276 
277  // A large set of tags was detected. Process it in chunks to guard against exceeding
278  // maximum SQL query limits.
279  if (count($tags) > 100) {
280  array_walk(array_chunk($tags, 100), [$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(function ($value) {
287  return '\'' . $value . '\'';
288  }, $tags);
289 
290  if ($this->‪isConnectionMysql($connection)) {
291  // Use a 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  ->execute();
309  $cacheEntryIdentifiers = $result->fetchAll(FetchMode::COLUMN, 0);
310  $quotedIdentifiers = $queryBuilder->createNamedParameter($cacheEntryIdentifiers, Connection::PARAM_STR_ARRAY);
311  $queryBuilder->delete($this->cacheTable)
312  ->where($queryBuilder->expr()->in('identifier', $quotedIdentifiers))
313  ->execute();
314  $queryBuilder->delete($this->tagsTable)
315  ->where($queryBuilder->expr()->in('identifier', $quotedIdentifiers))
316  ->execute();
317  }
318  }
319 
325  public function ‪flushByTag($tag)
326  {
328 
329  if (empty($tag)) {
330  return;
331  }
332 
334  $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($this->cacheTable);
335 
336  $quotedTag = '\'' . $tag . '\'';
337 
338  if ($this->‪isConnectionMysql($connection)) {
339  // Use a optimized query on mysql ... don't use on your own
340  // * ansi sql does not know about multi table delete
341  // * doctrine query builder does not support join on delete()
342  $connection->executeQuery(
343  'DELETE tags2, cache1'
344  . ' FROM ' . $this->tagsTable . ' AS tags1'
345  . ' JOIN ' . $this->tagsTable . ' AS tags2 ON tags1.identifier = tags2.identifier'
346  . ' JOIN ' . $this->cacheTable . ' AS cache1 ON tags1.identifier = cache1.identifier'
347  . ' WHERE tags1.tag = ' . $quotedTag
348  );
349  } else {
350  $queryBuilder = $connection->createQueryBuilder();
351  $result = $queryBuilder->select('identifier')
352  ->from($this->tagsTable)
353  ->where('tag = ' . $quotedTag)
354  // group by is like DISTINCT and used here to suppress possible duplicate identifiers
355  ->groupBy('identifier')
356  ->execute();
357  $cacheEntryIdentifiers = $result->fetchAll(FetchMode::COLUMN, 0);
358  $quotedIdentifiers = $queryBuilder->createNamedParameter($cacheEntryIdentifiers, Connection::PARAM_STR_ARRAY);
359  $queryBuilder->delete($this->cacheTable)
360  ->where($queryBuilder->expr()->in('identifier', $quotedIdentifiers))
361  ->execute();
362  $queryBuilder->delete($this->tagsTable)
363  ->where($queryBuilder->expr()->in('identifier', $quotedIdentifiers))
364  ->execute();
365  }
366  }
367 
371  public function ‪collectGarbage()
372  {
374 
375  $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($this->cacheTable);
376  if ($this->‪isConnectionMysql($connection)) {
377  // Use a optimized query on mysql ... don't use on your own
378  // * ansi sql does not know about multi table delete
379  // * doctrine query builder does not support join on delete()
380  // First delete all expired rows from cache table and their connected tag rows
381  $connection->executeQuery(
382  'DELETE cache, tags'
383  . ' FROM ' . $this->cacheTable . ' AS cache'
384  . ' LEFT OUTER JOIN ' . $this->tagsTable . ' AS tags ON cache.identifier = tags.identifier'
385  . ' WHERE cache.expires < ?',
386  [(int)‪$GLOBALS['EXEC_TIME']]
387  );
388  // Then delete possible "orphaned" rows from tags table - tags that have no cache row for whatever reason
389  $connection->executeQuery(
390  'DELETE tags'
391  . ' FROM ' . $this->tagsTable . ' AS tags'
392  . ' LEFT OUTER JOIN ' . $this->cacheTable . ' as cache ON tags.identifier = cache.identifier'
393  . ' WHERE cache.identifier IS NULL'
394  );
395  } else {
396  $queryBuilder = $connection->createQueryBuilder();
397  $result = $queryBuilder->select('identifier')
398  ->from($this->cacheTable)
399  ->where($queryBuilder->expr()->lt(
400  'expires',
401  $queryBuilder->createNamedParameter(‪$GLOBALS['EXEC_TIME'], \PDO::PARAM_INT)
402  ))
403  // group by is like DISTINCT and used here to suppress possible duplicate identifiers
404  ->groupBy('identifier')
405  ->execute();
406 
407  // Get identifiers of expired cache entries
408  $cacheEntryIdentifiers = $result->fetchAll(FetchMode::COLUMN, 0);
409  if (!empty($cacheEntryIdentifiers)) {
410  // Delete tag rows connected to expired cache entries
411  $quotedIdentifiers = $queryBuilder->createNamedParameter($cacheEntryIdentifiers, Connection::PARAM_STR_ARRAY);
412  $queryBuilder->delete($this->tagsTable)
413  ->where($queryBuilder->expr()->in('identifier', $quotedIdentifiers))
414  ->execute();
415  }
416  $queryBuilder->delete($this->cacheTable)
417  ->where($queryBuilder->expr()->lt(
418  'expires',
419  $queryBuilder->createNamedParameter(‪$GLOBALS['EXEC_TIME'], \PDO::PARAM_INT)
420  ))
421  ->execute();
422 
423  // Find out which "orphaned" tags rows exists that have no cache row and delete those, too.
424  $queryBuilder = $connection->createQueryBuilder();
425  $result = $queryBuilder->select('tags.identifier')
426  ->from($this->tagsTable, 'tags')
427  ->leftJoin(
428  'tags',
429  $this->cacheTable,
430  'cache',
431  $queryBuilder->expr()->eq('tags.identifier', $queryBuilder->quoteIdentifier('cache.identifier'))
432  )
433  ->where($queryBuilder->expr()->isNull('cache.identifier'))
434  ->groupBy('tags.identifier')
435  ->execute();
436  $tagsEntryIdentifiers = $result->fetchAll(FetchMode::COLUMN, 0);
437 
438  if (!empty($tagsEntryIdentifiers)) {
439  $quotedIdentifiers = $queryBuilder->createNamedParameter($tagsEntryIdentifiers, Connection::PARAM_STR_ARRAY);
440  $queryBuilder->delete($this->tagsTable)
441  ->where($queryBuilder->expr()->in('identifier', $quotedIdentifiers))
442  ->execute();
443  }
444  }
445  }
446 
452  public function ‪getCacheTable()
453  {
455  return ‪$this->cacheTable;
456  }
457 
463  public function ‪getTagsTable()
464  {
466  return ‪$this->tagsTable;
467  }
468 
474  public function ‪setCompression(‪$compression)
475  {
476  $this->compression = ‪$compression;
477  }
478 
487  {
488  if (‪$compressionLevel >= -1 && ‪$compressionLevel <= 9) {
489  $this->compressionLevel = ‪$compressionLevel;
490  }
491  }
492 
500  protected function ‪isConnectionMysql(Connection $connection): bool
501  {
502  $serverVersion = $connection->getServerVersion();
503  return (bool)(strpos($serverVersion, 'MySQL') === 0);
504  }
505 
511  protected function ‪throwExceptionIfFrontendDoesNotExist()
512  {
513  if (!$this->cache instanceof FrontendInterface) {
514  throw new Exception('No cache frontend has been set via setCache() yet.', 1236518288);
515  }
516  }
517 
525  public function ‪getTableDefinitions()
526  {
527  $cacheTableSql = file_get_contents(
529  'Resources/Private/Sql/Cache/Backend/Typo3DatabaseBackendCache.sql'
530  );
531  $requiredTableStructures = str_replace('###CACHE_TABLE###', $this->cacheTable, $cacheTableSql) . LF . LF;
532  $tagsTableSql = file_get_contents(
534  'Resources/Private/Sql/Cache/Backend/Typo3DatabaseBackendTags.sql'
535  );
536  $requiredTableStructures .= str_replace('###TAGS_TABLE###', $this->tagsTable, $tagsTableSql) . LF;
537  return $requiredTableStructures;
538  }
539 }
‪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:495
‪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\Typo3DatabaseBackend\$compression
‪bool $compression
Definition: Typo3DatabaseBackend.php:45
‪TYPO3\CMS\Core\Cache\Backend\Typo3DatabaseBackend\setCompression
‪setCompression($compression)
Definition: Typo3DatabaseBackend.php:469
‪TYPO3\CMS\Core\Cache\Backend\Typo3DatabaseBackend\getTableDefinitions
‪string getTableDefinitions()
Definition: Typo3DatabaseBackend.php:520
‪TYPO3\CMS\Core\Cache\Backend\Typo3DatabaseBackend\collectGarbage
‪collectGarbage()
Definition: Typo3DatabaseBackend.php:366
‪TYPO3\CMS\Core\Cache\Backend\Typo3DatabaseBackend\flushByTag
‪flushByTag($tag)
Definition: Typo3DatabaseBackend.php:320
‪TYPO3\CMS\Core\Cache\Backend\TaggableBackendInterface
Definition: TaggableBackendInterface.php:21
‪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: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:36
‪TYPO3\CMS\Core\Cache\Backend\Typo3DatabaseBackend\throwExceptionIfFrontendDoesNotExist
‪throwExceptionIfFrontendDoesNotExist()
Definition: Typo3DatabaseBackend.php:506
‪TYPO3\CMS\Core\Cache\Exception
Definition: DuplicateIdentifierException.php:2
‪TYPO3\CMS\Core\Database\Connection\getServerVersion
‪string getServerVersion()
Definition: Connection.php:370
‪TYPO3\CMS\Core\Cache\Exception\InvalidDataException
Definition: InvalidDataException.php:21
‪TYPO3\CMS\Core\Cache\Backend\AbstractBackend\$cache
‪TYPO3 CMS Core Cache Frontend FrontendInterface $cache
Definition: AbstractBackend.php:35
‪TYPO3\CMS\Core\Cache\Backend\Typo3DatabaseBackend\getCacheTable
‪string getCacheTable()
Definition: Typo3DatabaseBackend.php:447
‪TYPO3\CMS\Core\Database\Connection
Definition: Connection.php:31
‪TYPO3\CMS\Core\Cache\Frontend\FrontendInterface
Definition: FrontendInterface.php:21
‪TYPO3\CMS\Core\Cache\Backend\AbstractBackend\$cacheIdentifier
‪string $cacheIdentifier
Definition: AbstractBackend.php:39
‪TYPO3\CMS\Core\Cache\Backend\AbstractBackend
Definition: AbstractBackend.php:26
‪$GLOBALS
‪$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['adminpanel']['modules']
Definition: ext_localconf.php:5
‪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:481
‪TYPO3\CMS\Core\Utility\ExtensionManagementUtility\extPath
‪static string extPath($key, $script='')
Definition: ExtensionManagementUtility.php:149
‪TYPO3\CMS\Core\Cache\Backend\Typo3DatabaseBackend\$compressionLevel
‪int $compressionLevel
Definition: Typo3DatabaseBackend.php:49
‪TYPO3\CMS\Core\Cache\Backend
Definition: AbstractBackend.php:2
‪TYPO3\CMS\Core\Cache\Backend\Typo3DatabaseBackend\getTagsTable
‪string getTagsTable()
Definition: Typo3DatabaseBackend.php:458
‪TYPO3\CMS\Core\Database\ConnectionPool
Definition: ConnectionPool.php:44
‪TYPO3\CMS\Core\Utility\GeneralUtility
Definition: GeneralUtility.php:45
‪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:52