N°6934 - Symfony 6.4 - upgrade Symfony bundles to 6.4 (#580)

* Update Symfony lib to version ~6.4.0
* Update code missing return type
* Add an iTop general configuration entry to store application secret (Symfony mandatory parameter)
* Use dependency injection in ExceptionListener & UserProvider classes
This commit is contained in:
bdalsass
2023-12-05 13:56:56 +01:00
committed by GitHub
parent 863ab4560c
commit 27ce51ab07
1392 changed files with 44869 additions and 27799 deletions

View File

@@ -33,8 +33,7 @@ abstract class AbstractAdapter implements AdapterInterface, CacheInterface, Logg
*/
protected const NS_SEPARATOR = ':';
private static $apcuSupported;
private static $phpFilesSupported;
private static bool $apcuSupported;
protected function __construct(string $namespace = '', int $defaultLifetime = 0)
{
@@ -43,28 +42,20 @@ abstract class AbstractAdapter implements AdapterInterface, CacheInterface, Logg
if (null !== $this->maxIdLength && \strlen($namespace) > $this->maxIdLength - 24) {
throw new InvalidArgumentException(sprintf('Namespace must be %d chars max, %d given ("%s").', $this->maxIdLength - 24, \strlen($namespace), $namespace));
}
self::$createCacheItem ?? self::$createCacheItem = \Closure::bind(
self::$createCacheItem ??= \Closure::bind(
static function ($key, $value, $isHit) {
$item = new CacheItem();
$item->key = $key;
$item->value = $v = $value;
$item->value = $value;
$item->isHit = $isHit;
// Detect wrapped values that encode for their expiry and creation duration
// For compactness, these values are packed in the key of an array using
// magic numbers in the form 9D-..-..-..-..-00-..-..-..-5F
if (\is_array($v) && 1 === \count($v) && 10 === \strlen($k = (string) array_key_first($v)) && "\x9D" === $k[0] && "\0" === $k[5] && "\x5F" === $k[9]) {
$item->value = $v[$k];
$v = unpack('Ve/Nc', substr($k, 1, -1));
$item->metadata[CacheItem::METADATA_EXPIRY] = $v['e'] + CacheItem::METADATA_EXPIRY_OFFSET;
$item->metadata[CacheItem::METADATA_CTIME] = $v['c'];
}
$item->unpack();
return $item;
},
null,
CacheItem::class
);
self::$mergeByLifetime ?? self::$mergeByLifetime = \Closure::bind(
self::$mergeByLifetime ??= \Closure::bind(
static function ($deferred, $namespace, &$expiredIds, $getId, $defaultLifetime) {
$byLifetime = [];
$now = microtime(true);
@@ -80,11 +71,7 @@ abstract class AbstractAdapter implements AdapterInterface, CacheInterface, Logg
$expiredIds[] = $getId($key);
continue;
}
if (isset(($metadata = $item->newMetadata)[CacheItem::METADATA_TAGS])) {
unset($metadata[CacheItem::METADATA_TAGS]);
}
// For compactness, expiry and creation duration are packed in the key of an array, using magic numbers as separators
$byLifetime[$ttl][$getId($key)] = $metadata ? ["\x9D".pack('VN', (int) (0.1 + $metadata[self::METADATA_EXPIRY] - self::METADATA_EXPIRY_OFFSET), $metadata[self::METADATA_CTIME])."\x5F" => $item->value] : $item->value;
$byLifetime[$ttl][$getId($key)] = $item->pack();
}
return $byLifetime;
@@ -98,21 +85,19 @@ abstract class AbstractAdapter implements AdapterInterface, CacheInterface, Logg
* Returns the best possible adapter that your runtime supports.
*
* Using ApcuAdapter makes system caches compatible with read-only filesystems.
*
* @return AdapterInterface
*/
public static function createSystemCache(string $namespace, int $defaultLifetime, string $version, string $directory, LoggerInterface $logger = null)
public static function createSystemCache(string $namespace, int $defaultLifetime, string $version, string $directory, LoggerInterface $logger = null): AdapterInterface
{
$opcache = new PhpFilesAdapter($namespace, $defaultLifetime, $directory, true);
if (null !== $logger) {
$opcache->setLogger($logger);
}
if (!self::$apcuSupported = self::$apcuSupported ?? ApcuAdapter::isSupported()) {
if (!self::$apcuSupported ??= ApcuAdapter::isSupported()) {
return $opcache;
}
if (\in_array(\PHP_SAPI, ['cli', 'phpdbg'], true) && !filter_var(\ini_get('apc.enable_cli'), \FILTER_VALIDATE_BOOLEAN)) {
if ('cli' === \PHP_SAPI && !filter_var(\ini_get('apc.enable_cli'), \FILTER_VALIDATE_BOOL)) {
return $opcache;
}
@@ -124,7 +109,7 @@ abstract class AbstractAdapter implements AdapterInterface, CacheInterface, Logg
return new ChainAdapter([$apcu, $opcache]);
}
public static function createConnection(string $dsn, array $options = [])
public static function createConnection(#[\SensitiveParameter] string $dsn, array $options = []): mixed
{
if (str_starts_with($dsn, 'redis:') || str_starts_with($dsn, 'rediss:')) {
return RedisAdapter::createConnection($dsn, $options);
@@ -132,7 +117,7 @@ abstract class AbstractAdapter implements AdapterInterface, CacheInterface, Logg
if (str_starts_with($dsn, 'memcached:')) {
return MemcachedAdapter::createConnection($dsn, $options);
}
if (0 === strpos($dsn, 'couchbase:')) {
if (str_starts_with($dsn, 'couchbase:')) {
if (CouchbaseBucketAdapter::isSupported()) {
return CouchbaseBucketAdapter::createConnection($dsn, $options);
}
@@ -140,18 +125,13 @@ abstract class AbstractAdapter implements AdapterInterface, CacheInterface, Logg
return CouchbaseCollectionAdapter::createConnection($dsn, $options);
}
throw new InvalidArgumentException(sprintf('Unsupported DSN: "%s".', $dsn));
throw new InvalidArgumentException('Unsupported DSN: it does not start with "redis[s]:", "memcached:" nor "couchbase:".');
}
/**
* {@inheritdoc}
*
* @return bool
*/
public function commit()
public function commit(): bool
{
$ok = true;
$byLifetime = (self::$mergeByLifetime)($this->deferred, $this->namespace, $expiredIds, \Closure::fromCallable([$this, 'getId']), $this->defaultLifetime);
$byLifetime = (self::$mergeByLifetime)($this->deferred, $this->namespace, $expiredIds, $this->getId(...), $this->defaultLifetime);
$retry = $this->deferred = [];
if ($expiredIds) {

View File

@@ -35,7 +35,7 @@ abstract class AbstractTagAwareAdapter implements TagAwareAdapterInterface, TagA
use AbstractAdapterTrait;
use ContractsTrait;
private const TAGS_PREFIX = "\0tags\0";
private const TAGS_PREFIX = "\1tags\1";
protected function __construct(string $namespace = '', int $defaultLifetime = 0)
{
@@ -44,7 +44,7 @@ abstract class AbstractTagAwareAdapter implements TagAwareAdapterInterface, TagA
if (null !== $this->maxIdLength && \strlen($namespace) > $this->maxIdLength - 24) {
throw new InvalidArgumentException(sprintf('Namespace must be %d chars max, %d given ("%s").', $this->maxIdLength - 24, \strlen($namespace), $namespace));
}
self::$createCacheItem ?? self::$createCacheItem = \Closure::bind(
self::$createCacheItem ??= \Closure::bind(
static function ($key, $value, $isHit) {
$item = new CacheItem();
$item->key = $key;
@@ -56,7 +56,7 @@ abstract class AbstractTagAwareAdapter implements TagAwareAdapterInterface, TagA
$item->isHit = $isHit;
// Extract value, tags and meta data from the cache value
$item->value = $value['value'];
$item->metadata[CacheItem::METADATA_TAGS] = $value['tags'] ?? [];
$item->metadata[CacheItem::METADATA_TAGS] = isset($value['tags']) ? array_combine($value['tags'], $value['tags']) : [];
if (isset($value['meta'])) {
// For compactness these values are packed, & expiry is offset to reduce size
$v = unpack('Ve/Nc', $value['meta']);
@@ -69,7 +69,7 @@ abstract class AbstractTagAwareAdapter implements TagAwareAdapterInterface, TagA
null,
CacheItem::class
);
self::$mergeByLifetime ?? self::$mergeByLifetime = \Closure::bind(
self::$mergeByLifetime ??= \Closure::bind(
static function ($deferred, &$expiredIds, $getId, $tagPrefix, $defaultLifetime) {
$byLifetime = [];
$now = microtime(true);
@@ -95,18 +95,19 @@ abstract class AbstractTagAwareAdapter implements TagAwareAdapterInterface, TagA
if ($metadata) {
// For compactness, expiry and creation duration are packed, using magic numbers as separators
$value['meta'] = pack('VN', (int) (0.1 + $metadata[self::METADATA_EXPIRY] - self::METADATA_EXPIRY_OFFSET), $metadata[self::METADATA_CTIME]);
$value['meta'] = pack('VN', (int) (0.1 + $metadata[CacheItem::METADATA_EXPIRY] - CacheItem::METADATA_EXPIRY_OFFSET), $metadata[CacheItem::METADATA_CTIME]);
}
// Extract tag changes, these should be removed from values in doSave()
$value['tag-operations'] = ['add' => [], 'remove' => []];
$oldTags = $item->metadata[CacheItem::METADATA_TAGS] ?? [];
foreach (array_diff($value['tags'], $oldTags) as $addedTag) {
foreach (array_diff_key($value['tags'], $oldTags) as $addedTag) {
$value['tag-operations']['add'][] = $getId($tagPrefix.$addedTag);
}
foreach (array_diff($oldTags, $value['tags']) as $removedTag) {
foreach (array_diff_key($oldTags, $value['tags']) as $removedTag) {
$value['tag-operations']['remove'][] = $getId($tagPrefix.$removedTag);
}
$value['tags'] = array_keys($value['tags']);
$byLifetime[$ttl][$getId($key)] = $value;
$item->metadata = $item->newMetadata;
@@ -135,10 +136,8 @@ abstract class AbstractTagAwareAdapter implements TagAwareAdapterInterface, TagA
* Removes multiple items from the pool and their corresponding tags.
*
* @param array $ids An array of identifiers that should be removed from the pool
*
* @return bool
*/
abstract protected function doDelete(array $ids);
abstract protected function doDelete(array $ids): bool;
/**
* Removes relations between tags and deleted items.
@@ -166,13 +165,10 @@ abstract class AbstractTagAwareAdapter implements TagAwareAdapterInterface, TagA
$this->doDelete($ids);
}
/**
* {@inheritdoc}
*/
public function commit(): bool
{
$ok = true;
$byLifetime = (self::$mergeByLifetime)($this->deferred, $expiredIds, \Closure::fromCallable([$this, 'getId']), self::TAGS_PREFIX, $this->defaultLifetime);
$byLifetime = (self::$mergeByLifetime)($this->deferred, $expiredIds, $this->getId(...), self::TAGS_PREFIX, $this->defaultLifetime);
$retry = $this->deferred = [];
if ($expiredIds) {
@@ -230,9 +226,6 @@ abstract class AbstractTagAwareAdapter implements TagAwareAdapterInterface, TagA
return $ok;
}
/**
* {@inheritdoc}
*/
public function deleteItems(array $keys): bool
{
if (!$keys) {
@@ -254,7 +247,7 @@ abstract class AbstractTagAwareAdapter implements TagAwareAdapterInterface, TagA
$tagData[$this->getId(self::TAGS_PREFIX.$tag)][] = $id;
}
}
} catch (\Exception $e) {
} catch (\Exception) {
$ok = false;
}
@@ -262,7 +255,7 @@ abstract class AbstractTagAwareAdapter implements TagAwareAdapterInterface, TagA
if ((!$tagData || $this->doDeleteTagRelations($tagData)) && $ok) {
return true;
}
} catch (\Exception $e) {
} catch (\Exception) {
}
// When bulk-delete failed, retry each item individually
@@ -282,12 +275,9 @@ abstract class AbstractTagAwareAdapter implements TagAwareAdapterInterface, TagA
return $ok;
}
/**
* {@inheritdoc}
*/
public function invalidateTags(array $tags)
public function invalidateTags(array $tags): bool
{
if (empty($tags)) {
if (!$tags) {
return false;
}

View File

@@ -24,24 +24,12 @@ class_exists(CacheItem::class);
*/
interface AdapterInterface extends CacheItemPoolInterface
{
/**
* {@inheritdoc}
*
* @return CacheItem
*/
public function getItem($key);
public function getItem(mixed $key): CacheItem;
/**
* {@inheritdoc}
*
* @return \Traversable<string, CacheItem>
* @return iterable<string, CacheItem>
*/
public function getItems(array $keys = []);
public function getItems(array $keys = []): iterable;
/**
* {@inheritdoc}
*
* @return bool
*/
public function clear(string $prefix = '');
public function clear(string $prefix = ''): bool;
}

View File

@@ -20,7 +20,7 @@ use Symfony\Component\Cache\Marshaller\MarshallerInterface;
*/
class ApcuAdapter extends AbstractAdapter
{
private $marshaller;
private ?MarshallerInterface $marshaller;
/**
* @throws CacheException if APCu is not enabled
@@ -47,27 +47,20 @@ class ApcuAdapter extends AbstractAdapter
$this->marshaller = $marshaller;
}
/**
* @return bool
*/
public static function isSupported()
{
return \function_exists('apcu_fetch') && filter_var(\ini_get('apc.enabled'), \FILTER_VALIDATE_BOOLEAN);
return \function_exists('apcu_fetch') && filter_var(\ini_get('apc.enabled'), \FILTER_VALIDATE_BOOL);
}
/**
* {@inheritdoc}
*/
protected function doFetch(array $ids)
protected function doFetch(array $ids): iterable
{
$unserializeCallbackHandler = ini_set('unserialize_callback_func', __CLASS__.'::handleUnserializeCallback');
try {
$values = [];
$ids = array_flip($ids);
foreach (apcu_fetch(array_keys($ids), $ok) ?: [] as $k => $v) {
if (!isset($ids[$k])) {
// work around https://github.com/krakjoe/apcu/issues/247
$k = key($ids);
}
unset($ids[$k]);
foreach (apcu_fetch($ids, $ok) ?: [] as $k => $v) {
if (null !== $v || $ok) {
$values[$k] = null !== $this->marshaller ? $this->marshaller->unmarshall($v) : $v;
}
@@ -81,28 +74,19 @@ class ApcuAdapter extends AbstractAdapter
}
}
/**
* {@inheritdoc}
*/
protected function doHave(string $id)
protected function doHave(string $id): bool
{
return apcu_exists($id);
}
/**
* {@inheritdoc}
*/
protected function doClear(string $namespace)
protected function doClear(string $namespace): bool
{
return isset($namespace[0]) && class_exists(\APCUIterator::class, false) && ('cli' !== \PHP_SAPI || filter_var(\ini_get('apc.enable_cli'), \FILTER_VALIDATE_BOOLEAN))
return isset($namespace[0]) && class_exists(\APCUIterator::class, false) && ('cli' !== \PHP_SAPI || filter_var(\ini_get('apc.enable_cli'), \FILTER_VALIDATE_BOOL))
? apcu_delete(new \APCUIterator(sprintf('/^%s/', preg_quote($namespace, '/')), \APC_ITER_KEY))
: apcu_clear_cache();
}
/**
* {@inheritdoc}
*/
protected function doDelete(array $ids)
protected function doDelete(array $ids): bool
{
foreach ($ids as $id) {
apcu_delete($id);
@@ -111,10 +95,7 @@ class ApcuAdapter extends AbstractAdapter
return true;
}
/**
* {@inheritdoc}
*/
protected function doSave(array $values, int $lifetime)
protected function doSave(array $values, int $lifetime): array|bool
{
if (null !== $this->marshaller && (!$values = $this->marshaller->marshall($values, $failed))) {
return $failed;

View File

@@ -30,14 +30,15 @@ class ArrayAdapter implements AdapterInterface, CacheInterface, LoggerAwareInter
{
use LoggerAwareTrait;
private $storeSerialized;
private $values = [];
private $expiries = [];
private $defaultLifetime;
private $maxLifetime;
private $maxItems;
private bool $storeSerialized;
private array $values = [];
private array $tags = [];
private array $expiries = [];
private int $defaultLifetime;
private float $maxLifetime;
private int $maxItems;
private static $createCacheItem;
private static \Closure $createCacheItem;
/**
* @param bool $storeSerialized Disabling serialization can lead to cache corruptions when storing mutable values but increases performance otherwise
@@ -56,12 +57,15 @@ class ArrayAdapter implements AdapterInterface, CacheInterface, LoggerAwareInter
$this->storeSerialized = $storeSerialized;
$this->maxLifetime = $maxLifetime;
$this->maxItems = $maxItems;
self::$createCacheItem ?? self::$createCacheItem = \Closure::bind(
static function ($key, $value, $isHit) {
self::$createCacheItem ??= \Closure::bind(
static function ($key, $value, $isHit, $tags) {
$item = new CacheItem();
$item->key = $key;
$item->value = $value;
$item->isHit = $isHit;
if (null !== $tags) {
$item->metadata[CacheItem::METADATA_TAGS] = $tags;
}
return $item;
},
@@ -70,10 +74,7 @@ class ArrayAdapter implements AdapterInterface, CacheInterface, LoggerAwareInter
);
}
/**
* {@inheritdoc}
*/
public function get(string $key, callable $callback, float $beta = null, array &$metadata = null)
public function get(string $key, callable $callback, float $beta = null, array &$metadata = null): mixed
{
$item = $this->getItem($key);
$metadata = $item->getMetadata();
@@ -90,20 +91,12 @@ class ArrayAdapter implements AdapterInterface, CacheInterface, LoggerAwareInter
return $item->get();
}
/**
* {@inheritdoc}
*/
public function delete(string $key): bool
{
return $this->deleteItem($key);
}
/**
* {@inheritdoc}
*
* @return bool
*/
public function hasItem($key)
public function hasItem(mixed $key): bool
{
if (\is_string($key) && isset($this->expiries[$key]) && $this->expiries[$key] > microtime(true)) {
if ($this->maxItems) {
@@ -120,10 +113,7 @@ class ArrayAdapter implements AdapterInterface, CacheInterface, LoggerAwareInter
return isset($this->expiries[$key]) && !$this->deleteItem($key);
}
/**
* {@inheritdoc}
*/
public function getItem($key)
public function getItem(mixed $key): CacheItem
{
if (!$isHit = $this->hasItem($key)) {
$value = null;
@@ -136,38 +126,25 @@ class ArrayAdapter implements AdapterInterface, CacheInterface, LoggerAwareInter
$value = $this->storeSerialized ? $this->unfreeze($key, $isHit) : $this->values[$key];
}
return (self::$createCacheItem)($key, $value, $isHit);
return (self::$createCacheItem)($key, $value, $isHit, $this->tags[$key] ?? null);
}
/**
* {@inheritdoc}
*/
public function getItems(array $keys = [])
public function getItems(array $keys = []): iterable
{
\assert(self::validateKeys($keys));
return $this->generateItems($keys, microtime(true), self::$createCacheItem);
}
/**
* {@inheritdoc}
*
* @return bool
*/
public function deleteItem($key)
public function deleteItem(mixed $key): bool
{
\assert('' !== CacheItem::validateKey($key));
unset($this->values[$key], $this->expiries[$key]);
unset($this->values[$key], $this->tags[$key], $this->expiries[$key]);
return true;
}
/**
* {@inheritdoc}
*
* @return bool
*/
public function deleteItems(array $keys)
public function deleteItems(array $keys): bool
{
foreach ($keys as $key) {
$this->deleteItem($key);
@@ -176,12 +153,7 @@ class ArrayAdapter implements AdapterInterface, CacheInterface, LoggerAwareInter
return true;
}
/**
* {@inheritdoc}
*
* @return bool
*/
public function save(CacheItemInterface $item)
public function save(CacheItemInterface $item): bool
{
if (!$item instanceof CacheItem) {
return false;
@@ -213,7 +185,7 @@ class ArrayAdapter implements AdapterInterface, CacheInterface, LoggerAwareInter
}
if ($this->maxItems) {
unset($this->values[$key]);
unset($this->values[$key], $this->tags[$key]);
// Iterate items and vacuum expired ones while we are at it
foreach ($this->values as $k => $v) {
@@ -221,49 +193,38 @@ class ArrayAdapter implements AdapterInterface, CacheInterface, LoggerAwareInter
break;
}
unset($this->values[$k], $this->expiries[$k]);
unset($this->values[$k], $this->tags[$k], $this->expiries[$k]);
}
}
$this->values[$key] = $value;
$this->expiries[$key] = $expiry ?? \PHP_INT_MAX;
if (null === $this->tags[$key] = $item["\0*\0newMetadata"][CacheItem::METADATA_TAGS] ?? null) {
unset($this->tags[$key]);
}
return true;
}
/**
* {@inheritdoc}
*
* @return bool
*/
public function saveDeferred(CacheItemInterface $item)
public function saveDeferred(CacheItemInterface $item): bool
{
return $this->save($item);
}
/**
* {@inheritdoc}
*
* @return bool
*/
public function commit()
public function commit(): bool
{
return true;
}
/**
* {@inheritdoc}
*
* @return bool
*/
public function clear(string $prefix = '')
public function clear(string $prefix = ''): bool
{
if ('' !== $prefix) {
$now = microtime(true);
foreach ($this->values as $key => $value) {
if (!isset($this->expiries[$key]) || $this->expiries[$key] <= $now || 0 === strpos($key, $prefix)) {
unset($this->values[$key], $this->expiries[$key]);
if (!isset($this->expiries[$key]) || $this->expiries[$key] <= $now || str_starts_with($key, $prefix)) {
unset($this->values[$key], $this->tags[$key], $this->expiries[$key]);
}
}
@@ -272,17 +233,15 @@ class ArrayAdapter implements AdapterInterface, CacheInterface, LoggerAwareInter
}
}
$this->values = $this->expiries = [];
$this->values = $this->tags = $this->expiries = [];
return true;
}
/**
* Returns all cached values, with cache miss as null.
*
* @return array
*/
public function getValues()
public function getValues(): array
{
if (!$this->storeSerialized) {
return $this->values;
@@ -302,7 +261,7 @@ class ArrayAdapter implements AdapterInterface, CacheInterface, LoggerAwareInter
}
/**
* {@inheritdoc}
* @return void
*/
public function reset()
{
@@ -331,7 +290,7 @@ class ArrayAdapter implements AdapterInterface, CacheInterface, LoggerAwareInter
}
unset($keys[$i]);
yield $key => $f($key, $value, $isHit);
yield $key => $f($key, $value, $isHit, $this->tags[$key] ?? null);
}
foreach ($keys as $key) {
@@ -339,7 +298,7 @@ class ArrayAdapter implements AdapterInterface, CacheInterface, LoggerAwareInter
}
}
private function freeze($value, string $key)
private function freeze($value, string $key): string|int|float|bool|array|\UnitEnum|null
{
if (null === $value) {
return 'N;';
@@ -353,12 +312,12 @@ class ArrayAdapter implements AdapterInterface, CacheInterface, LoggerAwareInter
try {
$serialized = serialize($value);
} catch (\Exception $e) {
unset($this->values[$key]);
unset($this->values[$key], $this->tags[$key]);
$type = get_debug_type($value);
$message = sprintf('Failed to save key "{key}" of type %s: %s', $type, $e->getMessage());
CacheItem::log($this->logger, $message, ['key' => $key, 'exception' => $e, 'cache-adapter' => get_debug_type($this)]);
return;
return null;
}
// Keep value serialized if it contains any objects or any internal references
if ('C' === $serialized[0] || 'O' === $serialized[0] || preg_match('/;[OCRr]:[1-9]/', $serialized)) {
@@ -369,7 +328,7 @@ class ArrayAdapter implements AdapterInterface, CacheInterface, LoggerAwareInter
return $value;
}
private function unfreeze(string $key, bool &$isHit)
private function unfreeze(string $key, bool &$isHit): mixed
{
if ('N;' === $value = $this->values[$key]) {
return null;

View File

@@ -33,11 +33,11 @@ class ChainAdapter implements AdapterInterface, CacheInterface, PruneableInterfa
{
use ContractsTrait;
private $adapters = [];
private $adapterCount;
private $defaultLifetime;
private array $adapters = [];
private int $adapterCount;
private int $defaultLifetime;
private static $syncItem;
private static \Closure $syncItem;
/**
* @param CacheItemPoolInterface[] $adapters The ordered list of adapters used to fetch cached items
@@ -53,7 +53,7 @@ class ChainAdapter implements AdapterInterface, CacheInterface, PruneableInterfa
if (!$adapter instanceof CacheItemPoolInterface) {
throw new InvalidArgumentException(sprintf('The class "%s" does not implement the "%s" interface.', get_debug_type($adapter), CacheItemPoolInterface::class));
}
if (\in_array(\PHP_SAPI, ['cli', 'phpdbg'], true) && $adapter instanceof ApcuAdapter && !filter_var(\ini_get('apc.enable_cli'), \FILTER_VALIDATE_BOOLEAN)) {
if ('cli' === \PHP_SAPI && $adapter instanceof ApcuAdapter && !filter_var(\ini_get('apc.enable_cli'), \FILTER_VALIDATE_BOOL)) {
continue; // skip putting APCu in the chain when the backend is disabled
}
@@ -66,18 +66,17 @@ class ChainAdapter implements AdapterInterface, CacheInterface, PruneableInterfa
$this->adapterCount = \count($this->adapters);
$this->defaultLifetime = $defaultLifetime;
self::$syncItem ?? self::$syncItem = \Closure::bind(
self::$syncItem ??= \Closure::bind(
static function ($sourceItem, $item, $defaultLifetime, $sourceMetadata = null) {
$sourceItem->isTaggable = false;
$sourceMetadata = $sourceMetadata ?? $sourceItem->metadata;
unset($sourceMetadata[CacheItem::METADATA_TAGS]);
$sourceMetadata ??= $sourceItem->metadata;
$item->value = $sourceItem->value;
$item->isHit = $sourceItem->isHit;
$item->metadata = $item->newMetadata = $sourceItem->metadata = $sourceMetadata;
if (isset($item->metadata[CacheItem::METADATA_EXPIRY])) {
$item->expiresAt(\DateTime::createFromFormat('U.u', sprintf('%.6F', $item->metadata[CacheItem::METADATA_EXPIRY])));
$item->expiresAt(\DateTimeImmutable::createFromFormat('U.u', sprintf('%.6F', $item->metadata[CacheItem::METADATA_EXPIRY])));
} elseif (0 < $defaultLifetime) {
$item->expiresAfter($defaultLifetime);
}
@@ -89,10 +88,7 @@ class ChainAdapter implements AdapterInterface, CacheInterface, PruneableInterfa
);
}
/**
* {@inheritdoc}
*/
public function get(string $key, callable $callback, float $beta = null, array &$metadata = null)
public function get(string $key, callable $callback, float $beta = null, array &$metadata = null): mixed
{
$doSave = true;
$callback = static function (CacheItem $item, bool &$save) use ($callback, &$doSave) {
@@ -102,9 +98,9 @@ class ChainAdapter implements AdapterInterface, CacheInterface, PruneableInterfa
return $value;
};
$lastItem = null;
$i = 0;
$wrap = function (CacheItem $item = null, bool &$save = true) use ($key, $callback, $beta, &$wrap, &$i, &$doSave, &$lastItem, &$metadata) {
$wrap = function (CacheItem $item = null, bool &$save = true) use ($key, $callback, $beta, &$wrap, &$doSave, &$metadata) {
static $lastItem;
static $i = 0;
$adapter = $this->adapters[$i];
if (isset($this->adapters[++$i])) {
$callback = $wrap;
@@ -116,7 +112,7 @@ class ChainAdapter implements AdapterInterface, CacheInterface, PruneableInterfa
$value = $this->doGet($adapter, $key, $callback, $beta, $metadata);
}
if (null !== $item) {
(self::$syncItem)($lastItem = $lastItem ?? $item, $item, $this->defaultLifetime, $metadata);
(self::$syncItem)($lastItem ??= $item, $item, $this->defaultLifetime, $metadata);
}
$save = $doSave;
@@ -126,10 +122,7 @@ class ChainAdapter implements AdapterInterface, CacheInterface, PruneableInterfa
return $wrap();
}
/**
* {@inheritdoc}
*/
public function getItem($key)
public function getItem(mixed $key): CacheItem
{
$syncItem = self::$syncItem;
$misses = [];
@@ -151,10 +144,7 @@ class ChainAdapter implements AdapterInterface, CacheInterface, PruneableInterfa
return $item;
}
/**
* {@inheritdoc}
*/
public function getItems(array $keys = [])
public function getItems(array $keys = []): iterable
{
return $this->generateItems($this->adapters[0]->getItems($keys), 0);
}
@@ -190,12 +180,7 @@ class ChainAdapter implements AdapterInterface, CacheInterface, PruneableInterfa
}
}
/**
* {@inheritdoc}
*
* @return bool
*/
public function hasItem($key)
public function hasItem(mixed $key): bool
{
foreach ($this->adapters as $adapter) {
if ($adapter->hasItem($key)) {
@@ -206,12 +191,7 @@ class ChainAdapter implements AdapterInterface, CacheInterface, PruneableInterfa
return false;
}
/**
* {@inheritdoc}
*
* @return bool
*/
public function clear(string $prefix = '')
public function clear(string $prefix = ''): bool
{
$cleared = true;
$i = $this->adapterCount;
@@ -227,12 +207,7 @@ class ChainAdapter implements AdapterInterface, CacheInterface, PruneableInterfa
return $cleared;
}
/**
* {@inheritdoc}
*
* @return bool
*/
public function deleteItem($key)
public function deleteItem(mixed $key): bool
{
$deleted = true;
$i = $this->adapterCount;
@@ -244,12 +219,7 @@ class ChainAdapter implements AdapterInterface, CacheInterface, PruneableInterfa
return $deleted;
}
/**
* {@inheritdoc}
*
* @return bool
*/
public function deleteItems(array $keys)
public function deleteItems(array $keys): bool
{
$deleted = true;
$i = $this->adapterCount;
@@ -261,12 +231,7 @@ class ChainAdapter implements AdapterInterface, CacheInterface, PruneableInterfa
return $deleted;
}
/**
* {@inheritdoc}
*
* @return bool
*/
public function save(CacheItemInterface $item)
public function save(CacheItemInterface $item): bool
{
$saved = true;
$i = $this->adapterCount;
@@ -278,12 +243,7 @@ class ChainAdapter implements AdapterInterface, CacheInterface, PruneableInterfa
return $saved;
}
/**
* {@inheritdoc}
*
* @return bool
*/
public function saveDeferred(CacheItemInterface $item)
public function saveDeferred(CacheItemInterface $item): bool
{
$saved = true;
$i = $this->adapterCount;
@@ -295,12 +255,7 @@ class ChainAdapter implements AdapterInterface, CacheInterface, PruneableInterfa
return $saved;
}
/**
* {@inheritdoc}
*
* @return bool
*/
public function commit()
public function commit(): bool
{
$committed = true;
$i = $this->adapterCount;
@@ -312,10 +267,7 @@ class ChainAdapter implements AdapterInterface, CacheInterface, PruneableInterfa
return $committed;
}
/**
* {@inheritdoc}
*/
public function prune()
public function prune(): bool
{
$pruned = true;
@@ -329,7 +281,7 @@ class ChainAdapter implements AdapterInterface, CacheInterface, PruneableInterfa
}
/**
* {@inheritdoc}
* @return void
*/
public function reset()
{

View File

@@ -36,8 +36,8 @@ class CouchbaseBucketAdapter extends AbstractAdapter
'durabilityTimeout',
];
private $bucket;
private $marshaller;
private \CouchbaseBucket $bucket;
private MarshallerInterface $marshaller;
public function __construct(\CouchbaseBucket $bucket, string $namespace = '', int $defaultLifetime = 0, MarshallerInterface $marshaller = null)
{
@@ -54,22 +54,17 @@ class CouchbaseBucketAdapter extends AbstractAdapter
$this->marshaller = $marshaller ?? new DefaultMarshaller();
}
/**
* @param array|string $servers
*/
public static function createConnection($servers, array $options = []): \CouchbaseBucket
public static function createConnection(#[\SensitiveParameter] array|string $servers, array $options = []): \CouchbaseBucket
{
if (\is_string($servers)) {
$servers = [$servers];
} elseif (!\is_array($servers)) {
throw new \TypeError(sprintf('Argument 1 passed to "%s()" must be array or string, "%s" given.', __METHOD__, get_debug_type($servers)));
}
if (!static::isSupported()) {
throw new CacheException('Couchbase >= 2.6.0 < 3.0.0 is required.');
}
set_error_handler(function ($type, $msg, $file, $line) { throw new \ErrorException($msg, 0, $type, $file, $line); });
set_error_handler(static fn ($type, $msg, $file, $line) => throw new \ErrorException($msg, 0, $type, $file, $line));
$dsnPattern = '/^(?<protocol>couchbase(?:s)?)\:\/\/(?:(?<username>[^\:]+)\:(?<password>[^\@]{6,})@)?'
.'(?<host>[^\:]+(?:\:\d+)?)(?:\/(?<bucketName>[^\?]+))(?:\?(?<options>.*))?$/i';
@@ -82,8 +77,8 @@ class CouchbaseBucketAdapter extends AbstractAdapter
$password = $options['password'];
foreach ($servers as $dsn) {
if (0 !== strpos($dsn, 'couchbase:')) {
throw new InvalidArgumentException(sprintf('Invalid Couchbase DSN: "%s" does not start with "couchbase:".', $dsn));
if (!str_starts_with($dsn, 'couchbase:')) {
throw new InvalidArgumentException('Invalid Couchbase DSN: it does not start with "couchbase:".');
}
preg_match($dsnPattern, $dsn, $matches);
@@ -146,25 +141,22 @@ class CouchbaseBucketAdapter extends AbstractAdapter
private static function initOptions(array $options): array
{
$options['username'] = $options['username'] ?? '';
$options['password'] = $options['password'] ?? '';
$options['operationTimeout'] = $options['operationTimeout'] ?? 0;
$options['configTimeout'] = $options['configTimeout'] ?? 0;
$options['configNodeTimeout'] = $options['configNodeTimeout'] ?? 0;
$options['n1qlTimeout'] = $options['n1qlTimeout'] ?? 0;
$options['httpTimeout'] = $options['httpTimeout'] ?? 0;
$options['configDelay'] = $options['configDelay'] ?? 0;
$options['htconfigIdleTimeout'] = $options['htconfigIdleTimeout'] ?? 0;
$options['durabilityInterval'] = $options['durabilityInterval'] ?? 0;
$options['durabilityTimeout'] = $options['durabilityTimeout'] ?? 0;
$options['username'] ??= '';
$options['password'] ??= '';
$options['operationTimeout'] ??= 0;
$options['configTimeout'] ??= 0;
$options['configNodeTimeout'] ??= 0;
$options['n1qlTimeout'] ??= 0;
$options['httpTimeout'] ??= 0;
$options['configDelay'] ??= 0;
$options['htconfigIdleTimeout'] ??= 0;
$options['durabilityInterval'] ??= 0;
$options['durabilityTimeout'] ??= 0;
return $options;
}
/**
* {@inheritdoc}
*/
protected function doFetch(array $ids)
protected function doFetch(array $ids): iterable
{
$resultsCouchbase = $this->bucket->get($ids);
@@ -179,17 +171,11 @@ class CouchbaseBucketAdapter extends AbstractAdapter
return $results;
}
/**
* {@inheritdoc}
*/
protected function doHave(string $id): bool
{
return false !== $this->bucket->get($id);
}
/**
* {@inheritdoc}
*/
protected function doClear(string $namespace): bool
{
if ('' === $namespace) {
@@ -201,9 +187,6 @@ class CouchbaseBucketAdapter extends AbstractAdapter
return false;
}
/**
* {@inheritdoc}
*/
protected function doDelete(array $ids): bool
{
$results = $this->bucket->remove(array_values($ids));
@@ -218,10 +201,7 @@ class CouchbaseBucketAdapter extends AbstractAdapter
return 0 === \count($results);
}
/**
* {@inheritdoc}
*/
protected function doSave(array $values, int $lifetime)
protected function doSave(array $values, int $lifetime): array|bool
{
if (!$values = $this->marshaller->marshall($values, $failed)) {
return $failed;

View File

@@ -29,9 +29,8 @@ class CouchbaseCollectionAdapter extends AbstractAdapter
{
private const MAX_KEY_LENGTH = 250;
/** @var Collection */
private $connection;
private $marshaller;
private Collection $connection;
private MarshallerInterface $marshaller;
public function __construct(Collection $connection, string $namespace = '', int $defaultLifetime = 0, MarshallerInterface $marshaller = null)
{
@@ -48,24 +47,17 @@ class CouchbaseCollectionAdapter extends AbstractAdapter
$this->marshaller = $marshaller ?? new DefaultMarshaller();
}
/**
* @param array|string $dsn
*
* @return Bucket|Collection
*/
public static function createConnection($dsn, array $options = [])
public static function createConnection(#[\SensitiveParameter] array|string $dsn, array $options = []): Bucket|Collection
{
if (\is_string($dsn)) {
$dsn = [$dsn];
} elseif (!\is_array($dsn)) {
throw new \TypeError(sprintf('Argument 1 passed to "%s()" must be array or string, "%s" given.', __METHOD__, get_debug_type($dsn)));
}
if (!static::isSupported()) {
throw new CacheException('Couchbase >= 3.0.0 < 4.0.0 is required.');
}
set_error_handler(function ($type, $msg, $file, $line): bool { throw new \ErrorException($msg, 0, $type, $file, $line); });
set_error_handler(static fn ($type, $msg, $file, $line) => throw new \ErrorException($msg, 0, $type, $file, $line));
$dsnPattern = '/^(?<protocol>couchbase(?:s)?)\:\/\/(?:(?<username>[^\:]+)\:(?<password>[^\@]{6,})@)?'
.'(?<host>[^\:]+(?:\:\d+)?)(?:\/(?<bucketName>[^\/\?]+))(?:(?:\/(?<scopeName>[^\/]+))'
@@ -78,8 +70,8 @@ class CouchbaseCollectionAdapter extends AbstractAdapter
$password = $options['password'] ?? '';
foreach ($dsn as $server) {
if (0 !== strpos($server, 'couchbase:')) {
throw new InvalidArgumentException(sprintf('Invalid Couchbase DSN: "%s" does not start with "couchbase:".', $server));
if (!str_starts_with($server, 'couchbase:')) {
throw new InvalidArgumentException('Invalid Couchbase DSN: it does not start with "couchbase:".');
}
preg_match($dsnPattern, $server, $matches);
@@ -139,16 +131,13 @@ class CouchbaseCollectionAdapter extends AbstractAdapter
return $results;
}
/**
* {@inheritdoc}
*/
protected function doFetch(array $ids): array
{
$results = [];
foreach ($ids as $id) {
try {
$resultCouchbase = $this->connection->get($id);
} catch (DocumentNotFoundException $exception) {
} catch (DocumentNotFoundException) {
continue;
}
@@ -160,25 +149,16 @@ class CouchbaseCollectionAdapter extends AbstractAdapter
return $results;
}
/**
* {@inheritdoc}
*/
protected function doHave($id): bool
{
return $this->connection->exists($id)->exists();
}
/**
* {@inheritdoc}
*/
protected function doClear($namespace): bool
{
return false;
}
/**
* {@inheritdoc}
*/
protected function doDelete(array $ids): bool
{
$idsErrors = [];
@@ -189,17 +169,14 @@ class CouchbaseCollectionAdapter extends AbstractAdapter
if (null === $result->mutationToken()) {
$idsErrors[] = $id;
}
} catch (DocumentNotFoundException $exception) {
} catch (DocumentNotFoundException) {
}
}
return 0 === \count($idsErrors);
}
/**
* {@inheritdoc}
*/
protected function doSave(array $values, $lifetime)
protected function doSave(array $values, $lifetime): array|bool
{
if (!$values = $this->marshaller->marshall($values, $failed)) {
return $failed;
@@ -212,7 +189,7 @@ class CouchbaseCollectionAdapter extends AbstractAdapter
foreach ($values as $key => $value) {
try {
$this->connection->upsert($key, $value, $upsertOptions);
} catch (\Exception $exception) {
} catch (\Exception) {
$ko[$key] = '';
}
}

View File

@@ -1,110 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Cache\Adapter;
use Doctrine\Common\Cache\CacheProvider;
use Doctrine\Common\Cache\Psr6\CacheAdapter;
/**
* @author Nicolas Grekas <p@tchwork.com>
*
* @deprecated Since Symfony 5.4, use Doctrine\Common\Cache\Psr6\CacheAdapter instead
*/
class DoctrineAdapter extends AbstractAdapter
{
private $provider;
public function __construct(CacheProvider $provider, string $namespace = '', int $defaultLifetime = 0)
{
trigger_deprecation('symfony/cache', '5.4', '"%s" is deprecated, use "%s" instead.', __CLASS__, CacheAdapter::class);
parent::__construct('', $defaultLifetime);
$this->provider = $provider;
$provider->setNamespace($namespace);
}
/**
* {@inheritdoc}
*/
public function reset()
{
parent::reset();
$this->provider->setNamespace($this->provider->getNamespace());
}
/**
* {@inheritdoc}
*/
protected function doFetch(array $ids)
{
$unserializeCallbackHandler = ini_set('unserialize_callback_func', parent::class.'::handleUnserializeCallback');
try {
return $this->provider->fetchMultiple($ids);
} catch (\Error $e) {
$trace = $e->getTrace();
if (isset($trace[0]['function']) && !isset($trace[0]['class'])) {
switch ($trace[0]['function']) {
case 'unserialize':
case 'apcu_fetch':
case 'apc_fetch':
throw new \ErrorException($e->getMessage(), $e->getCode(), \E_ERROR, $e->getFile(), $e->getLine());
}
}
throw $e;
} finally {
ini_set('unserialize_callback_func', $unserializeCallbackHandler);
}
}
/**
* {@inheritdoc}
*/
protected function doHave(string $id)
{
return $this->provider->contains($id);
}
/**
* {@inheritdoc}
*/
protected function doClear(string $namespace)
{
$namespace = $this->provider->getNamespace();
return isset($namespace[0])
? $this->provider->deleteAll()
: $this->provider->flushAll();
}
/**
* {@inheritdoc}
*/
protected function doDelete(array $ids)
{
$ok = true;
foreach ($ids as $id) {
$ok = $this->provider->delete($id) && $ok;
}
return $ok;
}
/**
* {@inheritdoc}
*/
protected function doSave(array $values, int $lifetime)
{
return $this->provider->saveMultiple($values, $lifetime);
}
}

View File

@@ -11,13 +11,18 @@
namespace Symfony\Component\Cache\Adapter;
use Doctrine\DBAL\ArrayParameterType;
use Doctrine\DBAL\Configuration;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Driver\ServerInfoAwareConnection;
use Doctrine\DBAL\DriverManager;
use Doctrine\DBAL\Exception as DBALException;
use Doctrine\DBAL\Exception\TableNotFoundException;
use Doctrine\DBAL\ParameterType;
use Doctrine\DBAL\Schema\DefaultSchemaManagerFactory;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\DBAL\ServerVersionProvider;
use Doctrine\DBAL\Tools\DsnParser;
use Symfony\Component\Cache\Exception\InvalidArgumentException;
use Symfony\Component\Cache\Marshaller\DefaultMarshaller;
use Symfony\Component\Cache\Marshaller\MarshallerInterface;
@@ -25,18 +30,18 @@ use Symfony\Component\Cache\PruneableInterface;
class DoctrineDbalAdapter extends AbstractAdapter implements PruneableInterface
{
protected $maxIdLength = 255;
private const MAX_KEY_LENGTH = 255;
private $marshaller;
private $conn;
private $platformName;
private $serverVersion;
private $table = 'cache_items';
private $idCol = 'item_id';
private $dataCol = 'item_data';
private $lifetimeCol = 'item_lifetime';
private $timeCol = 'item_time';
private $namespace;
private MarshallerInterface $marshaller;
private Connection $conn;
private string $platformName;
private string $serverVersion;
private string $table = 'cache_items';
private string $idCol = 'item_id';
private string $dataCol = 'item_data';
private string $lifetimeCol = 'item_lifetime';
private string $timeCol = 'item_time';
private string $namespace;
/**
* You can either pass an existing database Doctrine DBAL Connection or
@@ -52,11 +57,9 @@ class DoctrineDbalAdapter extends AbstractAdapter implements PruneableInterface
* * db_lifetime_col: The column where to store the lifetime [default: item_lifetime]
* * db_time_col: The column where to store the timestamp [default: item_time]
*
* @param Connection|string $connOrDsn
*
* @throws InvalidArgumentException When namespace contains invalid characters
*/
public function __construct($connOrDsn, string $namespace = '', int $defaultLifetime = 0, array $options = [], MarshallerInterface $marshaller = null)
public function __construct(Connection|string $connOrDsn, string $namespace = '', int $defaultLifetime = 0, array $options = [], MarshallerInterface $marshaller = null)
{
if (isset($namespace[0]) && preg_match('#[^-+.A-Za-z0-9]#', $namespace, $match)) {
throw new InvalidArgumentException(sprintf('Namespace contains "%s" but only characters in [-+.A-Za-z0-9] are allowed.', $match[0]));
@@ -64,15 +67,35 @@ class DoctrineDbalAdapter extends AbstractAdapter implements PruneableInterface
if ($connOrDsn instanceof Connection) {
$this->conn = $connOrDsn;
} elseif (\is_string($connOrDsn)) {
if (!class_exists(DriverManager::class)) {
throw new InvalidArgumentException(sprintf('Failed to parse the DSN "%s". Try running "composer require doctrine/dbal".', $connOrDsn));
}
$this->conn = DriverManager::getConnection(['url' => $connOrDsn]);
} else {
throw new \TypeError(sprintf('Argument 1 passed to "%s()" must be "%s" or string, "%s" given.', __METHOD__, Connection::class, get_debug_type($connOrDsn)));
if (!class_exists(DriverManager::class)) {
throw new InvalidArgumentException('Failed to parse DSN. Try running "composer require doctrine/dbal".');
}
if (class_exists(DsnParser::class)) {
$params = (new DsnParser([
'db2' => 'ibm_db2',
'mssql' => 'pdo_sqlsrv',
'mysql' => 'pdo_mysql',
'mysql2' => 'pdo_mysql',
'postgres' => 'pdo_pgsql',
'postgresql' => 'pdo_pgsql',
'pgsql' => 'pdo_pgsql',
'sqlite' => 'pdo_sqlite',
'sqlite3' => 'pdo_sqlite',
]))->parse($connOrDsn);
} else {
$params = ['url' => $connOrDsn];
}
$config = new Configuration();
if (class_exists(DefaultSchemaManagerFactory::class)) {
$config->setSchemaManagerFactory(new DefaultSchemaManagerFactory());
}
$this->conn = DriverManager::getConnection($params, $config);
}
$this->maxIdLength = self::MAX_KEY_LENGTH;
$this->table = $options['db_table'] ?? $this->table;
$this->idCol = $options['db_id_col'] ?? $this->idCol;
$this->dataCol = $options['db_data_col'] ?? $this->dataCol;
@@ -92,7 +115,7 @@ class DoctrineDbalAdapter extends AbstractAdapter implements PruneableInterface
*
* @throws DBALException When the table already exists
*/
public function createTable()
public function createTable(): void
{
$schema = new Schema();
$this->addTableToSchema($schema);
@@ -103,25 +126,23 @@ class DoctrineDbalAdapter extends AbstractAdapter implements PruneableInterface
}
/**
* {@inheritdoc}
* @param \Closure $isSameDatabase
*/
public function configureSchema(Schema $schema, Connection $forConnection): void
public function configureSchema(Schema $schema, Connection $forConnection/* , \Closure $isSameDatabase */): void
{
// only update the schema for this connection
if ($forConnection !== $this->conn) {
if ($schema->hasTable($this->table)) {
return;
}
if ($schema->hasTable($this->table)) {
$isSameDatabase = 2 < \func_num_args() ? func_get_arg(2) : static fn () => false;
if ($forConnection !== $this->conn && !$isSameDatabase($this->conn->executeStatement(...))) {
return;
}
$this->addTableToSchema($schema);
}
/**
* {@inheritdoc}
*/
public function prune(): bool
{
$deleteSql = "DELETE FROM $this->table WHERE $this->lifetimeCol + $this->timeCol <= ?";
@@ -136,15 +157,12 @@ class DoctrineDbalAdapter extends AbstractAdapter implements PruneableInterface
try {
$this->conn->executeStatement($deleteSql, $params, $paramTypes);
} catch (TableNotFoundException $e) {
} catch (TableNotFoundException) {
}
return true;
}
/**
* {@inheritdoc}
*/
protected function doFetch(array $ids): iterable
{
$now = time();
@@ -156,7 +174,7 @@ class DoctrineDbalAdapter extends AbstractAdapter implements PruneableInterface
$ids,
], [
ParameterType::INTEGER,
Connection::PARAM_STR_ARRAY,
class_exists(ArrayParameterType::class) ? ArrayParameterType::STRING : Connection::PARAM_STR_ARRAY,
])->iterateNumeric();
foreach ($result as $row) {
@@ -174,14 +192,11 @@ class DoctrineDbalAdapter extends AbstractAdapter implements PruneableInterface
$expired,
], [
ParameterType::INTEGER,
Connection::PARAM_STR_ARRAY,
class_exists(ArrayParameterType::class) ? ArrayParameterType::STRING : Connection::PARAM_STR_ARRAY,
]);
}
}
/**
* {@inheritdoc}
*/
protected function doHave(string $id): bool
{
$sql = "SELECT 1 FROM $this->table WHERE $this->idCol = ? AND ($this->lifetimeCol IS NULL OR $this->lifetimeCol + $this->timeCol > ?)";
@@ -196,47 +211,34 @@ class DoctrineDbalAdapter extends AbstractAdapter implements PruneableInterface
return (bool) $result->fetchOne();
}
/**
* {@inheritdoc}
*/
protected function doClear(string $namespace): bool
{
if ('' === $namespace) {
if ('sqlite' === $this->getPlatformName()) {
$sql = "DELETE FROM $this->table";
} else {
$sql = "TRUNCATE TABLE $this->table";
}
$sql = $this->conn->getDatabasePlatform()->getTruncateTableSQL($this->table);
} else {
$sql = "DELETE FROM $this->table WHERE $this->idCol LIKE '$namespace%'";
}
try {
$this->conn->executeStatement($sql);
} catch (TableNotFoundException $e) {
} catch (TableNotFoundException) {
}
return true;
}
/**
* {@inheritdoc}
*/
protected function doDelete(array $ids): bool
{
$sql = "DELETE FROM $this->table WHERE $this->idCol IN (?)";
try {
$this->conn->executeStatement($sql, [array_values($ids)], [Connection::PARAM_STR_ARRAY]);
} catch (TableNotFoundException $e) {
$this->conn->executeStatement($sql, [array_values($ids)], [class_exists(ArrayParameterType::class) ? ArrayParameterType::STRING : Connection::PARAM_STR_ARRAY]);
} catch (TableNotFoundException) {
}
return true;
}
/**
* {@inheritdoc}
*/
protected function doSave(array $values, int $lifetime)
protected function doSave(array $values, int $lifetime): array|bool
{
if (!$values = $this->marshaller->marshall($values, $failed)) {
return $failed;
@@ -278,45 +280,52 @@ class DoctrineDbalAdapter extends AbstractAdapter implements PruneableInterface
$lifetime = $lifetime ?: null;
try {
$stmt = $this->conn->prepare($sql);
} catch (TableNotFoundException $e) {
} catch (TableNotFoundException) {
if (!$this->conn->isTransactionActive() || \in_array($platformName, ['pgsql', 'sqlite', 'sqlsrv'], true)) {
$this->createTable();
}
$stmt = $this->conn->prepare($sql);
}
// $id and $data are defined later in the loop. Binding is done by reference, values are read on execution.
if ('sqlsrv' === $platformName || 'oci' === $platformName) {
$stmt->bindParam(1, $id);
$stmt->bindParam(2, $id);
$stmt->bindParam(3, $data, ParameterType::LARGE_OBJECT);
$bind = static function ($id, $data) use ($stmt) {
$stmt->bindValue(1, $id);
$stmt->bindValue(2, $id);
$stmt->bindValue(3, $data, ParameterType::LARGE_OBJECT);
$stmt->bindValue(6, $data, ParameterType::LARGE_OBJECT);
};
$stmt->bindValue(4, $lifetime, ParameterType::INTEGER);
$stmt->bindValue(5, $now, ParameterType::INTEGER);
$stmt->bindParam(6, $data, ParameterType::LARGE_OBJECT);
$stmt->bindValue(7, $lifetime, ParameterType::INTEGER);
$stmt->bindValue(8, $now, ParameterType::INTEGER);
} elseif (null !== $platformName) {
$stmt->bindParam(1, $id);
$stmt->bindParam(2, $data, ParameterType::LARGE_OBJECT);
$bind = static function ($id, $data) use ($stmt) {
$stmt->bindValue(1, $id);
$stmt->bindValue(2, $data, ParameterType::LARGE_OBJECT);
};
$stmt->bindValue(3, $lifetime, ParameterType::INTEGER);
$stmt->bindValue(4, $now, ParameterType::INTEGER);
} else {
$stmt->bindParam(1, $data, ParameterType::LARGE_OBJECT);
$stmt->bindValue(2, $lifetime, ParameterType::INTEGER);
$stmt->bindValue(3, $now, ParameterType::INTEGER);
$stmt->bindParam(4, $id);
$insertStmt = $this->conn->prepare($insertSql);
$insertStmt->bindParam(1, $id);
$insertStmt->bindParam(2, $data, ParameterType::LARGE_OBJECT);
$insertStmt->bindValue(3, $lifetime, ParameterType::INTEGER);
$insertStmt->bindValue(4, $now, ParameterType::INTEGER);
$bind = static function ($id, $data) use ($stmt, $insertStmt) {
$stmt->bindValue(1, $data, ParameterType::LARGE_OBJECT);
$stmt->bindValue(4, $id);
$insertStmt->bindValue(1, $id);
$insertStmt->bindValue(2, $data, ParameterType::LARGE_OBJECT);
};
}
foreach ($values as $id => $data) {
$bind($id, $data);
try {
$rowCount = $stmt->executeStatement();
} catch (TableNotFoundException $e) {
} catch (TableNotFoundException) {
if (!$this->conn->isTransactionActive() || \in_array($platformName, ['pgsql', 'sqlite', 'sqlsrv'], true)) {
$this->createTable();
}
@@ -325,7 +334,7 @@ class DoctrineDbalAdapter extends AbstractAdapter implements PruneableInterface
if (null === $platformName && 0 === $rowCount) {
try {
$insertStmt->executeStatement();
} catch (DBALException $e) {
} catch (DBALException) {
// A concurrent write won, let it be
}
}
@@ -334,6 +343,22 @@ class DoctrineDbalAdapter extends AbstractAdapter implements PruneableInterface
return $failed;
}
/**
* @internal
*/
protected function getId(mixed $key): string
{
if ('pgsql' !== $this->platformName ??= $this->getPlatformName()) {
return parent::getId($key);
}
if (str_contains($key, "\0") || str_contains($key, '%') || !preg_match('//u', $key)) {
$key = rawurlencode($key);
}
return parent::getId($key);
}
private function getPlatformName(): string
{
if (isset($this->platformName)) {
@@ -342,28 +367,17 @@ class DoctrineDbalAdapter extends AbstractAdapter implements PruneableInterface
$platform = $this->conn->getDatabasePlatform();
switch (true) {
case $platform instanceof \Doctrine\DBAL\Platforms\MySQLPlatform:
case $platform instanceof \Doctrine\DBAL\Platforms\MySQL57Platform:
return $this->platformName = 'mysql';
case $platform instanceof \Doctrine\DBAL\Platforms\SqlitePlatform:
return $this->platformName = 'sqlite';
case $platform instanceof \Doctrine\DBAL\Platforms\PostgreSQLPlatform:
case $platform instanceof \Doctrine\DBAL\Platforms\PostgreSQL94Platform:
return $this->platformName = 'pgsql';
case $platform instanceof \Doctrine\DBAL\Platforms\OraclePlatform:
return $this->platformName = 'oci';
case $platform instanceof \Doctrine\DBAL\Platforms\SQLServerPlatform:
case $platform instanceof \Doctrine\DBAL\Platforms\SQLServer2012Platform:
return $this->platformName = 'sqlsrv';
default:
return $this->platformName = \get_class($platform);
}
return $this->platformName = match (true) {
$platform instanceof \Doctrine\DBAL\Platforms\MySQLPlatform,
$platform instanceof \Doctrine\DBAL\Platforms\MySQL57Platform => 'mysql',
$platform instanceof \Doctrine\DBAL\Platforms\SqlitePlatform => 'sqlite',
$platform instanceof \Doctrine\DBAL\Platforms\PostgreSQLPlatform,
$platform instanceof \Doctrine\DBAL\Platforms\PostgreSQL94Platform => 'pgsql',
$platform instanceof \Doctrine\DBAL\Platforms\OraclePlatform => 'oci',
$platform instanceof \Doctrine\DBAL\Platforms\SQLServerPlatform,
$platform instanceof \Doctrine\DBAL\Platforms\SQLServer2012Platform => 'sqlsrv',
default => $platform::class,
};
}
private function getServerVersion(): string
@@ -372,12 +386,14 @@ class DoctrineDbalAdapter extends AbstractAdapter implements PruneableInterface
return $this->serverVersion;
}
$conn = $this->conn->getWrappedConnection();
if ($conn instanceof ServerInfoAwareConnection) {
return $this->serverVersion = $conn->getServerVersion();
if ($this->conn instanceof ServerVersionProvider || $this->conn instanceof ServerInfoAwareConnection) {
return $this->serverVersion = $this->conn->getServerVersion();
}
return $this->serverVersion = '0';
// The condition should be removed once support for DBAL <3.3 is dropped
$conn = method_exists($this->conn, 'getNativeConnection') ? $this->conn->getNativeConnection() : $this->conn->getWrappedConnection();
return $this->serverVersion = $conn->getAttribute(\PDO::ATTR_SERVER_VERSION);
}
private function addTableToSchema(Schema $schema): void

View File

@@ -25,6 +25,7 @@ use Symfony\Component\Cache\Traits\FilesystemTrait;
class FilesystemTagAwareAdapter extends AbstractTagAwareAdapter implements PruneableInterface
{
use FilesystemTrait {
prune as private doPrune;
doClear as private doClearCache;
doSave as private doSaveCache;
}
@@ -41,10 +42,49 @@ class FilesystemTagAwareAdapter extends AbstractTagAwareAdapter implements Prune
$this->init($namespace, $directory);
}
/**
* {@inheritdoc}
*/
protected function doClear(string $namespace)
public function prune(): bool
{
$ok = $this->doPrune();
set_error_handler(static function () {});
$chars = '+-ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
try {
foreach ($this->scanHashDir($this->directory.self::TAG_FOLDER.\DIRECTORY_SEPARATOR) as $dir) {
$dir .= \DIRECTORY_SEPARATOR;
$keepDir = false;
for ($i = 0; $i < 38; ++$i) {
if (!is_dir($dir.$chars[$i])) {
continue;
}
for ($j = 0; $j < 38; ++$j) {
if (!is_dir($d = $dir.$chars[$i].\DIRECTORY_SEPARATOR.$chars[$j])) {
continue;
}
foreach (scandir($d, \SCANDIR_SORT_NONE) ?: [] as $link) {
if ('.' === $link || '..' === $link) {
continue;
}
if ('_' !== $dir[-2] && realpath($d.\DIRECTORY_SEPARATOR.$link)) {
$keepDir = true;
} else {
unlink($d.\DIRECTORY_SEPARATOR.$link);
}
}
$keepDir ?: rmdir($d);
}
$keepDir ?: rmdir($dir.$chars[$i]);
}
$keepDir ?: rmdir($dir);
}
} finally {
restore_error_handler();
}
return $ok;
}
protected function doClear(string $namespace): bool
{
$ok = $this->doClearCache($namespace);
@@ -55,9 +95,11 @@ class FilesystemTagAwareAdapter extends AbstractTagAwareAdapter implements Prune
set_error_handler(static function () {});
$chars = '+-ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
$this->tmpSuffix ??= str_replace('/', '-', base64_encode(random_bytes(6)));
try {
foreach ($this->scanHashDir($this->directory.self::TAG_FOLDER.\DIRECTORY_SEPARATOR) as $dir) {
if (rename($dir, $renamed = substr_replace($dir, bin2hex(random_bytes(4)), -8))) {
if (rename($dir, $renamed = substr_replace($dir, $this->tmpSuffix.'_', -9))) {
$dir = $renamed.\DIRECTORY_SEPARATOR;
} else {
$dir .= \DIRECTORY_SEPARATOR;
@@ -90,9 +132,6 @@ class FilesystemTagAwareAdapter extends AbstractTagAwareAdapter implements Prune
return $ok;
}
/**
* {@inheritdoc}
*/
protected function doSave(array $values, int $lifetime, array $addTagData = [], array $removeTagData = []): array
{
$failed = $this->doSaveCache($values, $lifetime);
@@ -129,9 +168,6 @@ class FilesystemTagAwareAdapter extends AbstractTagAwareAdapter implements Prune
return $failed;
}
/**
* {@inheritdoc}
*/
protected function doDeleteYieldTags(array $ids): iterable
{
foreach ($ids as $id) {
@@ -140,7 +176,7 @@ class FilesystemTagAwareAdapter extends AbstractTagAwareAdapter implements Prune
continue;
}
if ((\PHP_VERSION_ID >= 70300 || '\\' !== \DIRECTORY_SEPARATOR) && !@unlink($file)) {
if (!@unlink($file)) {
fclose($h);
continue;
}
@@ -159,22 +195,15 @@ class FilesystemTagAwareAdapter extends AbstractTagAwareAdapter implements Prune
try {
yield $id => '' === $meta ? [] : $this->marshaller->unmarshall($meta);
} catch (\Exception $e) {
} catch (\Exception) {
yield $id => [];
}
}
fclose($h);
if (\PHP_VERSION_ID < 70300 && '\\' === \DIRECTORY_SEPARATOR) {
@unlink($file);
}
}
}
/**
* {@inheritdoc}
*/
protected function doDeleteTagRelations(array $tagData): bool
{
foreach ($tagData as $tagId => $idList) {
@@ -187,9 +216,6 @@ class FilesystemTagAwareAdapter extends AbstractTagAwareAdapter implements Prune
return true;
}
/**
* {@inheritdoc}
*/
protected function doInvalidate(array $tagIds): bool
{
foreach ($tagIds as $tagId) {
@@ -197,10 +223,12 @@ class FilesystemTagAwareAdapter extends AbstractTagAwareAdapter implements Prune
continue;
}
$this->tmpSuffix ??= str_replace('/', '-', base64_encode(random_bytes(6)));
set_error_handler(static function () {});
try {
if (rename($tagFolder, $renamed = substr_replace($tagFolder, bin2hex(random_bytes(4)), -9))) {
if (rename($tagFolder, $renamed = substr_replace($tagFolder, $this->tmpSuffix.'_', -10))) {
$tagFolder = $renamed.\DIRECTORY_SEPARATOR;
} else {
$renamed = null;

View File

@@ -29,19 +29,11 @@ class MemcachedAdapter extends AbstractAdapter
*/
private const RESERVED_MEMCACHED = " \n\r\t\v\f\0";
private const RESERVED_PSR6 = '@()\{}/';
private const MAX_KEY_LENGTH = 250;
protected $maxIdLength = 250;
private const DEFAULT_CLIENT_OPTIONS = [
'persistent_id' => null,
'username' => null,
'password' => null,
\Memcached::OPT_SERIALIZER => \Memcached::SERIALIZER_PHP,
];
private $marshaller;
private $client;
private $lazyClient;
private MarshallerInterface $marshaller;
private \Memcached $client;
private \Memcached $lazyClient;
/**
* Using a MemcachedAdapter with a TagAwareAdapter for storing tags is discouraged.
@@ -56,9 +48,11 @@ class MemcachedAdapter extends AbstractAdapter
public function __construct(\Memcached $client, string $namespace = '', int $defaultLifetime = 0, MarshallerInterface $marshaller = null)
{
if (!static::isSupported()) {
throw new CacheException('Memcached '.(\PHP_VERSION_ID >= 80100 ? '> 3.1.5' : '>= 2.2.0').' is required.');
throw new CacheException('Memcached > 3.1.5 is required.');
}
if ('Memcached' === \get_class($client)) {
$this->maxIdLength = self::MAX_KEY_LENGTH;
if ('Memcached' === $client::class) {
$opt = $client->getOption(\Memcached::OPT_SERIALIZER);
if (\Memcached::SERIALIZER_PHP !== $opt && \Memcached::SERIALIZER_IGBINARY !== $opt) {
throw new CacheException('MemcachedAdapter: "serializer" option must be "php" or "igbinary".');
@@ -74,9 +68,12 @@ class MemcachedAdapter extends AbstractAdapter
$this->marshaller = $marshaller ?? new DefaultMarshaller();
}
/**
* @return bool
*/
public static function isSupported()
{
return \extension_loaded('memcached') && version_compare(phpversion('memcached'), \PHP_VERSION_ID >= 80100 ? '3.1.6' : '2.2.0', '>=');
return \extension_loaded('memcached') && version_compare(phpversion('memcached'), '3.1.6', '>=');
}
/**
@@ -90,26 +87,21 @@ class MemcachedAdapter extends AbstractAdapter
*
* @param array[]|string|string[] $servers An array of servers, a DSN, or an array of DSNs
*
* @return \Memcached
*
* @throws \ErrorException When invalid options or servers are provided
*/
public static function createConnection($servers, array $options = [])
public static function createConnection(#[\SensitiveParameter] array|string $servers, array $options = []): \Memcached
{
if (\is_string($servers)) {
$servers = [$servers];
} elseif (!\is_array($servers)) {
throw new InvalidArgumentException(sprintf('MemcachedAdapter::createClient() expects array or string as first argument, "%s" given.', get_debug_type($servers)));
}
if (!static::isSupported()) {
throw new CacheException('Memcached '.(\PHP_VERSION_ID >= 80100 ? '> 3.1.5' : '>= 2.2.0').' is required.');
throw new CacheException('Memcached > 3.1.5 is required.');
}
set_error_handler(function ($type, $msg, $file, $line) { throw new \ErrorException($msg, 0, $type, $file, $line); });
set_error_handler(static fn ($type, $msg, $file, $line) => throw new \ErrorException($msg, 0, $type, $file, $line));
try {
$options += static::DEFAULT_CLIENT_OPTIONS;
$client = new \Memcached($options['persistent_id']);
$username = $options['username'];
$password = $options['password'];
$client = new \Memcached($options['persistent_id'] ?? null);
$username = $options['username'] ?? null;
$password = $options['password'] ?? null;
// parse any DSN in $servers
foreach ($servers as $i => $dsn) {
@@ -117,7 +109,7 @@ class MemcachedAdapter extends AbstractAdapter
continue;
}
if (!str_starts_with($dsn, 'memcached:')) {
throw new InvalidArgumentException(sprintf('Invalid Memcached DSN: "%s" does not start with "memcached:".', $dsn));
throw new InvalidArgumentException('Invalid Memcached DSN: it does not start with "memcached:".');
}
$params = preg_replace_callback('#^memcached:(//)?(?:([^@]*+)@)?#', function ($m) use (&$username, &$password) {
if (!empty($m[2])) {
@@ -127,7 +119,7 @@ class MemcachedAdapter extends AbstractAdapter
return 'file:'.($m[1] ?? '');
}, $dsn);
if (false === $params = parse_url($params)) {
throw new InvalidArgumentException(sprintf('Invalid Memcached DSN: "%s".', $dsn));
throw new InvalidArgumentException('Invalid Memcached DSN.');
}
$query = $hosts = [];
if (isset($params['query'])) {
@@ -135,7 +127,7 @@ class MemcachedAdapter extends AbstractAdapter
if (isset($query['host'])) {
if (!\is_array($hosts = $query['host'])) {
throw new InvalidArgumentException(sprintf('Invalid Memcached DSN: "%s".', $dsn));
throw new InvalidArgumentException('Invalid Memcached DSN: query parameter "host" must be an array.');
}
foreach ($hosts as $host => $weight) {
if (false === $port = strrpos($host, ':')) {
@@ -154,7 +146,7 @@ class MemcachedAdapter extends AbstractAdapter
}
}
if (!isset($params['host']) && !isset($params['path'])) {
throw new InvalidArgumentException(sprintf('Invalid Memcached DSN: "%s".', $dsn));
throw new InvalidArgumentException('Invalid Memcached DSN: missing host or path.');
}
if (isset($params['path']) && preg_match('#/(\d+)$#', $params['path'], $m)) {
$params['weight'] = $m[1];
@@ -199,7 +191,7 @@ class MemcachedAdapter extends AbstractAdapter
$options[\constant('Memcached::OPT_'.$name)] = $value;
}
}
$client->setOptions($options);
$client->setOptions($options + [\Memcached::OPT_SERIALIZER => \Memcached::SERIALIZER_PHP]);
// set client's servers, taking care of persistent connections
if (!$client->isPristine()) {
@@ -239,10 +231,7 @@ class MemcachedAdapter extends AbstractAdapter
}
}
/**
* {@inheritdoc}
*/
protected function doSave(array $values, int $lifetime)
protected function doSave(array $values, int $lifetime): array|bool
{
if (!$values = $this->marshaller->marshall($values, $failed)) {
return $failed;
@@ -260,10 +249,7 @@ class MemcachedAdapter extends AbstractAdapter
return $this->checkResultCode($this->getClient()->setMulti($encodedValues, $lifetime)) ? $failed : false;
}
/**
* {@inheritdoc}
*/
protected function doFetch(array $ids)
protected function doFetch(array $ids): iterable
{
try {
$encodedIds = array_map([__CLASS__, 'encodeKey'], $ids);
@@ -281,18 +267,12 @@ class MemcachedAdapter extends AbstractAdapter
}
}
/**
* {@inheritdoc}
*/
protected function doHave(string $id)
protected function doHave(string $id): bool
{
return false !== $this->getClient()->get(self::encodeKey($id)) || $this->checkResultCode(\Memcached::RES_SUCCESS === $this->client->getResultCode());
}
/**
* {@inheritdoc}
*/
protected function doDelete(array $ids)
protected function doDelete(array $ids): bool
{
$ok = true;
$encodedIds = array_map([__CLASS__, 'encodeKey'], $ids);
@@ -305,15 +285,12 @@ class MemcachedAdapter extends AbstractAdapter
return $ok;
}
/**
* {@inheritdoc}
*/
protected function doClear(string $namespace)
protected function doClear(string $namespace): bool
{
return '' === $namespace && $this->getClient()->flush();
}
private function checkResultCode($result)
private function checkResultCode(mixed $result): mixed
{
$code = $this->client->getResultCode();
@@ -326,7 +303,7 @@ class MemcachedAdapter extends AbstractAdapter
private function getClient(): \Memcached
{
if ($this->client) {
if (isset($this->client)) {
return $this->client;
}

View File

@@ -20,11 +20,11 @@ use Symfony\Contracts\Cache\CacheInterface;
*/
class NullAdapter implements AdapterInterface, CacheInterface
{
private static $createCacheItem;
private static \Closure $createCacheItem;
public function __construct()
{
self::$createCacheItem ?? self::$createCacheItem = \Closure::bind(
self::$createCacheItem ??= \Closure::bind(
static function ($key) {
$item = new CacheItem();
$item->key = $key;
@@ -37,105 +37,58 @@ class NullAdapter implements AdapterInterface, CacheInterface
);
}
/**
* {@inheritdoc}
*/
public function get(string $key, callable $callback, float $beta = null, array &$metadata = null)
public function get(string $key, callable $callback, float $beta = null, array &$metadata = null): mixed
{
$save = true;
return $callback((self::$createCacheItem)($key), $save);
}
/**
* {@inheritdoc}
*/
public function getItem($key)
public function getItem(mixed $key): CacheItem
{
return (self::$createCacheItem)($key);
}
/**
* {@inheritdoc}
*/
public function getItems(array $keys = [])
public function getItems(array $keys = []): iterable
{
return $this->generateItems($keys);
}
/**
* {@inheritdoc}
*
* @return bool
*/
public function hasItem($key)
public function hasItem(mixed $key): bool
{
return false;
}
/**
* {@inheritdoc}
*
* @return bool
*/
public function clear(string $prefix = '')
public function clear(string $prefix = ''): bool
{
return true;
}
/**
* {@inheritdoc}
*
* @return bool
*/
public function deleteItem($key)
public function deleteItem(mixed $key): bool
{
return true;
}
/**
* {@inheritdoc}
*
* @return bool
*/
public function deleteItems(array $keys)
public function deleteItems(array $keys): bool
{
return true;
}
/**
* {@inheritdoc}
*
* @return bool
*/
public function save(CacheItemInterface $item)
public function save(CacheItemInterface $item): bool
{
return true;
}
/**
* {@inheritdoc}
*
* @return bool
*/
public function saveDeferred(CacheItemInterface $item)
public function saveDeferred(CacheItemInterface $item): bool
{
return true;
}
/**
* {@inheritdoc}
*
* @return bool
*/
public function commit()
public function commit(): bool
{
return true;
}
/**
* {@inheritdoc}
*/
public function delete(string $key): bool
{
return $this->deleteItem($key);

View File

@@ -27,7 +27,7 @@ final class ParameterNormalizer
}
try {
return \DateTime::createFromFormat('U', 0)->add(new \DateInterval($duration))->getTimestamp();
return \DateTimeImmutable::createFromFormat('U', 0)->add(new \DateInterval($duration))->getTimestamp();
} catch (\Exception $e) {
throw new \InvalidArgumentException(sprintf('Cannot parse date interval "%s".', $duration), 0, $e);
}

View File

@@ -11,10 +11,6 @@
namespace Symfony\Component\Cache\Adapter;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Schema\Schema;
use Psr\Cache\CacheItemInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\Cache\Exception\InvalidArgumentException;
use Symfony\Component\Cache\Marshaller\DefaultMarshaller;
use Symfony\Component\Cache\Marshaller\MarshallerInterface;
@@ -22,24 +18,22 @@ use Symfony\Component\Cache\PruneableInterface;
class PdoAdapter extends AbstractAdapter implements PruneableInterface
{
protected $maxIdLength = 255;
private const MAX_KEY_LENGTH = 255;
private $marshaller;
private $conn;
private $dsn;
private $driver;
private $serverVersion;
private $table = 'cache_items';
private $idCol = 'item_id';
private $dataCol = 'item_data';
private $lifetimeCol = 'item_lifetime';
private $timeCol = 'item_time';
private $username = '';
private $password = '';
private $connectionOptions = [];
private $namespace;
private $dbalAdapter;
private MarshallerInterface $marshaller;
private \PDO $conn;
private string $dsn;
private string $driver;
private string $serverVersion;
private string $table = 'cache_items';
private string $idCol = 'item_id';
private string $dataCol = 'item_data';
private string $lifetimeCol = 'item_lifetime';
private string $timeCol = 'item_time';
private ?string $username = null;
private ?string $password = null;
private array $connectionOptions = [];
private string $namespace;
/**
* You can either pass an existing database connection as PDO instance or
@@ -56,19 +50,14 @@ class PdoAdapter extends AbstractAdapter implements PruneableInterface
* * db_password: The password when lazy-connect [default: '']
* * db_connection_options: An array of driver-specific connection options [default: []]
*
* @param \PDO|string $connOrDsn
*
* @throws InvalidArgumentException When first argument is not PDO nor Connection nor string
* @throws InvalidArgumentException When PDO error mode is not PDO::ERRMODE_EXCEPTION
* @throws InvalidArgumentException When namespace contains invalid characters
*/
public function __construct($connOrDsn, string $namespace = '', int $defaultLifetime = 0, array $options = [], MarshallerInterface $marshaller = null)
public function __construct(#[\SensitiveParameter] \PDO|string $connOrDsn, string $namespace = '', int $defaultLifetime = 0, array $options = [], MarshallerInterface $marshaller = null)
{
if ($connOrDsn instanceof Connection || (\is_string($connOrDsn) && str_contains($connOrDsn, '://'))) {
trigger_deprecation('symfony/cache', '5.4', 'Usage of a DBAL Connection with "%s" is deprecated and will be removed in symfony 6.0. Use "%s" instead.', __CLASS__, DoctrineDbalAdapter::class);
$this->dbalAdapter = new DoctrineDbalAdapter($connOrDsn, $namespace, $defaultLifetime, $options, $marshaller);
return;
if (\is_string($connOrDsn) && str_contains($connOrDsn, '://')) {
throw new InvalidArgumentException(sprintf('Usage of Doctrine DBAL URL with "%s" is not supported. Use a PDO DSN or "%s" instead.', __CLASS__, DoctrineDbalAdapter::class));
}
if (isset($namespace[0]) && preg_match('#[^-+.A-Za-z0-9]#', $namespace, $match)) {
@@ -81,12 +70,11 @@ class PdoAdapter extends AbstractAdapter implements PruneableInterface
}
$this->conn = $connOrDsn;
} elseif (\is_string($connOrDsn)) {
$this->dsn = $connOrDsn;
} else {
throw new InvalidArgumentException(sprintf('"%s" requires PDO or Doctrine\DBAL\Connection instance or DSN string as first argument, "%s" given.', __CLASS__, get_debug_type($connOrDsn)));
$this->dsn = $connOrDsn;
}
$this->maxIdLength = self::MAX_KEY_LENGTH;
$this->table = $options['db_table'] ?? $this->table;
$this->idCol = $options['db_id_col'] ?? $this->idCol;
$this->dataCol = $options['db_data_col'] ?? $this->dataCol;
@@ -101,235 +89,38 @@ class PdoAdapter extends AbstractAdapter implements PruneableInterface
parent::__construct($namespace, $defaultLifetime);
}
/**
* {@inheritDoc}
*/
public function getItem($key)
{
if (isset($this->dbalAdapter)) {
return $this->dbalAdapter->getItem($key);
}
return parent::getItem($key);
}
/**
* {@inheritDoc}
*/
public function getItems(array $keys = [])
{
if (isset($this->dbalAdapter)) {
return $this->dbalAdapter->getItems($keys);
}
return parent::getItems($keys);
}
/**
* {@inheritDoc}
*/
public function hasItem($key)
{
if (isset($this->dbalAdapter)) {
return $this->dbalAdapter->hasItem($key);
}
return parent::hasItem($key);
}
/**
* {@inheritDoc}
*/
public function deleteItem($key)
{
if (isset($this->dbalAdapter)) {
return $this->dbalAdapter->deleteItem($key);
}
return parent::deleteItem($key);
}
/**
* {@inheritDoc}
*/
public function deleteItems(array $keys)
{
if (isset($this->dbalAdapter)) {
return $this->dbalAdapter->deleteItems($keys);
}
return parent::deleteItems($keys);
}
/**
* {@inheritDoc}
*/
public function clear(string $prefix = '')
{
if (isset($this->dbalAdapter)) {
return $this->dbalAdapter->clear($prefix);
}
return parent::clear($prefix);
}
/**
* {@inheritDoc}
*/
public function get(string $key, callable $callback, float $beta = null, array &$metadata = null)
{
if (isset($this->dbalAdapter)) {
return $this->dbalAdapter->get($key, $callback, $beta, $metadata);
}
return parent::get($key, $callback, $beta, $metadata);
}
/**
* {@inheritDoc}
*/
public function delete(string $key): bool
{
if (isset($this->dbalAdapter)) {
return $this->dbalAdapter->delete($key);
}
return parent::delete($key);
}
/**
* {@inheritDoc}
*/
public function save(CacheItemInterface $item)
{
if (isset($this->dbalAdapter)) {
return $this->dbalAdapter->save($item);
}
return parent::save($item);
}
/**
* {@inheritDoc}
*/
public function saveDeferred(CacheItemInterface $item)
{
if (isset($this->dbalAdapter)) {
return $this->dbalAdapter->saveDeferred($item);
}
return parent::saveDeferred($item);
}
/**
* {@inheritDoc}
*/
public function setLogger(LoggerInterface $logger): void
{
if (isset($this->dbalAdapter)) {
$this->dbalAdapter->setLogger($logger);
return;
}
parent::setLogger($logger);
}
/**
* {@inheritDoc}
*/
public function commit()
{
if (isset($this->dbalAdapter)) {
return $this->dbalAdapter->commit();
}
return parent::commit();
}
/**
* {@inheritDoc}
*/
public function reset()
{
if (isset($this->dbalAdapter)) {
$this->dbalAdapter->reset();
return;
}
parent::reset();
}
/**
* Creates the table to store cache items which can be called once for setup.
*
* Cache ID are saved in a column of maximum length 255. Cache data is
* saved in a BLOB.
*
* @return void
*
* @throws \PDOException When the table already exists
* @throws \DomainException When an unsupported PDO driver is used
*/
public function createTable()
{
if (isset($this->dbalAdapter)) {
$this->dbalAdapter->createTable();
$sql = match ($driver = $this->getDriver()) {
// We use varbinary for the ID column because it prevents unwanted conversions:
// - character set conversions between server and client
// - trailing space removal
// - case-insensitivity
// - language processing like é == e
'mysql' => "CREATE TABLE $this->table ($this->idCol VARBINARY(255) NOT NULL PRIMARY KEY, $this->dataCol MEDIUMBLOB NOT NULL, $this->lifetimeCol INTEGER UNSIGNED, $this->timeCol INTEGER UNSIGNED NOT NULL) COLLATE utf8mb4_bin, ENGINE = InnoDB",
'sqlite' => "CREATE TABLE $this->table ($this->idCol TEXT NOT NULL PRIMARY KEY, $this->dataCol BLOB NOT NULL, $this->lifetimeCol INTEGER, $this->timeCol INTEGER NOT NULL)",
'pgsql' => "CREATE TABLE $this->table ($this->idCol VARCHAR(255) NOT NULL PRIMARY KEY, $this->dataCol BYTEA NOT NULL, $this->lifetimeCol INTEGER, $this->timeCol INTEGER NOT NULL)",
'oci' => "CREATE TABLE $this->table ($this->idCol VARCHAR2(255) NOT NULL PRIMARY KEY, $this->dataCol BLOB NOT NULL, $this->lifetimeCol INTEGER, $this->timeCol INTEGER NOT NULL)",
'sqlsrv' => "CREATE TABLE $this->table ($this->idCol VARCHAR(255) NOT NULL PRIMARY KEY, $this->dataCol VARBINARY(MAX) NOT NULL, $this->lifetimeCol INTEGER, $this->timeCol INTEGER NOT NULL)",
default => throw new \DomainException(sprintf('Creating the cache table is currently not implemented for PDO driver "%s".', $driver)),
};
return;
}
// connect if we are not yet
$conn = $this->getConnection();
switch ($this->driver) {
case 'mysql':
// We use varbinary for the ID column because it prevents unwanted conversions:
// - character set conversions between server and client
// - trailing space removal
// - case-insensitivity
// - language processing like é == e
$sql = "CREATE TABLE $this->table ($this->idCol VARBINARY(255) NOT NULL PRIMARY KEY, $this->dataCol MEDIUMBLOB NOT NULL, $this->lifetimeCol INTEGER UNSIGNED, $this->timeCol INTEGER UNSIGNED NOT NULL) COLLATE utf8mb4_bin, ENGINE = InnoDB";
break;
case 'sqlite':
$sql = "CREATE TABLE $this->table ($this->idCol TEXT NOT NULL PRIMARY KEY, $this->dataCol BLOB NOT NULL, $this->lifetimeCol INTEGER, $this->timeCol INTEGER NOT NULL)";
break;
case 'pgsql':
$sql = "CREATE TABLE $this->table ($this->idCol VARCHAR(255) NOT NULL PRIMARY KEY, $this->dataCol BYTEA NOT NULL, $this->lifetimeCol INTEGER, $this->timeCol INTEGER NOT NULL)";
break;
case 'oci':
$sql = "CREATE TABLE $this->table ($this->idCol VARCHAR2(255) NOT NULL PRIMARY KEY, $this->dataCol BLOB NOT NULL, $this->lifetimeCol INTEGER, $this->timeCol INTEGER NOT NULL)";
break;
case 'sqlsrv':
$sql = "CREATE TABLE $this->table ($this->idCol VARCHAR(255) NOT NULL PRIMARY KEY, $this->dataCol VARBINARY(MAX) NOT NULL, $this->lifetimeCol INTEGER, $this->timeCol INTEGER NOT NULL)";
break;
default:
throw new \DomainException(sprintf('Creating the cache table is currently not implemented for PDO driver "%s".', $this->driver));
}
$conn->exec($sql);
$this->getConnection()->exec($sql);
}
/**
* Adds the Table to the Schema if the adapter uses this Connection.
*
* @deprecated since symfony/cache 5.4 use DoctrineDbalAdapter instead
*/
public function configureSchema(Schema $schema, Connection $forConnection): void
public function prune(): bool
{
if (isset($this->dbalAdapter)) {
$this->dbalAdapter->configureSchema($schema, $forConnection);
}
}
/**
* {@inheritdoc}
*/
public function prune()
{
if (isset($this->dbalAdapter)) {
return $this->dbalAdapter->prune();
}
$deleteSql = "DELETE FROM $this->table WHERE $this->lifetimeCol + $this->timeCol <= :time";
if ('' !== $this->namespace) {
@@ -340,7 +131,7 @@ class PdoAdapter extends AbstractAdapter implements PruneableInterface
try {
$delete = $connection->prepare($deleteSql);
} catch (\PDOException $e) {
} catch (\PDOException) {
return true;
}
$delete->bindValue(':time', time(), \PDO::PARAM_INT);
@@ -350,15 +141,12 @@ class PdoAdapter extends AbstractAdapter implements PruneableInterface
}
try {
return $delete->execute();
} catch (\PDOException $e) {
} catch (\PDOException) {
return true;
}
}
/**
* {@inheritdoc}
*/
protected function doFetch(array $ids)
protected function doFetch(array $ids): iterable
{
$connection = $this->getConnection();
@@ -401,10 +189,7 @@ class PdoAdapter extends AbstractAdapter implements PruneableInterface
}
}
/**
* {@inheritdoc}
*/
protected function doHave(string $id)
protected function doHave(string $id): bool
{
$connection = $this->getConnection();
@@ -418,15 +203,12 @@ class PdoAdapter extends AbstractAdapter implements PruneableInterface
return (bool) $stmt->fetchColumn();
}
/**
* {@inheritdoc}
*/
protected function doClear(string $namespace)
protected function doClear(string $namespace): bool
{
$conn = $this->getConnection();
if ('' === $namespace) {
if ('sqlite' === $this->driver) {
if ('sqlite' === $this->getDriver()) {
$sql = "DELETE FROM $this->table";
} else {
$sql = "TRUNCATE TABLE $this->table";
@@ -437,32 +219,26 @@ class PdoAdapter extends AbstractAdapter implements PruneableInterface
try {
$conn->exec($sql);
} catch (\PDOException $e) {
} catch (\PDOException) {
}
return true;
}
/**
* {@inheritdoc}
*/
protected function doDelete(array $ids)
protected function doDelete(array $ids): bool
{
$sql = str_pad('', (\count($ids) << 1) - 1, '?,');
$sql = "DELETE FROM $this->table WHERE $this->idCol IN ($sql)";
try {
$stmt = $this->getConnection()->prepare($sql);
$stmt->execute(array_values($ids));
} catch (\PDOException $e) {
} catch (\PDOException) {
}
return true;
}
/**
* {@inheritdoc}
*/
protected function doSave(array $values, int $lifetime)
protected function doSave(array $values, int $lifetime): array|bool
{
if (!$values = $this->marshaller->marshall($values, $failed)) {
return $failed;
@@ -470,7 +246,7 @@ class PdoAdapter extends AbstractAdapter implements PruneableInterface
$conn = $this->getConnection();
$driver = $this->driver;
$driver = $this->getDriver();
$insertSql = "INSERT INTO $this->table ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (:id, :data, :lifetime, :time)";
switch (true) {
@@ -507,7 +283,7 @@ class PdoAdapter extends AbstractAdapter implements PruneableInterface
try {
$stmt = $conn->prepare($sql);
} catch (\PDOException $e) {
if (!$conn->inTransaction() || \in_array($this->driver, ['pgsql', 'sqlite', 'sqlsrv'], true)) {
if ($this->isTableMissing($e) && (!$conn->inTransaction() || \in_array($driver, ['pgsql', 'sqlite', 'sqlsrv'], true))) {
$this->createTable();
}
$stmt = $conn->prepare($sql);
@@ -542,7 +318,7 @@ class PdoAdapter extends AbstractAdapter implements PruneableInterface
try {
$stmt->execute();
} catch (\PDOException $e) {
if (!$conn->inTransaction() || \in_array($this->driver, ['pgsql', 'sqlite', 'sqlsrv'], true)) {
if ($this->isTableMissing($e) && (!$conn->inTransaction() || \in_array($driver, ['pgsql', 'sqlite', 'sqlsrv'], true))) {
$this->createTable();
}
$stmt->execute();
@@ -550,7 +326,7 @@ class PdoAdapter extends AbstractAdapter implements PruneableInterface
if (null === $driver && !$stmt->rowCount()) {
try {
$insertStmt->execute();
} catch (\PDOException $e) {
} catch (\PDOException) {
// A concurrent write won, let it be
}
}
@@ -559,25 +335,54 @@ class PdoAdapter extends AbstractAdapter implements PruneableInterface
return $failed;
}
/**
* @internal
*/
protected function getId(mixed $key): string
{
if ('pgsql' !== $this->getDriver()) {
return parent::getId($key);
}
if (str_contains($key, "\0") || str_contains($key, '%') || !preg_match('//u', $key)) {
$key = rawurlencode($key);
}
return parent::getId($key);
}
private function getConnection(): \PDO
{
if (null === $this->conn) {
if (!isset($this->conn)) {
$this->conn = new \PDO($this->dsn, $this->username, $this->password, $this->connectionOptions);
$this->conn->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
}
if (null === $this->driver) {
$this->driver = $this->conn->getAttribute(\PDO::ATTR_DRIVER_NAME);
}
return $this->conn;
}
private function getDriver(): string
{
return $this->driver ??= $this->getConnection()->getAttribute(\PDO::ATTR_DRIVER_NAME);
}
private function getServerVersion(): string
{
if (null === $this->serverVersion) {
$this->serverVersion = $this->conn->getAttribute(\PDO::ATTR_SERVER_VERSION);
}
return $this->serverVersion ??= $this->getConnection()->getAttribute(\PDO::ATTR_SERVER_VERSION);
}
return $this->serverVersion;
private function isTableMissing(\PDOException $exception): bool
{
$driver = $this->getDriver();
$code = $exception->getCode();
return match ($driver) {
'pgsql' => '42P01' === $code,
'sqlite' => str_contains($exception->getMessage(), 'no such table:'),
'oci' => 942 === $code,
'sqlsrv' => 208 === $code,
'mysql' => 1146 === $code,
default => false,
};
}
}

View File

@@ -34,12 +34,12 @@ class PhpArrayAdapter implements AdapterInterface, CacheInterface, PruneableInte
use ContractsTrait;
use ProxyTrait;
private $file;
private $keys;
private $values;
private string $file;
private array $keys;
private array $values;
private static $createCacheItem;
private static $valuesCache = [];
private static \Closure $createCacheItem;
private static array $valuesCache = [];
/**
* @param string $file The PHP file were values are cached
@@ -49,7 +49,7 @@ class PhpArrayAdapter implements AdapterInterface, CacheInterface, PruneableInte
{
$this->file = $file;
$this->pool = $fallbackPool;
self::$createCacheItem ?? self::$createCacheItem = \Closure::bind(
self::$createCacheItem ??= \Closure::bind(
static function ($key, $value, $isHit) {
$item = new CacheItem();
$item->key = $key;
@@ -68,10 +68,8 @@ class PhpArrayAdapter implements AdapterInterface, CacheInterface, PruneableInte
*
* @param string $file The PHP file were values are cached
* @param CacheItemPoolInterface $fallbackPool A pool to fallback on when an item is not hit
*
* @return CacheItemPoolInterface
*/
public static function create(string $file, CacheItemPoolInterface $fallbackPool)
public static function create(string $file, CacheItemPoolInterface $fallbackPool): CacheItemPoolInterface
{
if (!$fallbackPool instanceof AdapterInterface) {
$fallbackPool = new ProxyAdapter($fallbackPool);
@@ -80,12 +78,9 @@ class PhpArrayAdapter implements AdapterInterface, CacheInterface, PruneableInte
return new static($file, $fallbackPool);
}
/**
* {@inheritdoc}
*/
public function get(string $key, callable $callback, float $beta = null, array &$metadata = null)
public function get(string $key, callable $callback, float $beta = null, array &$metadata = null): mixed
{
if (null === $this->values) {
if (!isset($this->values)) {
$this->initialize();
}
if (!isset($this->keys[$key])) {
@@ -105,7 +100,7 @@ class PhpArrayAdapter implements AdapterInterface, CacheInterface, PruneableInte
if ($value instanceof \Closure) {
return $value();
}
} catch (\Throwable $e) {
} catch (\Throwable) {
unset($this->keys[$key]);
goto get_from_pool;
}
@@ -113,15 +108,12 @@ class PhpArrayAdapter implements AdapterInterface, CacheInterface, PruneableInte
return $value;
}
/**
* {@inheritdoc}
*/
public function getItem($key)
public function getItem(mixed $key): CacheItem
{
if (!\is_string($key)) {
throw new InvalidArgumentException(sprintf('Cache key must be string, "%s" given.', get_debug_type($key)));
}
if (null === $this->values) {
if (!isset($this->values)) {
$this->initialize();
}
if (!isset($this->keys[$key])) {
@@ -136,7 +128,7 @@ class PhpArrayAdapter implements AdapterInterface, CacheInterface, PruneableInte
} elseif ($value instanceof \Closure) {
try {
$value = $value();
} catch (\Throwable $e) {
} catch (\Throwable) {
$value = null;
$isHit = false;
}
@@ -145,63 +137,45 @@ class PhpArrayAdapter implements AdapterInterface, CacheInterface, PruneableInte
return (self::$createCacheItem)($key, $value, $isHit);
}
/**
* {@inheritdoc}
*/
public function getItems(array $keys = [])
public function getItems(array $keys = []): iterable
{
foreach ($keys as $key) {
if (!\is_string($key)) {
throw new InvalidArgumentException(sprintf('Cache key must be string, "%s" given.', get_debug_type($key)));
}
}
if (null === $this->values) {
if (!isset($this->values)) {
$this->initialize();
}
return $this->generateItems($keys);
}
/**
* {@inheritdoc}
*
* @return bool
*/
public function hasItem($key)
public function hasItem(mixed $key): bool
{
if (!\is_string($key)) {
throw new InvalidArgumentException(sprintf('Cache key must be string, "%s" given.', get_debug_type($key)));
}
if (null === $this->values) {
if (!isset($this->values)) {
$this->initialize();
}
return isset($this->keys[$key]) || $this->pool->hasItem($key);
}
/**
* {@inheritdoc}
*
* @return bool
*/
public function deleteItem($key)
public function deleteItem(mixed $key): bool
{
if (!\is_string($key)) {
throw new InvalidArgumentException(sprintf('Cache key must be string, "%s" given.', get_debug_type($key)));
}
if (null === $this->values) {
if (!isset($this->values)) {
$this->initialize();
}
return !isset($this->keys[$key]) && $this->pool->deleteItem($key);
}
/**
* {@inheritdoc}
*
* @return bool
*/
public function deleteItems(array $keys)
public function deleteItems(array $keys): bool
{
$deleted = true;
$fallbackKeys = [];
@@ -217,7 +191,7 @@ class PhpArrayAdapter implements AdapterInterface, CacheInterface, PruneableInte
$fallbackKeys[] = $key;
}
}
if (null === $this->values) {
if (!isset($this->values)) {
$this->initialize();
}
@@ -228,50 +202,30 @@ class PhpArrayAdapter implements AdapterInterface, CacheInterface, PruneableInte
return $deleted;
}
/**
* {@inheritdoc}
*
* @return bool
*/
public function save(CacheItemInterface $item)
public function save(CacheItemInterface $item): bool
{
if (null === $this->values) {
if (!isset($this->values)) {
$this->initialize();
}
return !isset($this->keys[$item->getKey()]) && $this->pool->save($item);
}
/**
* {@inheritdoc}
*
* @return bool
*/
public function saveDeferred(CacheItemInterface $item)
public function saveDeferred(CacheItemInterface $item): bool
{
if (null === $this->values) {
if (!isset($this->values)) {
$this->initialize();
}
return !isset($this->keys[$item->getKey()]) && $this->pool->saveDeferred($item);
}
/**
* {@inheritdoc}
*
* @return bool
*/
public function commit()
public function commit(): bool
{
return $this->pool->commit();
}
/**
* {@inheritdoc}
*
* @return bool
*/
public function clear(string $prefix = '')
public function clear(string $prefix = ''): bool
{
$this->keys = $this->values = [];
@@ -292,7 +246,7 @@ class PhpArrayAdapter implements AdapterInterface, CacheInterface, PruneableInte
*
* @return string[] A list of classes to preload on PHP 7.4+
*/
public function warmUp(array $values)
public function warmUp(array $values): array
{
if (file_exists($this->file)) {
if (!is_file($this->file)) {
@@ -355,7 +309,7 @@ EOF;
$value = str_replace("\n", "\n ", $value);
$value = "static function () {\n return {$value};\n}";
}
$hash = hash('md5', $value);
$hash = hash('xxh128', $value);
if (null === $id = $dumpedMap[$hash] ?? null) {
$id = $dumpedMap[$hash] = \count($dumpedMap);
@@ -384,7 +338,7 @@ EOF;
/**
* Load the cache file.
*/
private function initialize()
private function initialize(): void
{
if (isset(self::$valuesCache[$this->file])) {
$values = self::$valuesCache[$this->file];
@@ -417,7 +371,7 @@ EOF;
} elseif ($value instanceof \Closure) {
try {
yield $key => $f($key, $value(), true);
} catch (\Throwable $e) {
} catch (\Throwable) {
yield $key => $f($key, null, false);
}
} else {

View File

@@ -29,13 +29,13 @@ class PhpFilesAdapter extends AbstractAdapter implements PruneableInterface
doDelete as private doCommonDelete;
}
private $includeHandler;
private $appendOnly;
private $values = [];
private $files = [];
private \Closure $includeHandler;
private bool $appendOnly;
private array $values = [];
private array $files = [];
private static $startTime;
private static $valuesCache = [];
private static int $startTime;
private static array $valuesCache = [];
/**
* @param $appendOnly Set to `true` to gain extra performance when the items stored in this pool never expire.
@@ -46,7 +46,7 @@ class PhpFilesAdapter extends AbstractAdapter implements PruneableInterface
public function __construct(string $namespace = '', int $defaultLifetime = 0, string $directory = null, bool $appendOnly = false)
{
$this->appendOnly = $appendOnly;
self::$startTime = self::$startTime ?? $_SERVER['REQUEST_TIME'] ?? time();
self::$startTime ??= $_SERVER['REQUEST_TIME'] ?? time();
parent::__construct('', $defaultLifetime);
$this->init($namespace, $directory);
$this->includeHandler = static function ($type, $msg, $file, $line) {
@@ -54,17 +54,17 @@ class PhpFilesAdapter extends AbstractAdapter implements PruneableInterface
};
}
public static function isSupported()
{
self::$startTime = self::$startTime ?? $_SERVER['REQUEST_TIME'] ?? time();
return \function_exists('opcache_invalidate') && filter_var(\ini_get('opcache.enable'), \FILTER_VALIDATE_BOOLEAN) && (!\in_array(\PHP_SAPI, ['cli', 'phpdbg'], true) || filter_var(\ini_get('opcache.enable_cli'), \FILTER_VALIDATE_BOOLEAN));
}
/**
* @return bool
*/
public function prune()
public static function isSupported()
{
self::$startTime ??= $_SERVER['REQUEST_TIME'] ?? time();
return \function_exists('opcache_invalidate') && filter_var(\ini_get('opcache.enable'), \FILTER_VALIDATE_BOOL) && (!\in_array(\PHP_SAPI, ['cli', 'phpdbg', 'embed'], true) || filter_var(\ini_get('opcache.enable_cli'), \FILTER_VALIDATE_BOOL));
}
public function prune(): bool
{
$time = time();
$pruned = true;
@@ -82,7 +82,7 @@ class PhpFilesAdapter extends AbstractAdapter implements PruneableInterface
}
if ($time >= $expiresAt) {
$pruned = $this->doUnlink($file) && !file_exists($file) && $pruned;
$pruned = ($this->doUnlink($file) || !file_exists($file)) && $pruned;
}
}
} finally {
@@ -92,10 +92,7 @@ class PhpFilesAdapter extends AbstractAdapter implements PruneableInterface
return $pruned;
}
/**
* {@inheritdoc}
*/
protected function doFetch(array $ids)
protected function doFetch(array $ids): iterable
{
if ($this->appendOnly) {
$now = 0;
@@ -138,7 +135,7 @@ class PhpFilesAdapter extends AbstractAdapter implements PruneableInterface
foreach ($missingIds as $k => $id) {
try {
$file = $this->files[$id] ?? $this->files[$id] = $this->getFile($id);
$file = $this->files[$id] ??= $this->getFile($id);
if (isset(self::$valuesCache[$file])) {
[$expiresAt, $this->values[$id]] = self::$valuesCache[$file];
@@ -168,10 +165,7 @@ class PhpFilesAdapter extends AbstractAdapter implements PruneableInterface
goto begin;
}
/**
* {@inheritdoc}
*/
protected function doHave(string $id)
protected function doHave(string $id): bool
{
if ($this->appendOnly && isset($this->values[$id])) {
return true;
@@ -179,7 +173,7 @@ class PhpFilesAdapter extends AbstractAdapter implements PruneableInterface
set_error_handler($this->includeHandler);
try {
$file = $this->files[$id] ?? $this->files[$id] = $this->getFile($id);
$file = $this->files[$id] ??= $this->getFile($id);
$getExpiry = true;
if (isset(self::$valuesCache[$file])) {
@@ -193,7 +187,7 @@ class PhpFilesAdapter extends AbstractAdapter implements PruneableInterface
} elseif ($this->appendOnly) {
$value = new LazyValue($file);
}
} catch (\ErrorException $e) {
} catch (\ErrorException) {
return false;
} finally {
restore_error_handler();
@@ -208,10 +202,7 @@ class PhpFilesAdapter extends AbstractAdapter implements PruneableInterface
return $now < $expiresAt;
}
/**
* {@inheritdoc}
*/
protected function doSave(array $values, int $lifetime)
protected function doSave(array $values, int $lifetime): array|bool
{
$ok = true;
$expiry = $lifetime ? time() + $lifetime : 'PHP_INT_MAX';
@@ -245,7 +236,7 @@ class PhpFilesAdapter extends AbstractAdapter implements PruneableInterface
if ($isStaticValue) {
$value = "return [{$expiry}, {$value}];";
} elseif ($this->appendOnly) {
$value = "return [{$expiry}, static function () { return {$value}; }];";
$value = "return [{$expiry}, static fn () => {$value}];";
} else {
// We cannot use a closure here because of https://bugs.php.net/76982
$value = str_replace('\Symfony\Component\VarExporter\Internal\\', '', $value);
@@ -270,20 +261,14 @@ class PhpFilesAdapter extends AbstractAdapter implements PruneableInterface
return $ok;
}
/**
* {@inheritdoc}
*/
protected function doClear(string $namespace)
protected function doClear(string $namespace): bool
{
$this->values = [];
return $this->doCommonClear($namespace);
}
/**
* {@inheritdoc}
*/
protected function doDelete(array $ids)
protected function doDelete(array $ids): bool
{
foreach ($ids as $id) {
unset($this->values[$id]);
@@ -292,6 +277,9 @@ class PhpFilesAdapter extends AbstractAdapter implements PruneableInterface
return $this->doCommonDelete($ids);
}
/**
* @return bool
*/
protected function doUnlink(string $file)
{
unset(self::$valuesCache[$file]);
@@ -321,7 +309,7 @@ class PhpFilesAdapter extends AbstractAdapter implements PruneableInterface
*/
class LazyValue
{
public $file;
public string $file;
public function __construct(string $file)
{

View File

@@ -28,25 +28,25 @@ class ProxyAdapter implements AdapterInterface, CacheInterface, PruneableInterfa
use ContractsTrait;
use ProxyTrait;
private $namespace = '';
private $namespaceLen;
private $poolHash;
private $defaultLifetime;
private string $namespace = '';
private int $namespaceLen;
private string $poolHash;
private int $defaultLifetime;
private static $createCacheItem;
private static $setInnerItem;
private static \Closure $createCacheItem;
private static \Closure $setInnerItem;
public function __construct(CacheItemPoolInterface $pool, string $namespace = '', int $defaultLifetime = 0)
{
$this->pool = $pool;
$this->poolHash = $poolHash = spl_object_hash($pool);
$this->poolHash = spl_object_hash($pool);
if ('' !== $namespace) {
\assert('' !== CacheItem::validateKey($namespace));
$this->namespace = $namespace;
}
$this->namespaceLen = \strlen($namespace);
$this->defaultLifetime = $defaultLifetime;
self::$createCacheItem ?? self::$createCacheItem = \Closure::bind(
self::$createCacheItem ??= \Closure::bind(
static function ($key, $innerItem, $poolHash) {
$item = new CacheItem();
$item->key = $key;
@@ -55,20 +55,12 @@ class ProxyAdapter implements AdapterInterface, CacheInterface, PruneableInterfa
return $item;
}
$item->value = $v = $innerItem->get();
$item->value = $innerItem->get();
$item->isHit = $innerItem->isHit();
$item->innerItem = $innerItem;
$item->poolHash = $poolHash;
// Detect wrapped values that encode for their expiry and creation duration
// For compactness, these values are packed in the key of an array using
// magic numbers in the form 9D-..-..-..-..-00-..-..-..-5F
if (\is_array($v) && 1 === \count($v) && 10 === \strlen($k = (string) array_key_first($v)) && "\x9D" === $k[0] && "\0" === $k[5] && "\x5F" === $k[9]) {
$item->value = $v[$k];
$v = unpack('Ve/Nc', substr($k, 1, -1));
$item->metadata[CacheItem::METADATA_EXPIRY] = $v['e'] + CacheItem::METADATA_EXPIRY_OFFSET;
$item->metadata[CacheItem::METADATA_CTIME] = $v['c'];
} elseif ($innerItem instanceof CacheItem) {
if (!$item->unpack() && $innerItem instanceof CacheItem) {
$item->metadata = $innerItem->metadata;
}
$innerItem->set(null);
@@ -78,31 +70,17 @@ class ProxyAdapter implements AdapterInterface, CacheInterface, PruneableInterfa
null,
CacheItem::class
);
self::$setInnerItem ?? self::$setInnerItem = \Closure::bind(
/**
* @param array $item A CacheItem cast to (array); accessing protected properties requires adding the "\0*\0" PHP prefix
*/
static function (CacheItemInterface $innerItem, array $item) {
// Tags are stored separately, no need to account for them when considering this item's newly set metadata
if (isset(($metadata = $item["\0*\0newMetadata"])[CacheItem::METADATA_TAGS])) {
unset($metadata[CacheItem::METADATA_TAGS]);
}
if ($metadata) {
// For compactness, expiry and creation duration are packed in the key of an array, using magic numbers as separators
$item["\0*\0value"] = ["\x9D".pack('VN', (int) (0.1 + $metadata[self::METADATA_EXPIRY] - self::METADATA_EXPIRY_OFFSET), $metadata[self::METADATA_CTIME])."\x5F" => $item["\0*\0value"]];
}
$innerItem->set($item["\0*\0value"]);
$innerItem->expiresAt(null !== $item["\0*\0expiry"] ? \DateTime::createFromFormat('U.u', sprintf('%.6F', $item["\0*\0expiry"])) : null);
self::$setInnerItem ??= \Closure::bind(
static function (CacheItemInterface $innerItem, CacheItem $item, $expiry = null) {
$innerItem->set($item->pack());
$innerItem->expiresAt(($expiry ?? $item->expiry) ? \DateTimeImmutable::createFromFormat('U.u', sprintf('%.6F', $expiry ?? $item->expiry)) : null);
},
null,
CacheItem::class
);
}
/**
* {@inheritdoc}
*/
public function get(string $key, callable $callback, float $beta = null, array &$metadata = null)
public function get(string $key, callable $callback, float $beta = null, array &$metadata = null): mixed
{
if (!$this->pool instanceof CacheInterface) {
return $this->doGet($this, $key, $callback, $beta, $metadata);
@@ -111,26 +89,20 @@ class ProxyAdapter implements AdapterInterface, CacheInterface, PruneableInterfa
return $this->pool->get($this->getId($key), function ($innerItem, bool &$save) use ($key, $callback) {
$item = (self::$createCacheItem)($key, $innerItem, $this->poolHash);
$item->set($value = $callback($item, $save));
(self::$setInnerItem)($innerItem, (array) $item);
(self::$setInnerItem)($innerItem, $item);
return $value;
}, $beta, $metadata);
}
/**
* {@inheritdoc}
*/
public function getItem($key)
public function getItem(mixed $key): CacheItem
{
$item = $this->pool->getItem($this->getId($key));
return (self::$createCacheItem)($key, $item, $this->poolHash);
}
/**
* {@inheritdoc}
*/
public function getItems(array $keys = [])
public function getItems(array $keys = []): iterable
{
if ($this->namespaceLen) {
foreach ($keys as $i => $key) {
@@ -141,22 +113,12 @@ class ProxyAdapter implements AdapterInterface, CacheInterface, PruneableInterfa
return $this->generateItems($this->pool->getItems($keys));
}
/**
* {@inheritdoc}
*
* @return bool
*/
public function hasItem($key)
public function hasItem(mixed $key): bool
{
return $this->pool->hasItem($this->getId($key));
}
/**
* {@inheritdoc}
*
* @return bool
*/
public function clear(string $prefix = '')
public function clear(string $prefix = ''): bool
{
if ($this->pool instanceof AdapterInterface) {
return $this->pool->clear($this->namespace.$prefix);
@@ -165,22 +127,12 @@ class ProxyAdapter implements AdapterInterface, CacheInterface, PruneableInterfa
return $this->pool->clear();
}
/**
* {@inheritdoc}
*
* @return bool
*/
public function deleteItem($key)
public function deleteItem(mixed $key): bool
{
return $this->pool->deleteItem($this->getId($key));
}
/**
* {@inheritdoc}
*
* @return bool
*/
public function deleteItems(array $keys)
public function deleteItems(array $keys): bool
{
if ($this->namespaceLen) {
foreach ($keys as $i => $key) {
@@ -191,57 +143,43 @@ class ProxyAdapter implements AdapterInterface, CacheInterface, PruneableInterfa
return $this->pool->deleteItems($keys);
}
/**
* {@inheritdoc}
*
* @return bool
*/
public function save(CacheItemInterface $item)
public function save(CacheItemInterface $item): bool
{
return $this->doSave($item, __FUNCTION__);
}
/**
* {@inheritdoc}
*
* @return bool
*/
public function saveDeferred(CacheItemInterface $item)
public function saveDeferred(CacheItemInterface $item): bool
{
return $this->doSave($item, __FUNCTION__);
}
/**
* {@inheritdoc}
*
* @return bool
*/
public function commit()
public function commit(): bool
{
return $this->pool->commit();
}
private function doSave(CacheItemInterface $item, string $method)
private function doSave(CacheItemInterface $item, string $method): bool
{
if (!$item instanceof CacheItem) {
return false;
}
$item = (array) $item;
if (null === $item["\0*\0expiry"] && 0 < $this->defaultLifetime) {
$item["\0*\0expiry"] = microtime(true) + $this->defaultLifetime;
$castItem = (array) $item;
if (null === $castItem["\0*\0expiry"] && 0 < $this->defaultLifetime) {
$castItem["\0*\0expiry"] = microtime(true) + $this->defaultLifetime;
}
if ($item["\0*\0poolHash"] === $this->poolHash && $item["\0*\0innerItem"]) {
$innerItem = $item["\0*\0innerItem"];
if ($castItem["\0*\0poolHash"] === $this->poolHash && $castItem["\0*\0innerItem"]) {
$innerItem = $castItem["\0*\0innerItem"];
} elseif ($this->pool instanceof AdapterInterface) {
// this is an optimization specific for AdapterInterface implementations
// so we can save a round-trip to the backend by just creating a new item
$innerItem = (self::$createCacheItem)($this->namespace.$item["\0*\0key"], null, $this->poolHash);
$innerItem = (self::$createCacheItem)($this->namespace.$castItem["\0*\0key"], null, $this->poolHash);
} else {
$innerItem = $this->pool->getItem($this->namespace.$item["\0*\0key"]);
$innerItem = $this->pool->getItem($this->namespace.$castItem["\0*\0key"]);
}
(self::$setInnerItem)($innerItem, $item);
(self::$setInnerItem)($innerItem, $item, $castItem["\0*\0expiry"]);
return $this->pool->$method($innerItem);
}
@@ -259,7 +197,7 @@ class ProxyAdapter implements AdapterInterface, CacheInterface, PruneableInterfa
}
}
private function getId($key): string
private function getId(mixed $key): string
{
\assert('' !== CacheItem::validateKey($key));

View File

@@ -30,7 +30,7 @@ class Psr16Adapter extends AbstractAdapter implements PruneableInterface, Resett
*/
protected const NS_SEPARATOR = '_';
private $miss;
private object $miss;
public function __construct(CacheInterface $pool, string $namespace = '', int $defaultLifetime = 0)
{
@@ -40,10 +40,7 @@ class Psr16Adapter extends AbstractAdapter implements PruneableInterface, Resett
$this->miss = new \stdClass();
}
/**
* {@inheritdoc}
*/
protected function doFetch(array $ids)
protected function doFetch(array $ids): iterable
{
foreach ($this->pool->getMultiple($ids, $this->miss) as $key => $value) {
if ($this->miss !== $value) {
@@ -52,34 +49,22 @@ class Psr16Adapter extends AbstractAdapter implements PruneableInterface, Resett
}
}
/**
* {@inheritdoc}
*/
protected function doHave(string $id)
protected function doHave(string $id): bool
{
return $this->pool->has($id);
}
/**
* {@inheritdoc}
*/
protected function doClear(string $namespace)
protected function doClear(string $namespace): bool
{
return $this->pool->clear();
}
/**
* {@inheritdoc}
*/
protected function doDelete(array $ids)
protected function doDelete(array $ids): bool
{
return $this->pool->deleteMultiple($ids);
}
/**
* {@inheritdoc}
*/
protected function doSave(array $values, int $lifetime)
protected function doSave(array $values, int $lifetime): array|bool
{
return $this->pool->setMultiple($values, 0 === $lifetime ? null : $lifetime);
}

View File

@@ -12,20 +12,13 @@
namespace Symfony\Component\Cache\Adapter;
use Symfony\Component\Cache\Marshaller\MarshallerInterface;
use Symfony\Component\Cache\Traits\RedisClusterProxy;
use Symfony\Component\Cache\Traits\RedisProxy;
use Symfony\Component\Cache\Traits\RedisTrait;
class RedisAdapter extends AbstractAdapter
{
use RedisTrait;
/**
* @param \Redis|\RedisArray|\RedisCluster|\Predis\ClientInterface|RedisProxy|RedisClusterProxy $redis The redis client
* @param string $namespace The default namespace
* @param int $defaultLifetime The default lifetime
*/
public function __construct($redis, string $namespace = '', int $defaultLifetime = 0, MarshallerInterface $marshaller = null)
public function __construct(\Redis|\RedisArray|\RedisCluster|\Predis\ClientInterface|\Relay\Relay $redis, string $namespace = '', int $defaultLifetime = 0, MarshallerInterface $marshaller = null)
{
$this->init($redis, $namespace, $defaultLifetime, $marshaller);
}

View File

@@ -16,14 +16,13 @@ use Predis\Connection\Aggregate\PredisCluster;
use Predis\Connection\Aggregate\ReplicationInterface;
use Predis\Response\ErrorInterface;
use Predis\Response\Status;
use Relay\Relay;
use Symfony\Component\Cache\CacheItem;
use Symfony\Component\Cache\Exception\InvalidArgumentException;
use Symfony\Component\Cache\Exception\LogicException;
use Symfony\Component\Cache\Marshaller\DeflateMarshaller;
use Symfony\Component\Cache\Marshaller\MarshallerInterface;
use Symfony\Component\Cache\Marshaller\TagAwareMarshaller;
use Symfony\Component\Cache\Traits\RedisClusterProxy;
use Symfony\Component\Cache\Traits\RedisProxy;
use Symfony\Component\Cache\Traits\RedisTrait;
/**
@@ -56,28 +55,24 @@ class RedisTagAwareAdapter extends AbstractTagAwareAdapter
private const DEFAULT_CACHE_TTL = 8640000;
/**
* @var string|null detected eviction policy used on Redis server
* detected eviction policy used on Redis server.
*/
private $redisEvictionPolicy;
private $namespace;
private string $redisEvictionPolicy;
private string $namespace;
/**
* @param \Redis|\RedisArray|\RedisCluster|\Predis\ClientInterface|RedisProxy|RedisClusterProxy $redis The redis client
* @param string $namespace The default namespace
* @param int $defaultLifetime The default lifetime
*/
public function __construct($redis, string $namespace = '', int $defaultLifetime = 0, MarshallerInterface $marshaller = null)
public function __construct(\Redis|Relay|\RedisArray|\RedisCluster|\Predis\ClientInterface $redis, string $namespace = '', int $defaultLifetime = 0, MarshallerInterface $marshaller = null)
{
if ($redis instanceof \Predis\ClientInterface && $redis->getConnection() instanceof ClusterInterface && !$redis->getConnection() instanceof PredisCluster) {
throw new InvalidArgumentException(sprintf('Unsupported Predis cluster connection: only "%s" is, "%s" given.', PredisCluster::class, get_debug_type($redis->getConnection())));
}
if (\defined('Redis::OPT_COMPRESSION') && ($redis instanceof \Redis || $redis instanceof \RedisArray || $redis instanceof \RedisCluster)) {
$compression = $redis->getOption(\Redis::OPT_COMPRESSION);
$isRelay = $redis instanceof Relay;
if ($isRelay || \defined('Redis::OPT_COMPRESSION') && \in_array($redis::class, [\Redis::class, \RedisArray::class, \RedisCluster::class], true)) {
$compression = $redis->getOption($isRelay ? Relay::OPT_COMPRESSION : \Redis::OPT_COMPRESSION);
foreach (\is_array($compression) ? $compression : [$compression] as $c) {
if (\Redis::COMPRESSION_NONE !== $c) {
throw new InvalidArgumentException(sprintf('phpredis compression must be disabled when using "%s", use "%s" instead.', static::class, DeflateMarshaller::class));
if ($isRelay ? Relay::COMPRESSION_NONE : \Redis::COMPRESSION_NONE !== $c) {
throw new InvalidArgumentException(sprintf('redis compression must be disabled when using "%s", use "%s" instead.', static::class, DeflateMarshaller::class));
}
}
}
@@ -86,9 +81,6 @@ class RedisTagAwareAdapter extends AbstractTagAwareAdapter
$this->namespace = $namespace;
}
/**
* {@inheritdoc}
*/
protected function doSave(array $values, int $lifetime, array $addTagData = [], array $delTagData = []): array
{
$eviction = $this->getRedisEvictionPolicy();
@@ -140,9 +132,6 @@ class RedisTagAwareAdapter extends AbstractTagAwareAdapter
return $failed;
}
/**
* {@inheritdoc}
*/
protected function doDeleteYieldTags(array $ids): iterable
{
$lua = <<<'EOLUA'
@@ -167,7 +156,7 @@ EOLUA;
});
foreach ($results as $id => $result) {
if ($result instanceof \RedisException || $result instanceof ErrorInterface) {
if ($result instanceof \RedisException || $result instanceof \Relay\Exception || $result instanceof ErrorInterface) {
CacheItem::log($this->logger, 'Failed to delete key "{key}": '.$result->getMessage(), ['key' => substr($id, \strlen($this->namespace)), 'exception' => $result]);
continue;
@@ -175,15 +164,12 @@ EOLUA;
try {
yield $id => !\is_string($result) || '' === $result ? [] : $this->marshaller->unmarshall($result);
} catch (\Exception $e) {
} catch (\Exception) {
yield $id => [];
}
}
}
/**
* {@inheritdoc}
*/
protected function doDeleteTagRelations(array $tagData): bool
{
$results = $this->pipeline(static function () use ($tagData) {
@@ -199,16 +185,13 @@ EOLUA;
return true;
}
/**
* {@inheritdoc}
*/
protected function doInvalidate(array $tagIds): bool
{
// This script scans the set of items linked to tag: it empties the set
// and removes the linked items. When the set is still not empty after
// the scan, it means we're in cluster mode and that the linked items
// are on other nodes: we move the links to a temporary set and we
// gargage collect that set from the client side.
// garbage collect that set from the client side.
$lua = <<<'EOLUA'
redis.replicate_commands()
@@ -240,7 +223,7 @@ EOLUA;
$results = $this->pipeline(function () use ($tagIds, $lua) {
if ($this->redis instanceof \Predis\ClientInterface) {
$prefix = $this->redis->getOptions()->prefix ? $this->redis->getOptions()->prefix->getPrefix() : '';
} elseif (\is_array($prefix = $this->redis->getOption(\Redis::OPT_PREFIX) ?? '')) {
} elseif (\is_array($prefix = $this->redis->getOption($this->redis instanceof Relay ? Relay::OPT_PREFIX : \Redis::OPT_PREFIX) ?? '')) {
$prefix = current($prefix);
}
@@ -261,7 +244,7 @@ EOLUA;
$success = true;
foreach ($results as $id => $values) {
if ($values instanceof \RedisException || $values instanceof ErrorInterface) {
if ($values instanceof \RedisException || $values instanceof \Relay\Exception || $values instanceof ErrorInterface) {
CacheItem::log($this->logger, 'Failed to invalidate key "{key}": '.$values->getMessage(), ['key' => substr($id, \strlen($this->namespace)), 'exception' => $values]);
$success = false;
@@ -297,7 +280,7 @@ EOLUA;
private function getRedisEvictionPolicy(): string
{
if (null !== $this->redisEvictionPolicy) {
if (isset($this->redisEvictionPolicy)) {
return $this->redisEvictionPolicy;
}
@@ -311,13 +294,13 @@ EOLUA;
foreach ($hosts as $host) {
$info = $host->info('Memory');
if ($info instanceof ErrorInterface) {
if (false === $info || null === $info || $info instanceof ErrorInterface) {
continue;
}
$info = $info['Memory'] ?? $info;
return $this->redisEvictionPolicy = $info['maxmemory_policy'];
return $this->redisEvictionPolicy = $info['maxmemory_policy'] ?? '';
}
return $this->redisEvictionPolicy = '';

View File

@@ -19,70 +19,73 @@ use Symfony\Component\Cache\CacheItem;
use Symfony\Component\Cache\PruneableInterface;
use Symfony\Component\Cache\ResettableInterface;
use Symfony\Component\Cache\Traits\ContractsTrait;
use Symfony\Component\Cache\Traits\ProxyTrait;
use Symfony\Contracts\Cache\TagAwareCacheInterface;
/**
* Implements simple and robust tag-based invalidation suitable for use with volatile caches.
*
* This adapter works by storing a version for each tags. When saving an item, it is stored together with its tags and
* their corresponding versions. When retrieving an item, those tag versions are compared to the current version of
* each tags. Invalidation is achieved by deleting tags, thereby ensuring that their versions change even when the
* storage is out of space. When versions of non-existing tags are requested for item commits, this adapter assigns a
* new random version to them.
*
* @author Nicolas Grekas <p@tchwork.com>
* @author Sergey Belyshkin <sbelyshkin@gmail.com>
*/
class TagAwareAdapter implements TagAwareAdapterInterface, TagAwareCacheInterface, PruneableInterface, ResettableInterface, LoggerAwareInterface
{
use ContractsTrait;
use LoggerAwareTrait;
use ProxyTrait;
public const TAGS_PREFIX = "\0tags\0";
public const TAGS_PREFIX = "\1tags\1";
private $deferred = [];
private $tags;
private $knownTagVersions = [];
private $knownTagVersionsTtl;
private array $deferred = [];
private AdapterInterface $pool;
private AdapterInterface $tags;
private array $knownTagVersions = [];
private float $knownTagVersionsTtl;
private static $createCacheItem;
private static $setCacheItemTags;
private static $getTagsByKey;
private static $saveTags;
private static \Closure $setCacheItemTags;
private static \Closure $setTagVersions;
private static \Closure $getTagsByKey;
private static \Closure $saveTags;
public function __construct(AdapterInterface $itemsPool, AdapterInterface $tagsPool = null, float $knownTagVersionsTtl = 0.15)
{
$this->pool = $itemsPool;
$this->tags = $tagsPool ?: $itemsPool;
$this->tags = $tagsPool ?? $itemsPool;
$this->knownTagVersionsTtl = $knownTagVersionsTtl;
self::$createCacheItem ?? self::$createCacheItem = \Closure::bind(
static function ($key, $value, CacheItem $protoItem) {
$item = new CacheItem();
$item->key = $key;
$item->value = $value;
$item->expiry = $protoItem->expiry;
$item->poolHash = $protoItem->poolHash;
self::$setCacheItemTags ??= \Closure::bind(
static function (array $items, array $itemTags) {
foreach ($items as $key => $item) {
$item->isTaggable = true;
return $item;
},
null,
CacheItem::class
);
self::$setCacheItemTags ?? self::$setCacheItemTags = \Closure::bind(
static function (CacheItem $item, $key, array &$itemTags) {
$item->isTaggable = true;
if (!$item->isHit) {
return $item;
}
if (isset($itemTags[$key])) {
foreach ($itemTags[$key] as $tag => $version) {
$item->metadata[CacheItem::METADATA_TAGS][$tag] = $tag;
if (isset($itemTags[$key])) {
$tags = array_keys($itemTags[$key]);
$item->metadata[CacheItem::METADATA_TAGS] = array_combine($tags, $tags);
} else {
$item->value = null;
$item->isHit = false;
$item->metadata = [];
}
unset($itemTags[$key]);
} else {
$item->value = null;
$item->isHit = false;
}
return $item;
return $items;
},
null,
CacheItem::class
);
self::$getTagsByKey ?? self::$getTagsByKey = \Closure::bind(
self::$setTagVersions ??= \Closure::bind(
static function (array $items, array $tagVersions) {
foreach ($items as $item) {
$item->newMetadata[CacheItem::METADATA_TAGS] = array_intersect_key($tagVersions, $item->newMetadata[CacheItem::METADATA_TAGS] ?? []);
}
},
null,
CacheItem::class
);
self::$getTagsByKey ??= \Closure::bind(
static function ($deferred) {
$tagsByKey = [];
foreach ($deferred as $key => $item) {
@@ -95,7 +98,7 @@ class TagAwareAdapter implements TagAwareAdapterInterface, TagAwareCacheInterfac
null,
CacheItem::class
);
self::$saveTags ?? self::$saveTags = \Closure::bind(
self::$saveTags ??= \Closure::bind(
static function (AdapterInterface $tagsAdapter, array $tags) {
ksort($tags);
@@ -111,10 +114,7 @@ class TagAwareAdapter implements TagAwareAdapterInterface, TagAwareCacheInterfac
);
}
/**
* {@inheritdoc}
*/
public function invalidateTags(array $tags)
public function invalidateTags(array $tags): bool
{
$ids = [];
foreach ($tags as $tag) {
@@ -126,56 +126,19 @@ class TagAwareAdapter implements TagAwareAdapterInterface, TagAwareCacheInterfac
return !$tags || $this->tags->deleteItems($ids);
}
/**
* {@inheritdoc}
*
* @return bool
*/
public function hasItem($key)
public function hasItem(mixed $key): bool
{
if (\is_string($key) && isset($this->deferred[$key])) {
$this->commit();
}
if (!$this->pool->hasItem($key)) {
return false;
}
$itemTags = $this->pool->getItem(static::TAGS_PREFIX.$key);
if (!$itemTags->isHit()) {
return false;
}
if (!$itemTags = $itemTags->get()) {
return true;
}
foreach ($this->getTagVersions([$itemTags]) as $tag => $version) {
if ($itemTags[$tag] !== $version) {
return false;
}
}
return true;
return $this->getItem($key)->isHit();
}
/**
* {@inheritdoc}
*/
public function getItem($key)
public function getItem(mixed $key): CacheItem
{
foreach ($this->getItems([$key]) as $item) {
return $item;
}
return null;
}
/**
* {@inheritdoc}
*/
public function getItems(array $keys = [])
public function getItems(array $keys = []): iterable
{
$tagKeys = [];
$commit = false;
@@ -184,7 +147,7 @@ class TagAwareAdapter implements TagAwareAdapterInterface, TagAwareCacheInterfac
if ('' !== $key && \is_string($key)) {
$commit = $commit || isset($this->deferred[$key]);
$key = static::TAGS_PREFIX.$key;
$tagKeys[$key] = $key;
$tagKeys[$key] = $key; // BC with pools populated before v6.1
}
}
@@ -200,15 +163,38 @@ class TagAwareAdapter implements TagAwareAdapterInterface, TagAwareCacheInterfac
throw $e;
}
return $this->generateItems($items, $tagKeys);
$bufferedItems = $itemTags = [];
foreach ($items as $key => $item) {
if (isset($tagKeys[$key])) { // BC with pools populated before v6.1
if ($item->isHit()) {
$itemTags[substr($key, \strlen(static::TAGS_PREFIX))] = $item->get() ?: [];
}
continue;
}
if (null !== $tags = $item->getMetadata()[CacheItem::METADATA_TAGS] ?? null) {
$itemTags[$key] = $tags;
}
$bufferedItems[$key] = $item;
}
$tagVersions = $this->getTagVersions($itemTags, false);
foreach ($itemTags as $key => $tags) {
foreach ($tags as $tag => $version) {
if ($tagVersions[$tag] !== $version) {
unset($itemTags[$key]);
continue 2;
}
}
}
$tagVersions = null;
return (self::$setCacheItemTags)($bufferedItems, $itemTags);
}
/**
* {@inheritdoc}
*
* @return bool
*/
public function clear(string $prefix = '')
public function clear(string $prefix = ''): bool
{
if ('' !== $prefix) {
foreach ($this->deferred as $key => $item) {
@@ -227,38 +213,23 @@ class TagAwareAdapter implements TagAwareAdapterInterface, TagAwareCacheInterfac
return $this->pool->clear();
}
/**
* {@inheritdoc}
*
* @return bool
*/
public function deleteItem($key)
public function deleteItem(mixed $key): bool
{
return $this->deleteItems([$key]);
}
/**
* {@inheritdoc}
*
* @return bool
*/
public function deleteItems(array $keys)
public function deleteItems(array $keys): bool
{
foreach ($keys as $key) {
if ('' !== $key && \is_string($key)) {
$keys[] = static::TAGS_PREFIX.$key;
$keys[] = static::TAGS_PREFIX.$key; // BC with pools populated before v6.1
}
}
return $this->pool->deleteItems($keys);
}
/**
* {@inheritdoc}
*
* @return bool
*/
public function save(CacheItemInterface $item)
public function save(CacheItemInterface $item): bool
{
if (!$item instanceof CacheItem) {
return false;
@@ -268,12 +239,7 @@ class TagAwareAdapter implements TagAwareAdapterInterface, TagAwareCacheInterfac
return $this->commit();
}
/**
* {@inheritdoc}
*
* @return bool
*/
public function saveDeferred(CacheItemInterface $item)
public function saveDeferred(CacheItemInterface $item): bool
{
if (!$item instanceof CacheItem) {
return false;
@@ -283,47 +249,55 @@ class TagAwareAdapter implements TagAwareAdapterInterface, TagAwareCacheInterfac
return true;
}
/**
* {@inheritdoc}
*
* @return bool
*/
public function commit()
public function commit(): bool
{
if (!$this->deferred) {
if (!$items = $this->deferred) {
return true;
}
$tagVersions = $this->getTagVersions((self::$getTagsByKey)($items), true);
(self::$setTagVersions)($items, $tagVersions);
$ok = true;
foreach ($this->deferred as $key => $item) {
if (!$this->pool->saveDeferred($item)) {
foreach ($items as $key => $item) {
if ($this->pool->saveDeferred($item)) {
unset($this->deferred[$key]);
} else {
$ok = false;
}
}
$ok = $this->pool->commit() && $ok;
$items = $this->deferred;
$tagsByKey = (self::$getTagsByKey)($items);
$this->deferred = [];
$tagVersions = array_keys($tagVersions);
(self::$setTagVersions)($items, array_combine($tagVersions, $tagVersions));
$tagVersions = $this->getTagVersions($tagsByKey);
$f = self::$createCacheItem;
return $ok;
}
foreach ($tagsByKey as $key => $tags) {
$this->pool->saveDeferred($f(static::TAGS_PREFIX.$key, array_intersect_key($tagVersions, $tags), $items[$key]));
}
return $this->pool->commit() && $ok;
public function prune(): bool
{
return $this->pool instanceof PruneableInterface && $this->pool->prune();
}
/**
* @return array
* @return void
*/
public function __sleep()
public function reset()
{
$this->commit();
$this->knownTagVersions = [];
$this->pool instanceof ResettableInterface && $this->pool->reset();
$this->tags instanceof ResettableInterface && $this->tags->reset();
}
public function __sleep(): array
{
throw new \BadMethodCallException('Cannot serialize '.__CLASS__);
}
/**
* @return void
*/
public function __wakeup()
{
throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);
@@ -334,59 +308,19 @@ class TagAwareAdapter implements TagAwareAdapterInterface, TagAwareCacheInterfac
$this->commit();
}
private function generateItems(iterable $items, array $tagKeys): \Generator
{
$bufferedItems = $itemTags = [];
$f = self::$setCacheItemTags;
foreach ($items as $key => $item) {
if (!$tagKeys) {
yield $key => $f($item, static::TAGS_PREFIX.$key, $itemTags);
continue;
}
if (!isset($tagKeys[$key])) {
$bufferedItems[$key] = $item;
continue;
}
unset($tagKeys[$key]);
if ($item->isHit()) {
$itemTags[$key] = $item->get() ?: [];
}
if (!$tagKeys) {
$tagVersions = $this->getTagVersions($itemTags);
foreach ($itemTags as $key => $tags) {
foreach ($tags as $tag => $version) {
if ($tagVersions[$tag] !== $version) {
unset($itemTags[$key]);
continue 2;
}
}
}
$tagVersions = $tagKeys = null;
foreach ($bufferedItems as $key => $item) {
yield $key => $f($item, static::TAGS_PREFIX.$key, $itemTags);
}
$bufferedItems = null;
}
}
}
private function getTagVersions(array $tagsByKey)
private function getTagVersions(array $tagsByKey, bool $persistTags): array
{
$tagVersions = [];
$fetchTagVersions = false;
$fetchTagVersions = $persistTags;
foreach ($tagsByKey as $tags) {
$tagVersions += $tags;
if ($fetchTagVersions) {
continue;
}
foreach ($tags as $tag => $version) {
if ($tagVersions[$tag] !== $version) {
unset($this->knownTagVersions[$tag]);
$fetchTagVersions = true;
}
}
}
@@ -399,8 +333,9 @@ class TagAwareAdapter implements TagAwareAdapterInterface, TagAwareCacheInterfac
$tags = [];
foreach ($tagVersions as $tag => $version) {
$tags[$tag.static::TAGS_PREFIX] = $tag;
if ($fetchTagVersions || ($this->knownTagVersions[$tag][1] ?? null) !== $version || $now - $this->knownTagVersions[$tag][0] >= $this->knownTagVersionsTtl) {
// reuse previously fetched tag versions up to the ttl
$knownTagVersion = $this->knownTagVersions[$tag] ?? [0, null];
if ($fetchTagVersions || $now > $knownTagVersion[0] || $knownTagVersion[1] !== $version) {
// reuse previously fetched tag versions until the expiration
$fetchTagVersions = true;
}
}
@@ -411,18 +346,26 @@ class TagAwareAdapter implements TagAwareAdapterInterface, TagAwareCacheInterfac
$newTags = [];
$newVersion = null;
$expiration = $now + $this->knownTagVersionsTtl;
foreach ($this->tags->getItems(array_keys($tags)) as $tag => $version) {
if (!$version->isHit()) {
$newTags[$tag] = $version->set($newVersion ?? $newVersion = random_int(\PHP_INT_MIN, \PHP_INT_MAX));
unset($this->knownTagVersions[$tag = $tags[$tag]]); // update FIFO
if (null !== $tagVersions[$tag] = $version->get()) {
$this->knownTagVersions[$tag] = [$expiration, $tagVersions[$tag]];
} elseif ($persistTags) {
$newTags[$tag] = $version->set($newVersion ??= random_bytes(6));
$tagVersions[$tag] = $newVersion;
$this->knownTagVersions[$tag] = [$expiration, $newVersion];
}
$tagVersions[$tag = $tags[$tag]] = $version->get();
$this->knownTagVersions[$tag] = [$now, $tagVersions[$tag]];
}
if ($newTags) {
(self::$saveTags)($this->tags, $newTags);
}
while ($now > ($this->knownTagVersions[$tag = array_key_first($this->knownTagVersions)][0] ?? \INF)) {
unset($this->knownTagVersions[$tag]);
}
return $tagVersions;
}
}

View File

@@ -25,9 +25,7 @@ interface TagAwareAdapterInterface extends AdapterInterface
*
* @param string[] $tags An array of tags to invalidate
*
* @return bool
*
* @throws InvalidArgumentException When $tags is not valid
*/
public function invalidateTags(array $tags);
public function invalidateTags(array $tags): bool;
}

View File

@@ -28,17 +28,14 @@ use Symfony\Contracts\Service\ResetInterface;
class TraceableAdapter implements AdapterInterface, CacheInterface, PruneableInterface, ResettableInterface
{
protected $pool;
private $calls = [];
private array $calls = [];
public function __construct(AdapterInterface $pool)
{
$this->pool = $pool;
}
/**
* {@inheritdoc}
*/
public function get(string $key, callable $callback, float $beta = null, array &$metadata = null)
public function get(string $key, callable $callback, float $beta = null, array &$metadata = null): mixed
{
if (!$this->pool instanceof CacheInterface) {
throw new \BadMethodCallException(sprintf('Cannot call "%s::get()": this class doesn\'t implement "%s".', get_debug_type($this->pool), CacheInterface::class));
@@ -67,10 +64,7 @@ class TraceableAdapter implements AdapterInterface, CacheInterface, PruneableInt
return $value;
}
/**
* {@inheritdoc}
*/
public function getItem($key)
public function getItem(mixed $key): CacheItem
{
$event = $this->start(__FUNCTION__);
try {
@@ -87,12 +81,7 @@ class TraceableAdapter implements AdapterInterface, CacheInterface, PruneableInt
return $item;
}
/**
* {@inheritdoc}
*
* @return bool
*/
public function hasItem($key)
public function hasItem(mixed $key): bool
{
$event = $this->start(__FUNCTION__);
try {
@@ -102,12 +91,7 @@ class TraceableAdapter implements AdapterInterface, CacheInterface, PruneableInt
}
}
/**
* {@inheritdoc}
*
* @return bool
*/
public function deleteItem($key)
public function deleteItem(mixed $key): bool
{
$event = $this->start(__FUNCTION__);
try {
@@ -117,12 +101,7 @@ class TraceableAdapter implements AdapterInterface, CacheInterface, PruneableInt
}
}
/**
* {@inheritdoc}
*
* @return bool
*/
public function save(CacheItemInterface $item)
public function save(CacheItemInterface $item): bool
{
$event = $this->start(__FUNCTION__);
try {
@@ -132,12 +111,7 @@ class TraceableAdapter implements AdapterInterface, CacheInterface, PruneableInt
}
}
/**
* {@inheritdoc}
*
* @return bool
*/
public function saveDeferred(CacheItemInterface $item)
public function saveDeferred(CacheItemInterface $item): bool
{
$event = $this->start(__FUNCTION__);
try {
@@ -147,10 +121,7 @@ class TraceableAdapter implements AdapterInterface, CacheInterface, PruneableInt
}
}
/**
* {@inheritdoc}
*/
public function getItems(array $keys = [])
public function getItems(array $keys = []): iterable
{
$event = $this->start(__FUNCTION__);
try {
@@ -173,12 +144,7 @@ class TraceableAdapter implements AdapterInterface, CacheInterface, PruneableInt
return $f();
}
/**
* {@inheritdoc}
*
* @return bool
*/
public function clear(string $prefix = '')
public function clear(string $prefix = ''): bool
{
$event = $this->start(__FUNCTION__);
try {
@@ -192,12 +158,7 @@ class TraceableAdapter implements AdapterInterface, CacheInterface, PruneableInt
}
}
/**
* {@inheritdoc}
*
* @return bool
*/
public function deleteItems(array $keys)
public function deleteItems(array $keys): bool
{
$event = $this->start(__FUNCTION__);
$event->result['keys'] = $keys;
@@ -208,12 +169,7 @@ class TraceableAdapter implements AdapterInterface, CacheInterface, PruneableInt
}
}
/**
* {@inheritdoc}
*
* @return bool
*/
public function commit()
public function commit(): bool
{
$event = $this->start(__FUNCTION__);
try {
@@ -223,10 +179,7 @@ class TraceableAdapter implements AdapterInterface, CacheInterface, PruneableInt
}
}
/**
* {@inheritdoc}
*/
public function prune()
public function prune(): bool
{
if (!$this->pool instanceof PruneableInterface) {
return false;
@@ -240,7 +193,7 @@ class TraceableAdapter implements AdapterInterface, CacheInterface, PruneableInt
}
/**
* {@inheritdoc}
* @return void
*/
public function reset()
{
@@ -251,9 +204,6 @@ class TraceableAdapter implements AdapterInterface, CacheInterface, PruneableInt
$this->clearCalls();
}
/**
* {@inheritdoc}
*/
public function delete(string $key): bool
{
$event = $this->start(__FUNCTION__);
@@ -264,16 +214,30 @@ class TraceableAdapter implements AdapterInterface, CacheInterface, PruneableInt
}
}
/**
* @return array
*/
public function getCalls()
{
return $this->calls;
}
/**
* @return void
*/
public function clearCalls()
{
$this->calls = [];
}
public function getPool(): AdapterInterface
{
return $this->pool;
}
/**
* @return TraceableAdapterEvent
*/
protected function start(string $name)
{
$this->calls[] = $event = new TraceableAdapterEvent();
@@ -284,12 +248,15 @@ class TraceableAdapter implements AdapterInterface, CacheInterface, PruneableInt
}
}
/**
* @internal
*/
class TraceableAdapterEvent
{
public $name;
public $start;
public $end;
public $result;
public $hits = 0;
public $misses = 0;
public string $name;
public float $start;
public float $end;
public array|bool $result;
public int $hits = 0;
public int $misses = 0;
}

View File

@@ -23,10 +23,7 @@ class TraceableTagAwareAdapter extends TraceableAdapter implements TagAwareAdapt
parent::__construct($pool);
}
/**
* {@inheritdoc}
*/
public function invalidateTags(array $tags)
public function invalidateTags(array $tags): bool
{
$event = $this->start(__FUNCTION__);
try {