TYPO3 v15 dev-main snapshot ()
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
<?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 Psr\Log\LoggerAwareInterface;
|
||||
use Psr\Log\LoggerAwareTrait;
|
||||
use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
|
||||
use TYPO3\CMS\Core\Log\LogManager;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* An abstract caching backend
|
||||
*/
|
||||
abstract class AbstractBackend implements BackendInterface, LoggerAwareInterface
|
||||
{
|
||||
use LoggerAwareTrait;
|
||||
|
||||
protected string $cacheIdentifier;
|
||||
|
||||
/**
|
||||
* Default lifetime of a cache entry in seconds
|
||||
*/
|
||||
protected int $defaultLifetime = 3600;
|
||||
|
||||
/**
|
||||
* @param array $options Configuration options - depends on the actual backend
|
||||
*/
|
||||
public function __construct(array $options = [])
|
||||
{
|
||||
foreach ($options as $optionKey => $optionValue) {
|
||||
$methodName = 'set' . ucfirst($optionKey);
|
||||
if (method_exists($this, $methodName)) {
|
||||
$this->{$methodName}($optionValue);
|
||||
} else {
|
||||
throw new \InvalidArgumentException('Invalid cache backend option "' . $optionKey . '" for backend of type "' . static::class . '"', 1231267498);
|
||||
}
|
||||
}
|
||||
// Init logger. This is forces, even if $options['logger'] has been set, which shouldn't.
|
||||
$this->logger = GeneralUtility::makeInstance(LogManager::class)->getLogger(static::class);
|
||||
}
|
||||
|
||||
public function setCache(FrontendInterface $cache): void
|
||||
{
|
||||
$this->cacheIdentifier = $cache->getIdentifier();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the default lifetime for this cache backend
|
||||
*
|
||||
* @param int $defaultLifetime Default lifetime of this cache backend in seconds. If NULL is specified, the default lifetime is used. "0" means unlimited lifetime.
|
||||
* @internal Misused for testing purposes.
|
||||
* @todo: Fix tests and protect or remove
|
||||
*/
|
||||
public function setDefaultLifetime(int $defaultLifetime): void
|
||||
{
|
||||
if ($defaultLifetime < 0) {
|
||||
throw new \InvalidArgumentException('The default lifetime must be given as a positive integer.', 1233072774);
|
||||
}
|
||||
$this->defaultLifetime = $defaultLifetime;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
<?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\Exception;
|
||||
use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
|
||||
use TYPO3\CMS\Core\Core\Environment;
|
||||
|
||||
/**
|
||||
* A caching backend which stores cache entries by using APCu.
|
||||
*
|
||||
* The APCu backend is not very good with tagging and scales O(2n) with the
|
||||
* number of tags. Do not use this backend if the data to be cached has many tags!
|
||||
*
|
||||
* This backend uses the following types of keys:
|
||||
* - tag_xxx
|
||||
* xxx is tag name, value is array of associated identifiers identifier. This
|
||||
* is "forward" tag index. It is mainly used for obtaining content by tag
|
||||
* (get identifier by tag -> get content by identifier)
|
||||
* - ident_xxx
|
||||
* xxx is identifier, value is array of associated tags. This is "reverse" tag
|
||||
* index. It provides quick access for all tags associated with this identifier
|
||||
* and used when removing the identifier
|
||||
*
|
||||
* Each key is prepended with a prefix. The prefix makes sure keys from the different
|
||||
* installations do not conflict. By default, prefix consists from two parts
|
||||
* separated by underscore character and ends in yet another underscore character:
|
||||
* - "TYPO3"
|
||||
* - Hash of path to TYPO3 and user running TYPO3
|
||||
*/
|
||||
final class ApcuBackend extends AbstractBackend implements TaggableBackendInterface, TransientBackendInterface
|
||||
{
|
||||
/**
|
||||
* A prefix to separate stored data from other data possible stored in the APC.
|
||||
*/
|
||||
private string $identifierPrefix = '';
|
||||
|
||||
/**
|
||||
* Constructs this backend
|
||||
*
|
||||
* @param array $options Configuration options - unused here
|
||||
*/
|
||||
public function __construct(array $options = [])
|
||||
{
|
||||
if (!extension_loaded('apcu')) {
|
||||
throw new Exception('The PHP extension "apcu" must be installed and loaded in order to use the APCu backend.', 1232985914);
|
||||
}
|
||||
if (PHP_SAPI === 'cli' && ini_get('apc.enable_cli') == 0) {
|
||||
throw new Exception('The APCu backend cannot be used because apcu is disabled on CLI.', 1232985915);
|
||||
}
|
||||
parent::__construct($options);
|
||||
}
|
||||
|
||||
public function setCache(FrontendInterface $cache): void
|
||||
{
|
||||
parent::setCache($cache);
|
||||
$this->identifierPrefix = 'TYPO3_' . hash('xxh3', Environment::getProjectPath() . $cache->getIdentifier()) . '_';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $data The data to be stored. mixed is allowed due to TransientBackendInterface
|
||||
*/
|
||||
public function set(string $entryIdentifier, mixed $data, array $tags = [], ?int $lifetime = null): void
|
||||
{
|
||||
$lifetime ??= $this->defaultLifetime;
|
||||
$success = apcu_store($this->identifierPrefix . $entryIdentifier, $data, $lifetime);
|
||||
if ($success === true) {
|
||||
$this->removeIdentifierFromAllTags($entryIdentifier);
|
||||
$this->addIdentifierToTags($entryIdentifier, $tags);
|
||||
} else {
|
||||
$this->logger->alert('Error using APCu: Could not save data in the cache.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads data from the cache.
|
||||
*
|
||||
* @return mixed The cache entry's content as a string or FALSE if the cache entry could not be loaded
|
||||
*/
|
||||
public function get(string $entryIdentifier): mixed
|
||||
{
|
||||
$success = false;
|
||||
$value = apcu_fetch($this->identifierPrefix . $entryIdentifier, $success);
|
||||
return $success ? $value : $success;
|
||||
}
|
||||
|
||||
public function has(string $entryIdentifier): bool
|
||||
{
|
||||
$success = false;
|
||||
apcu_fetch($this->identifierPrefix . $entryIdentifier, $success);
|
||||
return $success;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all cache entries matching the specified identifier.
|
||||
* Usually this only affects one entry but if - for what reason ever -
|
||||
* old entries for the identifier still exist, they are removed as well.
|
||||
*
|
||||
* @return bool TRUE if (at least) an entry could be removed or FALSE if no entry was found
|
||||
*/
|
||||
public function remove(string $entryIdentifier): bool
|
||||
{
|
||||
$this->removeIdentifierFromAllTags($entryIdentifier);
|
||||
return apcu_delete($this->identifierPrefix . $entryIdentifier);
|
||||
}
|
||||
|
||||
public function findIdentifiersByTag(string $tag): array
|
||||
{
|
||||
$success = false;
|
||||
$identifiers = apcu_fetch($this->identifierPrefix . 'tag_' . $tag, $success);
|
||||
if ($success === false) {
|
||||
return [];
|
||||
}
|
||||
return (array)$identifiers;
|
||||
}
|
||||
|
||||
public function flush(): void
|
||||
{
|
||||
apcu_delete(new \APCUIterator('/^' . preg_quote($this->identifierPrefix, '/') . '/'));
|
||||
}
|
||||
|
||||
public function flushByTag(string $tag): void
|
||||
{
|
||||
$identifiers = $this->findIdentifiersByTag($tag);
|
||||
foreach ($identifiers as $identifier) {
|
||||
$this->remove($identifier);
|
||||
}
|
||||
}
|
||||
|
||||
public function flushByTags(array $tags): void
|
||||
{
|
||||
array_walk($tags, $this->flushByTag(...));
|
||||
}
|
||||
|
||||
public function collectGarbage(): void
|
||||
{
|
||||
// Noop, APCu has internal GC
|
||||
}
|
||||
|
||||
private function addIdentifierToTags(string $entryIdentifier, array $tags): void
|
||||
{
|
||||
// Get identifier-to-tag index to look for updates
|
||||
$existingTags = $this->findTagsByIdentifier($entryIdentifier);
|
||||
$existingTagsUpdated = false;
|
||||
|
||||
foreach ($tags as $tag) {
|
||||
// Update tag-to-identifier index
|
||||
$identifiers = $this->findIdentifiersByTag($tag);
|
||||
if (!in_array($entryIdentifier, $identifiers, true)) {
|
||||
$identifiers[] = $entryIdentifier;
|
||||
apcu_store($this->identifierPrefix . 'tag_' . $tag, $identifiers);
|
||||
}
|
||||
// Test if identifier-to-tag index needs update
|
||||
if (!in_array($tag, $existingTags, true)) {
|
||||
$existingTags[] = $tag;
|
||||
$existingTagsUpdated = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Update identifier-to-tag index if needed
|
||||
if ($existingTagsUpdated) {
|
||||
apcu_store($this->identifierPrefix . 'ident_' . $entryIdentifier, $existingTags);
|
||||
}
|
||||
}
|
||||
|
||||
private function removeIdentifierFromAllTags(string $entryIdentifier): void
|
||||
{
|
||||
// Get tags for this identifier
|
||||
$tags = $this->findTagsByIdentifier($entryIdentifier);
|
||||
// De-associate tags with this identifier
|
||||
foreach ($tags as $tag) {
|
||||
$identifiers = $this->findIdentifiersByTag($tag);
|
||||
// Formally array_search() below should never return FALSE due to
|
||||
// the behavior of findTagsByIdentifier(). But if reverse index is
|
||||
// corrupted, we still can get 'FALSE' from array_search(). This is
|
||||
// not a problem because we are removing this identifier from
|
||||
// anywhere.
|
||||
if (($key = array_search($entryIdentifier, $identifiers)) !== false) {
|
||||
unset($identifiers[$key]);
|
||||
if (!empty($identifiers)) {
|
||||
apcu_store($this->identifierPrefix . 'tag_' . $tag, $identifiers);
|
||||
} else {
|
||||
apcu_delete($this->identifierPrefix . 'tag_' . $tag);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Clear reverse tag index for this identifier
|
||||
apcu_delete($this->identifierPrefix . 'ident_' . $entryIdentifier);
|
||||
}
|
||||
|
||||
private function findTagsByIdentifier(string $identifier): array
|
||||
{
|
||||
$success = false;
|
||||
$tags = apcu_fetch($this->identifierPrefix . 'ident_' . $identifier, $success);
|
||||
return $success ? (array)$tags : [];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
<?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;
|
||||
|
||||
/**
|
||||
* A contract for a Cache Backend
|
||||
*/
|
||||
interface BackendInterface
|
||||
{
|
||||
/**
|
||||
* Sets a reference to the cache frontend which uses this backend
|
||||
*
|
||||
* @param FrontendInterface $cache The frontend for this backend
|
||||
*/
|
||||
public function setCache(FrontendInterface $cache): void;
|
||||
|
||||
/**
|
||||
* Saves data in the cache.
|
||||
*
|
||||
* @param string $entryIdentifier An identifier for this specific cache entry
|
||||
* @param string $data The data to be stored
|
||||
* @param array $tags Tags to associate with this cache entry. If the backend does not support tags, this option can be ignored.
|
||||
* @param int|null $lifetime Lifetime of this cache entry in seconds. If NULL is specified, the default lifetime is used. "0" means unlimited lifetime.
|
||||
*/
|
||||
public function set(string $entryIdentifier, string $data, array $tags = [], ?int $lifetime = null): void;
|
||||
|
||||
/**
|
||||
* Loads data from the cache.
|
||||
*
|
||||
* @param string $entryIdentifier An identifier which describes the cache entry to load
|
||||
* @return mixed The cache entry's content as a string or FALSE if the cache entry could not be loaded
|
||||
*/
|
||||
public function get(string $entryIdentifier): mixed;
|
||||
|
||||
/**
|
||||
* Checks if a cache entry with the specified identifier exists.
|
||||
*
|
||||
* @param string $entryIdentifier An identifier specifying the cache entry
|
||||
* @return bool TRUE if such an entry exists, FALSE if not
|
||||
*/
|
||||
public function has(string $entryIdentifier): bool;
|
||||
|
||||
/**
|
||||
* Removes all cache entries matching the specified identifier.
|
||||
* Usually this only affects one entry but if - for what reason ever -
|
||||
* old entries for the identifier still exist, they are removed as well.
|
||||
*
|
||||
* @param string $entryIdentifier Specifies the cache entry to remove
|
||||
* @return bool TRUE if (at least) an entry could be removed or FALSE if no entry was found
|
||||
*/
|
||||
public function remove(string $entryIdentifier): bool;
|
||||
|
||||
/**
|
||||
* Removes all cache entries of this cache.
|
||||
*/
|
||||
public function flush(): void;
|
||||
|
||||
/**
|
||||
* Does garbage collection
|
||||
*/
|
||||
public function collectGarbage(): void;
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
<?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\Exception;
|
||||
use TYPO3\CMS\Core\Service\OpcodeCacheService;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Core\Utility\PathUtility;
|
||||
use TYPO3\CMS\Core\Utility\StringUtility;
|
||||
|
||||
/**
|
||||
* A caching backend which stores cache entries in files
|
||||
*/
|
||||
class FileBackend extends SimpleFileBackend implements TaggableBackendInterface
|
||||
{
|
||||
protected const EXPIRYTIME_LENGTH = 14;
|
||||
protected const DATASIZE_DIGITS = 10;
|
||||
|
||||
/**
|
||||
* @throws Exception if the directory does not exist or is not writable or exceeds the maximum allowed path length, or if no cache frontend has been set.
|
||||
*/
|
||||
public function set(string $entryIdentifier, string $data, array $tags = [], ?int $lifetime = null): void
|
||||
{
|
||||
if ($entryIdentifier !== PathUtility::basename($entryIdentifier)) {
|
||||
throw new \InvalidArgumentException('The specified entry identifier must not contain a path segment.', 1282073032);
|
||||
}
|
||||
if ($entryIdentifier === '') {
|
||||
throw new \InvalidArgumentException('The specified entry identifier must not be empty.', 1298114280);
|
||||
}
|
||||
$this->remove($entryIdentifier);
|
||||
$temporaryCacheEntryPathAndFilename = $this->cacheDirectory . StringUtility::getUniqueId() . '.temp';
|
||||
$lifetime ??= $this->defaultLifetime;
|
||||
$expiryTime = $lifetime === 0 ? 0 : (int)($GLOBALS['EXEC_TIME'] + $lifetime);
|
||||
$metaData = str_pad((string)$expiryTime, self::EXPIRYTIME_LENGTH) . implode(' ', $tags) . str_pad((string)strlen($data), self::DATASIZE_DIGITS);
|
||||
$result = GeneralUtility::writeFile($temporaryCacheEntryPathAndFilename, $data . $metaData, true);
|
||||
if ($result === false) {
|
||||
throw new Exception('The temporary cache file "' . $temporaryCacheEntryPathAndFilename . '" could not be written.', 1204026251);
|
||||
}
|
||||
$i = 0;
|
||||
$cacheEntryPathAndFilename = $this->cacheDirectory . $entryIdentifier . $this->cacheEntryFileExtension;
|
||||
while (($result = rename($temporaryCacheEntryPathAndFilename, $cacheEntryPathAndFilename)) === false && $i < 5) {
|
||||
$i++;
|
||||
}
|
||||
if ($result === false) {
|
||||
throw new Exception('The cache file "' . $cacheEntryPathAndFilename . '" could not be written.', 1222361632);
|
||||
}
|
||||
if ($this->cacheEntryFileExtension === '.php') {
|
||||
GeneralUtility::makeInstance(OpcodeCacheService::class)->clearAllActive($cacheEntryPathAndFilename);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return false|string The cache entry's content as a string or FALSE if the cache entry could not be loaded
|
||||
*/
|
||||
public function get(string $entryIdentifier): false|string
|
||||
{
|
||||
if ($entryIdentifier !== PathUtility::basename($entryIdentifier)) {
|
||||
throw new \InvalidArgumentException('The specified entry identifier must not contain a path segment.', 1282073033);
|
||||
}
|
||||
$pathAndFilename = $this->cacheDirectory . $entryIdentifier . $this->cacheEntryFileExtension;
|
||||
if ($this->isCacheFileExpired($pathAndFilename)) {
|
||||
return false;
|
||||
}
|
||||
$dataSize = (int)file_get_contents(
|
||||
$pathAndFilename,
|
||||
false,
|
||||
null,
|
||||
filesize($pathAndFilename) - self::DATASIZE_DIGITS,
|
||||
self::DATASIZE_DIGITS
|
||||
);
|
||||
return file_get_contents($pathAndFilename, false, null, 0, $dataSize);
|
||||
}
|
||||
|
||||
public function has(string $entryIdentifier): bool
|
||||
{
|
||||
if ($entryIdentifier !== PathUtility::basename($entryIdentifier)) {
|
||||
throw new \InvalidArgumentException('The specified entry identifier must not contain a path segment.', 1282073034);
|
||||
}
|
||||
return !$this->isCacheFileExpired($this->cacheDirectory . $entryIdentifier . $this->cacheEntryFileExtension);
|
||||
}
|
||||
|
||||
public function findIdentifiersByTag(string $tag): array
|
||||
{
|
||||
$entryIdentifiers = [];
|
||||
$now = $GLOBALS['EXEC_TIME'];
|
||||
$cacheEntryFileExtensionLength = strlen($this->cacheEntryFileExtension);
|
||||
for ($directoryIterator = GeneralUtility::makeInstance(\DirectoryIterator::class, $this->cacheDirectory); $directoryIterator->valid(); $directoryIterator->next()) {
|
||||
if (!$directoryIterator->isFile()) {
|
||||
continue;
|
||||
}
|
||||
$cacheEntryPathAndFilename = $directoryIterator->getPathname();
|
||||
$index = (int)file_get_contents(
|
||||
$cacheEntryPathAndFilename,
|
||||
false,
|
||||
null,
|
||||
filesize($cacheEntryPathAndFilename) - self::DATASIZE_DIGITS,
|
||||
self::DATASIZE_DIGITS
|
||||
);
|
||||
$metaData = (string)file_get_contents($cacheEntryPathAndFilename, false, null, $index);
|
||||
$expiryTime = (int)substr($metaData, 0, self::EXPIRYTIME_LENGTH);
|
||||
if ($expiryTime !== 0 && $expiryTime < $now) {
|
||||
continue;
|
||||
}
|
||||
if (in_array($tag, explode(' ', substr($metaData, self::EXPIRYTIME_LENGTH, -self::DATASIZE_DIGITS)))) {
|
||||
if ($cacheEntryFileExtensionLength > 0) {
|
||||
$entryIdentifiers[] = substr((string)$directoryIterator->getFilename(), 0, -$cacheEntryFileExtensionLength);
|
||||
} else {
|
||||
$entryIdentifiers[] = $directoryIterator->getFilename();
|
||||
}
|
||||
}
|
||||
}
|
||||
return $entryIdentifiers;
|
||||
}
|
||||
|
||||
public function flushByTag(string $tag): void
|
||||
{
|
||||
$identifiers = $this->findIdentifiersByTag($tag);
|
||||
foreach ($identifiers as $entryIdentifier) {
|
||||
$this->remove($entryIdentifier);
|
||||
}
|
||||
}
|
||||
|
||||
public function flushByTags(array $tags): void
|
||||
{
|
||||
array_walk($tags, $this->flushByTag(...));
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given cache entry files are still valid or if their
|
||||
* lifetime has exceeded.
|
||||
*/
|
||||
protected function isCacheFileExpired(string $cacheEntryPathAndFilename): bool
|
||||
{
|
||||
if (file_exists($cacheEntryPathAndFilename) === false) {
|
||||
return true;
|
||||
}
|
||||
$index = (int)file_get_contents(
|
||||
$cacheEntryPathAndFilename,
|
||||
false,
|
||||
null,
|
||||
filesize($cacheEntryPathAndFilename) - self::DATASIZE_DIGITS,
|
||||
self::DATASIZE_DIGITS
|
||||
);
|
||||
$expiryTime = (int)file_get_contents($cacheEntryPathAndFilename, false, null, $index, self::EXPIRYTIME_LENGTH);
|
||||
return $expiryTime !== 0 && $expiryTime < $GLOBALS['EXEC_TIME'];
|
||||
}
|
||||
|
||||
public function collectGarbage(): void
|
||||
{
|
||||
for ($directoryIterator = new \DirectoryIterator($this->cacheDirectory); $directoryIterator->valid(); $directoryIterator->next()) {
|
||||
if (!$directoryIterator->isFile()) {
|
||||
continue;
|
||||
}
|
||||
if ($this->isCacheFileExpired($directoryIterator->getPathname())) {
|
||||
$cacheEntryFileExtensionLength = strlen($this->cacheEntryFileExtension);
|
||||
if ($cacheEntryFileExtensionLength > 0) {
|
||||
$this->remove(substr($directoryIterator->getFilename(), 0, -$cacheEntryFileExtensionLength));
|
||||
} else {
|
||||
$this->remove($directoryIterator->getFilename());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function requireOnce(string $entryIdentifier): mixed
|
||||
{
|
||||
if ($entryIdentifier !== PathUtility::basename($entryIdentifier)) {
|
||||
throw new \InvalidArgumentException('The specified entry identifier must not contain a path segment.', 1282073036);
|
||||
}
|
||||
$pathAndFilename = $this->cacheDirectory . $entryIdentifier . $this->cacheEntryFileExtension;
|
||||
return $this->isCacheFileExpired($pathAndFilename) ? false : require_once $pathAndFilename;
|
||||
}
|
||||
|
||||
public function require(string $entryIdentifier): mixed
|
||||
{
|
||||
if ($entryIdentifier !== PathUtility::basename($entryIdentifier)) {
|
||||
throw new \InvalidArgumentException('The specified entry identifier must not contain a path segment.', 1532528246);
|
||||
}
|
||||
$pathAndFilename = $this->cacheDirectory . $entryIdentifier . $this->cacheEntryFileExtension;
|
||||
return $this->isCacheFileExpired($pathAndFilename) ? false : require $pathAndFilename;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,369 @@
|
||||
<?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\Exception;
|
||||
use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
|
||||
use TYPO3\CMS\Core\Core\Environment;
|
||||
|
||||
/**
|
||||
* A caching backend which stores cache entries by using Memcached.
|
||||
*
|
||||
* This backend uses the following types of Memcache keys:
|
||||
* - tag_xxx
|
||||
* xxx is tag name, value is array of associated identifiers identifier. This
|
||||
* is "forward" tag index. It is mainly used for obtaining content by tag
|
||||
* (get identifier by tag -> get content by identifier)
|
||||
* - ident_xxx
|
||||
* xxx is identifier, value is array of associated tags. This is "reverse" tag
|
||||
* index. It provides quick access for all tags associated with this identifier
|
||||
* and used when removing the identifier
|
||||
*
|
||||
* Each key is prepended with a prefix. By default prefix consists from two parts
|
||||
* separated by underscore character and ends in yet another underscore character:
|
||||
* - "TYPO3"
|
||||
* - Current site path obtained from Environment::getProjectPath()
|
||||
* This prefix makes sure that keys from the different installations do not
|
||||
* conflict.
|
||||
*
|
||||
* Note: When using the Memcached backend to store values of more than ~1 MB,
|
||||
* the data will be split into chunks to make them fit into the memcached limits.
|
||||
*/
|
||||
class MemcachedBackend extends AbstractBackend implements TaggableBackendInterface, TransientBackendInterface
|
||||
{
|
||||
/**
|
||||
* Max bucket size, (1024*1024)-42 bytes
|
||||
*/
|
||||
protected const MAX_BUCKET_SIZE = 1048534;
|
||||
|
||||
/**
|
||||
* Instance of the PHP Memcache class
|
||||
*/
|
||||
protected \Memcache|\Memcached $memcache;
|
||||
|
||||
/**
|
||||
* Used PECL module for memcached
|
||||
*/
|
||||
protected string $usedPeclModule = '';
|
||||
|
||||
/**
|
||||
* Array of Memcache server configurations
|
||||
*/
|
||||
protected array $servers = [];
|
||||
|
||||
/**
|
||||
* Indicates whether the memcache uses compression or not (requires zlib),
|
||||
* either 0 or \Memcached::OPT_COMPRESSION / MEMCACHE_COMPRESSED
|
||||
*/
|
||||
protected int $flags = 0;
|
||||
|
||||
/**
|
||||
* A prefix to separate stored data from other data possibly stored in the memcache
|
||||
*/
|
||||
protected string $identifierPrefix;
|
||||
|
||||
public function __construct(array $options = [])
|
||||
{
|
||||
if (!extension_loaded('memcache') && !extension_loaded('memcached')) {
|
||||
throw new Exception('The PHP extension "memcache" or "memcached" must be installed and loaded in order to use the Memcached backend.', 1213987706);
|
||||
}
|
||||
if ($this->usedPeclModule === '') {
|
||||
if (extension_loaded('memcache')) {
|
||||
$this->usedPeclModule = 'memcache';
|
||||
} elseif (extension_loaded('memcached')) {
|
||||
$this->usedPeclModule = 'memcached';
|
||||
}
|
||||
}
|
||||
parent::__construct($options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Setter for servers to be used. Expects an array, the values are expected
|
||||
* to be formatted like "<host>[:<port>]" or "unix://<path>"
|
||||
*
|
||||
* @param array $servers An array of servers to add.
|
||||
*/
|
||||
protected function setServers(array $servers): void
|
||||
{
|
||||
$this->servers = $servers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setter for compression flags bit
|
||||
*/
|
||||
protected function setCompression(bool $useCompression): void
|
||||
{
|
||||
$compressionFlag = $this->usedPeclModule === 'memcache' ? MEMCACHE_COMPRESSED : \Memcached::OPT_COMPRESSION;
|
||||
if ($useCompression) {
|
||||
$this->flags ^= $compressionFlag;
|
||||
} else {
|
||||
$this->flags &= ~$compressionFlag;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Getter for compression flag
|
||||
*/
|
||||
protected function getCompression(): bool
|
||||
{
|
||||
return $this->flags !== 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the identifier prefix
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
public function initializeObject(): void
|
||||
{
|
||||
if (empty($this->servers)) {
|
||||
throw new Exception('No servers were given to Memcache', 1213115903);
|
||||
}
|
||||
$memcachedPlugin = '\\' . ucfirst($this->usedPeclModule);
|
||||
$this->memcache = new $memcachedPlugin();
|
||||
$defaultPort = $this->usedPeclModule === 'memcache' ? ini_get('memcache.default_port') : 11211;
|
||||
foreach ($this->servers as $server) {
|
||||
if (str_starts_with((string)$server, 'unix://')) {
|
||||
$host = $server;
|
||||
$port = 0;
|
||||
} else {
|
||||
if (str_starts_with((string)$server, 'tcp://')) {
|
||||
$server = substr((string)$server, 6);
|
||||
}
|
||||
if (str_contains((string)$server, ':')) {
|
||||
[$host, $port] = explode(':', (string)$server, 2);
|
||||
} else {
|
||||
$host = $server;
|
||||
$port = $defaultPort;
|
||||
}
|
||||
}
|
||||
$this->memcache->addserver($host, (int)$port);
|
||||
}
|
||||
if ($this->usedPeclModule === 'memcached') {
|
||||
$this->memcache->setOption(\Memcached::OPT_COMPRESSION, $this->getCompression());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the preferred PECL module
|
||||
*/
|
||||
public function setPeclModule(string $peclModule): void
|
||||
{
|
||||
if ($peclModule !== 'memcache' && $peclModule !== 'memcached') {
|
||||
throw new Exception('PECL module must be either "memcache" or "memcached".', 1442239768);
|
||||
}
|
||||
|
||||
$this->usedPeclModule = $peclModule;
|
||||
}
|
||||
|
||||
public function setCache(FrontendInterface $cache): void
|
||||
{
|
||||
parent::setCache($cache);
|
||||
$identifierHash = substr(md5(Environment::getProjectPath() . $this->cacheIdentifier), 0, 12);
|
||||
$this->identifierPrefix = 'TYPO3_' . $identifierHash . '_';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $data The data to be stored. mixed is allowed due to TransientBackendInterface
|
||||
*/
|
||||
public function set(string $entryIdentifier, mixed $data, array $tags = [], ?int $lifetime = null): void
|
||||
{
|
||||
if (strlen($this->identifierPrefix . $entryIdentifier) > 250) {
|
||||
throw new \InvalidArgumentException('Could not set value. Key more than 250 characters (' . $this->identifierPrefix . $entryIdentifier . ').', 1232969508);
|
||||
}
|
||||
$tags[] = '%MEMCACHEBE%' . $this->cacheIdentifier;
|
||||
$expiration = $lifetime ?? $this->defaultLifetime;
|
||||
|
||||
// Memcached considers values over 2592000 sec (30 days) as UNIX timestamp
|
||||
// thus $expiration should be converted from lifetime to UNIX timestamp
|
||||
if ($expiration > 2592000) {
|
||||
$expiration += $GLOBALS['EXEC_TIME'];
|
||||
}
|
||||
try {
|
||||
if (is_string($data) && strlen($data) > self::MAX_BUCKET_SIZE) {
|
||||
$data = str_split($data, 1024 * 1000);
|
||||
$success = true;
|
||||
$chunkNumber = 1;
|
||||
foreach ($data as $chunk) {
|
||||
$success = $success && $this->setInternal($entryIdentifier . '_chunk_' . $chunkNumber, $chunk, $expiration);
|
||||
$chunkNumber++;
|
||||
}
|
||||
$success = $success && $this->setInternal($entryIdentifier, 'TYPO3*chunked:' . $chunkNumber, $expiration);
|
||||
} else {
|
||||
$success = $this->setInternal($entryIdentifier, $data, $expiration);
|
||||
}
|
||||
if ($success) {
|
||||
$this->removeIdentifierFromAllTags($entryIdentifier);
|
||||
$this->addIdentifierToTags($entryIdentifier, $tags);
|
||||
} else {
|
||||
throw new Exception('Could not set data to memcache server.', 1275830266);
|
||||
}
|
||||
} catch (\Exception $exception) {
|
||||
$this->logger->alert('Memcache: could not set value.', ['exception' => $exception]);
|
||||
}
|
||||
}
|
||||
|
||||
public function get(string $entryIdentifier): mixed
|
||||
{
|
||||
$value = $this->memcache->get($this->identifierPrefix . $entryIdentifier);
|
||||
if (is_string($value) && str_starts_with($value, 'TYPO3*chunked:')) {
|
||||
[, $chunkCount] = explode(':', $value);
|
||||
$value = '';
|
||||
for ($chunkNumber = 1; $chunkNumber < $chunkCount; $chunkNumber++) {
|
||||
$value .= $this->memcache->get($this->identifierPrefix . $entryIdentifier . '_chunk_' . $chunkNumber);
|
||||
}
|
||||
}
|
||||
return $value;
|
||||
}
|
||||
|
||||
public function has(string $entryIdentifier): bool
|
||||
{
|
||||
if ($this->usedPeclModule === 'memcache') {
|
||||
return $this->memcache->get($this->identifierPrefix . $entryIdentifier) !== false;
|
||||
}
|
||||
// pecl-memcached supports storing literal FALSE
|
||||
$this->memcache->get($this->identifierPrefix . $entryIdentifier);
|
||||
return $this->memcache->getResultCode() !== \Memcached::RES_NOTFOUND;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all cache entries matching the specified identifier.
|
||||
* Usually this only affects one entry but if - for what reason ever -
|
||||
* old entries for the identifier still exist, they are removed as well.
|
||||
*
|
||||
* @param string $entryIdentifier Specifies the cache entry to remove
|
||||
* @return bool TRUE if (at least) an entry could be removed or FALSE if no entry was found
|
||||
*/
|
||||
public function remove(string $entryIdentifier): bool
|
||||
{
|
||||
$this->removeIdentifierFromAllTags($entryIdentifier);
|
||||
return $this->memcache->delete($this->identifierPrefix . $entryIdentifier, 0);
|
||||
}
|
||||
|
||||
public function findIdentifiersByTag(string $tag): array
|
||||
{
|
||||
$identifiers = $this->memcache->get($this->identifierPrefix . 'tag_' . $tag);
|
||||
if ($identifiers !== false) {
|
||||
return (array)$identifiers;
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
public function flush(): void
|
||||
{
|
||||
$this->flushByTag('%MEMCACHEBE%' . $this->cacheIdentifier);
|
||||
}
|
||||
|
||||
public function flushByTag(string $tag): void
|
||||
{
|
||||
$identifiers = $this->findIdentifiersByTag($tag);
|
||||
foreach ($identifiers as $identifier) {
|
||||
$this->remove($identifier);
|
||||
}
|
||||
}
|
||||
|
||||
public function flushByTags(array $tags): void
|
||||
{
|
||||
array_walk($tags, $this->flushByTag(...));
|
||||
}
|
||||
|
||||
/**
|
||||
* Does nothing, as memcached does GC itself
|
||||
*/
|
||||
public function collectGarbage(): void {}
|
||||
|
||||
/**
|
||||
* Stores the actual data inside memcache/memcached
|
||||
*/
|
||||
protected function setInternal(string $entryIdentifier, mixed $data, int $expiration): bool
|
||||
{
|
||||
if ($this->usedPeclModule === 'memcache') {
|
||||
return $this->memcache->set($this->identifierPrefix . $entryIdentifier, $data, $this->flags, $expiration);
|
||||
}
|
||||
return $this->memcache->set($this->identifierPrefix . $entryIdentifier, $data, $expiration);
|
||||
}
|
||||
|
||||
/**
|
||||
* Associates the identifier with the given tags
|
||||
*/
|
||||
protected function addIdentifierToTags(string $entryIdentifier, array $tags): void
|
||||
{
|
||||
// Get identifier-to-tag index to look for updates
|
||||
$existingTags = $this->findTagsByIdentifier($entryIdentifier);
|
||||
$existingTagsUpdated = false;
|
||||
|
||||
foreach ($tags as $tag) {
|
||||
// Update tag-to-identifier index
|
||||
$identifiers = $this->findIdentifiersByTag($tag);
|
||||
if (!in_array($entryIdentifier, $identifiers, true)) {
|
||||
$identifiers[] = $entryIdentifier;
|
||||
$this->memcache->set($this->identifierPrefix . 'tag_' . $tag, $identifiers);
|
||||
}
|
||||
// Test if identifier-to-tag index needs update
|
||||
if (!in_array($tag, $existingTags, true)) {
|
||||
$existingTags[] = $tag;
|
||||
$existingTagsUpdated = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Update identifier-to-tag index if needed
|
||||
if ($existingTagsUpdated) {
|
||||
$this->memcache->set($this->identifierPrefix . 'ident_' . $entryIdentifier, $existingTags);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes association of the identifier with the given tags
|
||||
*/
|
||||
protected function removeIdentifierFromAllTags(string $entryIdentifier): void
|
||||
{
|
||||
// Get tags for this identifier
|
||||
$tags = $this->findTagsByIdentifier($entryIdentifier);
|
||||
// De-associate tags with this identifier
|
||||
foreach ($tags as $tag) {
|
||||
$identifiers = $this->findIdentifiersByTag($tag);
|
||||
// Formally array_search() below should never return FALSE due to
|
||||
// the behavior of findTagsByIdentifier(). But if reverse index is
|
||||
// corrupted, we still can get 'FALSE' from array_search(). This is
|
||||
// not a problem because we are removing this identifier from
|
||||
// anywhere.
|
||||
if (($key = array_search($entryIdentifier, $identifiers)) !== false) {
|
||||
unset($identifiers[$key]);
|
||||
if (!empty($identifiers)) {
|
||||
$this->memcache->set($this->identifierPrefix . 'tag_' . $tag, $identifiers);
|
||||
} else {
|
||||
$this->memcache->delete($this->identifierPrefix . 'tag_' . $tag, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Clear reverse tag index for this identifier
|
||||
$this->memcache->delete($this->identifierPrefix . 'ident_' . $entryIdentifier, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds all tags for the given identifier. This function uses reverse tag
|
||||
* index to search for tags.
|
||||
*
|
||||
* @param string $identifier Identifier to find tags by
|
||||
*/
|
||||
protected function findTagsByIdentifier(string $identifier): array
|
||||
{
|
||||
$tags = $this->memcache->get($this->identifierPrefix . 'ident_' . $identifier);
|
||||
return $tags === false ? [] : (array)$tags;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
<?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;
|
||||
|
||||
/**
|
||||
* A caching backend which forgets everything immediately
|
||||
*/
|
||||
class NullBackend implements PhpCapableBackendInterface, TaggableBackendInterface, TransientBackendInterface
|
||||
{
|
||||
public function set(string $entryIdentifier, mixed $data, array $tags = [], ?int $lifetime = null): void {}
|
||||
|
||||
public function get(string $entryIdentifier): false
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function has(string $entryIdentifier): false
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function remove(string $entryIdentifier): false
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function findIdentifiersByTag($tag): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function flush(): void {}
|
||||
|
||||
public function flushByTag(string $tag): void {}
|
||||
|
||||
public function flushByTags(array $tags): void {}
|
||||
|
||||
public function setCache(FrontendInterface $cache): void {}
|
||||
|
||||
public function collectGarbage(): void {}
|
||||
|
||||
public function requireOnce(string $entryIdentifier): false
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function require(string $entryIdentifier): false
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?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;
|
||||
|
||||
/**
|
||||
* A contract for a cache backend which is capable of storing, retrieving and
|
||||
* including PHP source code.
|
||||
*/
|
||||
interface PhpCapableBackendInterface extends BackendInterface
|
||||
{
|
||||
/**
|
||||
* Loads PHP code from the cache and require_once() it right away.
|
||||
*
|
||||
* @param string $entryIdentifier An identifier which describes the cache entry to load
|
||||
* @return mixed Potential return value from the include operation
|
||||
*/
|
||||
public function requireOnce(string $entryIdentifier): mixed;
|
||||
|
||||
/**
|
||||
* Loads PHP code from the cache and require() it right away.
|
||||
*
|
||||
* @param string $entryIdentifier An identifier which describes the cache entry to load
|
||||
* @return mixed Potential return value from the include operation
|
||||
*/
|
||||
public function require(string $entryIdentifier): mixed;
|
||||
}
|
||||
@@ -0,0 +1,476 @@
|
||||
<?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\Exception;
|
||||
use TYPO3\CMS\Core\Utility\StringUtility;
|
||||
|
||||
/**
|
||||
* A caching backend which stores cache entries by using Redis with phpredis
|
||||
* PHP module. Redis is a noSQL database with very good scaling characteristics
|
||||
* in proportion to the amount of entries and data size.
|
||||
*
|
||||
* @see https://redis.io/
|
||||
* @see https://github.com/phpredis/phpredis
|
||||
*/
|
||||
class RedisBackend extends AbstractBackend implements TaggableBackendInterface
|
||||
{
|
||||
/**
|
||||
* Faked unlimited lifetime = 31536000 (1 Year).
|
||||
* In redis an entry does not have a lifetime by default (it's not "volatile").
|
||||
* Entries can be made volatile either with EXPIRE after it has been SET,
|
||||
* or with SETEX, which is a combined SET and EXPIRE command.
|
||||
* But an entry can not be made "unvolatile" again. To set a volatile entry to
|
||||
* not volatile again, it must be DELeted and SET without a following EXPIRE.
|
||||
* To save these additional calls on every set(),
|
||||
* we just make every entry volatile and treat a high number as "unlimited"
|
||||
*
|
||||
* @see https://redis.io/commands/expire
|
||||
*/
|
||||
protected const FAKED_UNLIMITED_LIFETIME = 31536000;
|
||||
|
||||
/**
|
||||
* Key prefix for identifier->data entries
|
||||
*/
|
||||
protected const IDENTIFIER_DATA_PREFIX = 'identData:';
|
||||
|
||||
/**
|
||||
* Key prefix for identifier->tags sets
|
||||
*/
|
||||
protected const IDENTIFIER_TAGS_PREFIX = 'identTags:';
|
||||
|
||||
/**
|
||||
* Key prefix for tag->identifiers sets
|
||||
*/
|
||||
protected const TAG_IDENTIFIERS_PREFIX = 'tagIdents:';
|
||||
|
||||
protected \Redis $redis;
|
||||
|
||||
/**
|
||||
* Indicates whether the server is connected
|
||||
*/
|
||||
protected bool $connected = false;
|
||||
|
||||
/**
|
||||
* Persistent connection
|
||||
*/
|
||||
protected bool $persistentConnection = false;
|
||||
|
||||
/**
|
||||
* Hostname / IP of the Redis server, defaults to 127.0.0.1.
|
||||
*/
|
||||
protected string $hostname = '127.0.0.1';
|
||||
|
||||
/**
|
||||
* Port of the Redis server, defaults to 6379
|
||||
*/
|
||||
protected int $port = 6379;
|
||||
|
||||
/**
|
||||
* Number of selected database, defaults to 0
|
||||
*/
|
||||
protected int $database = 0;
|
||||
|
||||
/**
|
||||
* Username for authentication
|
||||
*/
|
||||
protected ?string $username = null;
|
||||
|
||||
/**
|
||||
* Password for authentication
|
||||
*/
|
||||
protected ?string $password = null;
|
||||
|
||||
/**
|
||||
* Indicates whether data is compressed or not (requires php zlib)
|
||||
*/
|
||||
protected bool $compression = false;
|
||||
|
||||
/**
|
||||
* -1 to 9, indicates zlib compression level: -1 = default level 6, 0 = no compression, 9 maximum compression
|
||||
*/
|
||||
protected int $compressionLevel = -1;
|
||||
|
||||
/**
|
||||
* limit in seconds (default is 0 meaning unlimited)
|
||||
*/
|
||||
protected int $connectionTimeout = 0;
|
||||
|
||||
/**
|
||||
* Used as prefix for all Redis keys/identifiers
|
||||
*/
|
||||
protected string $keyPrefix = '';
|
||||
|
||||
public function __construct(array $options = [])
|
||||
{
|
||||
if (!extension_loaded('redis')) {
|
||||
throw new Exception('The PHP extension "redis" must be installed and loaded in order to use the redis backend.', 1279462933);
|
||||
}
|
||||
parent::__construct($options);
|
||||
}
|
||||
|
||||
public function initializeObject(): void
|
||||
{
|
||||
$this->redis = new \Redis();
|
||||
try {
|
||||
if ($this->persistentConnection) {
|
||||
$this->connected = $this->redis->pconnect($this->hostname, $this->port, $this->connectionTimeout, (string)$this->database);
|
||||
} else {
|
||||
$this->connected = $this->redis->connect($this->hostname, $this->port, $this->connectionTimeout);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$this->logger->alert('Could not connect to redis server.', ['exception' => $e]);
|
||||
}
|
||||
if ($this->connected) {
|
||||
$authentication = $this->getAuthentication();
|
||||
if ($authentication !== null) {
|
||||
$success = $this->redis->auth($this->getAuthentication());
|
||||
if (!$success) {
|
||||
throw new Exception('Authentication to Redis failed”.', 1279765134);
|
||||
}
|
||||
}
|
||||
if ($this->database >= 0) {
|
||||
$success = $this->redis->select($this->database);
|
||||
if (!$success) {
|
||||
throw new Exception('The given database "' . $this->database . '" could not be selected.', 1279765144);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected function setPersistentConnection(bool $persistentConnection): void
|
||||
{
|
||||
$this->persistentConnection = $persistentConnection;
|
||||
}
|
||||
|
||||
protected function setHostname(string $hostname): void
|
||||
{
|
||||
$this->hostname = $hostname;
|
||||
}
|
||||
|
||||
protected function setPort(int $port): void
|
||||
{
|
||||
$this->port = $port;
|
||||
}
|
||||
|
||||
protected function setDatabase(int $database): void
|
||||
{
|
||||
if ($database < 0) {
|
||||
throw new \InvalidArgumentException('The specified database "' . $database . '" must be greater or equal than zero.', 1279763534);
|
||||
}
|
||||
$this->database = $database;
|
||||
}
|
||||
|
||||
protected function setUsername(string $username): void
|
||||
{
|
||||
$this->username = $username;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setter for authentication password
|
||||
*/
|
||||
protected function setPassword(#[\SensitiveParameter] string $password): void
|
||||
{
|
||||
$this->password = $password;
|
||||
}
|
||||
|
||||
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;
|
||||
} else {
|
||||
throw new \InvalidArgumentException('The specified compression level must be an integer between -1 and 9.', 1289679155);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set connection timeout.
|
||||
* This value in seconds is used as a maximum number
|
||||
* of seconds to wait if a connection can be established.
|
||||
*
|
||||
* @param int $connectionTimeout limit in seconds, a value greater or equal than 0
|
||||
*/
|
||||
protected function setConnectionTimeout(int $connectionTimeout): void
|
||||
{
|
||||
if ($connectionTimeout < 0) {
|
||||
throw new \InvalidArgumentException('The specified connection timeout "' . $connectionTimeout . '" must be greater or equal than zero.', 1487849326);
|
||||
}
|
||||
|
||||
$this->connectionTimeout = $connectionTimeout;
|
||||
}
|
||||
|
||||
protected function setKeyPrefix(string $keyPrefix): void
|
||||
{
|
||||
$this->keyPrefix = $keyPrefix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save data in the cache
|
||||
*
|
||||
* Scales O(1) with number of cache entries
|
||||
* Scales O(n) with number of tags
|
||||
*/
|
||||
public function set(string $entryIdentifier, string $data, array $tags = [], ?int $lifetime = null): void
|
||||
{
|
||||
$lifetime ??= $this->defaultLifetime;
|
||||
if ($lifetime < 0) {
|
||||
throw new \InvalidArgumentException('The specified lifetime "' . $lifetime . '" must be greater or equal than zero.', 1279487573);
|
||||
}
|
||||
if ($this->connected) {
|
||||
$expiration = $lifetime === 0 ? self::FAKED_UNLIMITED_LIFETIME : $lifetime;
|
||||
if ($this->compression) {
|
||||
$data = gzcompress($data, $this->compressionLevel);
|
||||
}
|
||||
$this->redis->setex($this->getDataIdentifier($entryIdentifier), $expiration, $data);
|
||||
$addTags = $tags;
|
||||
$removeTags = [];
|
||||
$existingTags = $this->redis->sMembers($this->getTagsIdentifier($entryIdentifier));
|
||||
if (!empty($existingTags)) {
|
||||
$addTags = array_diff($tags, $existingTags);
|
||||
$removeTags = array_diff($existingTags, $tags);
|
||||
}
|
||||
if (!empty($removeTags) || !empty($addTags)) {
|
||||
$queue = $this->redis->multi(\Redis::PIPELINE);
|
||||
foreach ($removeTags as $tag) {
|
||||
$queue->sRem($this->getTagsIdentifier($entryIdentifier), $tag);
|
||||
$queue->sRem($this->getTagIdentifier($tag), $entryIdentifier);
|
||||
}
|
||||
foreach ($addTags as $tag) {
|
||||
$queue->sAdd($this->getTagsIdentifier($entryIdentifier), $tag);
|
||||
$queue->sAdd($this->getTagIdentifier($tag), $entryIdentifier);
|
||||
}
|
||||
$queue->exec();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads data from the cache.
|
||||
*
|
||||
* Scales O(1) with number of cache entries
|
||||
*/
|
||||
public function get(string $entryIdentifier): mixed
|
||||
{
|
||||
$storedEntry = false;
|
||||
if ($this->connected) {
|
||||
$storedEntry = $this->redis->get($this->getDataIdentifier($entryIdentifier));
|
||||
}
|
||||
if ($this->compression && (string)$storedEntry !== '') {
|
||||
return gzuncompress((string)$storedEntry);
|
||||
}
|
||||
return $storedEntry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a cache entry with the specified identifier exists.
|
||||
*
|
||||
* Scales O(1) with number of cache entries
|
||||
*/
|
||||
public function has(string $entryIdentifier): bool
|
||||
{
|
||||
return $this->connected && $this->redis->exists($this->getDataIdentifier($entryIdentifier));
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all cache entries matching the specified identifier.
|
||||
*
|
||||
* Scales O(1) with number of cache entries
|
||||
* Scales O(n) with number of tags
|
||||
*/
|
||||
public function remove(string $entryIdentifier): bool
|
||||
{
|
||||
if (!$this->connected) {
|
||||
return false;
|
||||
}
|
||||
if (!$this->redis->exists($this->getDataIdentifier($entryIdentifier))) {
|
||||
return false;
|
||||
}
|
||||
$assignedTags = $this->redis->sMembers($this->getTagsIdentifier($entryIdentifier));
|
||||
$queue = $this->redis->multi(\Redis::PIPELINE);
|
||||
foreach ($assignedTags as $tag) {
|
||||
$queue->sRem($this->getTagIdentifier($tag), $entryIdentifier);
|
||||
}
|
||||
$queue->del($this->getDataIdentifier($entryIdentifier), $this->getTagsIdentifier($entryIdentifier));
|
||||
$queue->exec();
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds and returns all cache entry identifiers which are tagged by the specified tag.
|
||||
*
|
||||
* Scales O(1) with number of cache entries
|
||||
* Scales O(n) with number of tag entries
|
||||
*/
|
||||
public function findIdentifiersByTag(string $tag): array
|
||||
{
|
||||
if (!$this->connected) {
|
||||
return [];
|
||||
}
|
||||
return $this->redis->sMembers($this->getTagIdentifier($tag));
|
||||
}
|
||||
|
||||
public function flush(): void
|
||||
{
|
||||
if (!$this->connected) {
|
||||
return;
|
||||
}
|
||||
// unless we have a key prefix all data can be flushed
|
||||
if ($this->keyPrefix === '') {
|
||||
$this->redis->flushDB();
|
||||
return;
|
||||
}
|
||||
$keys = $this->redis->keys($this->keyPrefix . '*');
|
||||
$queue = $this->redis->multi();
|
||||
$queue->del($keys);
|
||||
$queue->exec();
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all cache entries of this cache which are tagged with the specified tag.
|
||||
*
|
||||
* Scales O(1) with number of cache entries
|
||||
* Scales O(n^2) with number of tag entries
|
||||
*/
|
||||
public function flushByTag(string $tag): void
|
||||
{
|
||||
if (!$this->connected) {
|
||||
return;
|
||||
}
|
||||
$identifiers = $this->redis->sMembers($this->getTagIdentifier($tag));
|
||||
if (!empty($identifiers)) {
|
||||
$this->removeIdentifierEntriesAndRelations($identifiers, [$tag]);
|
||||
}
|
||||
}
|
||||
|
||||
public function flushByTags(array $tags): void
|
||||
{
|
||||
array_walk($tags, $this->flushByTag(...));
|
||||
}
|
||||
|
||||
/**
|
||||
* With the current internal structure, only the identifier to data entries
|
||||
* have a redis internal lifetime. If an entry expires, attached
|
||||
* identifier to tags and tag to identifiers entries will be left over.
|
||||
* This method finds those entries and cleans them up.
|
||||
*
|
||||
* Scales O(n*m) with number of cache entries (n) and number of tags (m)
|
||||
*/
|
||||
public function collectGarbage(): void
|
||||
{
|
||||
$identifierToTagsKeys = $this->redis->keys($this->getTagsIdentifier('*'));
|
||||
foreach ($identifierToTagsKeys as $identifierToTagsKey) {
|
||||
[, $identifier] = explode(':', $identifierToTagsKey);
|
||||
// Check if the data entry still exists
|
||||
if (!$this->redis->exists($this->getDataIdentifier($identifier))) {
|
||||
$tagsToRemoveIdentifierFrom = $this->redis->sMembers($identifierToTagsKey);
|
||||
$queue = $this->redis->multi(\Redis::PIPELINE);
|
||||
$queue->del($identifierToTagsKey);
|
||||
foreach ($tagsToRemoveIdentifierFrom as $tag) {
|
||||
$queue->sRem($this->getTagIdentifier($tag), $identifier);
|
||||
}
|
||||
$queue->exec();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method for flushByTag()
|
||||
* Gets list of identifiers and tags and removes all relations of those tags
|
||||
*
|
||||
* Scales O(1) with number of cache entries
|
||||
* Scales O(n^2) with number of tags
|
||||
*/
|
||||
protected function removeIdentifierEntriesAndRelations(array $identifiers, array $tags): void
|
||||
{
|
||||
// Set a temporary entry which holds all identifiers that need to be removed from
|
||||
// the tag to identifiers sets
|
||||
$uniqueTempKey = 'temp:' . StringUtility::getUniqueId();
|
||||
$prefixedKeysToDelete = [$uniqueTempKey];
|
||||
$prefixedIdentifierToTagsKeysToDelete = [];
|
||||
foreach ($identifiers as $identifier) {
|
||||
$prefixedKeysToDelete[] = $this->getDataIdentifier($identifier);
|
||||
$prefixedIdentifierToTagsKeysToDelete[] = $this->getTagsIdentifier($identifier);
|
||||
}
|
||||
foreach ($tags as $tag) {
|
||||
$prefixedKeysToDelete[] = $this->getTagIdentifier($tag);
|
||||
}
|
||||
$tagToIdentifiersSetsToRemoveIdentifiersFrom = $this->redis->sUnion(...$prefixedIdentifierToTagsKeysToDelete);
|
||||
// Remove the tag to identifier set of the given tags, they will be removed anyway
|
||||
$tagToIdentifiersSetsToRemoveIdentifiersFrom = array_diff($tagToIdentifiersSetsToRemoveIdentifiersFrom, $tags);
|
||||
// Diff all identifiers that must be removed from tag to identifiers sets off from a
|
||||
// tag to identifiers set and store result in same tag to identifiers set again
|
||||
$queue = $this->redis->multi(\Redis::PIPELINE);
|
||||
foreach ($identifiers as $identifier) {
|
||||
$queue->sAdd($uniqueTempKey, $identifier);
|
||||
}
|
||||
foreach ($tagToIdentifiersSetsToRemoveIdentifiersFrom as $tagToIdentifiersSet) {
|
||||
$queue->sDiffStore($this->getTagIdentifier($tagToIdentifiersSet), $this->getTagIdentifier($tagToIdentifiersSet), $uniqueTempKey);
|
||||
}
|
||||
$queue->del(array_merge($prefixedKeysToDelete, $prefixedIdentifierToTagsKeysToDelete));
|
||||
$queue->exec();
|
||||
}
|
||||
|
||||
protected function getDataIdentifier(string $identifier): string
|
||||
{
|
||||
return $this->keyPrefix . self::IDENTIFIER_DATA_PREFIX . $identifier;
|
||||
}
|
||||
|
||||
protected function getTagsIdentifier(string $identifier): string
|
||||
{
|
||||
return $this->keyPrefix . self::IDENTIFIER_TAGS_PREFIX . $identifier;
|
||||
}
|
||||
|
||||
protected function getTagIdentifier(string $tag): string
|
||||
{
|
||||
return $this->keyPrefix . self::TAG_IDENTIFIERS_PREFIX . $tag;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the authentication value based on the configuration, returning an associative array
|
||||
* in case `username` and `password` has been configured, the `password` as string if only
|
||||
* password has been configured or `null` to indicate no-authentication configuration, which
|
||||
* is also possible to be used with `redis`.
|
||||
*/
|
||||
protected function getAuthentication(): array|string|null
|
||||
{
|
||||
return match (true) {
|
||||
// Username and password configured for authentication, build associative array
|
||||
// out of possible and supported array variants by `php-redis::auth()`.
|
||||
($this->username !== null && $this->password !== null) => [
|
||||
'user' => $this->username,
|
||||
'pass' => $this->password,
|
||||
],
|
||||
// Password-only authentication configured.
|
||||
($this->username === null && $this->password !== null) => $this->password,
|
||||
// No authentication configured.
|
||||
default => null,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,280 @@
|
||||
<?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\Exception;
|
||||
use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
|
||||
use TYPO3\CMS\Core\Cache\Frontend\PhpFrontend;
|
||||
use TYPO3\CMS\Core\Core\Environment;
|
||||
use TYPO3\CMS\Core\Service\OpcodeCacheService;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Core\Utility\PathUtility;
|
||||
use TYPO3\CMS\Core\Utility\StringUtility;
|
||||
|
||||
/**
|
||||
* A caching backend which stores cache entries in files, but does not support or
|
||||
* care about expiry times and tags.
|
||||
*/
|
||||
class SimpleFileBackend extends AbstractBackend implements PhpCapableBackendInterface
|
||||
{
|
||||
/**
|
||||
* Directory where the files are stored
|
||||
*/
|
||||
protected string $cacheDirectory = '';
|
||||
|
||||
/**
|
||||
* Temporary path to cache directory before setCache() was called. It is
|
||||
* set by setCacheDirectory() and used in setCache() method which calls
|
||||
* the directory creation if needed. The variable is not used afterwards,
|
||||
* the final cache directory path is stored in $this->cacheDirectory then.
|
||||
*/
|
||||
protected string $temporaryCacheDirectory = '';
|
||||
|
||||
/**
|
||||
* A file extension to use for each cache entry.
|
||||
*/
|
||||
protected string $cacheEntryFileExtension = '';
|
||||
|
||||
public function setCache(FrontendInterface $cache): void
|
||||
{
|
||||
parent::setCache($cache);
|
||||
if (empty($this->temporaryCacheDirectory)) {
|
||||
// If no cache directory was given with cacheDirectory
|
||||
// configuration option, set it to a path below var/ folder
|
||||
$temporaryCacheDirectory = Environment::getVarPath() . '/';
|
||||
} else {
|
||||
$temporaryCacheDirectory = $this->temporaryCacheDirectory;
|
||||
}
|
||||
$codeOrData = $cache instanceof PhpFrontend ? 'code' : 'data';
|
||||
$finalCacheDirectory = $temporaryCacheDirectory . 'cache/' . $codeOrData . '/' . $this->cacheIdentifier . '/';
|
||||
$this->createFinalCacheDirectory($finalCacheDirectory);
|
||||
$this->temporaryCacheDirectory = '';
|
||||
$this->cacheDirectory = $finalCacheDirectory;
|
||||
$this->cacheEntryFileExtension = $cache instanceof PhpFrontend ? '.php' : '';
|
||||
if (strlen($this->cacheDirectory) + 23 > PHP_MAXPATHLEN) {
|
||||
throw new Exception('The length of the temporary cache file path "' . $this->cacheDirectory . '" exceeds the maximum path length of ' . (PHP_MAXPATHLEN - 23) . '. Please consider setting the temporaryDirectoryBase option to a shorter path.', 1248710426);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the directory where the cache files are stored. By default it is
|
||||
* assumed that the directory is below TYPO3's Project Path. However, an
|
||||
* absolute path can be selected, too.
|
||||
*
|
||||
* This method enables to use a cache path outside of TYPO3's Project Path. The final
|
||||
* cache path is checked and created in createFinalCacheDirectory(),
|
||||
* called by setCache() method, which is done _after_ the cacheDirectory
|
||||
* option was handled.
|
||||
*
|
||||
* @internal Misused in tests
|
||||
* @todo: Fix tests and protect
|
||||
*/
|
||||
public function setCacheDirectory(string $cacheDirectory): void
|
||||
{
|
||||
$documentRoot = Environment::getProjectPath() . '/';
|
||||
if ($open_basedir = ini_get('open_basedir')) {
|
||||
if (Environment::isWindows()) {
|
||||
$delimiter = ';';
|
||||
$cacheDirectory = str_replace('\\', '/', $cacheDirectory);
|
||||
if (!preg_match('/[A-Z]:/', substr($cacheDirectory, 0, 2))) {
|
||||
$cacheDirectory = Environment::getProjectPath() . $cacheDirectory;
|
||||
}
|
||||
} else {
|
||||
$delimiter = ':';
|
||||
if ($cacheDirectory[0] !== '/') {
|
||||
// relative path to cache directory.
|
||||
$cacheDirectory = Environment::getProjectPath() . $cacheDirectory;
|
||||
}
|
||||
}
|
||||
$basedirs = explode($delimiter, $open_basedir);
|
||||
$cacheDirectoryInBaseDir = false;
|
||||
foreach ($basedirs as $basedir) {
|
||||
if (Environment::isWindows()) {
|
||||
$basedir = str_replace('\\', '/', $basedir);
|
||||
}
|
||||
if ($basedir[strlen($basedir) - 1] !== '/') {
|
||||
$basedir .= '/';
|
||||
}
|
||||
if (str_starts_with($cacheDirectory, $basedir)) {
|
||||
$documentRoot = $basedir;
|
||||
$cacheDirectory = str_replace($basedir, '', $cacheDirectory);
|
||||
$cacheDirectoryInBaseDir = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!$cacheDirectoryInBaseDir) {
|
||||
throw new Exception(
|
||||
'Open_basedir restriction in effect. The directory "' . $cacheDirectory . '" is not in an allowed path.',
|
||||
1476045417
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if ($cacheDirectory[0] === '/') {
|
||||
// Absolute path to cache directory.
|
||||
$documentRoot = '';
|
||||
}
|
||||
if (Environment::isWindows() && (!empty($documentRoot) && str_starts_with($cacheDirectory, $documentRoot))) {
|
||||
$documentRoot = '';
|
||||
}
|
||||
}
|
||||
// After this point all paths have '/' as directory separator
|
||||
if ($cacheDirectory[strlen($cacheDirectory) - 1] !== '/') {
|
||||
$cacheDirectory .= '/';
|
||||
}
|
||||
$this->temporaryCacheDirectory = $documentRoot . $cacheDirectory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the final cache directory if it does not exist.
|
||||
*/
|
||||
protected function createFinalCacheDirectory(string $finalCacheDirectory): void
|
||||
{
|
||||
if (!is_dir($finalCacheDirectory)) {
|
||||
try {
|
||||
GeneralUtility::mkdir_deep($finalCacheDirectory);
|
||||
} catch (\RuntimeException $e) {
|
||||
throw new Exception('The directory "' . $finalCacheDirectory . '" can not be created.', 1303669848, $e);
|
||||
}
|
||||
}
|
||||
if (!is_writable($finalCacheDirectory)) {
|
||||
throw new Exception('The directory "' . $finalCacheDirectory . '" is not writable.', 1203965200);
|
||||
}
|
||||
$tmpFilesCacheDirectory = $finalCacheDirectory . 'tmp/';
|
||||
if (!is_dir($tmpFilesCacheDirectory)) {
|
||||
try {
|
||||
GeneralUtility::mkdir_deep($tmpFilesCacheDirectory);
|
||||
} catch (\RuntimeException $e) {
|
||||
throw new Exception('The temporary cache directory "' . $tmpFilesCacheDirectory . '" can not be created.', 1727176780, $e);
|
||||
}
|
||||
}
|
||||
if (!is_writable($tmpFilesCacheDirectory)) {
|
||||
throw new Exception('The temporary cache directory "' . $tmpFilesCacheDirectory . '" is not writable.', 1727176781);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the directory where the cache files are stored
|
||||
*
|
||||
* @return string Full path of the cache directory
|
||||
* @internal Misused in tests
|
||||
* @todo: Fix tests and protect
|
||||
*/
|
||||
public function getCacheDirectory(): string
|
||||
{
|
||||
return $this->cacheDirectory;
|
||||
}
|
||||
|
||||
public function set(string $entryIdentifier, string $data, array $tags = [], ?int $lifetime = null): void
|
||||
{
|
||||
if ($entryIdentifier !== PathUtility::basename($entryIdentifier)) {
|
||||
throw new \InvalidArgumentException('The specified entry identifier must not contain a path segment.', 1334756735);
|
||||
}
|
||||
if ($entryIdentifier === '') {
|
||||
throw new \InvalidArgumentException('The specified entry identifier must not be empty.', 1334756736);
|
||||
}
|
||||
$temporaryCacheEntryPathAndFilename = $this->cacheDirectory . 'tmp/' . StringUtility::getUniqueId() . '.temp';
|
||||
$result = GeneralUtility::writeFile($temporaryCacheEntryPathAndFilename, $data, true);
|
||||
if ($result === false) {
|
||||
throw new Exception('The temporary cache file "' . $temporaryCacheEntryPathAndFilename . '" could not be written.', 1334756737);
|
||||
}
|
||||
$cacheEntryPathAndFilename = $this->cacheDirectory . $entryIdentifier . $this->cacheEntryFileExtension;
|
||||
$result = @rename($temporaryCacheEntryPathAndFilename, $cacheEntryPathAndFilename);
|
||||
if ($result === false) {
|
||||
throw new Exception('The cache file "' . $cacheEntryPathAndFilename . '" could not be written.', 1727178709);
|
||||
}
|
||||
if ($this->cacheEntryFileExtension === '.php') {
|
||||
GeneralUtility::makeInstance(OpcodeCacheService::class)->clearAllActive($cacheEntryPathAndFilename);
|
||||
}
|
||||
}
|
||||
|
||||
public function get(string $entryIdentifier): false|string
|
||||
{
|
||||
if ($entryIdentifier !== PathUtility::basename($entryIdentifier)) {
|
||||
throw new \InvalidArgumentException('The specified entry identifier must not contain a path segment.', 1334756877);
|
||||
}
|
||||
$pathAndFilename = $this->cacheDirectory . $entryIdentifier . $this->cacheEntryFileExtension;
|
||||
if (!file_exists($pathAndFilename)) {
|
||||
return false;
|
||||
}
|
||||
return file_get_contents($pathAndFilename);
|
||||
}
|
||||
|
||||
public function has(string $entryIdentifier): bool
|
||||
{
|
||||
if ($entryIdentifier !== PathUtility::basename($entryIdentifier)) {
|
||||
throw new \InvalidArgumentException('The specified entry identifier must not contain a path segment.', 1334756878);
|
||||
}
|
||||
return file_exists($this->cacheDirectory . $entryIdentifier . $this->cacheEntryFileExtension);
|
||||
}
|
||||
|
||||
public function remove(string $entryIdentifier): bool
|
||||
{
|
||||
if ($entryIdentifier !== PathUtility::basename($entryIdentifier)) {
|
||||
throw new \InvalidArgumentException('The specified entry identifier must not contain a path segment.', 1334756960);
|
||||
}
|
||||
if ($entryIdentifier === '') {
|
||||
throw new \InvalidArgumentException('The specified entry identifier must not be empty.', 1334756961);
|
||||
}
|
||||
$file = $this->cacheDirectory . $entryIdentifier . $this->cacheEntryFileExtension;
|
||||
return @unlink($file);
|
||||
}
|
||||
|
||||
public function flush(): void
|
||||
{
|
||||
$directoryIterator = new \DirectoryIterator($this->cacheDirectory);
|
||||
foreach ($directoryIterator as $fileInfo) {
|
||||
if (!$fileInfo->isFile()) {
|
||||
continue;
|
||||
}
|
||||
if (@unlink($this->cacheDirectory . $fileInfo->getFilename())) {
|
||||
continue;
|
||||
}
|
||||
$this->logger->error('Failed to unlink cache entry: {filename}', [
|
||||
'filename' => $this->cacheDirectory . $fileInfo->getFilename(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
protected function isCacheFileExpired(string $cacheEntryPathAndFilename): bool
|
||||
{
|
||||
return file_exists($cacheEntryPathAndFilename) === false;
|
||||
}
|
||||
|
||||
/**
|
||||
* No-op
|
||||
*/
|
||||
public function collectGarbage(): void {}
|
||||
|
||||
public function requireOnce(string $entryIdentifier): mixed
|
||||
{
|
||||
$pathAndFilename = $this->cacheDirectory . $entryIdentifier . $this->cacheEntryFileExtension;
|
||||
if ($entryIdentifier !== PathUtility::basename($entryIdentifier)) {
|
||||
throw new \InvalidArgumentException('The specified entry identifier must not contain a path segment.', 1282073037);
|
||||
}
|
||||
return file_exists($pathAndFilename) ? require_once $pathAndFilename : false;
|
||||
}
|
||||
|
||||
public function require(string $entryIdentifier): mixed
|
||||
{
|
||||
$pathAndFilename = $this->cacheDirectory . $entryIdentifier . $this->cacheEntryFileExtension;
|
||||
if ($entryIdentifier !== PathUtility::basename($entryIdentifier)) {
|
||||
throw new \InvalidArgumentException('The specified entry identifier must not contain a path segment.', 1532528267);
|
||||
}
|
||||
return file_exists($pathAndFilename) ? require $pathAndFilename : false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<?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;
|
||||
|
||||
/**
|
||||
* A contract for a cache backend which supports tagging.
|
||||
*/
|
||||
interface TaggableBackendInterface extends BackendInterface
|
||||
{
|
||||
/**
|
||||
* Removes all cache entries of this cache which are tagged by the specified tag.
|
||||
*
|
||||
* @param string $tag The tag the entries must have
|
||||
*/
|
||||
public function flushByTag(string $tag): void;
|
||||
|
||||
/**
|
||||
* Removes all cache entries of this cache which are tagged by any of the specified tags.
|
||||
*
|
||||
* @param string[] $tags List of tags
|
||||
*/
|
||||
public function flushByTags(array $tags): void;
|
||||
|
||||
/**
|
||||
* Finds and returns all cache entry identifiers which are tagged by the
|
||||
* specified tag
|
||||
*
|
||||
* @param string $tag The tag to search for
|
||||
* @return array An array with identifiers of all matching entries. An empty array if no entries matched
|
||||
*/
|
||||
public function findIdentifiersByTag(string $tag): array;
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
<?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\Exception;
|
||||
use TYPO3\CMS\Core\Cache\Exception\InvalidDataException;
|
||||
|
||||
/**
|
||||
* A contract for a cache backends which store variables in volatile
|
||||
* memory and as such support receiving any variable type to store.
|
||||
*
|
||||
* Note: respect for this contract is up to each individual frontend.
|
||||
* The contract can be respected for a small performance boost, but
|
||||
* the result is marginal except for cases with huge serialized
|
||||
* data sets.
|
||||
*
|
||||
* Respected by the VariableFrontend which checks if the backend
|
||||
* has this interface, in which case it allows the backend to store
|
||||
* the value directly without serializing it to a string, and does
|
||||
* not attempt to unserialize the string on every get() request.
|
||||
*/
|
||||
interface TransientBackendInterface extends BackendInterface
|
||||
{
|
||||
/**
|
||||
* Saves data in the cache.
|
||||
*
|
||||
* @param string $entryIdentifier An identifier for this specific cache entry
|
||||
* @param mixed $data The data to be stored
|
||||
* @param array $tags Tags to associate with this cache entry. If the backend does not support tags, this option can be ignored.
|
||||
* @param int|null $lifetime Lifetime of this cache entry in seconds. If NULL is specified, the default lifetime is used. "0" means unlimited lifetime.
|
||||
* @throws Exception if no cache frontend has been set.
|
||||
* @throws InvalidDataException if the data is not a string
|
||||
*/
|
||||
public function set(string $entryIdentifier, mixed $data, array $tags = [], ?int $lifetime = null): void;
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
<?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;
|
||||
|
||||
/**
|
||||
* A caching backend which stores cache entries during one script run.
|
||||
*/
|
||||
class TransientMemoryBackend extends AbstractBackend implements TaggableBackendInterface, TransientBackendInterface
|
||||
{
|
||||
protected array $entries = [];
|
||||
|
||||
protected array $tagsAndEntries = [];
|
||||
|
||||
/**
|
||||
* @param mixed $data The data to be stored. mixed is allowed due to TransientBackendInterface
|
||||
*/
|
||||
public function set(string $entryIdentifier, mixed $data, array $tags = [], $lifetime = null): void
|
||||
{
|
||||
$this->entries[$entryIdentifier] = $data;
|
||||
foreach ($tags as $tag) {
|
||||
$this->tagsAndEntries[$tag][$entryIdentifier] = true;
|
||||
}
|
||||
}
|
||||
|
||||
public function get(string $entryIdentifier): mixed
|
||||
{
|
||||
return $this->entries[$entryIdentifier] ?? false;
|
||||
}
|
||||
|
||||
public function has(string $entryIdentifier): bool
|
||||
{
|
||||
return isset($this->entries[$entryIdentifier]);
|
||||
}
|
||||
|
||||
public function remove(string $entryIdentifier): bool
|
||||
{
|
||||
if (isset($this->entries[$entryIdentifier])) {
|
||||
unset($this->entries[$entryIdentifier]);
|
||||
foreach (array_keys($this->tagsAndEntries) as $tag) {
|
||||
if (isset($this->tagsAndEntries[$tag][$entryIdentifier])) {
|
||||
unset($this->tagsAndEntries[$tag][$entryIdentifier]);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public function findIdentifiersByTag(string $tag): array
|
||||
{
|
||||
if (isset($this->tagsAndEntries[$tag])) {
|
||||
return array_keys($this->tagsAndEntries[$tag]);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
public function flush(): void
|
||||
{
|
||||
$this->entries = [];
|
||||
$this->tagsAndEntries = [];
|
||||
}
|
||||
|
||||
public function flushByTag(string $tag): void
|
||||
{
|
||||
$identifiers = $this->findIdentifiersByTag($tag);
|
||||
foreach ($identifiers as $identifier) {
|
||||
$this->remove($identifier);
|
||||
}
|
||||
}
|
||||
|
||||
public function flushByTags(array $tags): void
|
||||
{
|
||||
array_walk($tags, $this->flushByTag(...));
|
||||
}
|
||||
|
||||
/**
|
||||
* No-op
|
||||
*/
|
||||
public function collectGarbage(): void {}
|
||||
}
|
||||
@@ -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