‪TYPO3CMS  10.4
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 
18 use Doctrine\DBAL\FetchMode;
26 
31 {
35  const ‪FAKED_UNLIMITED_EXPIRE = 2145909600;
39  protected ‪$cacheTable;
40 
44  protected ‪$tagsTable;
45 
49  protected ‪$compression = false;
50 
54  protected ‪$compressionLevel = -1;
55 
59  protected ‪$maximumLifetime;
60 
67  {
68  parent::setCache(‪$cache);
69  $this->cacheTable = 'cache_' . ‪$this->cacheIdentifier;
70  $this->tagsTable = 'cache_' . $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 ($lifetime === null) {
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  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
226  ->getQueryBuilderForTable($this->tagsTable);
227  $result = $queryBuilder->select($this->cacheTable . '.identifier')
228  ->from($this->cacheTable)
229  ->from($this->tagsTable)
230  ->where(
231  $queryBuilder->expr()->eq($this->cacheTable . '.identifier', $queryBuilder->quoteIdentifier($this->tagsTable . '.identifier')),
232  $queryBuilder->expr()->eq(
233  $this->tagsTable . '.tag',
234  $queryBuilder->createNamedParameter($tag, \PDO::PARAM_STR)
235  ),
236  $queryBuilder->expr()->gte(
237  $this->cacheTable . '.expires',
238  $queryBuilder->createNamedParameter(‪$GLOBALS['EXEC_TIME'], \PDO::PARAM_INT)
239  )
240  )
241  ->groupBy($this->cacheTable . '.identifier')
242  ->execute();
243  $identifiers = $result->fetchAll(FetchMode::COLUMN, 0);
244  return array_combine($identifiers, $identifiers);
245  }
246 
250  public function ‪flush()
251  {
253  GeneralUtility::makeInstance(ConnectionPool::class)
254  ->getConnectionForTable($this->cacheTable)
255  ->truncate($this->cacheTable);
256  GeneralUtility::makeInstance(ConnectionPool::class)
257  ->getConnectionForTable($this->tagsTable)
258  ->truncate($this->tagsTable);
259  }
260 
267  public function ‪flushByTags(array $tags)
268  {
270 
271  if (empty($tags)) {
272  return;
273  }
274 
276  $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($this->cacheTable);
277 
278  // A large set of tags was detected. Process it in chunks to guard against exceeding
279  // maximum SQL query limits.
280  if (count($tags) > 100) {
281  array_walk(array_chunk($tags, 100), [$this, 'flushByTags']);
282  return;
283  }
284  // VERY simple quoting of tags is sufficient here for performance. Tags are already
285  // validated to not contain any bad characters, e.g. they are automatically generated
286  // inside this class and suffixed with a pure integer enforced by DB.
287  $quotedTagList = array_map(function ($value) {
288  return '\'' . $value . '\'';
289  }, $tags);
290 
291  if ($this->‪isConnectionMysql($connection)) {
292  // Use an optimized query on mysql ... don't use on your own
293  // * ansi sql does not know about multi table delete
294  // * doctrine query builder does not support join on delete()
295  $connection->executeQuery(
296  'DELETE tags2, cache1'
297  . ' FROM ' . $this->tagsTable . ' AS tags1'
298  . ' JOIN ' . $this->tagsTable . ' AS tags2 ON tags1.identifier = tags2.identifier'
299  . ' JOIN ' . $this->cacheTable . ' AS cache1 ON tags1.identifier = cache1.identifier'
300  . ' WHERE tags1.tag IN (' . implode(',', $quotedTagList) . ')'
301  );
302  } else {
303  $queryBuilder = $connection->createQueryBuilder();
304  $result = $queryBuilder->select('identifier')
305  ->from($this->tagsTable)
306  ->where('tag IN (' . implode(',', $quotedTagList) . ')')
307  // group by is like DISTINCT and used here to suppress possible duplicate identifiers
308  ->groupBy('identifier')
309  ->execute();
310  $cacheEntryIdentifiers = $result->fetchAll(FetchMode::COLUMN, 0);
311  $quotedIdentifiers = $queryBuilder->createNamedParameter($cacheEntryIdentifiers, Connection::PARAM_STR_ARRAY);
312  $queryBuilder->delete($this->cacheTable)
313  ->where($queryBuilder->expr()->in('identifier', $quotedIdentifiers))
314  ->execute();
315  $queryBuilder->delete($this->tagsTable)
316  ->where($queryBuilder->expr()->in('identifier', $quotedIdentifiers))
317  ->execute();
318  }
319  }
320 
326  public function ‪flushByTag($tag)
327  {
329 
330  if (empty($tag)) {
331  return;
332  }
333 
335  $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($this->cacheTable);
336 
337  $quotedTag = '\'' . $tag . '\'';
338 
339  if ($this->‪isConnectionMysql($connection)) {
340  // Use an optimized query on mysql ... don't use on your own
341  // * ansi sql does not know about multi table delete
342  // * doctrine query builder does not support join on delete()
343  $connection->executeQuery(
344  'DELETE tags2, cache1'
345  . ' FROM ' . $this->tagsTable . ' AS tags1'
346  . ' JOIN ' . $this->tagsTable . ' AS tags2 ON tags1.identifier = tags2.identifier'
347  . ' JOIN ' . $this->cacheTable . ' AS cache1 ON tags1.identifier = cache1.identifier'
348  . ' WHERE tags1.tag = ' . $quotedTag
349  );
350  } else {
351  $queryBuilder = $connection->createQueryBuilder();
352  $result = $queryBuilder->select('identifier')
353  ->from($this->tagsTable)
354  ->where('tag = ' . $quotedTag)
355  // group by is like DISTINCT and used here to suppress possible duplicate identifiers
356  ->groupBy('identifier')
357  ->execute();
358  $cacheEntryIdentifiers = $result->fetchAll(FetchMode::COLUMN, 0);
359  $quotedIdentifiers = $queryBuilder->createNamedParameter($cacheEntryIdentifiers, Connection::PARAM_STR_ARRAY);
360  $queryBuilder->delete($this->cacheTable)
361  ->where($queryBuilder->expr()->in('identifier', $quotedIdentifiers))
362  ->execute();
363  $queryBuilder->delete($this->tagsTable)
364  ->where($queryBuilder->expr()->in('identifier', $quotedIdentifiers))
365  ->execute();
366  }
367  }
368 
372  public function ‪collectGarbage()
373  {
375 
376  $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($this->cacheTable);
377  if ($this->‪isConnectionMysql($connection)) {
378  // Use an optimized query on mysql ... don't use on your own
379  // * ansi sql does not know about multi table delete
380  // * doctrine query builder does not support join on delete()
381  // First delete all expired rows from cache table and their connected tag rows
382  $connection->executeQuery(
383  'DELETE cache, tags'
384  . ' FROM ' . $this->cacheTable . ' AS cache'
385  . ' LEFT OUTER JOIN ' . $this->tagsTable . ' AS tags ON cache.identifier = tags.identifier'
386  . ' WHERE cache.expires < ?',
387  [(int)‪$GLOBALS['EXEC_TIME']]
388  );
389  // Then delete possible "orphaned" rows from tags table - tags that have no cache row for whatever reason
390  $connection->executeQuery(
391  'DELETE tags'
392  . ' FROM ' . $this->tagsTable . ' AS tags'
393  . ' LEFT OUTER JOIN ' . $this->cacheTable . ' as cache ON tags.identifier = cache.identifier'
394  . ' WHERE cache.identifier IS NULL'
395  );
396  } else {
397  $queryBuilder = $connection->createQueryBuilder();
398  $result = $queryBuilder->select('identifier')
399  ->from($this->cacheTable)
400  ->where($queryBuilder->expr()->lt(
401  'expires',
402  $queryBuilder->createNamedParameter(‪$GLOBALS['EXEC_TIME'], \PDO::PARAM_INT)
403  ))
404  // group by is like DISTINCT and used here to suppress possible duplicate identifiers
405  ->groupBy('identifier')
406  ->execute();
407 
408  // Get identifiers of expired cache entries
409  $cacheEntryIdentifiers = $result->fetchAll(FetchMode::COLUMN, 0);
410  if (!empty($cacheEntryIdentifiers)) {
411  // Delete tag rows connected to expired cache entries
412  $quotedIdentifiers = $queryBuilder->createNamedParameter($cacheEntryIdentifiers, Connection::PARAM_STR_ARRAY);
413  $queryBuilder->delete($this->tagsTable)
414  ->where($queryBuilder->expr()->in('identifier', $quotedIdentifiers))
415  ->execute();
416  }
417  $queryBuilder->delete($this->cacheTable)
418  ->where($queryBuilder->expr()->lt(
419  'expires',
420  $queryBuilder->createNamedParameter(‪$GLOBALS['EXEC_TIME'], \PDO::PARAM_INT)
421  ))
422  ->execute();
423 
424  // Find out which "orphaned" tags rows exists that have no cache row and delete those, too.
425  $queryBuilder = $connection->createQueryBuilder();
426  $result = $queryBuilder->select('tags.identifier')
427  ->from($this->tagsTable, 'tags')
428  ->leftJoin(
429  'tags',
430  $this->cacheTable,
431  'cache',
432  $queryBuilder->expr()->eq('tags.identifier', $queryBuilder->quoteIdentifier('cache.identifier'))
433  )
434  ->where($queryBuilder->expr()->isNull('cache.identifier'))
435  ->groupBy('tags.identifier')
436  ->execute();
437  $tagsEntryIdentifiers = $result->fetchAll(FetchMode::COLUMN, 0);
438 
439  if (!empty($tagsEntryIdentifiers)) {
440  $quotedIdentifiers = $queryBuilder->createNamedParameter($tagsEntryIdentifiers, Connection::PARAM_STR_ARRAY);
441  $queryBuilder->delete($this->tagsTable)
442  ->where($queryBuilder->expr()->in('identifier', $quotedIdentifiers))
443  ->execute();
444  }
445  }
446  }
447 
453  public function ‪getCacheTable()
454  {
456  return ‪$this->cacheTable;
457  }
458 
464  public function ‪getTagsTable()
465  {
467  return ‪$this->tagsTable;
468  }
469 
475  public function ‪setCompression(‪$compression)
476  {
477  $this->compression = ‪$compression;
478  }
479 
488  {
489  if (‪$compressionLevel >= -1 && ‪$compressionLevel <= 9) {
490  $this->compressionLevel = ‪$compressionLevel;
491  }
492  }
493 
501  protected function ‪isConnectionMysql(Connection $connection): bool
502  {
503  $serverVersion = $connection->getServerVersion();
504  return (bool)(strpos($serverVersion, 'MySQL') === 0);
505  }
506 
512  protected function ‪throwExceptionIfFrontendDoesNotExist()
513  {
514  if (!$this->cache instanceof FrontendInterface) {
515  throw new Exception('No cache frontend has been set via setCache() yet.', 1236518288);
516  }
517  }
518 
526  public function ‪getTableDefinitions()
527  {
528  $cacheTableSql = (string)file_get_contents(
530  'Resources/Private/Sql/Cache/Backend/Typo3DatabaseBackendCache.sql'
531  );
532  $requiredTableStructures = str_replace('###CACHE_TABLE###', $this->cacheTable, $cacheTableSql) . LF . LF;
533  $tagsTableSql = (string)file_get_contents(
535  'Resources/Private/Sql/Cache/Backend/Typo3DatabaseBackendTags.sql'
536  );
537  $requiredTableStructures .= str_replace('###TAGS_TABLE###', $this->tagsTable, $tagsTableSql) . LF;
538  return $requiredTableStructures;
539  }
540 }
‪TYPO3\CMS\Core\Cache\Backend\Typo3DatabaseBackend\flush
‪flush()
Definition: Typo3DatabaseBackend.php:245
‪TYPO3\CMS\Core\Cache\Backend\Typo3DatabaseBackend\$cacheTable
‪string $cacheTable
Definition: Typo3DatabaseBackend.php:38
‪TYPO3\CMS\Core\Cache\Backend\Typo3DatabaseBackend\isConnectionMysql
‪bool isConnectionMysql(Connection $connection)
Definition: Typo3DatabaseBackend.php:496
‪TYPO3\CMS\Core\Cache\Backend\Typo3DatabaseBackend\findIdentifiersByTag
‪array findIdentifiersByTag($tag)
Definition: Typo3DatabaseBackend.php:217
‪TYPO3\CMS\Core\Cache\Backend\Typo3DatabaseBackend\has
‪bool has($entryIdentifier)
Definition: Typo3DatabaseBackend.php:164
‪TYPO3\CMS\Core\Cache\Backend\Typo3DatabaseBackend\$compression
‪bool $compression
Definition: Typo3DatabaseBackend.php:46
‪TYPO3\CMS\Core\Cache\Backend\Typo3DatabaseBackend\setCompression
‪setCompression($compression)
Definition: Typo3DatabaseBackend.php:470
‪TYPO3\CMS\Core\Cache\Backend\Typo3DatabaseBackend\getTableDefinitions
‪string getTableDefinitions()
Definition: Typo3DatabaseBackend.php:521
‪TYPO3\CMS\Core\Cache\Backend\Typo3DatabaseBackend\collectGarbage
‪collectGarbage()
Definition: Typo3DatabaseBackend.php:367
‪TYPO3\CMS\Core\Cache\Backend\Typo3DatabaseBackend\flushByTag
‪flushByTag($tag)
Definition: Typo3DatabaseBackend.php:321
‪TYPO3\CMS\Core\Cache\Backend\TaggableBackendInterface
Definition: TaggableBackendInterface.php:22
‪TYPO3\CMS\Core\Cache\Backend\Typo3DatabaseBackend\setCache
‪setCache(FrontendInterface $cache)
Definition: Typo3DatabaseBackend.php:61
‪TYPO3\CMS\Core\Cache\Backend\Typo3DatabaseBackend
Definition: Typo3DatabaseBackend.php:31
‪TYPO3\CMS\Core\Cache\Backend\AbstractBackend\$defaultLifetime
‪int $defaultLifetime
Definition: AbstractBackend.php:56
‪TYPO3\CMS\Core\Cache\Backend\Typo3DatabaseBackend\$maximumLifetime
‪int $maximumLifetime
Definition: Typo3DatabaseBackend.php:54
‪TYPO3\CMS\Core\Cache\Backend\Typo3DatabaseBackend\flushByTags
‪flushByTags(array $tags)
Definition: Typo3DatabaseBackend.php:262
‪TYPO3\CMS\Core\Utility\ExtensionManagementUtility
Definition: ExtensionManagementUtility.php:43
‪TYPO3\CMS\Core\Cache\Backend\Typo3DatabaseBackend\throwExceptionIfFrontendDoesNotExist
‪throwExceptionIfFrontendDoesNotExist()
Definition: Typo3DatabaseBackend.php:507
‪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:24
‪TYPO3\CMS\Core\Cache\Backend\AbstractBackend\$cache
‪TYPO3 CMS Core Cache Frontend FrontendInterface $cache
Definition: AbstractBackend.php:37
‪TYPO3\CMS\Core\Cache\Backend\Typo3DatabaseBackend\getCacheTable
‪string getCacheTable()
Definition: Typo3DatabaseBackend.php:448
‪TYPO3\CMS\Core\Database\Connection
Definition: Connection.php:36
‪TYPO3\CMS\Core\Cache\Frontend\FrontendInterface
Definition: FrontendInterface.php:22
‪TYPO3\CMS\Core\Cache\Backend\AbstractBackend\$cacheIdentifier
‪string $cacheIdentifier
Definition: AbstractBackend.php:41
‪TYPO3\CMS\Core\Cache\Backend\AbstractBackend
Definition: AbstractBackend.php:28
‪$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:35
‪TYPO3\CMS\Core\Cache\Backend\Typo3DatabaseBackend\setCompressionLevel
‪setCompressionLevel($compressionLevel)
Definition: Typo3DatabaseBackend.php:482
‪TYPO3\CMS\Core\Utility\ExtensionManagementUtility\extPath
‪static string extPath($key, $script='')
Definition: ExtensionManagementUtility.php:127
‪TYPO3\CMS\Core\Cache\Backend\Typo3DatabaseBackend\$compressionLevel
‪int $compressionLevel
Definition: Typo3DatabaseBackend.php:50
‪TYPO3\CMS\Core\Cache\Backend
Definition: AbstractBackend.php:16
‪TYPO3\CMS\Core\Cache\Backend\Typo3DatabaseBackend\getTagsTable
‪string getTagsTable()
Definition: Typo3DatabaseBackend.php:459
‪TYPO3\CMS\Core\Database\ConnectionPool
Definition: ConnectionPool.php:46
‪TYPO3\CMS\Core\Utility\GeneralUtility
Definition: GeneralUtility.php:46
‪TYPO3\CMS\Core\Cache\Backend\Typo3DatabaseBackend\$tagsTable
‪string $tagsTable
Definition: Typo3DatabaseBackend.php:42
‪TYPO3\CMS\Core\Cache\Exception
Definition: Exception.php:22
‪TYPO3\CMS\Core\Database\Connection\PARAM_LOB
‪const PARAM_LOB
Definition: Connection.php:57