TYPO3 v15 dev-main snapshot ()
This commit is contained in:
@@ -0,0 +1,374 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Cache\Backend;
|
||||
|
||||
use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
|
||||
use TYPO3\CMS\Core\Database\Connection;
|
||||
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||
use TYPO3\CMS\Core\Database\Platform\PlatformInformation;
|
||||
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* A caching backend which stores cache entries in database tables
|
||||
*/
|
||||
class Typo3DatabaseBackend extends AbstractBackend implements TaggableBackendInterface
|
||||
{
|
||||
/**
|
||||
* @var int Timestamp of 2038-01-01
|
||||
*/
|
||||
protected const FAKED_UNLIMITED_EXPIRE = 2145909600;
|
||||
/**
|
||||
* @var string Name of the cache data table
|
||||
*/
|
||||
protected string $cacheTable;
|
||||
|
||||
/**
|
||||
* @var string Name of the cache tags table
|
||||
*/
|
||||
protected string $tagsTable;
|
||||
|
||||
/**
|
||||
* @var bool Indicates whether data is compressed or not (requires php zlib)
|
||||
*/
|
||||
protected bool $compression = false;
|
||||
|
||||
/**
|
||||
* @var int -1 to 9, indicates zlib compression level: -1 = default level 6, 0 = no compression, 9 maximum compression
|
||||
*/
|
||||
protected int $compressionLevel = -1;
|
||||
|
||||
/**
|
||||
* @var int Maximum lifetime to stay with expire field below FAKED_UNLIMITED_LIFETIME
|
||||
*/
|
||||
protected int $maximumLifetime;
|
||||
|
||||
public function setCache(FrontendInterface $cache): void
|
||||
{
|
||||
parent::setCache($cache);
|
||||
$this->cacheTable = 'cache_' . $this->cacheIdentifier;
|
||||
$this->tagsTable = 'cache_' . $this->cacheIdentifier . '_tags';
|
||||
$this->maximumLifetime = self::FAKED_UNLIMITED_EXPIRE - $GLOBALS['EXEC_TIME'];
|
||||
}
|
||||
|
||||
public function set(string $entryIdentifier, string $data, array $tags = [], $lifetime = null): void
|
||||
{
|
||||
if ($lifetime === null) {
|
||||
$lifetime = $this->defaultLifetime;
|
||||
}
|
||||
if ($lifetime === 0 || $lifetime > $this->maximumLifetime) {
|
||||
$lifetime = $this->maximumLifetime;
|
||||
}
|
||||
$expires = $GLOBALS['EXEC_TIME'] + $lifetime;
|
||||
$this->remove($entryIdentifier);
|
||||
if ($this->compression) {
|
||||
$data = gzcompress($data, $this->compressionLevel);
|
||||
}
|
||||
GeneralUtility::makeInstance(ConnectionPool::class)
|
||||
->getConnectionForTable($this->cacheTable)
|
||||
->insert(
|
||||
$this->cacheTable,
|
||||
[
|
||||
'identifier' => $entryIdentifier,
|
||||
'expires' => $expires,
|
||||
'content' => $data,
|
||||
],
|
||||
[
|
||||
'content' => Connection::PARAM_LOB,
|
||||
]
|
||||
);
|
||||
if (!empty($tags)) {
|
||||
$tagRows = [];
|
||||
foreach ($tags as $tag) {
|
||||
$tagRows[] = [$entryIdentifier, $tag];
|
||||
}
|
||||
GeneralUtility::makeInstance(ConnectionPool::class)
|
||||
->getConnectionForTable($this->tagsTable)
|
||||
->bulkInsert($this->tagsTable, $tagRows, ['identifier', 'tag'], ['identifier' => Connection::PARAM_STR, 'tag' => Connection::PARAM_STR]);
|
||||
}
|
||||
}
|
||||
|
||||
public function get(string $entryIdentifier): mixed
|
||||
{
|
||||
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($this->cacheTable);
|
||||
$cacheRow = $queryBuilder->select('content')
|
||||
->from($this->cacheTable)
|
||||
->where(
|
||||
$queryBuilder->expr()->eq(
|
||||
'identifier',
|
||||
$queryBuilder->createNamedParameter($entryIdentifier)
|
||||
),
|
||||
$queryBuilder->expr()->gte(
|
||||
'expires',
|
||||
$queryBuilder->createNamedParameter($GLOBALS['EXEC_TIME'], Connection::PARAM_INT)
|
||||
)
|
||||
)
|
||||
->executeQuery()
|
||||
->fetchAssociative();
|
||||
$content = '';
|
||||
if (!empty($cacheRow)) {
|
||||
$content = $cacheRow['content'];
|
||||
}
|
||||
if ($this->compression && (string)$content !== '') {
|
||||
$content = gzuncompress($content);
|
||||
}
|
||||
return empty($cacheRow) ? false : $content;
|
||||
}
|
||||
|
||||
public function has(string $entryIdentifier): bool
|
||||
{
|
||||
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($this->cacheTable);
|
||||
$count = $queryBuilder->count('*')
|
||||
->from($this->cacheTable)
|
||||
->where(
|
||||
$queryBuilder->expr()->eq(
|
||||
'identifier',
|
||||
$queryBuilder->createNamedParameter($entryIdentifier)
|
||||
),
|
||||
$queryBuilder->expr()->gte(
|
||||
'expires',
|
||||
$queryBuilder->createNamedParameter($GLOBALS['EXEC_TIME'], Connection::PARAM_INT)
|
||||
)
|
||||
)
|
||||
->executeQuery()
|
||||
->fetchOne();
|
||||
return (bool)$count;
|
||||
}
|
||||
|
||||
public function remove(string $entryIdentifier): bool
|
||||
{
|
||||
$numberOfRowsRemoved = GeneralUtility::makeInstance(ConnectionPool::class)
|
||||
->getConnectionForTable($this->cacheTable)
|
||||
->delete(
|
||||
$this->cacheTable,
|
||||
['identifier' => $entryIdentifier],
|
||||
['identifier' => Connection::PARAM_STR]
|
||||
);
|
||||
GeneralUtility::makeInstance(ConnectionPool::class)
|
||||
->getConnectionForTable($this->tagsTable)
|
||||
->delete(
|
||||
$this->tagsTable,
|
||||
['identifier' => $entryIdentifier],
|
||||
['identifier' => Connection::PARAM_STR]
|
||||
);
|
||||
return (bool)$numberOfRowsRemoved;
|
||||
}
|
||||
|
||||
public function findIdentifiersByTag(string $tag): array
|
||||
{
|
||||
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($this->tagsTable);
|
||||
$result = $queryBuilder->select($this->cacheTable . '.identifier')
|
||||
->from($this->cacheTable)
|
||||
->from($this->tagsTable)
|
||||
->where(
|
||||
$queryBuilder->expr()->eq($this->cacheTable . '.identifier', $queryBuilder->quoteIdentifier($this->tagsTable . '.identifier')),
|
||||
$queryBuilder->expr()->eq(
|
||||
$this->tagsTable . '.tag',
|
||||
$queryBuilder->createNamedParameter($tag)
|
||||
),
|
||||
$queryBuilder->expr()->gte(
|
||||
$this->cacheTable . '.expires',
|
||||
$queryBuilder->createNamedParameter($GLOBALS['EXEC_TIME'], Connection::PARAM_INT)
|
||||
)
|
||||
)
|
||||
->groupBy($this->cacheTable . '.identifier')
|
||||
->executeQuery();
|
||||
$identifiers = $result->fetchFirstColumn();
|
||||
return array_combine($identifiers, $identifiers);
|
||||
}
|
||||
|
||||
public function flush(): void
|
||||
{
|
||||
GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($this->cacheTable)->truncate($this->cacheTable);
|
||||
GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($this->tagsTable)->truncate($this->tagsTable);
|
||||
}
|
||||
|
||||
public function flushByTags(array $tags): void
|
||||
{
|
||||
if (empty($tags)) {
|
||||
return;
|
||||
}
|
||||
$connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($this->cacheTable);
|
||||
// A large set of tags was detected. Process it in chunks to guard against exceeding
|
||||
// maximum SQL query limits.
|
||||
if (count($tags) > 100) {
|
||||
$chunks = array_chunk($tags, 100);
|
||||
array_walk($chunks, $this->flushByTags(...));
|
||||
return;
|
||||
}
|
||||
$queryBuilder = $connection->createQueryBuilder();
|
||||
$result = $queryBuilder->select('identifier')
|
||||
->from($this->tagsTable)
|
||||
->where(
|
||||
$queryBuilder->expr()->in('tag', $queryBuilder->quoteArrayBasedValueListToStringList($tags)),
|
||||
)
|
||||
// group by is like DISTINCT and used here to suppress possible duplicate identifiers
|
||||
->groupBy('identifier')
|
||||
->executeQuery();
|
||||
$cacheEntryIdentifiers = $result->fetchFirstColumn();
|
||||
$this->flushCacheByCacheEntryIdentifiers($cacheEntryIdentifiers);
|
||||
}
|
||||
|
||||
public function flushByTag(string $tag): void
|
||||
{
|
||||
if (empty($tag)) {
|
||||
return;
|
||||
}
|
||||
$connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($this->cacheTable);
|
||||
$queryBuilder = $connection->createQueryBuilder();
|
||||
$result = $queryBuilder->select('identifier')
|
||||
->from($this->tagsTable)
|
||||
->where(
|
||||
$queryBuilder->expr()->eq('tag', $queryBuilder->quote($tag)),
|
||||
)
|
||||
// group by is like DISTINCT and used here to suppress possible duplicate identifiers
|
||||
->groupBy('identifier')
|
||||
->executeQuery();
|
||||
$cacheEntryIdentifiers = $result->fetchFirstColumn();
|
||||
$this->flushCacheByCacheEntryIdentifiers($cacheEntryIdentifiers);
|
||||
}
|
||||
|
||||
private function flushCacheByCacheEntryIdentifiers(array $cacheEntryIdentifiers): void
|
||||
{
|
||||
if ($cacheEntryIdentifiers === []) {
|
||||
// Nothing to do, return early.
|
||||
return;
|
||||
}
|
||||
$connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($this->cacheTable);
|
||||
$maxBindParameters = PlatformInformation::getMaxBindParameters($connection->getDatabasePlatform());
|
||||
foreach (array_chunk($cacheEntryIdentifiers, $maxBindParameters) as $chunk) {
|
||||
// Don't reuse QueryBuilder instance, create new one.
|
||||
$queryBuilder = $connection->createQueryBuilder();
|
||||
// Using string-list here directly is okay and mitigates additional processing
|
||||
// for database driver without named placeholder support, which comes with a
|
||||
// performance penalty we can work around and also do it only once per chunk.
|
||||
$quotedIdentifiers = $queryBuilder->quoteArrayBasedValueListToStringList($chunk);
|
||||
$queryBuilder->delete($this->cacheTable)
|
||||
->where($queryBuilder->expr()->in('identifier', $quotedIdentifiers))
|
||||
->executeStatement();
|
||||
// Don't reuse QueryBuilder instance, create new one.
|
||||
$queryBuilder = $connection->createQueryBuilder();
|
||||
$queryBuilder->delete($this->tagsTable)
|
||||
->where($queryBuilder->expr()->in('identifier', $quotedIdentifiers))
|
||||
->executeStatement();
|
||||
}
|
||||
}
|
||||
|
||||
public function collectGarbage(): void
|
||||
{
|
||||
$connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($this->cacheTable);
|
||||
$queryBuilder = $connection->createQueryBuilder();
|
||||
$result = $queryBuilder->select('identifier')
|
||||
->from($this->cacheTable)
|
||||
->where($queryBuilder->expr()->lt(
|
||||
'expires',
|
||||
$queryBuilder->createNamedParameter($GLOBALS['EXEC_TIME'], Connection::PARAM_INT)
|
||||
))
|
||||
// group by is like DISTINCT and used here to suppress possible duplicate identifiers
|
||||
->groupBy('identifier')
|
||||
->executeQuery();
|
||||
|
||||
// Get identifiers of expired cache entries
|
||||
$cacheEntryIdentifiers = $result->fetchFirstColumn();
|
||||
if (!empty($cacheEntryIdentifiers)) {
|
||||
// Delete tag rows connected to expired cache entries
|
||||
$this->deleteTagsChunked($cacheEntryIdentifiers);
|
||||
}
|
||||
$queryBuilder->delete($this->cacheTable)
|
||||
->where($queryBuilder->expr()->lt(
|
||||
'expires',
|
||||
$queryBuilder->createNamedParameter($GLOBALS['EXEC_TIME'], Connection::PARAM_INT)
|
||||
))
|
||||
->executeStatement();
|
||||
|
||||
// Find out which "orphaned" tags rows exists that have no cache row and delete those, too.
|
||||
$queryBuilder = $connection->createQueryBuilder();
|
||||
$result = $queryBuilder->select('tags.identifier')
|
||||
->from($this->tagsTable, 'tags')
|
||||
->leftJoin(
|
||||
'tags',
|
||||
$this->cacheTable,
|
||||
'cache',
|
||||
$queryBuilder->expr()->eq('tags.identifier', $queryBuilder->quoteIdentifier('cache.identifier'))
|
||||
)
|
||||
->where($queryBuilder->expr()->isNull('cache.identifier'))
|
||||
->groupBy('tags.identifier')
|
||||
->executeQuery();
|
||||
$tagsEntryIdentifiers = $result->fetchFirstColumn();
|
||||
|
||||
if (!empty($tagsEntryIdentifiers)) {
|
||||
$this->deleteTagsChunked($tagsEntryIdentifiers);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $items
|
||||
*/
|
||||
protected function deleteTagsChunked(array $items): void
|
||||
{
|
||||
$connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($this->tagsTable);
|
||||
$maxBindParameters = PlatformInformation::getMaxBindParameters($connection->getDatabasePlatform());
|
||||
foreach (array_chunk($items, $maxBindParameters, true) as $itemsChunk) {
|
||||
$queryBuilder = $connection->createQueryBuilder();
|
||||
$queryBuilder
|
||||
->delete($this->tagsTable)
|
||||
->where($queryBuilder->expr()->in('identifier', $queryBuilder->quoteArrayBasedValueListToStringList($itemsChunk)))
|
||||
->executeStatement();
|
||||
}
|
||||
}
|
||||
|
||||
protected function setCompression(bool $compression): void
|
||||
{
|
||||
$this->compression = $compression;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set data compression level.
|
||||
* If compression is enabled and this is not set,
|
||||
* gzcompress default level will be used
|
||||
*
|
||||
* @param int $compressionLevel -1 to 9: Compression level
|
||||
*/
|
||||
protected function setCompressionLevel(int $compressionLevel): void
|
||||
{
|
||||
if ($compressionLevel >= -1 && $compressionLevel <= 9) {
|
||||
$this->compressionLevel = $compressionLevel;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate needed table definitions for this cache.
|
||||
* This helper method is used by install tool and extension manager
|
||||
* and is not part of the public API!
|
||||
*
|
||||
* @return string SQL of table definitions
|
||||
*/
|
||||
public function getTableDefinitions(): string
|
||||
{
|
||||
$cacheTableSql = (string)file_get_contents(
|
||||
ExtensionManagementUtility::extPath('core')
|
||||
. 'Resources/Private/Sql/Cache/Backend/Typo3DatabaseBackendCache.sql'
|
||||
);
|
||||
$requiredTableStructures = str_replace('###CACHE_TABLE###', $this->cacheTable, $cacheTableSql) . LF . LF;
|
||||
$tagsTableSql = (string)file_get_contents(
|
||||
ExtensionManagementUtility::extPath('core')
|
||||
. 'Resources/Private/Sql/Cache/Backend/Typo3DatabaseBackendTags.sql'
|
||||
);
|
||||
return $requiredTableStructures . (str_replace('###TAGS_TABLE###', $this->tagsTable, $tagsTableSql) . LF);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user