The start of something beautiful

This commit is contained in:
2024-09-11 22:48:07 -06:00
parent 45acea47f3
commit f5997ee5ec
5614 changed files with 630696 additions and 0 deletions
+5
View File
@@ -0,0 +1,5 @@
CHANGELOG
=========
The changelog is maintained for all Symfony contracts at the following URL:
https://github.com/symfony/contracts/blob/main/CHANGELOG.md
+59
View File
@@ -0,0 +1,59 @@
<?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\Contracts\Cache;
use Psr\Cache\CacheItemInterface;
use Psr\Cache\InvalidArgumentException;
/**
* Covers most simple to advanced caching needs.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
interface CacheInterface
{
/**
* Fetches a value from the pool or computes it if not found.
*
* On cache misses, a callback is called that should return the missing value.
* This callback is given a PSR-6 CacheItemInterface instance corresponding to the
* requested key, that could be used e.g. for expiration control. It could also
* be an ItemInterface instance when its additional features are needed.
*
* @template T
*
* @param string $key The key of the item to retrieve from the cache
* @param (callable(CacheItemInterface,bool):T)|(callable(ItemInterface,bool):T)|CallbackInterface<T> $callback
* @param float|null $beta A float that, as it grows, controls the likeliness of triggering
* early expiration. 0 disables it, INF forces immediate expiration.
* The default (or providing null) is implementation dependent but should
* typically be 1.0, which should provide optimal stampede protection.
* See https://en.wikipedia.org/wiki/Cache_stampede#Probabilistic_early_expiration
* @param array &$metadata The metadata of the cached item {@see ItemInterface::getMetadata()}
*
* @return T
*
* @throws InvalidArgumentException When $key is not valid or when $beta is negative
*/
public function get(string $key, callable $callback, ?float $beta = null, ?array &$metadata = null): mixed;
/**
* Removes an item from the pool.
*
* @param string $key The key to delete
*
* @return bool True if the item was successfully removed, false if there was any error
*
* @throws InvalidArgumentException When $key is not valid
*/
public function delete(string $key): bool;
}
+72
View File
@@ -0,0 +1,72 @@
<?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\Contracts\Cache;
use Psr\Cache\CacheItemPoolInterface;
use Psr\Cache\InvalidArgumentException;
use Psr\Log\LoggerInterface;
// Help opcache.preload discover always-needed symbols
class_exists(InvalidArgumentException::class);
/**
* An implementation of CacheInterface for PSR-6 CacheItemPoolInterface classes.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
trait CacheTrait
{
public function get(string $key, callable $callback, ?float $beta = null, ?array &$metadata = null): mixed
{
return $this->doGet($this, $key, $callback, $beta, $metadata);
}
public function delete(string $key): bool
{
return $this->deleteItem($key);
}
private function doGet(CacheItemPoolInterface $pool, string $key, callable $callback, ?float $beta, ?array &$metadata = null, ?LoggerInterface $logger = null): mixed
{
if (0 > $beta ??= 1.0) {
throw new class(sprintf('Argument "$beta" provided to "%s::get()" must be a positive number, %f given.', static::class, $beta)) extends \InvalidArgumentException implements InvalidArgumentException {};
}
$item = $pool->getItem($key);
$recompute = !$item->isHit() || \INF === $beta;
$metadata = $item instanceof ItemInterface ? $item->getMetadata() : [];
if (!$recompute && $metadata) {
$expiry = $metadata[ItemInterface::METADATA_EXPIRY] ?? false;
$ctime = $metadata[ItemInterface::METADATA_CTIME] ?? false;
if ($recompute = $ctime && $expiry && $expiry <= ($now = microtime(true)) - $ctime / 1000 * $beta * log(random_int(1, \PHP_INT_MAX) / \PHP_INT_MAX)) {
// force applying defaultLifetime to expiry
$item->expiresAt(null);
$logger?->info('Item "{key}" elected for early recomputation {delta}s before its expiration', [
'key' => $key,
'delta' => sprintf('%.1f', $expiry - $now),
]);
}
}
if ($recompute) {
$save = true;
$item->set($callback($item, $save));
if ($save) {
$pool->save($item);
}
}
return $item->get();
}
}
+32
View File
@@ -0,0 +1,32 @@
<?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\Contracts\Cache;
use Psr\Cache\CacheItemInterface;
/**
* Computes and returns the cached value of an item.
*
* @author Nicolas Grekas <p@tchwork.com>
*
* @template T
*/
interface CallbackInterface
{
/**
* @param CacheItemInterface|ItemInterface $item The item to compute the value for
* @param bool &$save Should be set to false when the value should not be saved in the pool
*
* @return T The computed value for the passed item
*/
public function __invoke(CacheItemInterface $item, bool &$save): mixed;
}
+65
View File
@@ -0,0 +1,65 @@
<?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\Contracts\Cache;
use Psr\Cache\CacheException;
use Psr\Cache\CacheItemInterface;
use Psr\Cache\InvalidArgumentException;
/**
* Augments PSR-6's CacheItemInterface with support for tags and metadata.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
interface ItemInterface extends CacheItemInterface
{
/**
* References the Unix timestamp stating when the item will expire.
*/
public const METADATA_EXPIRY = 'expiry';
/**
* References the time the item took to be created, in milliseconds.
*/
public const METADATA_CTIME = 'ctime';
/**
* References the list of tags that were assigned to the item, as string[].
*/
public const METADATA_TAGS = 'tags';
/**
* Reserved characters that cannot be used in a key or tag.
*/
public const RESERVED_CHARACTERS = '{}()/\@:';
/**
* Adds a tag to a cache item.
*
* Tags are strings that follow the same validation rules as keys.
*
* @param string|string[] $tags A tag or array of tags
*
* @return $this
*
* @throws InvalidArgumentException When $tag is not valid
* @throws CacheException When the item comes from a pool that is not tag-aware
*/
public function tag(string|iterable $tags): static;
/**
* Returns a list of metadata info that were saved alongside with the cached value.
*
* See ItemInterface::METADATA_* consts for keys potentially found in the returned array.
*/
public function getMetadata(): array;
}
+19
View File
@@ -0,0 +1,19 @@
Copyright (c) 2018-present Fabien Potencier
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
+9
View File
@@ -0,0 +1,9 @@
Symfony Cache Contracts
=======================
A set of abstractions extracted out of the Symfony components.
Can be used to build on semantics that the Symfony components proved useful and
that already have battle tested implementations.
See https://github.com/symfony/contracts/blob/main/README.md for more information.
@@ -0,0 +1,38 @@
<?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\Contracts\Cache;
use Psr\Cache\InvalidArgumentException;
/**
* Allows invalidating cached items using tags.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
interface TagAwareCacheInterface extends CacheInterface
{
/**
* Invalidates cached items using tags.
*
* When implemented on a PSR-6 pool, invalidation should not apply
* to deferred items. Instead, they should be committed as usual.
* This allows replacing old tagged values by new ones without
* race conditions.
*
* @param string[] $tags An array of tags to invalidate
*
* @return bool True on success
*
* @throws InvalidArgumentException When $tags is not valid
*/
public function invalidateTags(array $tags): bool;
}
+35
View File
@@ -0,0 +1,35 @@
{
"name": "symfony/cache-contracts",
"type": "library",
"description": "Generic abstractions related to caching",
"keywords": ["abstractions", "contracts", "decoupling", "interfaces", "interoperability", "standards"],
"homepage": "https://symfony.com",
"license": "MIT",
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"require": {
"php": ">=8.1",
"psr/cache": "^3.0"
},
"autoload": {
"psr-4": { "Symfony\\Contracts\\Cache\\": "" }
},
"minimum-stability": "dev",
"extra": {
"branch-alias": {
"dev-main": "3.5-dev"
},
"thanks": {
"name": "symfony/contracts",
"url": "https://github.com/symfony/contracts"
}
}
}
+191
View File
@@ -0,0 +1,191 @@
<?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 Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\Cache\CacheItem;
use Symfony\Component\Cache\Exception\InvalidArgumentException;
use Symfony\Component\Cache\ResettableInterface;
use Symfony\Component\Cache\Traits\AbstractAdapterTrait;
use Symfony\Component\Cache\Traits\ContractsTrait;
use Symfony\Contracts\Cache\CacheInterface;
/**
* @author Nicolas Grekas <p@tchwork.com>
*/
abstract class AbstractAdapter implements AdapterInterface, CacheInterface, LoggerAwareInterface, ResettableInterface
{
use AbstractAdapterTrait;
use ContractsTrait;
/**
* @internal
*/
protected const NS_SEPARATOR = ':';
private static bool $apcuSupported;
protected function __construct(string $namespace = '', int $defaultLifetime = 0)
{
$this->namespace = '' === $namespace ? '' : CacheItem::validateKey($namespace).static::NS_SEPARATOR;
$this->defaultLifetime = $defaultLifetime;
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 ??= \Closure::bind(
static function ($key, $value, $isHit) {
$item = new CacheItem();
$item->key = $key;
$item->value = $value;
$item->isHit = $isHit;
$item->unpack();
return $item;
},
null,
CacheItem::class
);
self::$mergeByLifetime ??= \Closure::bind(
static function ($deferred, $namespace, &$expiredIds, $getId, $defaultLifetime) {
$byLifetime = [];
$now = microtime(true);
$expiredIds = [];
foreach ($deferred as $key => $item) {
$key = (string) $key;
if (null === $item->expiry) {
$ttl = 0 < $defaultLifetime ? $defaultLifetime : 0;
} elseif (!$item->expiry) {
$ttl = 0;
} elseif (0 >= $ttl = (int) (0.1 + $item->expiry - $now)) {
$expiredIds[] = $getId($key);
continue;
}
$byLifetime[$ttl][$getId($key)] = $item->pack();
}
return $byLifetime;
},
null,
CacheItem::class
);
}
/**
* Returns the best possible adapter that your runtime supports.
*
* Using ApcuAdapter makes system caches compatible with read-only filesystems.
*/
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 ??= ApcuAdapter::isSupported()) {
return $opcache;
}
if ('cli' === \PHP_SAPI && !filter_var(\ini_get('apc.enable_cli'), \FILTER_VALIDATE_BOOL)) {
return $opcache;
}
$apcu = new ApcuAdapter($namespace, intdiv($defaultLifetime, 5), $version);
if (null !== $logger) {
$apcu->setLogger($logger);
}
return new ChainAdapter([$apcu, $opcache]);
}
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);
}
if (str_starts_with($dsn, 'memcached:')) {
return MemcachedAdapter::createConnection($dsn, $options);
}
if (str_starts_with($dsn, 'couchbase:')) {
if (class_exists('CouchbaseBucket') && CouchbaseBucketAdapter::isSupported()) {
return CouchbaseBucketAdapter::createConnection($dsn, $options);
}
return CouchbaseCollectionAdapter::createConnection($dsn, $options);
}
if (preg_match('/^(mysql|oci|pgsql|sqlsrv|sqlite):/', $dsn)) {
return PdoAdapter::createConnection($dsn, $options);
}
throw new InvalidArgumentException('Unsupported DSN: it does not start with "redis[s]:", "memcached:", "couchbase:", "mysql:", "oci:", "pgsql:", "sqlsrv:" nor "sqlite:".');
}
public function commit(): bool
{
$ok = true;
$byLifetime = (self::$mergeByLifetime)($this->deferred, $this->namespace, $expiredIds, $this->getId(...), $this->defaultLifetime);
$retry = $this->deferred = [];
if ($expiredIds) {
try {
$this->doDelete($expiredIds);
} catch (\Exception $e) {
$ok = false;
CacheItem::log($this->logger, 'Failed to delete expired items: '.$e->getMessage(), ['exception' => $e, 'cache-adapter' => get_debug_type($this)]);
}
}
foreach ($byLifetime as $lifetime => $values) {
try {
$e = $this->doSave($values, $lifetime);
} catch (\Exception $e) {
}
if (true === $e || [] === $e) {
continue;
}
if (\is_array($e) || 1 === \count($values)) {
foreach (\is_array($e) ? $e : array_keys($values) as $id) {
$ok = false;
$v = $values[$id];
$type = get_debug_type($v);
$message = sprintf('Failed to save key "{key}" of type %s%s', $type, $e instanceof \Exception ? ': '.$e->getMessage() : '.');
CacheItem::log($this->logger, $message, ['key' => substr($id, \strlen($this->namespace)), 'exception' => $e instanceof \Exception ? $e : null, 'cache-adapter' => get_debug_type($this)]);
}
} else {
foreach ($values as $id => $v) {
$retry[$lifetime][] = $id;
}
}
}
// When bulk-save failed, retry each item individually
foreach ($retry as $lifetime => $ids) {
foreach ($ids as $id) {
try {
$v = $byLifetime[$lifetime][$id];
$e = $this->doSave([$id => $v], $lifetime);
} catch (\Exception $e) {
}
if (true === $e || [] === $e) {
continue;
}
$ok = false;
$type = get_debug_type($v);
$message = sprintf('Failed to save key "{key}" of type %s%s', $type, $e instanceof \Exception ? ': '.$e->getMessage() : '.');
CacheItem::log($this->logger, $message, ['key' => substr($id, \strlen($this->namespace)), 'exception' => $e instanceof \Exception ? $e : null, 'cache-adapter' => get_debug_type($this)]);
}
}
return $ok;
}
}
+320
View File
@@ -0,0 +1,320 @@
<?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 Psr\Log\LoggerAwareInterface;
use Symfony\Component\Cache\CacheItem;
use Symfony\Component\Cache\Exception\InvalidArgumentException;
use Symfony\Component\Cache\ResettableInterface;
use Symfony\Component\Cache\Traits\AbstractAdapterTrait;
use Symfony\Component\Cache\Traits\ContractsTrait;
use Symfony\Contracts\Cache\TagAwareCacheInterface;
/**
* Abstract for native TagAware adapters.
*
* To keep info on tags, the tags are both serialized as part of cache value and provided as tag ids
* to Adapters on operations when needed for storage to doSave(), doDelete() & doInvalidate().
*
* @author Nicolas Grekas <p@tchwork.com>
* @author André Rømcke <andre.romcke+symfony@gmail.com>
*
* @internal
*/
abstract class AbstractTagAwareAdapter implements TagAwareAdapterInterface, TagAwareCacheInterface, LoggerAwareInterface, ResettableInterface
{
use AbstractAdapterTrait;
use ContractsTrait;
private const TAGS_PREFIX = "\1tags\1";
protected function __construct(string $namespace = '', int $defaultLifetime = 0)
{
$this->namespace = '' === $namespace ? '' : CacheItem::validateKey($namespace).':';
$this->defaultLifetime = $defaultLifetime;
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 ??= \Closure::bind(
static function ($key, $value, $isHit) {
$item = new CacheItem();
$item->key = $key;
$item->isTaggable = true;
// If structure does not match what we expect return item as is (no value and not a hit)
if (!\is_array($value) || !\array_key_exists('value', $value)) {
return $item;
}
$item->isHit = $isHit;
// Extract value, tags and meta data from the cache value
$item->value = $value['value'];
$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']);
$item->metadata[CacheItem::METADATA_EXPIRY] = $v['e'] + CacheItem::METADATA_EXPIRY_OFFSET;
$item->metadata[CacheItem::METADATA_CTIME] = $v['c'];
}
return $item;
},
null,
CacheItem::class
);
self::$mergeByLifetime ??= \Closure::bind(
static function ($deferred, &$expiredIds, $getId, $tagPrefix, $defaultLifetime) {
$byLifetime = [];
$now = microtime(true);
$expiredIds = [];
foreach ($deferred as $key => $item) {
$key = (string) $key;
if (null === $item->expiry) {
$ttl = 0 < $defaultLifetime ? $defaultLifetime : 0;
} elseif (!$item->expiry) {
$ttl = 0;
} elseif (0 >= $ttl = (int) (0.1 + $item->expiry - $now)) {
$expiredIds[] = $getId($key);
continue;
}
// Store Value and Tags on the cache value
if (isset(($metadata = $item->newMetadata)[CacheItem::METADATA_TAGS])) {
$value = ['value' => $item->value, 'tags' => $metadata[CacheItem::METADATA_TAGS]];
unset($metadata[CacheItem::METADATA_TAGS]);
} else {
$value = ['value' => $item->value, 'tags' => []];
}
if ($metadata) {
// For compactness, expiry and creation duration are packed, using magic numbers as separators
$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_key($value['tags'], $oldTags) as $addedTag) {
$value['tag-operations']['add'][] = $getId($tagPrefix.$addedTag);
}
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;
}
return $byLifetime;
},
null,
CacheItem::class
);
}
/**
* Persists several cache items immediately.
*
* @param array $values The values to cache, indexed by their cache identifier
* @param int $lifetime The lifetime of the cached values, 0 for persisting until manual cleaning
* @param array[] $addTagData Hash where key is tag id, and array value is list of cache id's to add to tag
* @param array[] $removeTagData Hash where key is tag id, and array value is list of cache id's to remove to tag
*
* @return array The identifiers that failed to be cached or a boolean stating if caching succeeded or not
*/
abstract protected function doSave(array $values, int $lifetime, array $addTagData = [], array $removeTagData = []): array;
/**
* Removes multiple items from the pool and their corresponding tags.
*
* @param array $ids An array of identifiers that should be removed from the pool
*/
abstract protected function doDelete(array $ids): bool;
/**
* Removes relations between tags and deleted items.
*
* @param array $tagData Array of tag => key identifiers that should be removed from the pool
*/
abstract protected function doDeleteTagRelations(array $tagData): bool;
/**
* Invalidates cached items using tags.
*
* @param string[] $tagIds An array of tags to invalidate, key is tag and value is tag id
*/
abstract protected function doInvalidate(array $tagIds): bool;
/**
* Delete items and yields the tags they were bound to.
*/
protected function doDeleteYieldTags(array $ids): iterable
{
foreach ($this->doFetch($ids) as $id => $value) {
yield $id => \is_array($value) && \is_array($value['tags'] ?? null) ? $value['tags'] : [];
}
$this->doDelete($ids);
}
public function commit(): bool
{
$ok = true;
$byLifetime = (self::$mergeByLifetime)($this->deferred, $expiredIds, $this->getId(...), self::TAGS_PREFIX, $this->defaultLifetime);
$retry = $this->deferred = [];
if ($expiredIds) {
// Tags are not cleaned up in this case, however that is done on invalidateTags().
try {
$this->doDelete($expiredIds);
} catch (\Exception $e) {
$ok = false;
CacheItem::log($this->logger, 'Failed to delete expired items: '.$e->getMessage(), ['exception' => $e, 'cache-adapter' => get_debug_type($this)]);
}
}
foreach ($byLifetime as $lifetime => $values) {
try {
$values = $this->extractTagData($values, $addTagData, $removeTagData);
$e = $this->doSave($values, $lifetime, $addTagData, $removeTagData);
} catch (\Exception $e) {
}
if (true === $e || [] === $e) {
continue;
}
if (\is_array($e) || 1 === \count($values)) {
foreach (\is_array($e) ? $e : array_keys($values) as $id) {
$ok = false;
$v = $values[$id];
$type = get_debug_type($v);
$message = sprintf('Failed to save key "{key}" of type %s%s', $type, $e instanceof \Exception ? ': '.$e->getMessage() : '.');
CacheItem::log($this->logger, $message, ['key' => substr($id, \strlen($this->namespace)), 'exception' => $e instanceof \Exception ? $e : null, 'cache-adapter' => get_debug_type($this)]);
}
} else {
foreach ($values as $id => $v) {
$retry[$lifetime][] = $id;
}
}
}
// When bulk-save failed, retry each item individually
foreach ($retry as $lifetime => $ids) {
foreach ($ids as $id) {
try {
$v = $byLifetime[$lifetime][$id];
$values = $this->extractTagData([$id => $v], $addTagData, $removeTagData);
$e = $this->doSave($values, $lifetime, $addTagData, $removeTagData);
} catch (\Exception $e) {
}
if (true === $e || [] === $e) {
continue;
}
$ok = false;
$type = get_debug_type($v);
$message = sprintf('Failed to save key "{key}" of type %s%s', $type, $e instanceof \Exception ? ': '.$e->getMessage() : '.');
CacheItem::log($this->logger, $message, ['key' => substr($id, \strlen($this->namespace)), 'exception' => $e instanceof \Exception ? $e : null, 'cache-adapter' => get_debug_type($this)]);
}
}
return $ok;
}
public function deleteItems(array $keys): bool
{
if (!$keys) {
return true;
}
$ok = true;
$ids = [];
$tagData = [];
foreach ($keys as $key) {
$ids[$key] = $this->getId($key);
unset($this->deferred[$key]);
}
try {
foreach ($this->doDeleteYieldTags(array_values($ids)) as $id => $tags) {
foreach ($tags as $tag) {
$tagData[$this->getId(self::TAGS_PREFIX.$tag)][] = $id;
}
}
} catch (\Exception) {
$ok = false;
}
try {
if ((!$tagData || $this->doDeleteTagRelations($tagData)) && $ok) {
return true;
}
} catch (\Exception) {
}
// When bulk-delete failed, retry each item individually
foreach ($ids as $key => $id) {
try {
$e = null;
if ($this->doDelete([$id])) {
continue;
}
} catch (\Exception $e) {
}
$message = 'Failed to delete key "{key}"'.($e instanceof \Exception ? ': '.$e->getMessage() : '.');
CacheItem::log($this->logger, $message, ['key' => $key, 'exception' => $e, 'cache-adapter' => get_debug_type($this)]);
$ok = false;
}
return $ok;
}
public function invalidateTags(array $tags): bool
{
if (!$tags) {
return false;
}
$tagIds = [];
foreach (array_unique($tags) as $tag) {
$tagIds[] = $this->getId(self::TAGS_PREFIX.$tag);
}
try {
if ($this->doInvalidate($tagIds)) {
return true;
}
} catch (\Exception $e) {
CacheItem::log($this->logger, 'Failed to invalidate tags: '.$e->getMessage(), ['exception' => $e, 'cache-adapter' => get_debug_type($this)]);
}
return false;
}
/**
* Extracts tags operation data from $values set in mergeByLifetime, and returns values without it.
*/
private function extractTagData(array $values, ?array &$addTagData, ?array &$removeTagData): array
{
$addTagData = $removeTagData = [];
foreach ($values as $id => $value) {
foreach ($value['tag-operations']['add'] as $tag => $tagId) {
$addTagData[$tagId][] = $id;
}
foreach ($value['tag-operations']['remove'] as $tag => $tagId) {
$removeTagData[$tagId][] = $id;
}
unset($values[$id]['tag-operations']);
}
return $values;
}
}
+35
View File
@@ -0,0 +1,35 @@
<?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 Psr\Cache\CacheItemPoolInterface;
use Symfony\Component\Cache\CacheItem;
// Help opcache.preload discover always-needed symbols
class_exists(CacheItem::class);
/**
* Interface for adapters managing instances of Symfony's CacheItem.
*
* @author Kévin Dunglas <dunglas@gmail.com>
*/
interface AdapterInterface extends CacheItemPoolInterface
{
public function getItem(mixed $key): CacheItem;
/**
* @return iterable<string, CacheItem>
*/
public function getItems(array $keys = []): iterable;
public function clear(string $prefix = ''): bool;
}
+116
View File
@@ -0,0 +1,116 @@
<?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 Symfony\Component\Cache\CacheItem;
use Symfony\Component\Cache\Exception\CacheException;
use Symfony\Component\Cache\Marshaller\MarshallerInterface;
/**
* @author Nicolas Grekas <p@tchwork.com>
*/
class ApcuAdapter extends AbstractAdapter
{
/**
* @throws CacheException if APCu is not enabled
*/
public function __construct(
string $namespace = '',
int $defaultLifetime = 0,
?string $version = null,
private ?MarshallerInterface $marshaller = null,
) {
if (!static::isSupported()) {
throw new CacheException('APCu is not enabled.');
}
if ('cli' === \PHP_SAPI) {
ini_set('apc.use_request_time', 0);
}
parent::__construct($namespace, $defaultLifetime);
if (null !== $version) {
CacheItem::validateKey($version);
if (!apcu_exists($version.'@'.$namespace)) {
$this->doClear($namespace);
apcu_add($version.'@'.$namespace, null);
}
}
}
public static function isSupported(): bool
{
return \function_exists('apcu_fetch') && filter_var(\ini_get('apc.enabled'), \FILTER_VALIDATE_BOOL);
}
protected function doFetch(array $ids): iterable
{
$unserializeCallbackHandler = ini_set('unserialize_callback_func', __CLASS__.'::handleUnserializeCallback');
try {
$values = [];
foreach (apcu_fetch($ids, $ok) ?: [] as $k => $v) {
if (null !== $v || $ok) {
$values[$k] = null !== $this->marshaller ? $this->marshaller->unmarshall($v) : $v;
}
}
return $values;
} catch (\Error $e) {
throw new \ErrorException($e->getMessage(), $e->getCode(), \E_ERROR, $e->getFile(), $e->getLine());
} finally {
ini_set('unserialize_callback_func', $unserializeCallbackHandler);
}
}
protected function doHave(string $id): bool
{
return apcu_exists($id);
}
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_BOOL))
? apcu_delete(new \APCUIterator(sprintf('/^%s/', preg_quote($namespace, '/')), \APC_ITER_KEY))
: apcu_clear_cache();
}
protected function doDelete(array $ids): bool
{
foreach ($ids as $id) {
apcu_delete($id);
}
return true;
}
protected function doSave(array $values, int $lifetime): array|bool
{
if (null !== $this->marshaller && (!$values = $this->marshaller->marshall($values, $failed))) {
return $failed;
}
try {
if (false === $failures = apcu_store($values, null, $lifetime)) {
$failures = $values;
}
return array_keys($failures);
} catch (\Throwable $e) {
if (1 === \count($values)) {
// Workaround https://github.com/krakjoe/apcu/issues/170
apcu_delete(array_key_first($values));
}
throw $e;
}
}
}
+359
View File
@@ -0,0 +1,359 @@
<?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 Psr\Cache\CacheItemInterface;
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerAwareTrait;
use Symfony\Component\Cache\CacheItem;
use Symfony\Component\Cache\Exception\InvalidArgumentException;
use Symfony\Component\Cache\ResettableInterface;
use Symfony\Contracts\Cache\CacheInterface;
/**
* An in-memory cache storage.
*
* Acts as a least-recently-used (LRU) storage when configured with a maximum number of items.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
class ArrayAdapter implements AdapterInterface, CacheInterface, LoggerAwareInterface, ResettableInterface
{
use LoggerAwareTrait;
private array $values = [];
private array $tags = [];
private array $expiries = [];
private static \Closure $createCacheItem;
/**
* @param bool $storeSerialized Disabling serialization can lead to cache corruptions when storing mutable values but increases performance otherwise
*/
public function __construct(
private int $defaultLifetime = 0,
private bool $storeSerialized = true,
private float $maxLifetime = 0,
private int $maxItems = 0,
) {
if (0 > $maxLifetime) {
throw new InvalidArgumentException(sprintf('Argument $maxLifetime must be positive, %F passed.', $maxLifetime));
}
if (0 > $maxItems) {
throw new InvalidArgumentException(sprintf('Argument $maxItems must be a positive integer, %d passed.', $maxItems));
}
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;
},
null,
CacheItem::class
);
}
public function get(string $key, callable $callback, ?float $beta = null, ?array &$metadata = null): mixed
{
$item = $this->getItem($key);
$metadata = $item->getMetadata();
// ArrayAdapter works in memory, we don't care about stampede protection
if (\INF === $beta || !$item->isHit()) {
$save = true;
$item->set($callback($item, $save));
if ($save) {
$this->save($item);
}
}
return $item->get();
}
public function delete(string $key): bool
{
return $this->deleteItem($key);
}
public function hasItem(mixed $key): bool
{
if (\is_string($key) && isset($this->expiries[$key]) && $this->expiries[$key] > microtime(true)) {
if ($this->maxItems) {
// Move the item last in the storage
$value = $this->values[$key];
unset($this->values[$key]);
$this->values[$key] = $value;
}
return true;
}
\assert('' !== CacheItem::validateKey($key));
return isset($this->expiries[$key]) && !$this->deleteItem($key);
}
public function getItem(mixed $key): CacheItem
{
if (!$isHit = $this->hasItem($key)) {
$value = null;
if (!$this->maxItems) {
// Track misses in non-LRU mode only
$this->values[$key] = null;
}
} else {
$value = $this->storeSerialized ? $this->unfreeze($key, $isHit) : $this->values[$key];
}
return (self::$createCacheItem)($key, $value, $isHit, $this->tags[$key] ?? null);
}
public function getItems(array $keys = []): iterable
{
\assert(self::validateKeys($keys));
return $this->generateItems($keys, microtime(true), self::$createCacheItem);
}
public function deleteItem(mixed $key): bool
{
\assert('' !== CacheItem::validateKey($key));
unset($this->values[$key], $this->tags[$key], $this->expiries[$key]);
return true;
}
public function deleteItems(array $keys): bool
{
foreach ($keys as $key) {
$this->deleteItem($key);
}
return true;
}
public function save(CacheItemInterface $item): bool
{
if (!$item instanceof CacheItem) {
return false;
}
$item = (array) $item;
$key = $item["\0*\0key"];
$value = $item["\0*\0value"];
$expiry = $item["\0*\0expiry"];
$now = microtime(true);
if (null !== $expiry) {
if (!$expiry) {
$expiry = \PHP_INT_MAX;
} elseif ($expiry <= $now) {
$this->deleteItem($key);
return true;
}
}
if ($this->storeSerialized && null === $value = $this->freeze($value, $key)) {
return false;
}
if (null === $expiry && 0 < $this->defaultLifetime) {
$expiry = $this->defaultLifetime;
$expiry = $now + ($expiry > ($this->maxLifetime ?: $expiry) ? $this->maxLifetime : $expiry);
} elseif ($this->maxLifetime && (null === $expiry || $expiry > $now + $this->maxLifetime)) {
$expiry = $now + $this->maxLifetime;
}
if ($this->maxItems) {
unset($this->values[$key], $this->tags[$key]);
// Iterate items and vacuum expired ones while we are at it
foreach ($this->values as $k => $v) {
if ($this->expiries[$k] > $now && \count($this->values) < $this->maxItems) {
break;
}
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;
}
public function saveDeferred(CacheItemInterface $item): bool
{
return $this->save($item);
}
public function commit(): bool
{
return true;
}
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 || str_starts_with($key, $prefix)) {
unset($this->values[$key], $this->tags[$key], $this->expiries[$key]);
}
}
if ($this->values) {
return true;
}
}
$this->values = $this->tags = $this->expiries = [];
return true;
}
/**
* Returns all cached values, with cache miss as null.
*/
public function getValues(): array
{
if (!$this->storeSerialized) {
return $this->values;
}
$values = $this->values;
foreach ($values as $k => $v) {
if (null === $v || 'N;' === $v) {
continue;
}
if (!\is_string($v) || !isset($v[2]) || ':' !== $v[1]) {
$values[$k] = serialize($v);
}
}
return $values;
}
public function reset(): void
{
$this->clear();
}
private function generateItems(array $keys, float $now, \Closure $f): \Generator
{
foreach ($keys as $i => $key) {
if (!$isHit = isset($this->expiries[$key]) && ($this->expiries[$key] > $now || !$this->deleteItem($key))) {
$value = null;
if (!$this->maxItems) {
// Track misses in non-LRU mode only
$this->values[$key] = null;
}
} else {
if ($this->maxItems) {
// Move the item last in the storage
$value = $this->values[$key];
unset($this->values[$key]);
$this->values[$key] = $value;
}
$value = $this->storeSerialized ? $this->unfreeze($key, $isHit) : $this->values[$key];
}
unset($keys[$i]);
yield $key => $f($key, $value, $isHit, $this->tags[$key] ?? null);
}
foreach ($keys as $key) {
yield $key => $f($key, null, false);
}
}
private function freeze($value, string $key): string|int|float|bool|array|\UnitEnum|null
{
if (null === $value) {
return 'N;';
}
if (\is_string($value)) {
// Serialize strings if they could be confused with serialized objects or arrays
if ('N;' === $value || (isset($value[2]) && ':' === $value[1])) {
return serialize($value);
}
} elseif (!\is_scalar($value)) {
try {
$serialized = serialize($value);
} catch (\Exception $e) {
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 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)) {
return $serialized;
}
}
return $value;
}
private function unfreeze(string $key, bool &$isHit): mixed
{
if ('N;' === $value = $this->values[$key]) {
return null;
}
if (\is_string($value) && isset($value[2]) && ':' === $value[1]) {
try {
$value = unserialize($value);
} catch (\Exception $e) {
CacheItem::log($this->logger, 'Failed to unserialize key "{key}": '.$e->getMessage(), ['key' => $key, 'exception' => $e, 'cache-adapter' => get_debug_type($this)]);
$value = false;
}
if (false === $value) {
$value = null;
$isHit = false;
if (!$this->maxItems) {
$this->values[$key] = null;
}
}
}
return $value;
}
private function validateKeys(array $keys): bool
{
foreach ($keys as $key) {
if (!\is_string($key) || !isset($this->expiries[$key])) {
CacheItem::validateKey($key);
}
}
return true;
}
}
+291
View File
@@ -0,0 +1,291 @@
<?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 Psr\Cache\CacheItemInterface;
use Psr\Cache\CacheItemPoolInterface;
use Symfony\Component\Cache\CacheItem;
use Symfony\Component\Cache\Exception\InvalidArgumentException;
use Symfony\Component\Cache\PruneableInterface;
use Symfony\Component\Cache\ResettableInterface;
use Symfony\Component\Cache\Traits\ContractsTrait;
use Symfony\Contracts\Cache\CacheInterface;
use Symfony\Contracts\Service\ResetInterface;
/**
* Chains several adapters together.
*
* Cached items are fetched from the first adapter having them in its data store.
* They are saved and deleted in all adapters at once.
*
* @author Kévin Dunglas <dunglas@gmail.com>
*/
class ChainAdapter implements AdapterInterface, CacheInterface, PruneableInterface, ResettableInterface
{
use ContractsTrait;
private array $adapters = [];
private int $adapterCount;
private static \Closure $syncItem;
/**
* @param CacheItemPoolInterface[] $adapters The ordered list of adapters used to fetch cached items
* @param int $defaultLifetime The default lifetime of items propagated from lower adapters to upper ones
*/
public function __construct(
array $adapters,
private int $defaultLifetime = 0,
) {
if (!$adapters) {
throw new InvalidArgumentException('At least one adapter must be specified.');
}
foreach ($adapters as $adapter) {
if (!$adapter instanceof CacheItemPoolInterface) {
throw new InvalidArgumentException(sprintf('The class "%s" does not implement the "%s" interface.', get_debug_type($adapter), CacheItemPoolInterface::class));
}
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
}
if ($adapter instanceof AdapterInterface) {
$this->adapters[] = $adapter;
} else {
$this->adapters[] = new ProxyAdapter($adapter);
}
}
$this->adapterCount = \count($this->adapters);
self::$syncItem ??= \Closure::bind(
static function ($sourceItem, $item, $defaultLifetime, $sourceMetadata = null) {
$sourceItem->isTaggable = false;
$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(\DateTimeImmutable::createFromFormat('U.u', sprintf('%.6F', $item->metadata[CacheItem::METADATA_EXPIRY])));
} elseif (0 < $defaultLifetime) {
$item->expiresAfter($defaultLifetime);
}
return $item;
},
null,
CacheItem::class
);
}
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) {
$value = $callback($item, $save);
$doSave = $save;
return $value;
};
$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;
$beta = \INF === $beta ? \INF : 0;
}
if ($adapter instanceof CacheInterface) {
$value = $adapter->get($key, $callback, $beta, $metadata);
} else {
$value = $this->doGet($adapter, $key, $callback, $beta, $metadata);
}
if (null !== $item) {
(self::$syncItem)($lastItem ??= $item, $item, $this->defaultLifetime, $metadata);
}
$save = $doSave;
return $value;
};
return $wrap();
}
public function getItem(mixed $key): CacheItem
{
$syncItem = self::$syncItem;
$misses = [];
foreach ($this->adapters as $i => $adapter) {
$item = $adapter->getItem($key);
if ($item->isHit()) {
while (0 <= --$i) {
$this->adapters[$i]->save($syncItem($item, $misses[$i], $this->defaultLifetime));
}
return $item;
}
$misses[$i] = $item;
}
return $item;
}
public function getItems(array $keys = []): iterable
{
return $this->generateItems($this->adapters[0]->getItems($keys), 0);
}
private function generateItems(iterable $items, int $adapterIndex): \Generator
{
$missing = [];
$misses = [];
$nextAdapterIndex = $adapterIndex + 1;
$nextAdapter = $this->adapters[$nextAdapterIndex] ?? null;
foreach ($items as $k => $item) {
if (!$nextAdapter || $item->isHit()) {
yield $k => $item;
} else {
$missing[] = $k;
$misses[$k] = $item;
}
}
if ($missing) {
$syncItem = self::$syncItem;
$adapter = $this->adapters[$adapterIndex];
$items = $this->generateItems($nextAdapter->getItems($missing), $nextAdapterIndex);
foreach ($items as $k => $item) {
if ($item->isHit()) {
$adapter->save($syncItem($item, $misses[$k], $this->defaultLifetime));
}
yield $k => $item;
}
}
}
public function hasItem(mixed $key): bool
{
foreach ($this->adapters as $adapter) {
if ($adapter->hasItem($key)) {
return true;
}
}
return false;
}
public function clear(string $prefix = ''): bool
{
$cleared = true;
$i = $this->adapterCount;
while ($i--) {
if ($this->adapters[$i] instanceof AdapterInterface) {
$cleared = $this->adapters[$i]->clear($prefix) && $cleared;
} else {
$cleared = $this->adapters[$i]->clear() && $cleared;
}
}
return $cleared;
}
public function deleteItem(mixed $key): bool
{
$deleted = true;
$i = $this->adapterCount;
while ($i--) {
$deleted = $this->adapters[$i]->deleteItem($key) && $deleted;
}
return $deleted;
}
public function deleteItems(array $keys): bool
{
$deleted = true;
$i = $this->adapterCount;
while ($i--) {
$deleted = $this->adapters[$i]->deleteItems($keys) && $deleted;
}
return $deleted;
}
public function save(CacheItemInterface $item): bool
{
$saved = true;
$i = $this->adapterCount;
while ($i--) {
$saved = $this->adapters[$i]->save($item) && $saved;
}
return $saved;
}
public function saveDeferred(CacheItemInterface $item): bool
{
$saved = true;
$i = $this->adapterCount;
while ($i--) {
$saved = $this->adapters[$i]->saveDeferred($item) && $saved;
}
return $saved;
}
public function commit(): bool
{
$committed = true;
$i = $this->adapterCount;
while ($i--) {
$committed = $this->adapters[$i]->commit() && $committed;
}
return $committed;
}
public function prune(): bool
{
$pruned = true;
foreach ($this->adapters as $adapter) {
if ($adapter instanceof PruneableInterface) {
$pruned = $adapter->prune() && $pruned;
}
}
return $pruned;
}
public function reset(): void
{
foreach ($this->adapters as $adapter) {
if ($adapter instanceof ResetInterface) {
$adapter->reset();
}
}
}
}
+237
View File
@@ -0,0 +1,237 @@
<?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 Symfony\Component\Cache\Exception\CacheException;
use Symfony\Component\Cache\Exception\InvalidArgumentException;
use Symfony\Component\Cache\Marshaller\DefaultMarshaller;
use Symfony\Component\Cache\Marshaller\MarshallerInterface;
trigger_deprecation('symfony/cache', '7.1', 'The "%s" class is deprecated, use "%s" instead.', CouchbaseBucketAdapter::class, CouchbaseCollectionAdapter::class);
/**
* @author Antonio Jose Cerezo Aranda <aj.cerezo@gmail.com>
*
* @deprecated since Symfony 7.1, use {@see CouchbaseCollectionAdapter} instead
*/
class CouchbaseBucketAdapter extends AbstractAdapter
{
private const THIRTY_DAYS_IN_SECONDS = 2592000;
private const MAX_KEY_LENGTH = 250;
private const KEY_NOT_FOUND = 13;
private const VALID_DSN_OPTIONS = [
'operationTimeout',
'configTimeout',
'configNodeTimeout',
'n1qlTimeout',
'httpTimeout',
'configDelay',
'htconfigIdleTimeout',
'durabilityInterval',
'durabilityTimeout',
];
private MarshallerInterface $marshaller;
public function __construct(
private \CouchbaseBucket $bucket,
string $namespace = '',
int $defaultLifetime = 0,
?MarshallerInterface $marshaller = null,
) {
if (!static::isSupported()) {
throw new CacheException('Couchbase >= 2.6.0 < 3.0.0 is required.');
}
$this->maxIdLength = static::MAX_KEY_LENGTH;
parent::__construct($namespace, $defaultLifetime);
$this->enableVersioning();
$this->marshaller = $marshaller ?? new DefaultMarshaller();
}
public static function createConnection(#[\SensitiveParameter] array|string $servers, array $options = []): \CouchbaseBucket
{
if (\is_string($servers)) {
$servers = [$servers];
}
if (!static::isSupported()) {
throw new CacheException('Couchbase >= 2.6.0 < 3.0.0 is required.');
}
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';
$newServers = [];
$protocol = 'couchbase';
try {
$options = self::initOptions($options);
$username = $options['username'];
$password = $options['password'];
foreach ($servers as $dsn) {
if (!str_starts_with($dsn, 'couchbase:')) {
throw new InvalidArgumentException('Invalid Couchbase DSN: it does not start with "couchbase:".');
}
preg_match($dsnPattern, $dsn, $matches);
$username = $matches['username'] ?: $username;
$password = $matches['password'] ?: $password;
$protocol = $matches['protocol'] ?: $protocol;
if (isset($matches['options'])) {
$optionsInDsn = self::getOptions($matches['options']);
foreach ($optionsInDsn as $parameter => $value) {
$options[$parameter] = $value;
}
}
$newServers[] = $matches['host'];
}
$connectionString = $protocol.'://'.implode(',', $newServers);
$client = new \CouchbaseCluster($connectionString);
$client->authenticateAs($username, $password);
$bucket = $client->openBucket($matches['bucketName']);
unset($options['username'], $options['password']);
foreach ($options as $option => $value) {
if ($value) {
$bucket->$option = $value;
}
}
return $bucket;
} finally {
restore_error_handler();
}
}
public static function isSupported(): bool
{
return \extension_loaded('couchbase') && version_compare(phpversion('couchbase'), '2.6.0', '>=') && version_compare(phpversion('couchbase'), '3.0', '<');
}
private static function getOptions(string $options): array
{
$results = [];
$optionsInArray = explode('&', $options);
foreach ($optionsInArray as $option) {
[$key, $value] = explode('=', $option);
if (\in_array($key, static::VALID_DSN_OPTIONS, true)) {
$results[$key] = $value;
}
}
return $results;
}
private static function initOptions(array $options): array
{
$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;
}
protected function doFetch(array $ids): iterable
{
$resultsCouchbase = $this->bucket->get($ids);
$results = [];
foreach ($resultsCouchbase as $key => $value) {
if (null !== $value->error) {
continue;
}
$results[$key] = $this->marshaller->unmarshall($value->value);
}
return $results;
}
protected function doHave(string $id): bool
{
return false !== $this->bucket->get($id);
}
protected function doClear(string $namespace): bool
{
if ('' === $namespace) {
$this->bucket->manager()->flush();
return true;
}
return false;
}
protected function doDelete(array $ids): bool
{
$results = $this->bucket->remove(array_values($ids));
foreach ($results as $key => $result) {
if (null !== $result->error && static::KEY_NOT_FOUND !== $result->error->getCode()) {
continue;
}
unset($results[$key]);
}
return 0 === \count($results);
}
protected function doSave(array $values, int $lifetime): array|bool
{
if (!$values = $this->marshaller->marshall($values, $failed)) {
return $failed;
}
$lifetime = $this->normalizeExpiry($lifetime);
$ko = [];
foreach ($values as $key => $value) {
$result = $this->bucket->upsert($key, $value, ['expiry' => $lifetime]);
if (null !== $result->error) {
$ko[$key] = $result;
}
}
return [] === $ko ? true : $ko;
}
private function normalizeExpiry(int $expiry): int
{
if ($expiry && $expiry > static::THIRTY_DAYS_IN_SECONDS) {
$expiry += time();
}
return $expiry;
}
}
@@ -0,0 +1,198 @@
<?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 Couchbase\Bucket;
use Couchbase\Cluster;
use Couchbase\ClusterOptions;
use Couchbase\Collection;
use Couchbase\DocumentNotFoundException;
use Couchbase\UpsertOptions;
use Symfony\Component\Cache\Exception\CacheException;
use Symfony\Component\Cache\Exception\InvalidArgumentException;
use Symfony\Component\Cache\Marshaller\DefaultMarshaller;
use Symfony\Component\Cache\Marshaller\MarshallerInterface;
/**
* @author Antonio Jose Cerezo Aranda <aj.cerezo@gmail.com>
*/
class CouchbaseCollectionAdapter extends AbstractAdapter
{
private const MAX_KEY_LENGTH = 250;
private MarshallerInterface $marshaller;
public function __construct(
private Collection $connection,
string $namespace = '',
int $defaultLifetime = 0,
?MarshallerInterface $marshaller = null,
) {
if (!static::isSupported()) {
throw new CacheException('Couchbase >= 3.0.5 < 4.0.0 is required.');
}
$this->maxIdLength = static::MAX_KEY_LENGTH;
parent::__construct($namespace, $defaultLifetime);
$this->enableVersioning();
$this->marshaller = $marshaller ?? new DefaultMarshaller();
}
public static function createConnection(#[\SensitiveParameter] array|string $dsn, array $options = []): Bucket|Collection
{
if (\is_string($dsn)) {
$dsn = [$dsn];
}
if (!static::isSupported()) {
throw new CacheException('Couchbase >= 3.0.5 < 4.0.0 is required.');
}
set_error_handler(static fn ($type, $msg, $file, $line) => throw new \ErrorException($msg, 0, $type, $file, $line));
$pathPattern = '/^(?:\/(?<bucketName>[^\/\?]+))(?:(?:\/(?<scopeName>[^\/]+))(?:\/(?<collectionName>[^\/\?]+)))?(?:\/)?$/';
$newServers = [];
$protocol = 'couchbase';
try {
$username = $options['username'] ?? '';
$password = $options['password'] ?? '';
foreach ($dsn as $server) {
if (!str_starts_with($server, 'couchbase:')) {
throw new InvalidArgumentException('Invalid Couchbase DSN: it does not start with "couchbase:".');
}
$params = parse_url($server);
$username = isset($params['user']) ? rawurldecode($params['user']) : $username;
$password = isset($params['pass']) ? rawurldecode($params['pass']) : $password;
$protocol = $params['scheme'] ?? $protocol;
if (isset($params['query'])) {
$optionsInDsn = self::getOptions($params['query']);
foreach ($optionsInDsn as $parameter => $value) {
$options[$parameter] = $value;
}
}
$newServers[] = $params['host'];
}
$option = isset($params['query']) ? '?'.$params['query'] : '';
$connectionString = $protocol.'://'.implode(',', $newServers).$option;
$clusterOptions = new ClusterOptions();
$clusterOptions->credentials($username, $password);
$client = new Cluster($connectionString, $clusterOptions);
preg_match($pathPattern, $params['path'] ?? '', $matches);
$bucket = $client->bucket($matches['bucketName']);
$collection = $bucket->defaultCollection();
if (!empty($matches['scopeName'])) {
$scope = $bucket->scope($matches['scopeName']);
$collection = $scope->collection($matches['collectionName']);
}
return $collection;
} finally {
restore_error_handler();
}
}
public static function isSupported(): bool
{
return \extension_loaded('couchbase') && version_compare(phpversion('couchbase'), '3.0.5', '>=') && version_compare(phpversion('couchbase'), '4.0', '<');
}
private static function getOptions(string $options): array
{
$results = [];
$optionsInArray = explode('&', $options);
foreach ($optionsInArray as $option) {
[$key, $value] = explode('=', $option);
$results[$key] = $value;
}
return $results;
}
protected function doFetch(array $ids): array
{
$results = [];
foreach ($ids as $id) {
try {
$resultCouchbase = $this->connection->get($id);
} catch (DocumentNotFoundException) {
continue;
}
$content = $resultCouchbase->value ?? $resultCouchbase->content();
$results[$id] = $this->marshaller->unmarshall($content);
}
return $results;
}
protected function doHave($id): bool
{
return $this->connection->exists($id)->exists();
}
protected function doClear($namespace): bool
{
return false;
}
protected function doDelete(array $ids): bool
{
$idsErrors = [];
foreach ($ids as $id) {
try {
$result = $this->connection->remove($id);
if (null === $result->mutationToken()) {
$idsErrors[] = $id;
}
} catch (DocumentNotFoundException) {
}
}
return 0 === \count($idsErrors);
}
protected function doSave(array $values, $lifetime): array|bool
{
if (!$values = $this->marshaller->marshall($values, $failed)) {
return $failed;
}
$upsertOptions = new UpsertOptions();
$upsertOptions->expiry($lifetime);
$ko = [];
foreach ($values as $key => $value) {
try {
$this->connection->upsert($key, $value, $upsertOptions);
} catch (\Exception) {
$ko[$key] = '';
}
}
return [] === $ko ? true : $ko;
}
}
+383
View File
@@ -0,0 +1,383 @@
<?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\DBAL\ArrayParameterType;
use Doctrine\DBAL\Configuration;
use Doctrine\DBAL\Connection;
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\Tools\DsnParser;
use Symfony\Component\Cache\Exception\InvalidArgumentException;
use Symfony\Component\Cache\Marshaller\DefaultMarshaller;
use Symfony\Component\Cache\Marshaller\MarshallerInterface;
use Symfony\Component\Cache\PruneableInterface;
class DoctrineDbalAdapter extends AbstractAdapter implements PruneableInterface
{
private const MAX_KEY_LENGTH = 255;
private MarshallerInterface $marshaller;
private Connection $conn;
private string $platformName;
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';
/**
* You can either pass an existing database Doctrine DBAL Connection or
* a DSN string that will be used to connect to the database.
*
* The cache table is created automatically when possible.
* Otherwise, use the createTable() method.
*
* List of available options:
* * db_table: The name of the table [default: cache_items]
* * db_id_col: The column where to store the cache id [default: item_id]
* * db_data_col: The column where to store the cache data [default: item_data]
* * 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]
*
* @throws InvalidArgumentException When namespace contains invalid characters
*/
public function __construct(
Connection|string $connOrDsn,
private 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]));
}
if ($connOrDsn instanceof Connection) {
$this->conn = $connOrDsn;
} else {
if (!class_exists(DriverManager::class)) {
throw new InvalidArgumentException('Failed to parse DSN. Try running "composer require doctrine/dbal".');
}
$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);
$config = new Configuration();
$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;
$this->lifetimeCol = $options['db_lifetime_col'] ?? $this->lifetimeCol;
$this->timeCol = $options['db_time_col'] ?? $this->timeCol;
$this->marshaller = $marshaller ?? new DefaultMarshaller();
parent::__construct($namespace, $defaultLifetime);
}
/**
* 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.
*
* @throws DBALException When the table already exists
*/
public function createTable(): void
{
$schema = new Schema();
$this->addTableToSchema($schema);
foreach ($schema->toSql($this->conn->getDatabasePlatform()) as $sql) {
$this->conn->executeStatement($sql);
}
}
public function configureSchema(Schema $schema, Connection $forConnection, \Closure $isSameDatabase): void
{
if ($schema->hasTable($this->table)) {
return;
}
if ($forConnection !== $this->conn && !$isSameDatabase($this->conn->executeStatement(...))) {
return;
}
$this->addTableToSchema($schema);
}
public function prune(): bool
{
$deleteSql = "DELETE FROM $this->table WHERE $this->lifetimeCol + $this->timeCol <= ?";
$params = [time()];
$paramTypes = [ParameterType::INTEGER];
if ('' !== $this->namespace) {
$deleteSql .= " AND $this->idCol LIKE ?";
$params[] = sprintf('%s%%', $this->namespace);
$paramTypes[] = ParameterType::STRING;
}
try {
$this->conn->executeStatement($deleteSql, $params, $paramTypes);
} catch (TableNotFoundException) {
}
return true;
}
protected function doFetch(array $ids): iterable
{
$now = time();
$expired = [];
$sql = "SELECT $this->idCol, CASE WHEN $this->lifetimeCol IS NULL OR $this->lifetimeCol + $this->timeCol > ? THEN $this->dataCol ELSE NULL END FROM $this->table WHERE $this->idCol IN (?)";
$result = $this->conn->executeQuery($sql, [
$now,
$ids,
], [
ParameterType::INTEGER,
ArrayParameterType::STRING,
])->iterateNumeric();
foreach ($result as $row) {
if (null === $row[1]) {
$expired[] = $row[0];
} else {
yield $row[0] => $this->marshaller->unmarshall(\is_resource($row[1]) ? stream_get_contents($row[1]) : $row[1]);
}
}
if ($expired) {
$sql = "DELETE FROM $this->table WHERE $this->lifetimeCol + $this->timeCol <= ? AND $this->idCol IN (?)";
$this->conn->executeStatement($sql, [
$now,
$expired,
], [
ParameterType::INTEGER,
ArrayParameterType::STRING,
]);
}
}
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 > ?)";
$result = $this->conn->executeQuery($sql, [
$id,
time(),
], [
ParameterType::STRING,
ParameterType::INTEGER,
]);
return (bool) $result->fetchOne();
}
protected function doClear(string $namespace): bool
{
if ('' === $namespace) {
$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) {
}
return true;
}
protected function doDelete(array $ids): bool
{
$sql = "DELETE FROM $this->table WHERE $this->idCol IN (?)";
try {
$this->conn->executeStatement($sql, [array_values($ids)], [ArrayParameterType::STRING]);
} catch (TableNotFoundException) {
}
return true;
}
protected function doSave(array $values, int $lifetime): array|bool
{
if (!$values = $this->marshaller->marshall($values, $failed)) {
return $failed;
}
$platformName = $this->getPlatformName();
$insertSql = "INSERT INTO $this->table ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (?, ?, ?, ?)";
switch ($platformName) {
case 'mysql':
$sql = $insertSql." ON DUPLICATE KEY UPDATE $this->dataCol = VALUES($this->dataCol), $this->lifetimeCol = VALUES($this->lifetimeCol), $this->timeCol = VALUES($this->timeCol)";
break;
case 'oci':
// DUAL is Oracle specific dummy table
$sql = "MERGE INTO $this->table USING DUAL ON ($this->idCol = ?) ".
"WHEN NOT MATCHED THEN INSERT ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (?, ?, ?, ?) ".
"WHEN MATCHED THEN UPDATE SET $this->dataCol = ?, $this->lifetimeCol = ?, $this->timeCol = ?";
break;
case 'sqlsrv':
// MERGE is only available since SQL Server 2008 and must be terminated by semicolon
// It also requires HOLDLOCK according to http://weblogs.sqlteam.com/dang/archive/2009/01/31/UPSERT-Race-Condition-With-MERGE.aspx
$sql = "MERGE INTO $this->table WITH (HOLDLOCK) USING (SELECT 1 AS dummy) AS src ON ($this->idCol = ?) ".
"WHEN NOT MATCHED THEN INSERT ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (?, ?, ?, ?) ".
"WHEN MATCHED THEN UPDATE SET $this->dataCol = ?, $this->lifetimeCol = ?, $this->timeCol = ?;";
break;
case 'sqlite':
$sql = 'INSERT OR REPLACE'.substr($insertSql, 6);
break;
case 'pgsql':
$sql = $insertSql." ON CONFLICT ($this->idCol) DO UPDATE SET ($this->dataCol, $this->lifetimeCol, $this->timeCol) = (EXCLUDED.$this->dataCol, EXCLUDED.$this->lifetimeCol, EXCLUDED.$this->timeCol)";
break;
default:
$platformName = null;
$sql = "UPDATE $this->table SET $this->dataCol = ?, $this->lifetimeCol = ?, $this->timeCol = ? WHERE $this->idCol = ?";
break;
}
$now = time();
$lifetime = $lifetime ?: null;
try {
$stmt = $this->conn->prepare($sql);
} catch (TableNotFoundException) {
if (!$this->conn->isTransactionActive() || \in_array($platformName, ['pgsql', 'sqlite', 'sqlsrv'], true)) {
$this->createTable();
}
$stmt = $this->conn->prepare($sql);
}
if ('sqlsrv' === $platformName || 'oci' === $platformName) {
$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->bindValue(7, $lifetime, ParameterType::INTEGER);
$stmt->bindValue(8, $now, ParameterType::INTEGER);
} elseif (null !== $platformName) {
$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->bindValue(2, $lifetime, ParameterType::INTEGER);
$stmt->bindValue(3, $now, ParameterType::INTEGER);
$insertStmt = $this->conn->prepare($insertSql);
$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) {
if (!$this->conn->isTransactionActive() || \in_array($platformName, ['pgsql', 'sqlite', 'sqlsrv'], true)) {
$this->createTable();
}
$rowCount = $stmt->executeStatement();
}
if (null === $platformName && 0 === $rowCount) {
try {
$insertStmt->executeStatement();
} catch (DBALException) {
// A concurrent write won, let it be
}
}
}
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)) {
return $this->platformName;
}
$platform = $this->conn->getDatabasePlatform();
return $this->platformName = match (true) {
$platform instanceof \Doctrine\DBAL\Platforms\AbstractMySQLPlatform => 'mysql',
$platform instanceof \Doctrine\DBAL\Platforms\SqlitePlatform => 'sqlite',
$platform instanceof \Doctrine\DBAL\Platforms\PostgreSQLPlatform => 'pgsql',
$platform instanceof \Doctrine\DBAL\Platforms\OraclePlatform => 'oci',
$platform instanceof \Doctrine\DBAL\Platforms\SQLServerPlatform => 'sqlsrv',
default => $platform::class,
};
}
private function addTableToSchema(Schema $schema): void
{
$types = [
'mysql' => 'binary',
'sqlite' => 'text',
];
$table = $schema->createTable($this->table);
$table->addColumn($this->idCol, $types[$this->getPlatformName()] ?? 'string', ['length' => 255]);
$table->addColumn($this->dataCol, 'blob', ['length' => 16777215]);
$table->addColumn($this->lifetimeCol, 'integer', ['unsigned' => true, 'notnull' => false]);
$table->addColumn($this->timeCol, 'integer', ['unsigned' => true]);
$table->setPrimaryKey([$this->idCol]);
}
}
+29
View File
@@ -0,0 +1,29 @@
<?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 Symfony\Component\Cache\Marshaller\DefaultMarshaller;
use Symfony\Component\Cache\Marshaller\MarshallerInterface;
use Symfony\Component\Cache\PruneableInterface;
use Symfony\Component\Cache\Traits\FilesystemTrait;
class FilesystemAdapter extends AbstractAdapter implements PruneableInterface
{
use FilesystemTrait;
public function __construct(string $namespace = '', int $defaultLifetime = 0, ?string $directory = null, ?MarshallerInterface $marshaller = null)
{
$this->marshaller = $marshaller ?? new DefaultMarshaller();
parent::__construct('', $defaultLifetime);
$this->init($namespace, $directory);
}
}
@@ -0,0 +1,267 @@
<?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 Symfony\Component\Cache\Marshaller\MarshallerInterface;
use Symfony\Component\Cache\Marshaller\TagAwareMarshaller;
use Symfony\Component\Cache\PruneableInterface;
use Symfony\Component\Cache\Traits\FilesystemTrait;
/**
* Stores tag id <> cache id relationship as a symlink, and lookup on invalidation calls.
*
* @author Nicolas Grekas <p@tchwork.com>
* @author André Rømcke <andre.romcke+symfony@gmail.com>
*/
class FilesystemTagAwareAdapter extends AbstractTagAwareAdapter implements PruneableInterface
{
use FilesystemTrait {
prune as private doPrune;
doClear as private doClearCache;
doSave as private doSaveCache;
}
/**
* Folder used for tag symlinks.
*/
private const TAG_FOLDER = 'tags';
public function __construct(string $namespace = '', int $defaultLifetime = 0, ?string $directory = null, ?MarshallerInterface $marshaller = null)
{
$this->marshaller = new TagAwareMarshaller($marshaller);
parent::__construct('', $defaultLifetime);
$this->init($namespace, $directory);
}
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);
if ('' !== $namespace) {
return $ok;
}
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, $this->tmpSuffix.'_', -9))) {
$dir = $renamed.\DIRECTORY_SEPARATOR;
} else {
$dir .= \DIRECTORY_SEPARATOR;
$renamed = null;
}
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 && (null !== $renamed || !realpath($d.\DIRECTORY_SEPARATOR.$link))) {
unlink($d.\DIRECTORY_SEPARATOR.$link);
}
}
null === $renamed ?: rmdir($d);
}
null === $renamed ?: rmdir($dir.$chars[$i]);
}
null === $renamed ?: rmdir($renamed);
}
} finally {
restore_error_handler();
}
return $ok;
}
protected function doSave(array $values, int $lifetime, array $addTagData = [], array $removeTagData = []): array
{
$failed = $this->doSaveCache($values, $lifetime);
// Add Tags as symlinks
foreach ($addTagData as $tagId => $ids) {
$tagFolder = $this->getTagFolder($tagId);
foreach ($ids as $id) {
if ($failed && \in_array($id, $failed, true)) {
continue;
}
$file = $this->getFile($id);
if (!@symlink($file, $tagLink = $this->getFile($id, true, $tagFolder)) && !is_link($tagLink)) {
@unlink($file);
$failed[] = $id;
}
}
}
// Unlink removed Tags
foreach ($removeTagData as $tagId => $ids) {
$tagFolder = $this->getTagFolder($tagId);
foreach ($ids as $id) {
if ($failed && \in_array($id, $failed, true)) {
continue;
}
@unlink($this->getFile($id, false, $tagFolder));
}
}
return $failed;
}
protected function doDeleteYieldTags(array $ids): iterable
{
foreach ($ids as $id) {
$file = $this->getFile($id);
if (!is_file($file) || !$h = @fopen($file, 'r')) {
continue;
}
if (!@unlink($file)) {
fclose($h);
continue;
}
$meta = explode("\n", fread($h, 4096), 3)[2] ?? '';
// detect the compact format used in marshall() using magic numbers in the form 9D-..-..-..-..-00-..-..-..-5F
if (13 < \strlen($meta) && "\x9D" === $meta[0] && "\0" === $meta[5] && "\x5F" === $meta[9]) {
$meta[9] = "\0";
$tagLen = unpack('Nlen', $meta, 9)['len'];
$meta = substr($meta, 13, $tagLen);
if (0 < $tagLen -= \strlen($meta)) {
$meta .= fread($h, $tagLen);
}
try {
yield $id => '' === $meta ? [] : $this->marshaller->unmarshall($meta);
} catch (\Exception) {
yield $id => [];
}
}
fclose($h);
}
}
protected function doDeleteTagRelations(array $tagData): bool
{
foreach ($tagData as $tagId => $idList) {
$tagFolder = $this->getTagFolder($tagId);
foreach ($idList as $id) {
@unlink($this->getFile($id, false, $tagFolder));
}
}
return true;
}
protected function doInvalidate(array $tagIds): bool
{
foreach ($tagIds as $tagId) {
if (!is_dir($tagFolder = $this->getTagFolder($tagId))) {
continue;
}
$this->tmpSuffix ??= str_replace('/', '-', base64_encode(random_bytes(6)));
set_error_handler(static function () {});
try {
if (rename($tagFolder, $renamed = substr_replace($tagFolder, $this->tmpSuffix.'_', -10))) {
$tagFolder = $renamed.\DIRECTORY_SEPARATOR;
} else {
$renamed = null;
}
foreach ($this->scanHashDir($tagFolder) as $itemLink) {
unlink(realpath($itemLink) ?: $itemLink);
unlink($itemLink);
}
if (null === $renamed) {
continue;
}
$chars = '+-ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
for ($i = 0; $i < 38; ++$i) {
for ($j = 0; $j < 38; ++$j) {
rmdir($tagFolder.$chars[$i].\DIRECTORY_SEPARATOR.$chars[$j]);
}
rmdir($tagFolder.$chars[$i]);
}
rmdir($renamed);
} finally {
restore_error_handler();
}
}
return true;
}
private function getTagFolder(string $tagId): string
{
return $this->getFile($tagId, false, $this->directory.self::TAG_FOLDER.\DIRECTORY_SEPARATOR).\DIRECTORY_SEPARATOR;
}
}
+329
View File
@@ -0,0 +1,329 @@
<?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 Symfony\Component\Cache\Exception\CacheException;
use Symfony\Component\Cache\Exception\InvalidArgumentException;
use Symfony\Component\Cache\Marshaller\DefaultMarshaller;
use Symfony\Component\Cache\Marshaller\MarshallerInterface;
/**
* @author Rob Frawley 2nd <rmf@src.run>
* @author Nicolas Grekas <p@tchwork.com>
*/
class MemcachedAdapter extends AbstractAdapter
{
/**
* We are replacing characters that are illegal in Memcached keys with reserved characters from
* {@see \Symfony\Contracts\Cache\ItemInterface::RESERVED_CHARACTERS} that are legal in Memcached.
* Note: dont use {@see AbstractAdapter::NS_SEPARATOR}.
*/
private const RESERVED_MEMCACHED = " \n\r\t\v\f\0";
private const RESERVED_PSR6 = '@()\{}/';
private const MAX_KEY_LENGTH = 250;
private MarshallerInterface $marshaller;
private \Memcached $client;
private \Memcached $lazyClient;
/**
* Using a MemcachedAdapter with a TagAwareAdapter for storing tags is discouraged.
* Using a RedisAdapter is recommended instead. If you cannot do otherwise, be aware that:
* - the Memcached::OPT_BINARY_PROTOCOL must be enabled
* (that's the default when using MemcachedAdapter::createConnection());
* - tags eviction by Memcached's LRU algorithm will break by-tags invalidation;
* your Memcached memory should be large enough to never trigger LRU.
*
* Using a MemcachedAdapter as a pure items store is fine.
*/
public function __construct(\Memcached $client, string $namespace = '', int $defaultLifetime = 0, ?MarshallerInterface $marshaller = null)
{
if (!static::isSupported()) {
throw new CacheException('Memcached > 3.1.5 is required.');
}
$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".');
}
$this->maxIdLength -= \strlen($client->getOption(\Memcached::OPT_PREFIX_KEY));
$this->client = $client;
} else {
$this->lazyClient = $client;
}
parent::__construct($namespace, $defaultLifetime);
$this->enableVersioning();
$this->marshaller = $marshaller ?? new DefaultMarshaller();
}
public static function isSupported(): bool
{
return \extension_loaded('memcached') && version_compare(phpversion('memcached'), '3.1.6', '>=');
}
/**
* Creates a Memcached instance.
*
* By default, the binary protocol, no block, and libketama compatible options are enabled.
*
* Examples for servers:
* - 'memcached://user:pass@localhost?weight=33'
* - [['localhost', 11211, 33]]
*
* @param array[]|string|string[] $servers An array of servers, a DSN, or an array of DSNs
*
* @throws \ErrorException When invalid options or servers are provided
*/
public static function createConnection(#[\SensitiveParameter] array|string $servers, array $options = []): \Memcached
{
if (\is_string($servers)) {
$servers = [$servers];
}
if (!static::isSupported()) {
throw new CacheException('Memcached > 3.1.5 is required.');
}
set_error_handler(static fn ($type, $msg, $file, $line) => throw new \ErrorException($msg, 0, $type, $file, $line));
try {
$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) {
if (\is_array($dsn)) {
continue;
}
if (!str_starts_with($dsn, 'memcached:')) {
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])) {
[$username, $password] = explode(':', $m[2], 2) + [1 => null];
$username = rawurldecode($username);
$password = null !== $password ? rawurldecode($password) : null;
}
return 'file:'.($m[1] ?? '');
}, $dsn);
if (false === $params = parse_url($params)) {
throw new InvalidArgumentException('Invalid Memcached DSN.');
}
$query = $hosts = [];
if (isset($params['query'])) {
parse_str($params['query'], $query);
if (isset($query['host'])) {
if (!\is_array($hosts = $query['host'])) {
throw new InvalidArgumentException('Invalid Memcached DSN: query parameter "host" must be an array.');
}
foreach ($hosts as $host => $weight) {
if (false === $port = strrpos($host, ':')) {
$hosts[$host] = [$host, 11211, (int) $weight];
} else {
$hosts[$host] = [substr($host, 0, $port), (int) substr($host, 1 + $port), (int) $weight];
}
}
$hosts = array_values($hosts);
unset($query['host']);
}
if ($hosts && !isset($params['host']) && !isset($params['path'])) {
unset($servers[$i]);
$servers = array_merge($servers, $hosts);
continue;
}
}
if (!isset($params['host']) && !isset($params['path'])) {
throw new InvalidArgumentException('Invalid Memcached DSN: missing host or path.');
}
if (isset($params['path']) && preg_match('#/(\d+)$#', $params['path'], $m)) {
$params['weight'] = $m[1];
$params['path'] = substr($params['path'], 0, -\strlen($m[0]));
}
$params += [
'host' => $params['host'] ?? $params['path'],
'port' => isset($params['host']) ? 11211 : null,
'weight' => 0,
];
if ($query) {
$params += $query;
$options = $query + $options;
}
$servers[$i] = [$params['host'], $params['port'], $params['weight']];
if ($hosts) {
$servers = array_merge($servers, $hosts);
}
}
// set client's options
unset($options['persistent_id'], $options['username'], $options['password'], $options['weight'], $options['lazy']);
$options = array_change_key_case($options, \CASE_UPPER);
$client->setOption(\Memcached::OPT_BINARY_PROTOCOL, true);
$client->setOption(\Memcached::OPT_NO_BLOCK, true);
$client->setOption(\Memcached::OPT_TCP_NODELAY, true);
if (!\array_key_exists('LIBKETAMA_COMPATIBLE', $options) && !\array_key_exists(\Memcached::OPT_LIBKETAMA_COMPATIBLE, $options)) {
$client->setOption(\Memcached::OPT_LIBKETAMA_COMPATIBLE, true);
}
foreach ($options as $name => $value) {
if (\is_int($name)) {
continue;
}
if ('HASH' === $name || 'SERIALIZER' === $name || 'DISTRIBUTION' === $name) {
$value = \constant('Memcached::'.$name.'_'.strtoupper($value));
}
unset($options[$name]);
if (\defined('Memcached::OPT_'.$name)) {
$options[\constant('Memcached::OPT_'.$name)] = $value;
}
}
$client->setOptions($options + [\Memcached::OPT_SERIALIZER => \Memcached::SERIALIZER_PHP]);
// set client's servers, taking care of persistent connections
if (!$client->isPristine()) {
$oldServers = [];
foreach ($client->getServerList() as $server) {
$oldServers[] = [$server['host'], $server['port']];
}
$newServers = [];
foreach ($servers as $server) {
if (1 < \count($server)) {
$server = array_values($server);
unset($server[2]);
$server[1] = (int) $server[1];
}
$newServers[] = $server;
}
if ($oldServers !== $newServers) {
$client->resetServerList();
$client->addServers($servers);
}
} else {
$client->addServers($servers);
}
if (null !== $username || null !== $password) {
if (!method_exists($client, 'setSaslAuthData')) {
trigger_error('Missing SASL support: the memcached extension must be compiled with --enable-memcached-sasl.');
}
$client->setSaslAuthData($username, $password);
}
return $client;
} finally {
restore_error_handler();
}
}
protected function doSave(array $values, int $lifetime): array|bool
{
if (!$values = $this->marshaller->marshall($values, $failed)) {
return $failed;
}
if ($lifetime && $lifetime > 30 * 86400) {
$lifetime += time();
}
$encodedValues = [];
foreach ($values as $key => $value) {
$encodedValues[self::encodeKey($key)] = $value;
}
return $this->checkResultCode($this->getClient()->setMulti($encodedValues, $lifetime)) ? $failed : false;
}
protected function doFetch(array $ids): iterable
{
try {
$encodedIds = array_map([__CLASS__, 'encodeKey'], $ids);
$encodedResult = $this->checkResultCode($this->getClient()->getMulti($encodedIds));
$result = [];
foreach ($encodedResult as $key => $value) {
$result[self::decodeKey($key)] = $this->marshaller->unmarshall($value);
}
return $result;
} catch (\Error $e) {
throw new \ErrorException($e->getMessage(), $e->getCode(), \E_ERROR, $e->getFile(), $e->getLine());
}
}
protected function doHave(string $id): bool
{
return false !== $this->getClient()->get(self::encodeKey($id)) || $this->checkResultCode(\Memcached::RES_SUCCESS === $this->client->getResultCode());
}
protected function doDelete(array $ids): bool
{
$ok = true;
$encodedIds = array_map([__CLASS__, 'encodeKey'], $ids);
foreach ($this->checkResultCode($this->getClient()->deleteMulti($encodedIds)) as $result) {
if (\Memcached::RES_SUCCESS !== $result && \Memcached::RES_NOTFOUND !== $result) {
$ok = false;
}
}
return $ok;
}
protected function doClear(string $namespace): bool
{
return '' === $namespace && $this->getClient()->flush();
}
private function checkResultCode(mixed $result): mixed
{
$code = $this->client->getResultCode();
if (\Memcached::RES_SUCCESS === $code || \Memcached::RES_NOTFOUND === $code) {
return $result;
}
throw new CacheException('MemcachedAdapter client error: '.strtolower($this->client->getResultMessage()));
}
private function getClient(): \Memcached
{
if (isset($this->client)) {
return $this->client;
}
$opt = $this->lazyClient->getOption(\Memcached::OPT_SERIALIZER);
if (\Memcached::SERIALIZER_PHP !== $opt && \Memcached::SERIALIZER_IGBINARY !== $opt) {
throw new CacheException('MemcachedAdapter: "serializer" option must be "php" or "igbinary".');
}
if ('' !== $prefix = (string) $this->lazyClient->getOption(\Memcached::OPT_PREFIX_KEY)) {
throw new CacheException(sprintf('MemcachedAdapter: "prefix_key" option must be empty when using proxified connections, "%s" given.', $prefix));
}
return $this->client = $this->lazyClient;
}
private static function encodeKey(string $key): string
{
return strtr($key, self::RESERVED_MEMCACHED, self::RESERVED_PSR6);
}
private static function decodeKey(string $key): string
{
return strtr($key, self::RESERVED_PSR6, self::RESERVED_MEMCACHED);
}
}
+105
View File
@@ -0,0 +1,105 @@
<?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 Psr\Cache\CacheItemInterface;
use Symfony\Component\Cache\CacheItem;
use Symfony\Contracts\Cache\CacheInterface;
/**
* @author Titouan Galopin <galopintitouan@gmail.com>
*/
class NullAdapter implements AdapterInterface, CacheInterface
{
private static \Closure $createCacheItem;
public function __construct()
{
self::$createCacheItem ??= \Closure::bind(
static function ($key) {
$item = new CacheItem();
$item->key = $key;
$item->isHit = false;
return $item;
},
null,
CacheItem::class
);
}
public function get(string $key, callable $callback, ?float $beta = null, ?array &$metadata = null): mixed
{
$save = true;
return $callback((self::$createCacheItem)($key), $save);
}
public function getItem(mixed $key): CacheItem
{
return (self::$createCacheItem)($key);
}
public function getItems(array $keys = []): iterable
{
return $this->generateItems($keys);
}
public function hasItem(mixed $key): bool
{
return false;
}
public function clear(string $prefix = ''): bool
{
return true;
}
public function deleteItem(mixed $key): bool
{
return true;
}
public function deleteItems(array $keys): bool
{
return true;
}
public function save(CacheItemInterface $item): bool
{
return true;
}
public function saveDeferred(CacheItemInterface $item): bool
{
return true;
}
public function commit(): bool
{
return true;
}
public function delete(string $key): bool
{
return $this->deleteItem($key);
}
private function generateItems(array $keys): \Generator
{
$f = self::$createCacheItem;
foreach ($keys as $key) {
yield $key => $f($key);
}
}
}
+35
View File
@@ -0,0 +1,35 @@
<?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;
/**
* @author Lars Strojny <lars@strojny.net>
*/
final class ParameterNormalizer
{
public static function normalizeDuration(string $duration): int
{
if (is_numeric($duration)) {
return $duration;
}
if (false !== $time = strtotime($duration, 0)) {
return $time;
}
try {
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);
}
}
}
+398
View File
@@ -0,0 +1,398 @@
<?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 Symfony\Component\Cache\Exception\InvalidArgumentException;
use Symfony\Component\Cache\Marshaller\DefaultMarshaller;
use Symfony\Component\Cache\Marshaller\MarshallerInterface;
use Symfony\Component\Cache\PruneableInterface;
class PdoAdapter extends AbstractAdapter implements PruneableInterface
{
private const MAX_KEY_LENGTH = 255;
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
* a DSN string that will be used to lazy-connect to the database when the
* cache is actually used.
*
* List of available options:
* * db_table: The name of the table [default: cache_items]
* * db_id_col: The column where to store the cache id [default: item_id]
* * db_data_col: The column where to store the cache data [default: item_data]
* * 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]
* * db_username: The username when lazy-connect [default: '']
* * db_password: The password when lazy-connect [default: '']
* * db_connection_options: An array of driver-specific connection options [default: []]
*
* @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(#[\SensitiveParameter] \PDO|string $connOrDsn, string $namespace = '', int $defaultLifetime = 0, array $options = [], ?MarshallerInterface $marshaller = null)
{
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)) {
throw new InvalidArgumentException(sprintf('Namespace contains "%s" but only characters in [-+.A-Za-z0-9] are allowed.', $match[0]));
}
if ($connOrDsn instanceof \PDO) {
if (\PDO::ERRMODE_EXCEPTION !== $connOrDsn->getAttribute(\PDO::ATTR_ERRMODE)) {
throw new InvalidArgumentException(sprintf('"%s" requires PDO error mode attribute be set to throw Exceptions (i.e. $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION)).', __CLASS__));
}
$this->conn = $connOrDsn;
} else {
$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;
$this->lifetimeCol = $options['db_lifetime_col'] ?? $this->lifetimeCol;
$this->timeCol = $options['db_time_col'] ?? $this->timeCol;
$this->username = $options['db_username'] ?? $this->username;
$this->password = $options['db_password'] ?? $this->password;
$this->connectionOptions = $options['db_connection_options'] ?? $this->connectionOptions;
$this->namespace = $namespace;
$this->marshaller = $marshaller ?? new DefaultMarshaller();
parent::__construct($namespace, $defaultLifetime);
}
public static function createConnection(#[\SensitiveParameter] string $dsn, array $options = []): \PDO|string
{
if ($options['lazy'] ?? true) {
return $dsn;
}
$pdo = new \PDO($dsn);
$pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
return $pdo;
}
/**
* 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.
*
* @throws \PDOException When the table already exists
* @throws \DomainException When an unsupported PDO driver is used
*/
public function createTable(): void
{
$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)),
};
$this->getConnection()->exec($sql);
}
public function prune(): bool
{
$deleteSql = "DELETE FROM $this->table WHERE $this->lifetimeCol + $this->timeCol <= :time";
if ('' !== $this->namespace) {
$deleteSql .= " AND $this->idCol LIKE :namespace";
}
$connection = $this->getConnection();
try {
$delete = $connection->prepare($deleteSql);
} catch (\PDOException) {
return true;
}
$delete->bindValue(':time', time(), \PDO::PARAM_INT);
if ('' !== $this->namespace) {
$delete->bindValue(':namespace', sprintf('%s%%', $this->namespace), \PDO::PARAM_STR);
}
try {
return $delete->execute();
} catch (\PDOException) {
return true;
}
}
protected function doFetch(array $ids): iterable
{
$connection = $this->getConnection();
$now = time();
$expired = [];
$sql = str_pad('', (\count($ids) << 1) - 1, '?,');
$sql = "SELECT $this->idCol, CASE WHEN $this->lifetimeCol IS NULL OR $this->lifetimeCol + $this->timeCol > ? THEN $this->dataCol ELSE NULL END FROM $this->table WHERE $this->idCol IN ($sql)";
$stmt = $connection->prepare($sql);
$stmt->bindValue($i = 1, $now, \PDO::PARAM_INT);
foreach ($ids as $id) {
$stmt->bindValue(++$i, $id);
}
$result = $stmt->execute();
if (\is_object($result)) {
$result = $result->iterateNumeric();
} else {
$stmt->setFetchMode(\PDO::FETCH_NUM);
$result = $stmt;
}
foreach ($result as $row) {
if (null === $row[1]) {
$expired[] = $row[0];
} else {
yield $row[0] => $this->marshaller->unmarshall(\is_resource($row[1]) ? stream_get_contents($row[1]) : $row[1]);
}
}
if ($expired) {
$sql = str_pad('', (\count($expired) << 1) - 1, '?,');
$sql = "DELETE FROM $this->table WHERE $this->lifetimeCol + $this->timeCol <= ? AND $this->idCol IN ($sql)";
$stmt = $connection->prepare($sql);
$stmt->bindValue($i = 1, $now, \PDO::PARAM_INT);
foreach ($expired as $id) {
$stmt->bindValue(++$i, $id);
}
$stmt->execute();
}
}
protected function doHave(string $id): bool
{
$connection = $this->getConnection();
$sql = "SELECT 1 FROM $this->table WHERE $this->idCol = :id AND ($this->lifetimeCol IS NULL OR $this->lifetimeCol + $this->timeCol > :time)";
$stmt = $connection->prepare($sql);
$stmt->bindValue(':id', $id);
$stmt->bindValue(':time', time(), \PDO::PARAM_INT);
$stmt->execute();
return (bool) $stmt->fetchColumn();
}
protected function doClear(string $namespace): bool
{
$conn = $this->getConnection();
if ('' === $namespace) {
if ('sqlite' === $this->getDriver()) {
$sql = "DELETE FROM $this->table";
} else {
$sql = "TRUNCATE TABLE $this->table";
}
} else {
$sql = "DELETE FROM $this->table WHERE $this->idCol LIKE '$namespace%'";
}
try {
$conn->exec($sql);
} catch (\PDOException) {
}
return true;
}
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) {
}
return true;
}
protected function doSave(array $values, int $lifetime): array|bool
{
if (!$values = $this->marshaller->marshall($values, $failed)) {
return $failed;
}
$conn = $this->getConnection();
$driver = $this->getDriver();
$insertSql = "INSERT INTO $this->table ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (:id, :data, :lifetime, :time)";
switch (true) {
case 'mysql' === $driver:
$sql = $insertSql." ON DUPLICATE KEY UPDATE $this->dataCol = VALUES($this->dataCol), $this->lifetimeCol = VALUES($this->lifetimeCol), $this->timeCol = VALUES($this->timeCol)";
break;
case 'oci' === $driver:
// DUAL is Oracle specific dummy table
$sql = "MERGE INTO $this->table USING DUAL ON ($this->idCol = ?) ".
"WHEN NOT MATCHED THEN INSERT ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (?, ?, ?, ?) ".
"WHEN MATCHED THEN UPDATE SET $this->dataCol = ?, $this->lifetimeCol = ?, $this->timeCol = ?";
break;
case 'sqlsrv' === $driver && version_compare($this->getServerVersion(), '10', '>='):
// MERGE is only available since SQL Server 2008 and must be terminated by semicolon
// It also requires HOLDLOCK according to http://weblogs.sqlteam.com/dang/archive/2009/01/31/UPSERT-Race-Condition-With-MERGE.aspx
$sql = "MERGE INTO $this->table WITH (HOLDLOCK) USING (SELECT 1 AS dummy) AS src ON ($this->idCol = ?) ".
"WHEN NOT MATCHED THEN INSERT ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (?, ?, ?, ?) ".
"WHEN MATCHED THEN UPDATE SET $this->dataCol = ?, $this->lifetimeCol = ?, $this->timeCol = ?;";
break;
case 'sqlite' === $driver:
$sql = 'INSERT OR REPLACE'.substr($insertSql, 6);
break;
case 'pgsql' === $driver && version_compare($this->getServerVersion(), '9.5', '>='):
$sql = $insertSql." ON CONFLICT ($this->idCol) DO UPDATE SET ($this->dataCol, $this->lifetimeCol, $this->timeCol) = (EXCLUDED.$this->dataCol, EXCLUDED.$this->lifetimeCol, EXCLUDED.$this->timeCol)";
break;
default:
$driver = null;
$sql = "UPDATE $this->table SET $this->dataCol = :data, $this->lifetimeCol = :lifetime, $this->timeCol = :time WHERE $this->idCol = :id";
break;
}
$now = time();
$lifetime = $lifetime ?: null;
try {
$stmt = $conn->prepare($sql);
} catch (\PDOException $e) {
if ($this->isTableMissing($e) && (!$conn->inTransaction() || \in_array($driver, ['pgsql', 'sqlite', 'sqlsrv'], true))) {
$this->createTable();
}
$stmt = $conn->prepare($sql);
}
// $id and $data are defined later in the loop. Binding is done by reference, values are read on execution.
if ('sqlsrv' === $driver || 'oci' === $driver) {
$stmt->bindParam(1, $id);
$stmt->bindParam(2, $id);
$stmt->bindParam(3, $data, \PDO::PARAM_LOB);
$stmt->bindValue(4, $lifetime, \PDO::PARAM_INT);
$stmt->bindValue(5, $now, \PDO::PARAM_INT);
$stmt->bindParam(6, $data, \PDO::PARAM_LOB);
$stmt->bindValue(7, $lifetime, \PDO::PARAM_INT);
$stmt->bindValue(8, $now, \PDO::PARAM_INT);
} else {
$stmt->bindParam(':id', $id);
$stmt->bindParam(':data', $data, \PDO::PARAM_LOB);
$stmt->bindValue(':lifetime', $lifetime, \PDO::PARAM_INT);
$stmt->bindValue(':time', $now, \PDO::PARAM_INT);
}
if (null === $driver) {
$insertStmt = $conn->prepare($insertSql);
$insertStmt->bindParam(':id', $id);
$insertStmt->bindParam(':data', $data, \PDO::PARAM_LOB);
$insertStmt->bindValue(':lifetime', $lifetime, \PDO::PARAM_INT);
$insertStmt->bindValue(':time', $now, \PDO::PARAM_INT);
}
foreach ($values as $id => $data) {
try {
$stmt->execute();
} catch (\PDOException $e) {
if ($this->isTableMissing($e) && (!$conn->inTransaction() || \in_array($driver, ['pgsql', 'sqlite', 'sqlsrv'], true))) {
$this->createTable();
}
$stmt->execute();
}
if (null === $driver && !$stmt->rowCount()) {
try {
$insertStmt->execute();
} catch (\PDOException) {
// A concurrent write won, let it be
}
}
}
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 (!isset($this->conn)) {
$this->conn = new \PDO($this->dsn, $this->username, $this->password, $this->connectionOptions);
$this->conn->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
}
return $this->conn;
}
private function getDriver(): string
{
return $this->driver ??= $this->getConnection()->getAttribute(\PDO::ATTR_DRIVER_NAME);
}
private function getServerVersion(): string
{
return $this->serverVersion ??= $this->getConnection()->getAttribute(\PDO::ATTR_SERVER_VERSION);
}
private function isTableMissing(\PDOException $exception): bool
{
$driver = $this->getDriver();
[$sqlState, $code] = $exception->errorInfo ?? [null, $exception->getCode()];
return match ($driver) {
'pgsql' => '42P01' === $sqlState,
'sqlite' => str_contains($exception->getMessage(), 'no such table:'),
'oci' => 942 === $code,
'sqlsrv' => 208 === $code,
'mysql' => 1146 === $code,
default => false,
};
}
}
+389
View File
@@ -0,0 +1,389 @@
<?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 Psr\Cache\CacheItemInterface;
use Psr\Cache\CacheItemPoolInterface;
use Symfony\Component\Cache\CacheItem;
use Symfony\Component\Cache\Exception\InvalidArgumentException;
use Symfony\Component\Cache\PruneableInterface;
use Symfony\Component\Cache\ResettableInterface;
use Symfony\Component\Cache\Traits\ContractsTrait;
use Symfony\Component\Cache\Traits\ProxyTrait;
use Symfony\Component\VarExporter\VarExporter;
use Symfony\Contracts\Cache\CacheInterface;
/**
* Caches items at warm up time using a PHP array that is stored in shared memory by OPCache since PHP 7.0.
* Warmed up items are read-only and run-time discovered items are cached using a fallback adapter.
*
* @author Titouan Galopin <galopintitouan@gmail.com>
* @author Nicolas Grekas <p@tchwork.com>
*/
class PhpArrayAdapter implements AdapterInterface, CacheInterface, PruneableInterface, ResettableInterface
{
use ContractsTrait;
use ProxyTrait;
private array $keys;
private array $values;
private static \Closure $createCacheItem;
private static array $valuesCache = [];
/**
* @param string $file The PHP file were values are cached
* @param AdapterInterface $fallbackPool A pool to fallback on when an item is not hit
*/
public function __construct(
private string $file,
AdapterInterface $fallbackPool,
) {
$this->pool = $fallbackPool;
self::$createCacheItem ??= \Closure::bind(
static function ($key, $value, $isHit) {
$item = new CacheItem();
$item->key = $key;
$item->value = $value;
$item->isHit = $isHit;
return $item;
},
null,
CacheItem::class
);
}
/**
* This adapter takes advantage of how PHP stores arrays in its latest versions.
*
* @param string $file The PHP file were values are cached
* @param CacheItemPoolInterface $fallbackPool A pool to fallback on when an item is not hit
*/
public static function create(string $file, CacheItemPoolInterface $fallbackPool): CacheItemPoolInterface
{
if (!$fallbackPool instanceof AdapterInterface) {
$fallbackPool = new ProxyAdapter($fallbackPool);
}
return new static($file, $fallbackPool);
}
public function get(string $key, callable $callback, ?float $beta = null, ?array &$metadata = null): mixed
{
if (!isset($this->values)) {
$this->initialize();
}
if (!isset($this->keys[$key])) {
get_from_pool:
if ($this->pool instanceof CacheInterface) {
return $this->pool->get($key, $callback, $beta, $metadata);
}
return $this->doGet($this->pool, $key, $callback, $beta, $metadata);
}
$value = $this->values[$this->keys[$key]];
if ('N;' === $value) {
return null;
}
try {
if ($value instanceof \Closure) {
return $value();
}
} catch (\Throwable) {
unset($this->keys[$key]);
goto get_from_pool;
}
return $value;
}
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 (!isset($this->values)) {
$this->initialize();
}
if (!isset($this->keys[$key])) {
return $this->pool->getItem($key);
}
$value = $this->values[$this->keys[$key]];
$isHit = true;
if ('N;' === $value) {
$value = null;
} elseif ($value instanceof \Closure) {
try {
$value = $value();
} catch (\Throwable) {
$value = null;
$isHit = false;
}
}
return (self::$createCacheItem)($key, $value, $isHit);
}
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 (!isset($this->values)) {
$this->initialize();
}
return $this->generateItems($keys);
}
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 (!isset($this->values)) {
$this->initialize();
}
return isset($this->keys[$key]) || $this->pool->hasItem($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 (!isset($this->values)) {
$this->initialize();
}
return !isset($this->keys[$key]) && $this->pool->deleteItem($key);
}
public function deleteItems(array $keys): bool
{
$deleted = true;
$fallbackKeys = [];
foreach ($keys as $key) {
if (!\is_string($key)) {
throw new InvalidArgumentException(sprintf('Cache key must be string, "%s" given.', get_debug_type($key)));
}
if (isset($this->keys[$key])) {
$deleted = false;
} else {
$fallbackKeys[] = $key;
}
}
if (!isset($this->values)) {
$this->initialize();
}
if ($fallbackKeys) {
$deleted = $this->pool->deleteItems($fallbackKeys) && $deleted;
}
return $deleted;
}
public function save(CacheItemInterface $item): bool
{
if (!isset($this->values)) {
$this->initialize();
}
return !isset($this->keys[$item->getKey()]) && $this->pool->save($item);
}
public function saveDeferred(CacheItemInterface $item): bool
{
if (!isset($this->values)) {
$this->initialize();
}
return !isset($this->keys[$item->getKey()]) && $this->pool->saveDeferred($item);
}
public function commit(): bool
{
return $this->pool->commit();
}
public function clear(string $prefix = ''): bool
{
$this->keys = $this->values = [];
$cleared = @unlink($this->file) || !file_exists($this->file);
unset(self::$valuesCache[$this->file]);
if ($this->pool instanceof AdapterInterface) {
return $this->pool->clear($prefix) && $cleared;
}
return $this->pool->clear() && $cleared;
}
/**
* Store an array of cached values.
*
* @param array $values The cached values
*
* @return string[] A list of classes to preload on PHP 7.4+
*/
public function warmUp(array $values): array
{
if (file_exists($this->file)) {
if (!is_file($this->file)) {
throw new InvalidArgumentException(sprintf('Cache path exists and is not a file: "%s".', $this->file));
}
if (!is_writable($this->file)) {
throw new InvalidArgumentException(sprintf('Cache file is not writable: "%s".', $this->file));
}
} else {
$directory = \dirname($this->file);
if (!is_dir($directory) && !@mkdir($directory, 0777, true)) {
throw new InvalidArgumentException(sprintf('Cache directory does not exist and cannot be created: "%s".', $directory));
}
if (!is_writable($directory)) {
throw new InvalidArgumentException(sprintf('Cache directory is not writable: "%s".', $directory));
}
}
$preload = [];
$dumpedValues = '';
$dumpedMap = [];
$dump = <<<'EOF'
<?php
// This file has been auto-generated by the Symfony Cache Component.
return [[
EOF;
foreach ($values as $key => $value) {
CacheItem::validateKey(\is_int($key) ? (string) $key : $key);
$isStaticValue = true;
if (null === $value) {
$value = "'N;'";
} elseif (\is_object($value) || \is_array($value)) {
try {
$value = VarExporter::export($value, $isStaticValue, $preload);
} catch (\Exception $e) {
throw new InvalidArgumentException(sprintf('Cache key "%s" has non-serializable "%s" value.', $key, get_debug_type($value)), 0, $e);
}
} elseif (\is_string($value)) {
// Wrap "N;" in a closure to not confuse it with an encoded `null`
if ('N;' === $value) {
$isStaticValue = false;
}
$value = var_export($value, true);
} elseif (!\is_scalar($value)) {
throw new InvalidArgumentException(sprintf('Cache key "%s" has non-serializable "%s" value.', $key, get_debug_type($value)));
} else {
$value = var_export($value, true);
}
if (!$isStaticValue) {
$value = str_replace("\n", "\n ", $value);
$value = "static function () {\n return {$value};\n}";
}
$hash = hash('xxh128', $value);
if (null === $id = $dumpedMap[$hash] ?? null) {
$id = $dumpedMap[$hash] = \count($dumpedMap);
$dumpedValues .= "{$id} => {$value},\n";
}
$dump .= var_export($key, true)." => {$id},\n";
}
$dump .= "\n], [\n\n{$dumpedValues}\n]];\n";
$tmpFile = uniqid($this->file, true);
file_put_contents($tmpFile, $dump);
@chmod($tmpFile, 0666 & ~umask());
unset($serialized, $value, $dump);
@rename($tmpFile, $this->file);
unset(self::$valuesCache[$this->file]);
$this->initialize();
return $preload;
}
/**
* Load the cache file.
*/
private function initialize(): void
{
if (isset(self::$valuesCache[$this->file])) {
$values = self::$valuesCache[$this->file];
} elseif (!is_file($this->file)) {
$this->keys = $this->values = [];
return;
} else {
$values = self::$valuesCache[$this->file] = (include $this->file) ?: [[], []];
}
if (2 !== \count($values) || !isset($values[0], $values[1])) {
$this->keys = $this->values = [];
} else {
[$this->keys, $this->values] = $values;
}
}
private function generateItems(array $keys): \Generator
{
$f = self::$createCacheItem;
$fallbackKeys = [];
foreach ($keys as $key) {
if (isset($this->keys[$key])) {
$value = $this->values[$this->keys[$key]];
if ('N;' === $value) {
yield $key => $f($key, null, true);
} elseif ($value instanceof \Closure) {
try {
yield $key => $f($key, $value(), true);
} catch (\Throwable) {
yield $key => $f($key, null, false);
}
} else {
yield $key => $f($key, $value, true);
}
} else {
$fallbackKeys[] = $key;
}
}
if ($fallbackKeys) {
yield from $this->pool->getItems($fallbackKeys);
}
}
}
+314
View File
@@ -0,0 +1,314 @@
<?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 Symfony\Component\Cache\Exception\CacheException;
use Symfony\Component\Cache\Exception\InvalidArgumentException;
use Symfony\Component\Cache\PruneableInterface;
use Symfony\Component\Cache\Traits\FilesystemCommonTrait;
use Symfony\Component\VarExporter\VarExporter;
/**
* @author Piotr Stankowski <git@trakos.pl>
* @author Nicolas Grekas <p@tchwork.com>
* @author Rob Frawley 2nd <rmf@src.run>
*/
class PhpFilesAdapter extends AbstractAdapter implements PruneableInterface
{
use FilesystemCommonTrait {
doClear as private doCommonClear;
doDelete as private doCommonDelete;
}
private \Closure $includeHandler;
private array $values = [];
private array $files = [];
private static int $startTime;
private static array $valuesCache = [];
/**
* @param bool $appendOnly Set to `true` to gain extra performance when the items stored in this pool never expire.
* Doing so is encouraged because it fits perfectly OPcache's memory model.
*
* @throws CacheException if OPcache is not enabled
*/
public function __construct(
string $namespace = '',
int $defaultLifetime = 0,
?string $directory = null,
private bool $appendOnly = false,
) {
self::$startTime ??= $_SERVER['REQUEST_TIME'] ?? time();
parent::__construct('', $defaultLifetime);
$this->init($namespace, $directory);
$this->includeHandler = static function ($type, $msg, $file, $line) {
throw new \ErrorException($msg, 0, $type, $file, $line);
};
}
public static function isSupported(): bool
{
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;
$getExpiry = true;
set_error_handler($this->includeHandler);
try {
foreach ($this->scanHashDir($this->directory) as $file) {
try {
if (\is_array($expiresAt = include $file)) {
$expiresAt = $expiresAt[0];
}
} catch (\ErrorException $e) {
$expiresAt = $time;
}
if ($time >= $expiresAt) {
$pruned = ($this->doUnlink($file) || !file_exists($file)) && $pruned;
}
}
} finally {
restore_error_handler();
}
return $pruned;
}
protected function doFetch(array $ids): iterable
{
if ($this->appendOnly) {
$now = 0;
$missingIds = [];
} else {
$now = time();
$missingIds = $ids;
$ids = [];
}
$values = [];
begin:
$getExpiry = false;
foreach ($ids as $id) {
if (null === $value = $this->values[$id] ?? null) {
$missingIds[] = $id;
} elseif ('N;' === $value) {
$values[$id] = null;
} elseif (!\is_object($value)) {
$values[$id] = $value;
} elseif (!$value instanceof LazyValue) {
$values[$id] = $value();
} elseif (false === $values[$id] = include $value->file) {
unset($values[$id], $this->values[$id]);
$missingIds[] = $id;
}
if (!$this->appendOnly) {
unset($this->values[$id]);
}
}
if (!$missingIds) {
return $values;
}
set_error_handler($this->includeHandler);
try {
$getExpiry = true;
foreach ($missingIds as $k => $id) {
try {
$file = $this->files[$id] ??= $this->getFile($id);
if (isset(self::$valuesCache[$file])) {
[$expiresAt, $this->values[$id]] = self::$valuesCache[$file];
} elseif (\is_array($expiresAt = include $file)) {
if ($this->appendOnly) {
self::$valuesCache[$file] = $expiresAt;
}
[$expiresAt, $this->values[$id]] = $expiresAt;
} elseif ($now < $expiresAt) {
$this->values[$id] = new LazyValue($file);
}
if ($now >= $expiresAt) {
unset($this->values[$id], $missingIds[$k], self::$valuesCache[$file]);
}
} catch (\ErrorException $e) {
unset($missingIds[$k]);
}
}
} finally {
restore_error_handler();
}
$ids = $missingIds;
$missingIds = [];
goto begin;
}
protected function doHave(string $id): bool
{
if ($this->appendOnly && isset($this->values[$id])) {
return true;
}
set_error_handler($this->includeHandler);
try {
$file = $this->files[$id] ??= $this->getFile($id);
$getExpiry = true;
if (isset(self::$valuesCache[$file])) {
[$expiresAt, $value] = self::$valuesCache[$file];
} elseif (\is_array($expiresAt = include $file)) {
if ($this->appendOnly) {
self::$valuesCache[$file] = $expiresAt;
}
[$expiresAt, $value] = $expiresAt;
} elseif ($this->appendOnly) {
$value = new LazyValue($file);
}
} catch (\ErrorException) {
return false;
} finally {
restore_error_handler();
}
if ($this->appendOnly) {
$now = 0;
$this->values[$id] = $value;
} else {
$now = time();
}
return $now < $expiresAt;
}
protected function doSave(array $values, int $lifetime): array|bool
{
$ok = true;
$expiry = $lifetime ? time() + $lifetime : 'PHP_INT_MAX';
$allowCompile = self::isSupported();
foreach ($values as $key => $value) {
unset($this->values[$key]);
$isStaticValue = true;
if (null === $value) {
$value = "'N;'";
} elseif (\is_object($value) || \is_array($value)) {
try {
$value = VarExporter::export($value, $isStaticValue);
} catch (\Exception $e) {
throw new InvalidArgumentException(sprintf('Cache key "%s" has non-serializable "%s" value.', $key, get_debug_type($value)), 0, $e);
}
} elseif (\is_string($value)) {
// Wrap "N;" in a closure to not confuse it with an encoded `null`
if ('N;' === $value) {
$isStaticValue = false;
}
$value = var_export($value, true);
} elseif (!\is_scalar($value)) {
throw new InvalidArgumentException(sprintf('Cache key "%s" has non-serializable "%s" value.', $key, get_debug_type($value)));
} else {
$value = var_export($value, true);
}
$encodedKey = rawurlencode($key);
if ($isStaticValue) {
$value = "return [{$expiry}, {$value}];";
} elseif ($this->appendOnly) {
$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);
$value = "namespace Symfony\Component\VarExporter\Internal;\n\nreturn \$getExpiry ? {$expiry} : {$value};";
}
$file = $this->files[$key] = $this->getFile($key, true);
// Since OPcache only compiles files older than the script execution start, set the file's mtime in the past
$ok = $this->write($file, "<?php //{$encodedKey}\n\n{$value}\n", self::$startTime - 10) && $ok;
if ($allowCompile) {
@opcache_invalidate($file, true);
@opcache_compile_file($file);
}
unset(self::$valuesCache[$file]);
}
if (!$ok && !is_writable($this->directory)) {
throw new CacheException(sprintf('Cache directory is not writable (%s).', $this->directory));
}
return $ok;
}
protected function doClear(string $namespace): bool
{
$this->values = [];
return $this->doCommonClear($namespace);
}
protected function doDelete(array $ids): bool
{
foreach ($ids as $id) {
unset($this->values[$id]);
}
return $this->doCommonDelete($ids);
}
protected function doUnlink(string $file): bool
{
unset(self::$valuesCache[$file]);
if (self::isSupported()) {
@opcache_invalidate($file, true);
}
return @unlink($file);
}
private function getFileKey(string $file): string
{
if (!$h = @fopen($file, 'r')) {
return '';
}
$encodedKey = substr(fgets($h), 8);
fclose($h);
return rawurldecode(rtrim($encodedKey));
}
}
/**
* @internal
*/
class LazyValue
{
public string $file;
public function __construct(string $file)
{
$this->file = $file;
}
}
+206
View File
@@ -0,0 +1,206 @@
<?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 Psr\Cache\CacheItemInterface;
use Psr\Cache\CacheItemPoolInterface;
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\CacheInterface;
/**
* @author Nicolas Grekas <p@tchwork.com>
*/
class ProxyAdapter implements AdapterInterface, CacheInterface, PruneableInterface, ResettableInterface
{
use ContractsTrait;
use ProxyTrait;
private string $namespace = '';
private int $namespaceLen;
private string $poolHash;
private int $defaultLifetime;
private static \Closure $createCacheItem;
private static \Closure $setInnerItem;
public function __construct(CacheItemPoolInterface $pool, string $namespace = '', int $defaultLifetime = 0)
{
$this->pool = $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 ??= \Closure::bind(
static function ($key, $innerItem, $poolHash) {
$item = new CacheItem();
$item->key = $key;
if (null === $innerItem) {
return $item;
}
$item->value = $innerItem->get();
$item->isHit = $innerItem->isHit();
$item->innerItem = $innerItem;
$item->poolHash = $poolHash;
if (!$item->unpack() && $innerItem instanceof CacheItem) {
$item->metadata = $innerItem->metadata;
}
$innerItem->set(null);
return $item;
},
null,
CacheItem::class
);
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
);
}
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);
}
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, $item);
return $value;
}, $beta, $metadata);
}
public function getItem(mixed $key): CacheItem
{
$item = $this->pool->getItem($this->getId($key));
return (self::$createCacheItem)($key, $item, $this->poolHash);
}
public function getItems(array $keys = []): iterable
{
if ($this->namespaceLen) {
foreach ($keys as $i => $key) {
$keys[$i] = $this->getId($key);
}
}
return $this->generateItems($this->pool->getItems($keys));
}
public function hasItem(mixed $key): bool
{
return $this->pool->hasItem($this->getId($key));
}
public function clear(string $prefix = ''): bool
{
if ($this->pool instanceof AdapterInterface) {
return $this->pool->clear($this->namespace.$prefix);
}
return $this->pool->clear();
}
public function deleteItem(mixed $key): bool
{
return $this->pool->deleteItem($this->getId($key));
}
public function deleteItems(array $keys): bool
{
if ($this->namespaceLen) {
foreach ($keys as $i => $key) {
$keys[$i] = $this->getId($key);
}
}
return $this->pool->deleteItems($keys);
}
public function save(CacheItemInterface $item): bool
{
return $this->doSave($item, __FUNCTION__);
}
public function saveDeferred(CacheItemInterface $item): bool
{
return $this->doSave($item, __FUNCTION__);
}
public function commit(): bool
{
return $this->pool->commit();
}
private function doSave(CacheItemInterface $item, string $method): bool
{
if (!$item instanceof CacheItem) {
return false;
}
$castItem = (array) $item;
if (null === $castItem["\0*\0expiry"] && 0 < $this->defaultLifetime) {
$castItem["\0*\0expiry"] = microtime(true) + $this->defaultLifetime;
}
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.$castItem["\0*\0key"], null, $this->poolHash);
} else {
$innerItem = $this->pool->getItem($this->namespace.$castItem["\0*\0key"]);
}
(self::$setInnerItem)($innerItem, $item, $castItem["\0*\0expiry"]);
return $this->pool->$method($innerItem);
}
private function generateItems(iterable $items): \Generator
{
$f = self::$createCacheItem;
foreach ($items as $key => $item) {
if ($this->namespaceLen) {
$key = substr($key, $this->namespaceLen);
}
yield $key => $f($key, $item, $this->poolHash);
}
}
private function getId(mixed $key): string
{
\assert('' !== CacheItem::validateKey($key));
return $this->namespace.$key;
}
}
+71
View File
@@ -0,0 +1,71 @@
<?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 Psr\SimpleCache\CacheInterface;
use Symfony\Component\Cache\PruneableInterface;
use Symfony\Component\Cache\ResettableInterface;
use Symfony\Component\Cache\Traits\ProxyTrait;
/**
* Turns a PSR-16 cache into a PSR-6 one.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
class Psr16Adapter extends AbstractAdapter implements PruneableInterface, ResettableInterface
{
use ProxyTrait;
/**
* @internal
*/
protected const NS_SEPARATOR = '_';
private object $miss;
public function __construct(CacheInterface $pool, string $namespace = '', int $defaultLifetime = 0)
{
parent::__construct($namespace, $defaultLifetime);
$this->pool = $pool;
$this->miss = new \stdClass();
}
protected function doFetch(array $ids): iterable
{
foreach ($this->pool->getMultiple($ids, $this->miss) as $key => $value) {
if ($this->miss !== $value) {
yield $key => $value;
}
}
}
protected function doHave(string $id): bool
{
return $this->pool->has($id);
}
protected function doClear(string $namespace): bool
{
return $this->pool->clear();
}
protected function doDelete(array $ids): bool
{
return $this->pool->deleteMultiple($ids);
}
protected function doSave(array $values, int $lifetime): array|bool
{
return $this->pool->setMultiple($values, 0 === $lifetime ? null : $lifetime);
}
}
+25
View File
@@ -0,0 +1,25 @@
<?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 Symfony\Component\Cache\Marshaller\MarshallerInterface;
use Symfony\Component\Cache\Traits\RedisTrait;
class RedisAdapter extends AbstractAdapter
{
use RedisTrait;
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);
}
}
+310
View File
@@ -0,0 +1,310 @@
<?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 Predis\Connection\Aggregate\ClusterInterface;
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\RedisTrait;
/**
* Stores tag id <> cache id relationship as a Redis Set.
*
* Set (tag relation info) is stored without expiry (non-volatile), while cache always gets an expiry (volatile) even
* if not set by caller. Thus if you configure redis with the right eviction policy you can be safe this tag <> cache
* relationship survives eviction (cache cleanup when Redis runs out of memory).
*
* Redis server 2.8+ with any `volatile-*` eviction policy, OR `noeviction` if you're sure memory will NEVER fill up
*
* Design limitations:
* - Max 4 billion cache keys per cache tag as limited by Redis Set datatype.
* E.g. If you use a "all" items tag for expiry instead of clear(), that limits you to 4 billion cache items also.
*
* @see https://redis.io/topics/lru-cache#eviction-policies Documentation for Redis eviction policies.
* @see https://redis.io/topics/data-types#sets Documentation for Redis Set datatype.
*
* @author Nicolas Grekas <p@tchwork.com>
* @author André Rømcke <andre.romcke+symfony@gmail.com>
*/
class RedisTagAwareAdapter extends AbstractTagAwareAdapter
{
use RedisTrait;
/**
* On cache items without a lifetime set, we set it to 100 days. This is to make sure cache items are
* preferred to be evicted over tag Sets, if eviction policy is configured according to requirements.
*/
private const DEFAULT_CACHE_TTL = 8640000;
/**
* detected eviction policy used on Redis server.
*/
private string $redisEvictionPolicy;
public function __construct(
\Redis|Relay|\RedisArray|\RedisCluster|\Predis\ClientInterface $redis,
private 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())));
}
$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 ($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));
}
}
}
$this->init($redis, $namespace, $defaultLifetime, new TagAwareMarshaller($marshaller));
}
protected function doSave(array $values, int $lifetime, array $addTagData = [], array $delTagData = []): array
{
$eviction = $this->getRedisEvictionPolicy();
if ('noeviction' !== $eviction && !str_starts_with($eviction, 'volatile-')) {
throw new LogicException(sprintf('Redis maxmemory-policy setting "%s" is *not* supported by RedisTagAwareAdapter, use "noeviction" or "volatile-*" eviction policies.', $eviction));
}
// serialize values
if (!$serialized = $this->marshaller->marshall($values, $failed)) {
return $failed;
}
// While pipeline isn't supported on RedisCluster, other setups will at least benefit from doing this in one op
$results = $this->pipeline(static function () use ($serialized, $lifetime, $addTagData, $delTagData, $failed) {
// Store cache items, force a ttl if none is set, as there is no MSETEX we need to set each one
foreach ($serialized as $id => $value) {
yield 'setEx' => [
$id,
0 >= $lifetime ? self::DEFAULT_CACHE_TTL : $lifetime,
$value,
];
}
// Add and Remove Tags
foreach ($addTagData as $tagId => $ids) {
if (!$failed || $ids = array_diff($ids, $failed)) {
yield 'sAdd' => array_merge([$tagId], $ids);
}
}
foreach ($delTagData as $tagId => $ids) {
if (!$failed || $ids = array_diff($ids, $failed)) {
yield 'sRem' => array_merge([$tagId], $ids);
}
}
});
foreach ($results as $id => $result) {
// Skip results of SADD/SREM operations, they'll be 1 or 0 depending on if set value already existed or not
if (is_numeric($result)) {
continue;
}
// setEx results
if (true !== $result && (!$result instanceof Status || Status::get('OK') !== $result)) {
$failed[] = $id;
}
}
return $failed;
}
protected function doDeleteYieldTags(array $ids): iterable
{
$lua = <<<'EOLUA'
local v = redis.call('GET', KEYS[1])
local e = redis.pcall('UNLINK', KEYS[1])
if type(e) ~= 'number' then
redis.call('DEL', KEYS[1])
end
if not v or v:len() <= 13 or v:byte(1) ~= 0x9D or v:byte(6) ~= 0 or v:byte(10) ~= 0x5F then
return ''
end
return v:sub(14, 13 + v:byte(13) + v:byte(12) * 256 + v:byte(11) * 65536)
EOLUA;
$results = $this->pipeline(function () use ($ids, $lua) {
foreach ($ids as $id) {
yield 'eval' => $this->redis instanceof \Predis\ClientInterface ? [$lua, 1, $id] : [$lua, [$id], 1];
}
});
foreach ($results as $id => $result) {
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;
}
try {
yield $id => !\is_string($result) || '' === $result ? [] : $this->marshaller->unmarshall($result);
} catch (\Exception) {
yield $id => [];
}
}
}
protected function doDeleteTagRelations(array $tagData): bool
{
$results = $this->pipeline(static function () use ($tagData) {
foreach ($tagData as $tagId => $idList) {
array_unshift($idList, $tagId);
yield 'sRem' => $idList;
}
});
foreach ($results as $result) {
// no-op
}
return true;
}
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
// garbage collect that set from the client side.
$lua = <<<'EOLUA'
redis.replicate_commands()
local cursor = '0'
local id = KEYS[1]
repeat
local result = redis.call('SSCAN', id, cursor, 'COUNT', 5000);
cursor = result[1];
local rems = {}
for _, v in ipairs(result[2]) do
local ok, _ = pcall(redis.call, 'DEL', ARGV[1]..v)
if ok then
table.insert(rems, v)
end
end
if 0 < #rems then
redis.call('SREM', id, unpack(rems))
end
until '0' == cursor;
redis.call('SUNIONSTORE', '{'..id..'}'..id, id)
redis.call('DEL', id)
return redis.call('SSCAN', '{'..id..'}'..id, '0', 'COUNT', 5000)
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($this->redis instanceof Relay ? Relay::OPT_PREFIX : \Redis::OPT_PREFIX) ?? '')) {
$prefix = current($prefix);
}
foreach ($tagIds as $id) {
yield 'eval' => $this->redis instanceof \Predis\ClientInterface ? [$lua, 1, $id, $prefix] : [$lua, [$id, $prefix], 1];
}
});
$lua = <<<'EOLUA'
redis.replicate_commands()
local id = KEYS[1]
local cursor = table.remove(ARGV)
redis.call('SREM', '{'..id..'}'..id, unpack(ARGV))
return redis.call('SSCAN', '{'..id..'}'..id, cursor, 'COUNT', 5000)
EOLUA;
$success = true;
foreach ($results as $id => $values) {
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;
continue;
}
[$cursor, $ids] = $values;
while ($ids || '0' !== $cursor) {
$this->doDelete($ids);
$evalArgs = [$id, $cursor];
array_splice($evalArgs, 1, 0, $ids);
if ($this->redis instanceof \Predis\ClientInterface) {
array_unshift($evalArgs, $lua, 1);
} else {
$evalArgs = [$lua, $evalArgs, 1];
}
$results = $this->pipeline(function () use ($evalArgs) {
yield 'eval' => $evalArgs;
});
foreach ($results as [$cursor, $ids]) {
// no-op
}
}
}
return $success;
}
private function getRedisEvictionPolicy(): string
{
if (isset($this->redisEvictionPolicy)) {
return $this->redisEvictionPolicy;
}
$hosts = $this->getHosts();
$host = reset($hosts);
if ($host instanceof \Predis\Client && $host->getConnection() instanceof ReplicationInterface) {
// Predis supports info command only on the master in replication environments
$hosts = [$host->getClientFor('master')];
}
foreach ($hosts as $host) {
$info = $host->info('Memory');
if (false === $info || null === $info || $info instanceof ErrorInterface) {
continue;
}
$info = $info['Memory'] ?? $info;
return $this->redisEvictionPolicy = $info['maxmemory_policy'] ?? '';
}
return $this->redisEvictionPolicy = '';
}
}
+370
View File
@@ -0,0 +1,370 @@
<?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 Psr\Cache\CacheItemInterface;
use Psr\Cache\InvalidArgumentException;
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerAwareTrait;
use Symfony\Component\Cache\CacheItem;
use Symfony\Component\Cache\PruneableInterface;
use Symfony\Component\Cache\ResettableInterface;
use Symfony\Component\Cache\Traits\ContractsTrait;
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;
public const TAGS_PREFIX = "\1tags\1";
private array $deferred = [];
private AdapterInterface $pool;
private AdapterInterface $tags;
private array $knownTagVersions = [];
private static \Closure $setCacheItemTags;
private static \Closure $setTagVersions;
private static \Closure $getTagsByKey;
private static \Closure $saveTags;
public function __construct(
AdapterInterface $itemsPool,
?AdapterInterface $tagsPool = null,
private float $knownTagVersionsTtl = 0.15,
) {
$this->pool = $itemsPool;
$this->tags = $tagsPool ?? $itemsPool;
self::$setCacheItemTags ??= \Closure::bind(
static function (array $items, array $itemTags) {
foreach ($items as $key => $item) {
$item->isTaggable = true;
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 = [];
}
}
return $items;
},
null,
CacheItem::class
);
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) {
$tagsByKey[$key] = $item->newMetadata[CacheItem::METADATA_TAGS] ?? [];
$item->metadata = $item->newMetadata;
}
return $tagsByKey;
},
null,
CacheItem::class
);
self::$saveTags ??= \Closure::bind(
static function (AdapterInterface $tagsAdapter, array $tags) {
ksort($tags);
foreach ($tags as $v) {
$v->expiry = 0;
$tagsAdapter->saveDeferred($v);
}
return $tagsAdapter->commit();
},
null,
CacheItem::class
);
}
public function invalidateTags(array $tags): bool
{
$ids = [];
foreach ($tags as $tag) {
\assert('' !== CacheItem::validateKey($tag));
unset($this->knownTagVersions[$tag]);
$ids[] = $tag.static::TAGS_PREFIX;
}
return !$tags || $this->tags->deleteItems($ids);
}
public function hasItem(mixed $key): bool
{
return $this->getItem($key)->isHit();
}
public function getItem(mixed $key): CacheItem
{
foreach ($this->getItems([$key]) as $item) {
return $item;
}
}
public function getItems(array $keys = []): iterable
{
$tagKeys = [];
$commit = false;
foreach ($keys as $key) {
if ('' !== $key && \is_string($key)) {
$commit = $commit || isset($this->deferred[$key]);
}
}
if ($commit) {
$this->commit();
}
try {
$items = $this->pool->getItems($keys);
} catch (InvalidArgumentException $e) {
$this->pool->getItems($keys); // Should throw an exception
throw $e;
}
$bufferedItems = $itemTags = [];
foreach ($items as $key => $item) {
if (null !== $tags = $item->getMetadata()[CacheItem::METADATA_TAGS] ?? null) {
$itemTags[$key] = $tags;
}
$bufferedItems[$key] = $item;
if (null === $tags) {
$key = "\0tags\0".$key;
$tagKeys[$key] = $key; // BC with pools populated before v6.1
}
}
if ($tagKeys) {
foreach ($this->pool->getItems($tagKeys) as $key => $item) {
if ($item->isHit()) {
$itemTags[substr($key, \strlen("\0tags\0"))] = $item->get() ?: [];
}
}
}
$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);
}
public function clear(string $prefix = ''): bool
{
if ('' !== $prefix) {
foreach ($this->deferred as $key => $item) {
if (str_starts_with($key, $prefix)) {
unset($this->deferred[$key]);
}
}
} else {
$this->deferred = [];
}
if ($this->pool instanceof AdapterInterface) {
return $this->pool->clear($prefix);
}
return $this->pool->clear();
}
public function deleteItem(mixed $key): bool
{
return $this->deleteItems([$key]);
}
public function deleteItems(array $keys): bool
{
foreach ($keys as $key) {
if ('' !== $key && \is_string($key)) {
$keys[] = "\0tags\0".$key; // BC with pools populated before v6.1
}
}
return $this->pool->deleteItems($keys);
}
public function save(CacheItemInterface $item): bool
{
if (!$item instanceof CacheItem) {
return false;
}
$this->deferred[$item->getKey()] = $item;
return $this->commit();
}
public function saveDeferred(CacheItemInterface $item): bool
{
if (!$item instanceof CacheItem) {
return false;
}
$this->deferred[$item->getKey()] = $item;
return true;
}
public function commit(): bool
{
if (!$items = $this->deferred) {
return true;
}
$tagVersions = $this->getTagVersions((self::$getTagsByKey)($items), true);
(self::$setTagVersions)($items, $tagVersions);
$ok = true;
foreach ($items as $key => $item) {
if ($this->pool->saveDeferred($item)) {
unset($this->deferred[$key]);
} else {
$ok = false;
}
}
$ok = $this->pool->commit() && $ok;
$tagVersions = array_keys($tagVersions);
(self::$setTagVersions)($items, array_combine($tagVersions, $tagVersions));
return $ok;
}
public function prune(): bool
{
return $this->pool instanceof PruneableInterface && $this->pool->prune();
}
public function reset(): void
{
$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__);
}
public function __wakeup(): void
{
throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);
}
public function __destruct()
{
$this->commit();
}
private function getTagVersions(array $tagsByKey, bool $persistTags): array
{
$tagVersions = [];
$fetchTagVersions = $persistTags;
foreach ($tagsByKey as $tags) {
$tagVersions += $tags;
if ($fetchTagVersions) {
continue;
}
foreach ($tags as $tag => $version) {
if ($tagVersions[$tag] !== $version) {
$fetchTagVersions = true;
}
}
}
if (!$tagVersions) {
return [];
}
$now = microtime(true);
$tags = [];
foreach ($tagVersions as $tag => $version) {
$tags[$tag.static::TAGS_PREFIX] = $tag;
$knownTagVersion = $this->knownTagVersions[$tag] ?? [0, null];
if ($fetchTagVersions || $now > $knownTagVersion[0] || $knownTagVersion[1] !== $version) {
// reuse previously fetched tag versions until the expiration
$fetchTagVersions = true;
}
}
if (!$fetchTagVersions) {
return $tagVersions;
}
$newTags = [];
$newVersion = null;
$expiration = $now + $this->knownTagVersionsTtl;
foreach ($this->tags->getItems(array_keys($tags)) as $tag => $version) {
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];
}
}
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;
}
}
@@ -0,0 +1,31 @@
<?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 Psr\Cache\InvalidArgumentException;
/**
* Interface for invalidating cached items using tags.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
interface TagAwareAdapterInterface extends AdapterInterface
{
/**
* Invalidates cached items using tags.
*
* @param string[] $tags An array of tags to invalidate
*
* @throws InvalidArgumentException When $tags is not valid
*/
public function invalidateTags(array $tags): bool;
}
+250
View File
@@ -0,0 +1,250 @@
<?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 Psr\Cache\CacheItemInterface;
use Symfony\Component\Cache\CacheItem;
use Symfony\Component\Cache\PruneableInterface;
use Symfony\Component\Cache\ResettableInterface;
use Symfony\Contracts\Cache\CacheInterface;
use Symfony\Contracts\Service\ResetInterface;
/**
* An adapter that collects data about all cache calls.
*
* @author Aaron Scherer <aequasi@gmail.com>
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
* @author Nicolas Grekas <p@tchwork.com>
*/
class TraceableAdapter implements AdapterInterface, CacheInterface, PruneableInterface, ResettableInterface
{
protected AdapterInterface $pool;
private array $calls = [];
public function __construct(AdapterInterface $pool)
{
$this->pool = $pool;
}
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));
}
$isHit = true;
$callback = function (CacheItem $item, bool &$save) use ($callback, &$isHit) {
$isHit = $item->isHit();
return $callback($item, $save);
};
$event = $this->start(__FUNCTION__);
try {
$value = $this->pool->get($key, $callback, $beta, $metadata);
$event->result[$key] = get_debug_type($value);
} finally {
$event->end = microtime(true);
}
if ($isHit) {
++$event->hits;
} else {
++$event->misses;
}
return $value;
}
public function getItem(mixed $key): CacheItem
{
$event = $this->start(__FUNCTION__);
try {
$item = $this->pool->getItem($key);
} finally {
$event->end = microtime(true);
}
if ($event->result[$key] = $item->isHit()) {
++$event->hits;
} else {
++$event->misses;
}
return $item;
}
public function hasItem(mixed $key): bool
{
$event = $this->start(__FUNCTION__);
try {
return $event->result[$key] = $this->pool->hasItem($key);
} finally {
$event->end = microtime(true);
}
}
public function deleteItem(mixed $key): bool
{
$event = $this->start(__FUNCTION__);
try {
return $event->result[$key] = $this->pool->deleteItem($key);
} finally {
$event->end = microtime(true);
}
}
public function save(CacheItemInterface $item): bool
{
$event = $this->start(__FUNCTION__);
try {
return $event->result[$item->getKey()] = $this->pool->save($item);
} finally {
$event->end = microtime(true);
}
}
public function saveDeferred(CacheItemInterface $item): bool
{
$event = $this->start(__FUNCTION__);
try {
return $event->result[$item->getKey()] = $this->pool->saveDeferred($item);
} finally {
$event->end = microtime(true);
}
}
public function getItems(array $keys = []): iterable
{
$event = $this->start(__FUNCTION__);
try {
$result = $this->pool->getItems($keys);
} finally {
$event->end = microtime(true);
}
$f = function () use ($result, $event) {
$event->result = [];
foreach ($result as $key => $item) {
if ($event->result[$key] = $item->isHit()) {
++$event->hits;
} else {
++$event->misses;
}
yield $key => $item;
}
};
return $f();
}
public function clear(string $prefix = ''): bool
{
$event = $this->start(__FUNCTION__);
try {
if ($this->pool instanceof AdapterInterface) {
return $event->result = $this->pool->clear($prefix);
}
return $event->result = $this->pool->clear();
} finally {
$event->end = microtime(true);
}
}
public function deleteItems(array $keys): bool
{
$event = $this->start(__FUNCTION__);
$event->result['keys'] = $keys;
try {
return $event->result['result'] = $this->pool->deleteItems($keys);
} finally {
$event->end = microtime(true);
}
}
public function commit(): bool
{
$event = $this->start(__FUNCTION__);
try {
return $event->result = $this->pool->commit();
} finally {
$event->end = microtime(true);
}
}
public function prune(): bool
{
if (!$this->pool instanceof PruneableInterface) {
return false;
}
$event = $this->start(__FUNCTION__);
try {
return $event->result = $this->pool->prune();
} finally {
$event->end = microtime(true);
}
}
public function reset(): void
{
if ($this->pool instanceof ResetInterface) {
$this->pool->reset();
}
$this->clearCalls();
}
public function delete(string $key): bool
{
$event = $this->start(__FUNCTION__);
try {
return $event->result[$key] = $this->pool->deleteItem($key);
} finally {
$event->end = microtime(true);
}
}
public function getCalls(): array
{
return $this->calls;
}
public function clearCalls(): void
{
$this->calls = [];
}
public function getPool(): AdapterInterface
{
return $this->pool;
}
protected function start(string $name): TraceableAdapterEvent
{
$this->calls[] = $event = new TraceableAdapterEvent();
$event->name = $name;
$event->start = microtime(true);
return $event;
}
}
/**
* @internal
*/
class TraceableAdapterEvent
{
public string $name;
public float $start;
public float $end;
public array|bool $result;
public int $hits = 0;
public int $misses = 0;
}
@@ -0,0 +1,35 @@
<?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 Symfony\Contracts\Cache\TagAwareCacheInterface;
/**
* @author Robin Chalas <robin.chalas@gmail.com>
*/
class TraceableTagAwareAdapter extends TraceableAdapter implements TagAwareAdapterInterface, TagAwareCacheInterface
{
public function __construct(TagAwareAdapterInterface $pool)
{
parent::__construct($pool);
}
public function invalidateTags(array $tags): bool
{
$event = $this->start(__FUNCTION__);
try {
return $event->result = $this->pool->invalidateTags($tags);
} finally {
$event->end = microtime(true);
}
}
}
+147
View File
@@ -0,0 +1,147 @@
CHANGELOG
=========
7.1
---
* Add option `sentinel_master` as an alias for `redis_sentinel`
* Deprecate `CouchbaseBucketAdapter`, use `CouchbaseCollectionAdapter`
* Add support for URL encoded characters in Couchbase DSN
* Add support for using DSN with PDOAdapter
* The algorithm for the default cache namespace changed from SHA256 to XXH128
7.0
---
* Add parameter `$isSameDatabase` to `DoctrineDbalAdapter::configureSchema()`
* Drop support for Postgres < 9.5 and SQL Server < 2008 in `DoctrineDbalAdapter`
6.4
---
* `EarlyExpirationHandler` no longer implements `MessageHandlerInterface`, rely on `AsMessageHandler` instead
6.3
---
* Add support for Relay PHP extension for Redis
* Updates to allow Redis cluster connections using predis/predis:^2.0
* Add optional parameter `$isSameDatabase` to `DoctrineDbalAdapter::configureSchema()`
6.1
---
* Add support for ACL auth in RedisAdapter
* Improve reliability and performance of `TagAwareAdapter` by making tag versions an integral part of item value
6.0
---
* Remove `DoctrineProvider` and `DoctrineAdapter`
* Remove support of Doctrine DBAL in `PdoAdapter`
5.4
---
* Deprecate `DoctrineProvider` and `DoctrineAdapter` because these classes have been added to the `doctrine/cache` package
* Add `DoctrineDbalAdapter` identical to `PdoAdapter` for `Doctrine\DBAL\Connection` or DBAL URL
* Deprecate usage of `PdoAdapter` with `Doctrine\DBAL\Connection` or DBAL URL
5.3
---
* added support for connecting to Redis Sentinel clusters when using the Redis PHP extension
* add support for a custom serializer to the `ApcuAdapter` class
5.2.0
-----
* added integration with Messenger to allow computing cached values in a worker
* allow ISO 8601 time intervals to specify default lifetime
5.1.0
-----
* added max-items + LRU + max-lifetime capabilities to `ArrayCache`
* added `CouchbaseBucketAdapter`
* added context `cache-adapter` to log messages
5.0.0
-----
* removed all PSR-16 implementations in the `Simple` namespace
* removed `SimpleCacheAdapter`
* removed `AbstractAdapter::unserialize()`
* removed `CacheItem::getPreviousTags()`
4.4.0
-----
* added support for connecting to Redis Sentinel clusters
* added argument `$prefix` to `AdapterInterface::clear()`
* improved `RedisTagAwareAdapter` to support Redis server >= 2.8 and up to 4B items per tag
* added `TagAwareMarshaller` for optimized data storage when using `AbstractTagAwareAdapter`
* added `DeflateMarshaller` to compress serialized values
* removed support for phpredis 4 `compression`
* [BC BREAK] `RedisTagAwareAdapter` is not compatible with `RedisCluster` from `Predis` anymore, use `phpredis` instead
* Marked the `CacheDataCollector` class as `@final`.
* added `SodiumMarshaller` to encrypt/decrypt values using libsodium
4.3.0
-----
* removed `psr/simple-cache` dependency, run `composer require psr/simple-cache` if you need it
* deprecated all PSR-16 adapters, use `Psr16Cache` or `Symfony\Contracts\Cache\CacheInterface` implementations instead
* deprecated `SimpleCacheAdapter`, use `Psr16Adapter` instead
4.2.0
-----
* added support for connecting to Redis clusters via DSN
* added support for configuring multiple Memcached servers via DSN
* added `MarshallerInterface` and `DefaultMarshaller` to allow changing the serializer and provide one that automatically uses igbinary when available
* implemented `CacheInterface`, which provides stampede protection via probabilistic early expiration and should become the preferred way to use a cache
* added sub-second expiry accuracy for backends that support it
* added support for phpredis 4 `compression` and `tcp_keepalive` options
* added automatic table creation when using Doctrine DBAL with PDO-based backends
* throw `LogicException` when `CacheItem::tag()` is called on an item coming from a non tag-aware pool
* deprecated `CacheItem::getPreviousTags()`, use `CacheItem::getMetadata()` instead
* deprecated the `AbstractAdapter::unserialize()` and `AbstractCache::unserialize()` methods
* added `CacheCollectorPass` (originally in `FrameworkBundle`)
* added `CachePoolClearerPass` (originally in `FrameworkBundle`)
* added `CachePoolPass` (originally in `FrameworkBundle`)
* added `CachePoolPrunerPass` (originally in `FrameworkBundle`)
3.4.0
-----
* added using options from Memcached DSN
* added PruneableInterface so PSR-6 or PSR-16 cache implementations can declare support for manual stale cache pruning
* added prune logic to FilesystemTrait, PhpFilesTrait, PdoTrait, TagAwareAdapter and ChainTrait
* now FilesystemAdapter, PhpFilesAdapter, FilesystemCache, PhpFilesCache, PdoAdapter, PdoCache, ChainAdapter, and
ChainCache implement PruneableInterface and support manual stale cache pruning
3.3.0
-----
* added CacheItem::getPreviousTags() to get bound tags coming from the pool storage if any
* added PSR-16 "Simple Cache" implementations for all existing PSR-6 adapters
* added Psr6Cache and SimpleCacheAdapter for bidirectional interoperability between PSR-6 and PSR-16
* added MemcachedAdapter (PSR-6) and MemcachedCache (PSR-16)
* added TraceableAdapter (PSR-6) and TraceableCache (PSR-16)
3.2.0
-----
* added TagAwareAdapter for tags-based invalidation
* added PdoAdapter with PDO and Doctrine DBAL support
* added PhpArrayAdapter and PhpFilesAdapter for OPcache-backed shared memory storage (PHP 7+ only)
* added NullAdapter
3.1.0
-----
* added the component with strict PSR-6 implementations
* added ApcuAdapter, ArrayAdapter, FilesystemAdapter and RedisAdapter
* added AbstractAdapter, ChainAdapter and ProxyAdapter
* added DoctrineAdapter and DoctrineProvider for bidirectional interoperability with Doctrine Cache
+198
View File
@@ -0,0 +1,198 @@
<?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;
use Psr\Log\LoggerInterface;
use Symfony\Component\Cache\Exception\InvalidArgumentException;
use Symfony\Component\Cache\Exception\LogicException;
use Symfony\Contracts\Cache\ItemInterface;
/**
* @author Nicolas Grekas <p@tchwork.com>
*/
final class CacheItem implements ItemInterface
{
private const METADATA_EXPIRY_OFFSET = 1527506807;
private const VALUE_WRAPPER = "\xA9";
protected string $key;
protected mixed $value = null;
protected bool $isHit = false;
protected float|int|null $expiry = null;
protected array $metadata = [];
protected array $newMetadata = [];
protected ?ItemInterface $innerItem = null;
protected ?string $poolHash = null;
protected bool $isTaggable = false;
public function getKey(): string
{
return $this->key;
}
public function get(): mixed
{
return $this->value;
}
public function isHit(): bool
{
return $this->isHit;
}
/**
* @return $this
*/
public function set($value): static
{
$this->value = $value;
return $this;
}
/**
* @return $this
*/
public function expiresAt(?\DateTimeInterface $expiration): static
{
$this->expiry = null !== $expiration ? (float) $expiration->format('U.u') : null;
return $this;
}
/**
* @return $this
*/
public function expiresAfter(mixed $time): static
{
if (null === $time) {
$this->expiry = null;
} elseif ($time instanceof \DateInterval) {
$this->expiry = microtime(true) + \DateTimeImmutable::createFromFormat('U', 0)->add($time)->format('U.u');
} elseif (\is_int($time)) {
$this->expiry = $time + microtime(true);
} else {
throw new InvalidArgumentException(sprintf('Expiration date must be an integer, a DateInterval or null, "%s" given.', get_debug_type($time)));
}
return $this;
}
public function tag(mixed $tags): static
{
if (!$this->isTaggable) {
throw new LogicException(sprintf('Cache item "%s" comes from a non tag-aware pool: you cannot tag it.', $this->key));
}
if (!\is_array($tags) && !$tags instanceof \Traversable) { // don't use is_iterable(), it's slow
$tags = [$tags];
}
foreach ($tags as $tag) {
if (!\is_string($tag) && !$tag instanceof \Stringable) {
throw new InvalidArgumentException(sprintf('Cache tag must be string or object that implements __toString(), "%s" given.', get_debug_type($tag)));
}
$tag = (string) $tag;
if (isset($this->newMetadata[self::METADATA_TAGS][$tag])) {
continue;
}
if ('' === $tag) {
throw new InvalidArgumentException('Cache tag length must be greater than zero.');
}
if (false !== strpbrk($tag, self::RESERVED_CHARACTERS)) {
throw new InvalidArgumentException(sprintf('Cache tag "%s" contains reserved characters "%s".', $tag, self::RESERVED_CHARACTERS));
}
$this->newMetadata[self::METADATA_TAGS][$tag] = $tag;
}
return $this;
}
public function getMetadata(): array
{
return $this->metadata;
}
/**
* Validates a cache key according to PSR-6.
*
* @param mixed $key The key to validate
*
* @throws InvalidArgumentException When $key is not valid
*/
public static function validateKey($key): string
{
if (!\is_string($key)) {
throw new InvalidArgumentException(sprintf('Cache key must be string, "%s" given.', get_debug_type($key)));
}
if ('' === $key) {
throw new InvalidArgumentException('Cache key length must be greater than zero.');
}
if (false !== strpbrk($key, self::RESERVED_CHARACTERS)) {
throw new InvalidArgumentException(sprintf('Cache key "%s" contains reserved characters "%s".', $key, self::RESERVED_CHARACTERS));
}
return $key;
}
/**
* Internal logging helper.
*
* @internal
*/
public static function log(?LoggerInterface $logger, string $message, array $context = []): void
{
if ($logger) {
$logger->warning($message, $context);
} else {
$replace = [];
foreach ($context as $k => $v) {
if (\is_scalar($v)) {
$replace['{'.$k.'}'] = $v;
}
}
@trigger_error(strtr($message, $replace), \E_USER_WARNING);
}
}
private function pack(): mixed
{
if (!$m = $this->newMetadata) {
return $this->value;
}
$valueWrapper = self::VALUE_WRAPPER;
return new $valueWrapper($this->value, $m + ['expiry' => $this->expiry]);
}
private function unpack(): bool
{
$v = $this->value;
$valueWrapper = self::VALUE_WRAPPER;
if ($v instanceof $valueWrapper) {
$this->value = $v->value;
$this->metadata = $v->metadata;
return true;
}
if (!\is_array($v) || 1 !== \count($v) || 10 !== \strlen($k = (string) array_key_first($v)) || "\x9D" !== $k[0] || "\0" !== $k[5] || "\x5F" !== $k[9]) {
return false;
}
// BC with pools populated before v6.1
$this->value = $v[$k];
$this->metadata = unpack('Vexpiry/Nctime', substr($k, 1, -1));
$this->metadata['expiry'] += self::METADATA_EXPIRY_OFFSET;
return true;
}
}
@@ -0,0 +1,184 @@
<?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\DataCollector;
use Symfony\Component\Cache\Adapter\TraceableAdapter;
use Symfony\Component\Cache\Adapter\TraceableAdapterEvent;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\DataCollector\DataCollector;
use Symfony\Component\HttpKernel\DataCollector\LateDataCollectorInterface;
/**
* @author Aaron Scherer <aequasi@gmail.com>
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
*
* @final
*/
class CacheDataCollector extends DataCollector implements LateDataCollectorInterface
{
/**
* @var TraceableAdapter[]
*/
private array $instances = [];
public function addInstance(string $name, TraceableAdapter $instance): void
{
$this->instances[$name] = $instance;
}
public function collect(Request $request, Response $response, ?\Throwable $exception = null): void
{
$empty = ['calls' => [], 'adapters' => [], 'config' => [], 'options' => [], 'statistics' => []];
$this->data = ['instances' => $empty, 'total' => $empty];
foreach ($this->instances as $name => $instance) {
$this->data['instances']['calls'][$name] = $instance->getCalls();
$this->data['instances']['adapters'][$name] = get_debug_type($instance->getPool());
}
$this->data['instances']['statistics'] = $this->calculateStatistics();
$this->data['total']['statistics'] = $this->calculateTotalStatistics();
}
public function reset(): void
{
$this->data = [];
foreach ($this->instances as $instance) {
$instance->clearCalls();
}
}
public function lateCollect(): void
{
$this->data['instances']['calls'] = $this->cloneVar($this->data['instances']['calls']);
}
public function getName(): string
{
return 'cache';
}
/**
* Method returns amount of logged Cache reads: "get" calls.
*/
public function getStatistics(): array
{
return $this->data['instances']['statistics'];
}
/**
* Method returns the statistic totals.
*/
public function getTotals(): array
{
return $this->data['total']['statistics'];
}
/**
* Method returns all logged Cache call objects.
*/
public function getCalls(): mixed
{
return $this->data['instances']['calls'];
}
/**
* Method returns all logged Cache adapter classes.
*/
public function getAdapters(): array
{
return $this->data['instances']['adapters'];
}
private function calculateStatistics(): array
{
$statistics = [];
foreach ($this->data['instances']['calls'] as $name => $calls) {
$statistics[$name] = [
'calls' => 0,
'time' => 0,
'reads' => 0,
'writes' => 0,
'deletes' => 0,
'hits' => 0,
'misses' => 0,
];
/** @var TraceableAdapterEvent $call */
foreach ($calls as $call) {
++$statistics[$name]['calls'];
$statistics[$name]['time'] += ($call->end ?? microtime(true)) - $call->start;
if ('get' === $call->name) {
++$statistics[$name]['reads'];
if ($call->hits) {
++$statistics[$name]['hits'];
} else {
++$statistics[$name]['misses'];
++$statistics[$name]['writes'];
}
} elseif ('getItem' === $call->name) {
++$statistics[$name]['reads'];
if ($call->hits) {
++$statistics[$name]['hits'];
} else {
++$statistics[$name]['misses'];
}
} elseif ('getItems' === $call->name) {
$statistics[$name]['reads'] += $call->hits + $call->misses;
$statistics[$name]['hits'] += $call->hits;
$statistics[$name]['misses'] += $call->misses;
} elseif ('hasItem' === $call->name) {
++$statistics[$name]['reads'];
foreach ($call->result ?? [] as $result) {
++$statistics[$name][$result ? 'hits' : 'misses'];
}
} elseif ('save' === $call->name) {
++$statistics[$name]['writes'];
} elseif ('deleteItem' === $call->name) {
++$statistics[$name]['deletes'];
}
}
if ($statistics[$name]['reads']) {
$statistics[$name]['hit_read_ratio'] = round(100 * $statistics[$name]['hits'] / $statistics[$name]['reads'], 2);
} else {
$statistics[$name]['hit_read_ratio'] = null;
}
}
return $statistics;
}
private function calculateTotalStatistics(): array
{
$statistics = $this->getStatistics();
$totals = [
'calls' => 0,
'time' => 0,
'reads' => 0,
'writes' => 0,
'deletes' => 0,
'hits' => 0,
'misses' => 0,
];
foreach ($statistics as $name => $values) {
foreach ($totals as $key => $value) {
$totals[$key] += $statistics[$name][$key];
}
}
if ($totals['reads']) {
$totals['hit_read_ratio'] = round(100 * $totals['hits'] / $totals['reads'], 2);
} else {
$totals['hit_read_ratio'] = null;
}
return $totals;
}
}
@@ -0,0 +1,75 @@
<?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\DependencyInjection;
use Symfony\Component\Cache\Adapter\TagAwareAdapterInterface;
use Symfony\Component\Cache\Adapter\TraceableAdapter;
use Symfony\Component\Cache\Adapter\TraceableTagAwareAdapter;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;
/**
* Inject a data collector to all the cache services to be able to get detailed statistics.
*
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
*/
class CacheCollectorPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container): void
{
if (!$container->hasDefinition('data_collector.cache')) {
return;
}
foreach ($container->findTaggedServiceIds('cache.pool') as $id => $attributes) {
$poolName = $attributes[0]['name'] ?? $id;
$this->addToCollector($id, $poolName, $container);
}
}
private function addToCollector(string $id, string $name, ContainerBuilder $container): void
{
$definition = $container->getDefinition($id);
if ($definition->isAbstract()) {
return;
}
$collectorDefinition = $container->getDefinition('data_collector.cache');
$recorder = new Definition(is_subclass_of($definition->getClass(), TagAwareAdapterInterface::class) ? TraceableTagAwareAdapter::class : TraceableAdapter::class);
$recorder->setTags($definition->getTags());
if (!$definition->isPublic() || !$definition->isPrivate()) {
$recorder->setPublic($definition->isPublic());
}
$recorder->setArguments([new Reference($innerId = $id.'.recorder_inner')]);
foreach ($definition->getMethodCalls() as [$method, $args]) {
if ('setCallbackWrapper' !== $method || !$args[0] instanceof Definition || !($args[0]->getArguments()[2] ?? null) instanceof Definition) {
continue;
}
if ([new Reference($id), 'setCallbackWrapper'] == $args[0]->getArguments()[2]->getFactory()) {
$args[0]->getArguments()[2]->setFactory([new Reference($innerId), 'setCallbackWrapper']);
}
}
$definition->setTags([]);
$definition->setPublic(false);
$container->setDefinition($innerId, $definition);
$container->setDefinition($id, $recorder);
// Tell the collector to add the new instance
$collectorDefinition->addMethodCall('addInstance', [$name, new Reference($id)]);
}
}
@@ -0,0 +1,38 @@
<?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\DependencyInjection;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
/**
* @author Nicolas Grekas <p@tchwork.com>
*/
class CachePoolClearerPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container): void
{
$container->getParameterBag()->remove('cache.prefix.seed');
foreach ($container->findTaggedServiceIds('cache.pool.clearer') as $id => $attr) {
$clearer = $container->getDefinition($id);
$pools = [];
foreach ($clearer->getArgument(0) as $name => $ref) {
if ($container->hasDefinition($ref)) {
$pools[$name] = new Reference($ref);
}
}
$clearer->replaceArgument(0, $pools);
}
}
}
@@ -0,0 +1,243 @@
<?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\DependencyInjection;
use Symfony\Component\Cache\Adapter\AbstractAdapter;
use Symfony\Component\Cache\Adapter\ArrayAdapter;
use Symfony\Component\Cache\Adapter\ChainAdapter;
use Symfony\Component\Cache\Adapter\NullAdapter;
use Symfony\Component\Cache\Adapter\ParameterNormalizer;
use Symfony\Component\Cache\Messenger\EarlyExpirationDispatcher;
use Symfony\Component\DependencyInjection\ChildDefinition;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
use Symfony\Component\DependencyInjection\Reference;
/**
* @author Nicolas Grekas <p@tchwork.com>
*/
class CachePoolPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container): void
{
if ($container->hasParameter('cache.prefix.seed')) {
$seed = $container->getParameterBag()->resolveValue($container->getParameter('cache.prefix.seed'));
} else {
$seed = '_'.$container->getParameter('kernel.project_dir');
$seed .= '.'.$container->getParameter('kernel.container_class');
}
$needsMessageHandler = false;
$allPools = [];
$clearers = [];
$attributes = [
'provider',
'name',
'namespace',
'default_lifetime',
'early_expiration_message_bus',
'reset',
];
foreach ($container->findTaggedServiceIds('cache.pool') as $id => $tags) {
$adapter = $pool = $container->getDefinition($id);
if ($pool->isAbstract()) {
continue;
}
$class = $adapter->getClass();
while ($adapter instanceof ChildDefinition) {
$adapter = $container->findDefinition($adapter->getParent());
$class = $class ?: $adapter->getClass();
if ($t = $adapter->getTag('cache.pool')) {
$tags[0] += $t[0];
}
}
$name = $tags[0]['name'] ?? $id;
if (!isset($tags[0]['namespace'])) {
$namespaceSeed = $seed;
if (null !== $class) {
$namespaceSeed .= '.'.$class;
}
$tags[0]['namespace'] = $this->getNamespace($namespaceSeed, $name);
}
if (isset($tags[0]['clearer'])) {
$clearer = $tags[0]['clearer'];
while ($container->hasAlias($clearer)) {
$clearer = (string) $container->getAlias($clearer);
}
} else {
$clearer = null;
}
unset($tags[0]['clearer'], $tags[0]['name']);
if (isset($tags[0]['provider'])) {
$tags[0]['provider'] = new Reference(static::getServiceProvider($container, $tags[0]['provider']));
}
if (ChainAdapter::class === $class) {
$adapters = [];
foreach ($adapter->getArgument(0) as $provider => $adapter) {
if ($adapter instanceof ChildDefinition) {
$chainedPool = $adapter;
} else {
$chainedPool = $adapter = new ChildDefinition($adapter);
}
$chainedTags = [\is_int($provider) ? [] : ['provider' => $provider]];
$chainedClass = '';
while ($adapter instanceof ChildDefinition) {
$adapter = $container->findDefinition($adapter->getParent());
$chainedClass = $chainedClass ?: $adapter->getClass();
if ($t = $adapter->getTag('cache.pool')) {
$chainedTags[0] += $t[0];
}
}
if (ChainAdapter::class === $chainedClass) {
throw new InvalidArgumentException(sprintf('Invalid service "%s": chain of adapters cannot reference another chain, found "%s".', $id, $chainedPool->getParent()));
}
$i = 0;
if (isset($chainedTags[0]['provider'])) {
$chainedPool->replaceArgument($i++, new Reference(static::getServiceProvider($container, $chainedTags[0]['provider'])));
}
if (isset($tags[0]['namespace']) && !\in_array($adapter->getClass(), [ArrayAdapter::class, NullAdapter::class], true)) {
$chainedPool->replaceArgument($i++, $tags[0]['namespace']);
}
if (isset($tags[0]['default_lifetime'])) {
$chainedPool->replaceArgument($i++, $tags[0]['default_lifetime']);
}
$adapters[] = $chainedPool;
}
$pool->replaceArgument(0, $adapters);
unset($tags[0]['provider'], $tags[0]['namespace']);
$i = 1;
} else {
$i = 0;
}
foreach ($attributes as $attr) {
if (!isset($tags[0][$attr])) {
// no-op
} elseif ('reset' === $attr) {
if ($tags[0][$attr]) {
$pool->addTag('kernel.reset', ['method' => $tags[0][$attr]]);
}
} elseif ('early_expiration_message_bus' === $attr) {
$needsMessageHandler = true;
$pool->addMethodCall('setCallbackWrapper', [(new Definition(EarlyExpirationDispatcher::class))
->addArgument(new Reference($tags[0]['early_expiration_message_bus']))
->addArgument(new Reference('reverse_container'))
->addArgument((new Definition('callable'))
->setFactory([new Reference($id), 'setCallbackWrapper'])
->addArgument(null)
),
]);
$pool->addTag('container.reversible');
} elseif ('namespace' !== $attr || !\in_array($class, [ArrayAdapter::class, NullAdapter::class], true)) {
$argument = $tags[0][$attr];
if ('default_lifetime' === $attr && !is_numeric($argument)) {
$argument = (new Definition('int', [$argument]))
->setFactory([ParameterNormalizer::class, 'normalizeDuration']);
}
$pool->replaceArgument($i++, $argument);
}
unset($tags[0][$attr]);
}
if (!empty($tags[0])) {
throw new InvalidArgumentException(sprintf('Invalid "cache.pool" tag for service "%s": accepted attributes are "clearer", "provider", "name", "namespace", "default_lifetime", "early_expiration_message_bus" and "reset", found "%s".', $id, implode('", "', array_keys($tags[0]))));
}
if (null !== $clearer) {
$clearers[$clearer][$name] = new Reference($id, $container::IGNORE_ON_UNINITIALIZED_REFERENCE);
}
$allPools[$name] = new Reference($id, $container::IGNORE_ON_UNINITIALIZED_REFERENCE);
}
if (!$needsMessageHandler) {
$container->removeDefinition('cache.early_expiration_handler');
}
$notAliasedCacheClearerId = 'cache.global_clearer';
while ($container->hasAlias($notAliasedCacheClearerId)) {
$notAliasedCacheClearerId = (string) $container->getAlias($notAliasedCacheClearerId);
}
if ($container->hasDefinition($notAliasedCacheClearerId)) {
$clearers[$notAliasedCacheClearerId] = $allPools;
}
foreach ($clearers as $id => $pools) {
$clearer = $container->getDefinition($id);
if ($clearer instanceof ChildDefinition) {
$clearer->replaceArgument(0, $pools);
} else {
$clearer->setArgument(0, $pools);
}
$clearer->addTag('cache.pool.clearer');
if ('cache.system_clearer' === $id) {
$clearer->addTag('kernel.cache_clearer');
}
}
$allPoolsKeys = array_keys($allPools);
if ($container->hasDefinition('console.command.cache_pool_list')) {
$container->getDefinition('console.command.cache_pool_list')->replaceArgument(0, $allPoolsKeys);
}
if ($container->hasDefinition('console.command.cache_pool_clear')) {
$container->getDefinition('console.command.cache_pool_clear')->addArgument($allPoolsKeys);
}
if ($container->hasDefinition('console.command.cache_pool_delete')) {
$container->getDefinition('console.command.cache_pool_delete')->addArgument($allPoolsKeys);
}
}
private function getNamespace(string $seed, string $id): string
{
return substr(str_replace('/', '-', base64_encode(hash('xxh128', $id.$seed, true))), 0, 10);
}
/**
* @internal
*/
public static function getServiceProvider(ContainerBuilder $container, string $name): string
{
$container->resolveEnvPlaceholders($name, null, $usedEnvs);
if ($usedEnvs || preg_match('#^[a-z]++:#', $name)) {
$dsn = $name;
if (!$container->hasDefinition($name = '.cache_connection.'.ContainerBuilder::hash($dsn))) {
$definition = new Definition(AbstractAdapter::class);
$definition->setFactory([AbstractAdapter::class, 'createConnection']);
$definition->setArguments([$dsn, ['lazy' => true]]);
$container->setDefinition($name, $definition);
}
}
return $name;
}
}
@@ -0,0 +1,48 @@
<?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\DependencyInjection;
use Symfony\Component\Cache\PruneableInterface;
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
use Symfony\Component\DependencyInjection\Reference;
/**
* @author Rob Frawley 2nd <rmf@src.run>
*/
class CachePoolPrunerPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container): void
{
if (!$container->hasDefinition('console.command.cache_pool_prune')) {
return;
}
$services = [];
foreach ($container->findTaggedServiceIds('cache.pool') as $id => $tags) {
$class = $container->getParameterBag()->resolveValue($container->getDefinition($id)->getClass());
if (!$reflection = $container->getReflectionClass($class)) {
throw new InvalidArgumentException(sprintf('Class "%s" used for service "%s" cannot be found.', $class, $id));
}
if ($reflection->implementsInterface(PruneableInterface::class)) {
$services[$id] = new Reference($id);
}
}
$container->getDefinition('console.command.cache_pool_prune')->replaceArgument(0, new IteratorArgument($services));
}
}
+25
View File
@@ -0,0 +1,25 @@
<?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\Exception;
use Psr\Cache\CacheException as Psr6CacheInterface;
use Psr\SimpleCache\CacheException as SimpleCacheInterface;
if (interface_exists(SimpleCacheInterface::class)) {
class CacheException extends \Exception implements Psr6CacheInterface, SimpleCacheInterface
{
}
} else {
class CacheException extends \Exception implements Psr6CacheInterface
{
}
}
@@ -0,0 +1,25 @@
<?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\Exception;
use Psr\Cache\InvalidArgumentException as Psr6CacheInterface;
use Psr\SimpleCache\InvalidArgumentException as SimpleCacheInterface;
if (interface_exists(SimpleCacheInterface::class)) {
class InvalidArgumentException extends \InvalidArgumentException implements Psr6CacheInterface, SimpleCacheInterface
{
}
} else {
class InvalidArgumentException extends \InvalidArgumentException implements Psr6CacheInterface
{
}
}
+25
View File
@@ -0,0 +1,25 @@
<?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\Exception;
use Psr\Cache\CacheException as Psr6CacheInterface;
use Psr\SimpleCache\CacheException as SimpleCacheInterface;
if (interface_exists(SimpleCacheInterface::class)) {
class LogicException extends \LogicException implements Psr6CacheInterface, SimpleCacheInterface
{
}
} else {
class LogicException extends \LogicException implements Psr6CacheInterface
{
}
}
+19
View File
@@ -0,0 +1,19 @@
Copyright (c) 2016-present Fabien Potencier
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
+166
View File
@@ -0,0 +1,166 @@
<?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;
use Psr\Log\LoggerInterface;
use Symfony\Contracts\Cache\CacheInterface;
use Symfony\Contracts\Cache\ItemInterface;
/**
* LockRegistry is used internally by existing adapters to protect against cache stampede.
*
* It does so by wrapping the computation of items in a pool of locks.
* Foreach each apps, there can be at most 20 concurrent processes that
* compute items at the same time and only one per cache-key.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
final class LockRegistry
{
private static array $openedFiles = [];
private static ?array $lockedFiles = null;
private static \Exception $signalingException;
private static \Closure $signalingCallback;
/**
* The number of items in this list controls the max number of concurrent processes.
*/
private static array $files = [
__DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'AbstractAdapter.php',
__DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'AbstractTagAwareAdapter.php',
__DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'AdapterInterface.php',
__DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'ApcuAdapter.php',
__DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'ArrayAdapter.php',
__DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'ChainAdapter.php',
__DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'CouchbaseBucketAdapter.php',
__DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'CouchbaseCollectionAdapter.php',
__DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'DoctrineDbalAdapter.php',
__DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'FilesystemAdapter.php',
__DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'FilesystemTagAwareAdapter.php',
__DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'MemcachedAdapter.php',
__DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'NullAdapter.php',
__DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'ParameterNormalizer.php',
__DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'PdoAdapter.php',
__DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'PhpArrayAdapter.php',
__DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'PhpFilesAdapter.php',
__DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'ProxyAdapter.php',
__DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'Psr16Adapter.php',
__DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'RedisAdapter.php',
__DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'RedisTagAwareAdapter.php',
__DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'TagAwareAdapter.php',
__DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'TagAwareAdapterInterface.php',
__DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'TraceableAdapter.php',
__DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'TraceableTagAwareAdapter.php',
];
/**
* Defines a set of existing files that will be used as keys to acquire locks.
*
* @return array The previously defined set of files
*/
public static function setFiles(array $files): array
{
$previousFiles = self::$files;
self::$files = $files;
foreach (self::$openedFiles as $file) {
if ($file) {
flock($file, \LOCK_UN);
fclose($file);
}
}
self::$openedFiles = self::$lockedFiles = [];
return $previousFiles;
}
public static function compute(callable $callback, ItemInterface $item, bool &$save, CacheInterface $pool, ?\Closure $setMetadata = null, ?LoggerInterface $logger = null): mixed
{
if ('\\' === \DIRECTORY_SEPARATOR && null === self::$lockedFiles) {
// disable locking on Windows by default
self::$files = self::$lockedFiles = [];
}
$key = self::$files ? abs(crc32($item->getKey())) % \count(self::$files) : -1;
if ($key < 0 || self::$lockedFiles || !$lock = self::open($key)) {
return $callback($item, $save);
}
self::$signalingException ??= unserialize("O:9:\"Exception\":1:{s:16:\"\0Exception\0trace\";a:0:{}}");
self::$signalingCallback ??= fn () => throw self::$signalingException;
while (true) {
try {
// race to get the lock in non-blocking mode
$locked = flock($lock, \LOCK_EX | \LOCK_NB, $wouldBlock);
if ($locked || !$wouldBlock) {
$logger?->info(sprintf('Lock %s, now computing item "{key}"', $locked ? 'acquired' : 'not supported'), ['key' => $item->getKey()]);
self::$lockedFiles[$key] = true;
$value = $callback($item, $save);
if ($save) {
if ($setMetadata) {
$setMetadata($item);
}
$pool->save($item->set($value));
$save = false;
}
return $value;
}
// if we failed the race, retry locking in blocking mode to wait for the winner
$logger?->info('Item "{key}" is locked, waiting for it to be released', ['key' => $item->getKey()]);
flock($lock, \LOCK_SH);
} finally {
flock($lock, \LOCK_UN);
unset(self::$lockedFiles[$key]);
}
try {
$value = $pool->get($item->getKey(), self::$signalingCallback, 0);
$logger?->info('Item "{key}" retrieved after lock was released', ['key' => $item->getKey()]);
$save = false;
return $value;
} catch (\Exception $e) {
if (self::$signalingException !== $e) {
throw $e;
}
$logger?->info('Item "{key}" not found while lock was released, now retrying', ['key' => $item->getKey()]);
}
}
return null;
}
/**
* @return resource|false
*/
private static function open(int $key)
{
if (null !== $h = self::$openedFiles[$key] ?? null) {
return $h;
}
set_error_handler(static fn () => null);
try {
$h = fopen(self::$files[$key], 'r+');
} finally {
restore_error_handler();
}
return self::$openedFiles[$key] = $h ?: @fopen(self::$files[$key], 'r');
}
}
+98
View File
@@ -0,0 +1,98 @@
<?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\Marshaller;
use Symfony\Component\Cache\Exception\CacheException;
/**
* Serializes/unserializes values using igbinary_serialize() if available, serialize() otherwise.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
class DefaultMarshaller implements MarshallerInterface
{
private bool $useIgbinarySerialize = true;
private bool $throwOnSerializationFailure = false;
public function __construct(?bool $useIgbinarySerialize = null, bool $throwOnSerializationFailure = false)
{
if (null === $useIgbinarySerialize) {
$useIgbinarySerialize = \extension_loaded('igbinary') && version_compare('3.1.6', phpversion('igbinary'), '<=');
} elseif ($useIgbinarySerialize && (!\extension_loaded('igbinary') || version_compare('3.1.6', phpversion('igbinary'), '>'))) {
throw new CacheException(\extension_loaded('igbinary') ? 'Please upgrade the "igbinary" PHP extension to v3.1.6 or higher.' : 'The "igbinary" PHP extension is not loaded.');
}
$this->useIgbinarySerialize = $useIgbinarySerialize;
$this->throwOnSerializationFailure = $throwOnSerializationFailure;
}
public function marshall(array $values, ?array &$failed): array
{
$serialized = $failed = [];
foreach ($values as $id => $value) {
try {
if ($this->useIgbinarySerialize) {
$serialized[$id] = igbinary_serialize($value);
} else {
$serialized[$id] = serialize($value);
}
} catch (\Exception $e) {
if ($this->throwOnSerializationFailure) {
throw new \ValueError($e->getMessage(), 0, $e);
}
$failed[] = $id;
}
}
return $serialized;
}
public function unmarshall(string $value): mixed
{
if ('b:0;' === $value) {
return false;
}
if ('N;' === $value) {
return null;
}
static $igbinaryNull;
if ($value === $igbinaryNull ??= \extension_loaded('igbinary') ? igbinary_serialize(null) : false) {
return null;
}
$unserializeCallbackHandler = ini_set('unserialize_callback_func', __CLASS__.'::handleUnserializeCallback');
try {
if (':' === ($value[1] ?? ':')) {
if (false !== $value = unserialize($value)) {
return $value;
}
} elseif (false === $igbinaryNull) {
throw new \RuntimeException('Failed to unserialize values, did you forget to install the "igbinary" extension?');
} elseif (null !== $value = igbinary_unserialize($value)) {
return $value;
}
throw new \DomainException(error_get_last() ? error_get_last()['message'] : 'Failed to unserialize values.');
} catch (\Error $e) {
throw new \ErrorException($e->getMessage(), $e->getCode(), \E_ERROR, $e->getFile(), $e->getLine());
} finally {
ini_set('unserialize_callback_func', $unserializeCallbackHandler);
}
}
/**
* @internal
*/
public static function handleUnserializeCallback(string $class): never
{
throw new \DomainException('Class not found: '.$class);
}
}
+44
View File
@@ -0,0 +1,44 @@
<?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\Marshaller;
use Symfony\Component\Cache\Exception\CacheException;
/**
* Compresses values using gzdeflate().
*
* @author Nicolas Grekas <p@tchwork.com>
*/
class DeflateMarshaller implements MarshallerInterface
{
public function __construct(
private MarshallerInterface $marshaller,
) {
if (!\function_exists('gzdeflate')) {
throw new CacheException('The "zlib" PHP extension is not loaded.');
}
}
public function marshall(array $values, ?array &$failed): array
{
return array_map('gzdeflate', $this->marshaller->marshall($values, $failed));
}
public function unmarshall(string $value): mixed
{
if (false !== $inflatedValue = @gzinflate($value)) {
$value = $inflatedValue;
}
return $this->marshaller->unmarshall($value);
}
}
+38
View File
@@ -0,0 +1,38 @@
<?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\Marshaller;
/**
* Serializes/unserializes PHP values.
*
* Implementations of this interface MUST deal with errors carefully. They MUST
* also deal with forward and backward compatibility at the storage format level.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
interface MarshallerInterface
{
/**
* Serializes a list of values.
*
* When serialization fails for a specific value, no exception should be
* thrown. Instead, its key should be listed in $failed.
*/
public function marshall(array $values, ?array &$failed): array;
/**
* Unserializes a single value and throws an exception if anything goes wrong.
*
* @throws \Exception Whenever unserialization fails
*/
public function unmarshall(string $value): mixed;
}
+74
View File
@@ -0,0 +1,74 @@
<?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\Marshaller;
use Symfony\Component\Cache\Exception\CacheException;
use Symfony\Component\Cache\Exception\InvalidArgumentException;
/**
* Encrypt/decrypt values using Libsodium.
*
* @author Ahmed TAILOULOUTE <ahmed.tailouloute@gmail.com>
*/
class SodiumMarshaller implements MarshallerInterface
{
private MarshallerInterface $marshaller;
/**
* @param string[] $decryptionKeys The key at index "0" is required and is used to decrypt and encrypt values;
* more rotating keys can be provided to decrypt values;
* each key must be generated using sodium_crypto_box_keypair()
*/
public function __construct(
private array $decryptionKeys,
?MarshallerInterface $marshaller = null,
) {
if (!self::isSupported()) {
throw new CacheException('The "sodium" PHP extension is not loaded.');
}
if (!isset($decryptionKeys[0])) {
throw new InvalidArgumentException('At least one decryption key must be provided at index "0".');
}
$this->marshaller = $marshaller ?? new DefaultMarshaller();
}
public static function isSupported(): bool
{
return \function_exists('sodium_crypto_box_seal');
}
public function marshall(array $values, ?array &$failed): array
{
$encryptionKey = sodium_crypto_box_publickey($this->decryptionKeys[0]);
$encryptedValues = [];
foreach ($this->marshaller->marshall($values, $failed) as $k => $v) {
$encryptedValues[$k] = sodium_crypto_box_seal($v, $encryptionKey);
}
return $encryptedValues;
}
public function unmarshall(string $value): mixed
{
foreach ($this->decryptionKeys as $k) {
if (false !== $decryptedValue = @sodium_crypto_box_seal_open($value, $k)) {
$value = $decryptedValue;
break;
}
}
return $this->marshaller->unmarshall($value);
}
}
+83
View File
@@ -0,0 +1,83 @@
<?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\Marshaller;
/**
* A marshaller optimized for data structures generated by AbstractTagAwareAdapter.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
class TagAwareMarshaller implements MarshallerInterface
{
private MarshallerInterface $marshaller;
public function __construct(?MarshallerInterface $marshaller = null)
{
$this->marshaller = $marshaller ?? new DefaultMarshaller();
}
public function marshall(array $values, ?array &$failed): array
{
$failed = $notSerialized = $serialized = [];
foreach ($values as $id => $value) {
if (\is_array($value) && \is_array($value['tags'] ?? null) && \array_key_exists('value', $value) && \count($value) === 2 + (\is_string($value['meta'] ?? null) && 8 === \strlen($value['meta']))) {
// if the value is an array with keys "tags", "value" and "meta", use a compact serialization format
// magic numbers in the form 9D-..-..-..-..-00-..-..-..-5F allow detecting this format quickly in unmarshall()
$v = $this->marshaller->marshall($value, $f);
if ($f) {
$f = [];
$failed[] = $id;
} else {
if ([] === $value['tags']) {
$v['tags'] = '';
}
$serialized[$id] = "\x9D".($value['meta'] ?? "\0\0\0\0\0\0\0\0").pack('N', \strlen($v['tags'])).$v['tags'].$v['value'];
$serialized[$id][9] = "\x5F";
}
} else {
// other arbitrary values are serialized using the decorated marshaller below
$notSerialized[$id] = $value;
}
}
if ($notSerialized) {
$serialized += $this->marshaller->marshall($notSerialized, $f);
$failed = array_merge($failed, $f);
}
return $serialized;
}
public function unmarshall(string $value): mixed
{
// detect the compact format used in marshall() using magic numbers in the form 9D-..-..-..-..-00-..-..-..-5F
if (13 >= \strlen($value) || "\x9D" !== $value[0] || "\0" !== $value[5] || "\x5F" !== $value[9]) {
return $this->marshaller->unmarshall($value);
}
// data consists of value, tags and metadata which we need to unpack
$meta = substr($value, 1, 12);
$meta[8] = "\0";
$tagLen = unpack('Nlen', $meta, 8)['len'];
$meta = substr($meta, 0, 8);
return [
'value' => $this->marshaller->unmarshall(substr($value, 13 + $tagLen)),
'tags' => $tagLen ? $this->marshaller->unmarshall(substr($value, 13, $tagLen)) : [],
'meta' => "\0\0\0\0\0\0\0\0" === $meta ? null : $meta,
];
}
}
@@ -0,0 +1,60 @@
<?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\Messenger;
use Psr\Log\LoggerInterface;
use Symfony\Component\Cache\Adapter\AdapterInterface;
use Symfony\Component\Cache\CacheItem;
use Symfony\Component\DependencyInjection\ReverseContainer;
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\Messenger\Stamp\HandledStamp;
/**
* Sends the computation of cached values to a message bus.
*/
class EarlyExpirationDispatcher
{
private ?\Closure $callbackWrapper;
public function __construct(
private MessageBusInterface $bus,
private ReverseContainer $reverseContainer,
?callable $callbackWrapper = null,
) {
$this->callbackWrapper = null === $callbackWrapper ? null : $callbackWrapper(...);
}
public function __invoke(callable $callback, CacheItem $item, bool &$save, AdapterInterface $pool, \Closure $setMetadata, ?LoggerInterface $logger = null): mixed
{
if (!$item->isHit() || null === $message = EarlyExpirationMessage::create($this->reverseContainer, $callback, $item, $pool)) {
// The item is stale or the callback cannot be reversed: we must compute the value now
$logger?->info('Computing item "{key}" online: '.($item->isHit() ? 'callback cannot be reversed' : 'item is stale'), ['key' => $item->getKey()]);
return null !== $this->callbackWrapper ? ($this->callbackWrapper)($callback, $item, $save, $pool, $setMetadata, $logger) : $callback($item, $save);
}
$envelope = $this->bus->dispatch($message);
if ($logger) {
if ($envelope->last(HandledStamp::class)) {
$logger->info('Item "{key}" was computed online', ['key' => $item->getKey()]);
} else {
$logger->info('Item "{key}" sent for recomputation', ['key' => $item->getKey()]);
}
}
// The item's value is not stale, no need to write it to the backend
$save = false;
return $message->getItem()->get() ?? $item->get();
}
}
@@ -0,0 +1,81 @@
<?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\Messenger;
use Symfony\Component\Cache\CacheItem;
use Symfony\Component\DependencyInjection\ReverseContainer;
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
/**
* Computes cached values sent to a message bus.
*/
#[AsMessageHandler]
class EarlyExpirationHandler
{
private array $processedNonces = [];
public function __construct(
private ReverseContainer $reverseContainer,
) {
}
public function __invoke(EarlyExpirationMessage $message): void
{
$item = $message->getItem();
$metadata = $item->getMetadata();
$expiry = $metadata[CacheItem::METADATA_EXPIRY] ?? 0;
$ctime = $metadata[CacheItem::METADATA_CTIME] ?? 0;
if ($expiry && $ctime) {
// skip duplicate or expired messages
$processingNonce = [$expiry, $ctime];
$pool = $message->getPool();
$key = $item->getKey();
if (($this->processedNonces[$pool][$key] ?? null) === $processingNonce) {
return;
}
if (microtime(true) >= $expiry) {
return;
}
$this->processedNonces[$pool] = [$key => $processingNonce] + ($this->processedNonces[$pool] ?? []);
if (\count($this->processedNonces[$pool]) > 100) {
array_pop($this->processedNonces[$pool]);
}
}
static $setMetadata;
$setMetadata ??= \Closure::bind(
function (CacheItem $item, float $startTime) {
if ($item->expiry > $endTime = microtime(true)) {
$item->newMetadata[CacheItem::METADATA_EXPIRY] = $item->expiry;
$item->newMetadata[CacheItem::METADATA_CTIME] = (int) ceil(1000 * ($endTime - $startTime));
}
},
null,
CacheItem::class
);
$startTime = microtime(true);
$pool = $message->findPool($this->reverseContainer);
$callback = $message->findCallback($this->reverseContainer);
$save = true;
$value = $callback($item, $save);
$setMetadata($item, $startTime);
$pool->save($item->set($value));
}
}
@@ -0,0 +1,100 @@
<?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\Messenger;
use Symfony\Component\Cache\Adapter\AdapterInterface;
use Symfony\Component\Cache\CacheItem;
use Symfony\Component\DependencyInjection\ReverseContainer;
/**
* Conveys a cached value that needs to be computed.
*/
final class EarlyExpirationMessage
{
private CacheItem $item;
private string $pool;
private string|array $callback;
public static function create(ReverseContainer $reverseContainer, callable $callback, CacheItem $item, AdapterInterface $pool): ?self
{
try {
$item = clone $item;
$item->set(null);
} catch (\Exception) {
return null;
}
$pool = $reverseContainer->getId($pool);
if (\is_object($callback)) {
if (null === $id = $reverseContainer->getId($callback)) {
return null;
}
$callback = '@'.$id;
} elseif (!\is_array($callback)) {
$callback = (string) $callback;
} elseif (!\is_object($callback[0])) {
$callback = [(string) $callback[0], (string) $callback[1]];
} else {
if (null === $id = $reverseContainer->getId($callback[0])) {
return null;
}
$callback = ['@'.$id, (string) $callback[1]];
}
return new self($item, $pool, $callback);
}
public function getItem(): CacheItem
{
return $this->item;
}
public function getPool(): string
{
return $this->pool;
}
/**
* @return string|string[]
*/
public function getCallback(): string|array
{
return $this->callback;
}
public function findPool(ReverseContainer $reverseContainer): AdapterInterface
{
return $reverseContainer->getService($this->pool);
}
public function findCallback(ReverseContainer $reverseContainer): callable
{
if (\is_string($callback = $this->callback)) {
return '@' === $callback[0] ? $reverseContainer->getService(substr($callback, 1)) : $callback;
}
if ('@' === $callback[0][0]) {
$callback[0] = $reverseContainer->getService(substr($callback[0], 1));
}
return $callback;
}
private function __construct(CacheItem $item, string $pool, string|array $callback)
{
$this->item = $item;
$this->pool = $pool;
$this->callback = $callback;
}
}
+20
View File
@@ -0,0 +1,20 @@
<?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;
/**
* Interface extends psr-6 and psr-16 caches to allow for pruning (deletion) of all expired cache items.
*/
interface PruneableInterface
{
public function prune(): bool;
}
+240
View File
@@ -0,0 +1,240 @@
<?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;
use Psr\Cache\CacheException as Psr6CacheException;
use Psr\Cache\CacheItemPoolInterface;
use Psr\SimpleCache\CacheException as SimpleCacheException;
use Psr\SimpleCache\CacheInterface;
use Symfony\Component\Cache\Adapter\AdapterInterface;
use Symfony\Component\Cache\Exception\InvalidArgumentException;
use Symfony\Component\Cache\Traits\ProxyTrait;
/**
* Turns a PSR-6 cache into a PSR-16 one.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
class Psr16Cache implements CacheInterface, PruneableInterface, ResettableInterface
{
use ProxyTrait;
private ?\Closure $createCacheItem = null;
private ?CacheItem $cacheItemPrototype = null;
private static \Closure $packCacheItem;
public function __construct(CacheItemPoolInterface $pool)
{
$this->pool = $pool;
if (!$pool instanceof AdapterInterface) {
return;
}
$cacheItemPrototype = &$this->cacheItemPrototype;
$createCacheItem = \Closure::bind(
static function ($key, $value, $allowInt = false) use (&$cacheItemPrototype) {
$item = clone $cacheItemPrototype;
$item->poolHash = $item->innerItem = null;
if ($allowInt && \is_int($key)) {
$item->key = (string) $key;
} else {
\assert('' !== CacheItem::validateKey($key));
$item->key = $key;
}
$item->value = $value;
$item->isHit = false;
return $item;
},
null,
CacheItem::class
);
$this->createCacheItem = function ($key, $value, $allowInt = false) use ($createCacheItem) {
if (null === $this->cacheItemPrototype) {
$this->get($allowInt && \is_int($key) ? (string) $key : $key);
}
$this->createCacheItem = $createCacheItem;
return $createCacheItem($key, null, $allowInt)->set($value);
};
self::$packCacheItem ??= \Closure::bind(
static function (CacheItem $item) {
$item->newMetadata = $item->metadata;
return $item->pack();
},
null,
CacheItem::class
);
}
public function get($key, $default = null): mixed
{
try {
$item = $this->pool->getItem($key);
} catch (SimpleCacheException $e) {
throw $e;
} catch (Psr6CacheException $e) {
throw new InvalidArgumentException($e->getMessage(), $e->getCode(), $e);
}
if (null === $this->cacheItemPrototype) {
$this->cacheItemPrototype = clone $item;
$this->cacheItemPrototype->set(null);
}
return $item->isHit() ? $item->get() : $default;
}
public function set($key, $value, $ttl = null): bool
{
try {
if (null !== $f = $this->createCacheItem) {
$item = $f($key, $value);
} else {
$item = $this->pool->getItem($key)->set($value);
}
} catch (SimpleCacheException $e) {
throw $e;
} catch (Psr6CacheException $e) {
throw new InvalidArgumentException($e->getMessage(), $e->getCode(), $e);
}
if (null !== $ttl) {
$item->expiresAfter($ttl);
}
return $this->pool->save($item);
}
public function delete($key): bool
{
try {
return $this->pool->deleteItem($key);
} catch (SimpleCacheException $e) {
throw $e;
} catch (Psr6CacheException $e) {
throw new InvalidArgumentException($e->getMessage(), $e->getCode(), $e);
}
}
public function clear(): bool
{
return $this->pool->clear();
}
public function getMultiple($keys, $default = null): iterable
{
if ($keys instanceof \Traversable) {
$keys = iterator_to_array($keys, false);
} elseif (!\is_array($keys)) {
throw new InvalidArgumentException(sprintf('Cache keys must be array or Traversable, "%s" given.', get_debug_type($keys)));
}
try {
$items = $this->pool->getItems($keys);
} catch (SimpleCacheException $e) {
throw $e;
} catch (Psr6CacheException $e) {
throw new InvalidArgumentException($e->getMessage(), $e->getCode(), $e);
}
$values = [];
if (!$this->pool instanceof AdapterInterface) {
foreach ($items as $key => $item) {
$values[$key] = $item->isHit() ? $item->get() : $default;
}
return $values;
}
foreach ($items as $key => $item) {
$values[$key] = $item->isHit() ? (self::$packCacheItem)($item) : $default;
}
return $values;
}
public function setMultiple($values, $ttl = null): bool
{
$valuesIsArray = \is_array($values);
if (!$valuesIsArray && !$values instanceof \Traversable) {
throw new InvalidArgumentException(sprintf('Cache values must be array or Traversable, "%s" given.', get_debug_type($values)));
}
$items = [];
try {
if (null !== $f = $this->createCacheItem) {
$valuesIsArray = false;
foreach ($values as $key => $value) {
$items[$key] = $f($key, $value, true);
}
} elseif ($valuesIsArray) {
$items = [];
foreach ($values as $key => $value) {
$items[] = (string) $key;
}
$items = $this->pool->getItems($items);
} else {
foreach ($values as $key => $value) {
if (\is_int($key)) {
$key = (string) $key;
}
$items[$key] = $this->pool->getItem($key)->set($value);
}
}
} catch (SimpleCacheException $e) {
throw $e;
} catch (Psr6CacheException $e) {
throw new InvalidArgumentException($e->getMessage(), $e->getCode(), $e);
}
$ok = true;
foreach ($items as $key => $item) {
if ($valuesIsArray) {
$item->set($values[$key]);
}
if (null !== $ttl) {
$item->expiresAfter($ttl);
}
$ok = $this->pool->saveDeferred($item) && $ok;
}
return $this->pool->commit() && $ok;
}
public function deleteMultiple($keys): bool
{
if ($keys instanceof \Traversable) {
$keys = iterator_to_array($keys, false);
} elseif (!\is_array($keys)) {
throw new InvalidArgumentException(sprintf('Cache keys must be array or Traversable, "%s" given.', get_debug_type($keys)));
}
try {
return $this->pool->deleteItems($keys);
} catch (SimpleCacheException $e) {
throw $e;
} catch (Psr6CacheException $e) {
throw new InvalidArgumentException($e->getMessage(), $e->getCode(), $e);
}
}
public function has($key): bool
{
try {
return $this->pool->hasItem($key);
} catch (SimpleCacheException $e) {
throw $e;
} catch (Psr6CacheException $e) {
throw new InvalidArgumentException($e->getMessage(), $e->getCode(), $e);
}
}
}
+19
View File
@@ -0,0 +1,19 @@
Symfony PSR-6 implementation for caching
========================================
The Cache component provides extended
[PSR-6](https://www.php-fig.org/psr/psr-6/) implementations for adding cache to
your applications. It is designed to have a low overhead so that caching is
fastest. It ships with adapters for the most widespread caching backends.
It also provides a [PSR-16](https://www.php-fig.org/psr/psr-16/) adapter,
and implementations for [symfony/cache-contracts](https://github.com/symfony/cache-contracts)'
`CacheInterface` and `TagAwareCacheInterface`.
Resources
---------
* [Documentation](https://symfony.com/doc/current/components/cache.html)
* [Contributing](https://symfony.com/doc/current/contributing/index.html)
* [Report issues](https://github.com/symfony/symfony/issues) and
[send Pull Requests](https://github.com/symfony/symfony/pulls)
in the [main Symfony repository](https://github.com/symfony/symfony)
+21
View File
@@ -0,0 +1,21 @@
<?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;
use Symfony\Contracts\Service\ResetInterface;
/**
* Resets a pool's local state.
*/
interface ResettableInterface extends ResetInterface
{
}
+377
View File
@@ -0,0 +1,377 @@
<?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\Traits;
use Psr\Cache\CacheItemInterface;
use Psr\Log\LoggerAwareTrait;
use Symfony\Component\Cache\CacheItem;
use Symfony\Component\Cache\Exception\InvalidArgumentException;
/**
* @author Nicolas Grekas <p@tchwork.com>
*
* @internal
*/
trait AbstractAdapterTrait
{
use LoggerAwareTrait;
/**
* needs to be set by class, signature is function(string <key>, mixed <value>, bool <isHit>).
*/
private static \Closure $createCacheItem;
/**
* needs to be set by class, signature is function(array <deferred>, string <namespace>, array <&expiredIds>).
*/
private static \Closure $mergeByLifetime;
private string $namespace = '';
private int $defaultLifetime;
private string $namespaceVersion = '';
private bool $versioningIsEnabled = false;
private array $deferred = [];
private array $ids = [];
/**
* The maximum length to enforce for identifiers or null when no limit applies.
*/
protected ?int $maxIdLength = null;
/**
* Fetches several cache items.
*
* @param array $ids The cache identifiers to fetch
*/
abstract protected function doFetch(array $ids): iterable;
/**
* Confirms if the cache contains specified cache item.
*
* @param string $id The identifier for which to check existence
*/
abstract protected function doHave(string $id): bool;
/**
* Deletes all items in the pool.
*
* @param string $namespace The prefix used for all identifiers managed by this pool
*/
abstract protected function doClear(string $namespace): bool;
/**
* Removes multiple items from the pool.
*
* @param array $ids An array of identifiers that should be removed from the pool
*/
abstract protected function doDelete(array $ids): bool;
/**
* Persists several cache items immediately.
*
* @param array $values The values to cache, indexed by their cache identifier
* @param int $lifetime The lifetime of the cached values, 0 for persisting until manual cleaning
*
* @return array|bool The identifiers that failed to be cached or a boolean stating if caching succeeded or not
*/
abstract protected function doSave(array $values, int $lifetime): array|bool;
public function hasItem(mixed $key): bool
{
$id = $this->getId($key);
if (isset($this->deferred[$key])) {
$this->commit();
}
try {
return $this->doHave($id);
} catch (\Exception $e) {
CacheItem::log($this->logger, 'Failed to check if key "{key}" is cached: '.$e->getMessage(), ['key' => $key, 'exception' => $e, 'cache-adapter' => get_debug_type($this)]);
return false;
}
}
public function clear(string $prefix = ''): bool
{
$this->deferred = [];
if ($cleared = $this->versioningIsEnabled) {
if ('' === $namespaceVersionToClear = $this->namespaceVersion) {
foreach ($this->doFetch([static::NS_SEPARATOR.$this->namespace]) as $v) {
$namespaceVersionToClear = $v;
}
}
$namespaceToClear = $this->namespace.$namespaceVersionToClear;
$namespaceVersion = self::formatNamespaceVersion(mt_rand());
try {
$e = $this->doSave([static::NS_SEPARATOR.$this->namespace => $namespaceVersion], 0);
} catch (\Exception $e) {
}
if (true !== $e && [] !== $e) {
$cleared = false;
$message = 'Failed to save the new namespace'.($e instanceof \Exception ? ': '.$e->getMessage() : '.');
CacheItem::log($this->logger, $message, ['exception' => $e instanceof \Exception ? $e : null, 'cache-adapter' => get_debug_type($this)]);
} else {
$this->namespaceVersion = $namespaceVersion;
$this->ids = [];
}
} else {
$namespaceToClear = $this->namespace.$prefix;
}
try {
return $this->doClear($namespaceToClear) || $cleared;
} catch (\Exception $e) {
CacheItem::log($this->logger, 'Failed to clear the cache: '.$e->getMessage(), ['exception' => $e, 'cache-adapter' => get_debug_type($this)]);
return false;
}
}
public function deleteItem(mixed $key): bool
{
return $this->deleteItems([$key]);
}
public function deleteItems(array $keys): bool
{
$ids = [];
foreach ($keys as $key) {
$ids[$key] = $this->getId($key);
unset($this->deferred[$key]);
}
try {
if ($this->doDelete($ids)) {
return true;
}
} catch (\Exception) {
}
$ok = true;
// When bulk-delete failed, retry each item individually
foreach ($ids as $key => $id) {
try {
$e = null;
if ($this->doDelete([$id])) {
continue;
}
} catch (\Exception $e) {
}
$message = 'Failed to delete key "{key}"'.($e instanceof \Exception ? ': '.$e->getMessage() : '.');
CacheItem::log($this->logger, $message, ['key' => $key, 'exception' => $e, 'cache-adapter' => get_debug_type($this)]);
$ok = false;
}
return $ok;
}
public function getItem(mixed $key): CacheItem
{
$id = $this->getId($key);
if (isset($this->deferred[$key])) {
$this->commit();
}
$isHit = false;
$value = null;
try {
foreach ($this->doFetch([$id]) as $value) {
$isHit = true;
}
return (self::$createCacheItem)($key, $value, $isHit);
} catch (\Exception $e) {
CacheItem::log($this->logger, 'Failed to fetch key "{key}": '.$e->getMessage(), ['key' => $key, 'exception' => $e, 'cache-adapter' => get_debug_type($this)]);
}
return (self::$createCacheItem)($key, null, false);
}
public function getItems(array $keys = []): iterable
{
$ids = [];
$commit = false;
foreach ($keys as $key) {
$ids[] = $this->getId($key);
$commit = $commit || isset($this->deferred[$key]);
}
if ($commit) {
$this->commit();
}
try {
$items = $this->doFetch($ids);
} catch (\Exception $e) {
CacheItem::log($this->logger, 'Failed to fetch items: '.$e->getMessage(), ['keys' => $keys, 'exception' => $e, 'cache-adapter' => get_debug_type($this)]);
$items = [];
}
$ids = array_combine($ids, $keys);
return $this->generateItems($items, $ids);
}
public function save(CacheItemInterface $item): bool
{
if (!$item instanceof CacheItem) {
return false;
}
$this->deferred[$item->getKey()] = $item;
return $this->commit();
}
public function saveDeferred(CacheItemInterface $item): bool
{
if (!$item instanceof CacheItem) {
return false;
}
$this->deferred[$item->getKey()] = $item;
return true;
}
/**
* Enables/disables versioning of items.
*
* When versioning is enabled, clearing the cache is atomic and doesn't require listing existing keys to proceed,
* but old keys may need garbage collection and extra round-trips to the back-end are required.
*
* Calling this method also clears the memoized namespace version and thus forces a resynchronization of it.
*
* @return bool the previous state of versioning
*/
public function enableVersioning(bool $enable = true): bool
{
$wasEnabled = $this->versioningIsEnabled;
$this->versioningIsEnabled = $enable;
$this->namespaceVersion = '';
$this->ids = [];
return $wasEnabled;
}
public function reset(): void
{
if ($this->deferred) {
$this->commit();
}
$this->namespaceVersion = '';
$this->ids = [];
}
public function __sleep(): array
{
throw new \BadMethodCallException('Cannot serialize '.__CLASS__);
}
public function __wakeup(): void
{
throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);
}
public function __destruct()
{
if ($this->deferred) {
$this->commit();
}
}
private function generateItems(iterable $items, array &$keys): \Generator
{
$f = self::$createCacheItem;
try {
foreach ($items as $id => $value) {
if (!isset($keys[$id])) {
throw new InvalidArgumentException(sprintf('Could not match value id "%s" to keys "%s".', $id, implode('", "', $keys)));
}
$key = $keys[$id];
unset($keys[$id]);
yield $key => $f($key, $value, true);
}
} catch (\Exception $e) {
CacheItem::log($this->logger, 'Failed to fetch items: '.$e->getMessage(), ['keys' => array_values($keys), 'exception' => $e, 'cache-adapter' => get_debug_type($this)]);
}
foreach ($keys as $key) {
yield $key => $f($key, null, false);
}
}
/**
* @internal
*/
protected function getId(mixed $key): string
{
if ($this->versioningIsEnabled && '' === $this->namespaceVersion) {
$this->ids = [];
$this->namespaceVersion = '1'.static::NS_SEPARATOR;
try {
foreach ($this->doFetch([static::NS_SEPARATOR.$this->namespace]) as $v) {
$this->namespaceVersion = $v;
}
$e = true;
if ('1'.static::NS_SEPARATOR === $this->namespaceVersion) {
$this->namespaceVersion = self::formatNamespaceVersion(time());
$e = $this->doSave([static::NS_SEPARATOR.$this->namespace => $this->namespaceVersion], 0);
}
} catch (\Exception $e) {
}
if (true !== $e && [] !== $e) {
$message = 'Failed to save the new namespace'.($e instanceof \Exception ? ': '.$e->getMessage() : '.');
CacheItem::log($this->logger, $message, ['exception' => $e instanceof \Exception ? $e : null, 'cache-adapter' => get_debug_type($this)]);
}
}
if (\is_string($key) && isset($this->ids[$key])) {
return $this->namespace.$this->namespaceVersion.$this->ids[$key];
}
\assert('' !== CacheItem::validateKey($key));
$this->ids[$key] = $key;
if (\count($this->ids) > 1000) {
$this->ids = \array_slice($this->ids, 500, null, true); // stop memory leak if there are many keys
}
if (null === $this->maxIdLength) {
return $this->namespace.$this->namespaceVersion.$key;
}
if (\strlen($id = $this->namespace.$this->namespaceVersion.$key) > $this->maxIdLength) {
// Use xxh128 to favor speed over security, which is not an issue here
$this->ids[$key] = $id = substr_replace(base64_encode(hash('xxh128', $key, true)), static::NS_SEPARATOR, -(\strlen($this->namespaceVersion) + 2));
$id = $this->namespace.$this->namespaceVersion.$id;
}
return $id;
}
/**
* @internal
*/
public static function handleUnserializeCallback(string $class): never
{
throw new \DomainException('Class not found: '.$class);
}
private static function formatNamespaceVersion(int $value): string
{
return strtr(substr_replace(base64_encode(pack('V', $value)), static::NS_SEPARATOR, 5), '/', '_');
}
}
+113
View File
@@ -0,0 +1,113 @@
<?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\Traits;
use Psr\Log\LoggerInterface;
use Symfony\Component\Cache\Adapter\AdapterInterface;
use Symfony\Component\Cache\CacheItem;
use Symfony\Component\Cache\Exception\InvalidArgumentException;
use Symfony\Component\Cache\LockRegistry;
use Symfony\Contracts\Cache\CacheInterface;
use Symfony\Contracts\Cache\CacheTrait;
use Symfony\Contracts\Cache\ItemInterface;
/**
* @author Nicolas Grekas <p@tchwork.com>
*
* @internal
*/
trait ContractsTrait
{
use CacheTrait {
doGet as private contractsGet;
}
private \Closure $callbackWrapper;
private array $computing = [];
/**
* Wraps the callback passed to ->get() in a callable.
*
* @return callable the previous callback wrapper
*/
public function setCallbackWrapper(?callable $callbackWrapper): callable
{
if (!isset($this->callbackWrapper)) {
$this->callbackWrapper = LockRegistry::compute(...);
if (\in_array(\PHP_SAPI, ['cli', 'phpdbg', 'embed'], true)) {
$this->setCallbackWrapper(null);
}
}
if (null !== $callbackWrapper && !$callbackWrapper instanceof \Closure) {
$callbackWrapper = $callbackWrapper(...);
}
$previousWrapper = $this->callbackWrapper;
$this->callbackWrapper = $callbackWrapper ?? static fn (callable $callback, ItemInterface $item, bool &$save, CacheInterface $pool, \Closure $setMetadata, ?LoggerInterface $logger) => $callback($item, $save);
return $previousWrapper;
}
private function doGet(AdapterInterface $pool, string $key, callable $callback, ?float $beta, ?array &$metadata = null): mixed
{
if (0 > $beta ??= 1.0) {
throw new InvalidArgumentException(sprintf('Argument "$beta" provided to "%s::get()" must be a positive number, %f given.', static::class, $beta));
}
static $setMetadata;
$setMetadata ??= \Closure::bind(
static function (CacheItem $item, float $startTime, ?array &$metadata) {
if ($item->expiry > $endTime = microtime(true)) {
$item->newMetadata[CacheItem::METADATA_EXPIRY] = $metadata[CacheItem::METADATA_EXPIRY] = $item->expiry;
$item->newMetadata[CacheItem::METADATA_CTIME] = $metadata[CacheItem::METADATA_CTIME] = (int) ceil(1000 * ($endTime - $startTime));
} else {
unset($metadata[CacheItem::METADATA_EXPIRY], $metadata[CacheItem::METADATA_CTIME], $metadata[CacheItem::METADATA_TAGS]);
}
},
null,
CacheItem::class
);
$this->callbackWrapper ??= LockRegistry::compute(...);
return $this->contractsGet($pool, $key, function (CacheItem $item, bool &$save) use ($pool, $callback, $setMetadata, &$metadata, $key) {
// don't wrap nor save recursive calls
if (isset($this->computing[$key])) {
$value = $callback($item, $save);
$save = false;
return $value;
}
$this->computing[$key] = $key;
$startTime = microtime(true);
if (!isset($this->callbackWrapper)) {
$this->setCallbackWrapper($this->setCallbackWrapper(null));
}
try {
$value = ($this->callbackWrapper)($callback, $item, $save, $pool, function (CacheItem $item) use ($setMetadata, $startTime, &$metadata) {
$setMetadata($item, $startTime, $metadata);
}, $this->logger ?? null);
$setMetadata($item, $startTime, $metadata);
return $value;
} finally {
unset($this->computing[$key]);
}
}, $beta, $metadata, $this->logger ?? null);
}
}
+191
View File
@@ -0,0 +1,191 @@
<?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\Traits;
use Symfony\Component\Cache\Exception\InvalidArgumentException;
/**
* @author Nicolas Grekas <p@tchwork.com>
*
* @internal
*/
trait FilesystemCommonTrait
{
private string $directory;
private string $tmpSuffix;
private function init(string $namespace, ?string $directory): void
{
if (!isset($directory[0])) {
$directory = sys_get_temp_dir().\DIRECTORY_SEPARATOR.'symfony-cache';
} else {
$directory = realpath($directory) ?: $directory;
}
if (isset($namespace[0])) {
if (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]));
}
$directory .= \DIRECTORY_SEPARATOR.$namespace;
} else {
$directory .= \DIRECTORY_SEPARATOR.'@';
}
if (!is_dir($directory)) {
@mkdir($directory, 0777, true);
}
$directory .= \DIRECTORY_SEPARATOR;
// On Windows the whole path is limited to 258 chars
if ('\\' === \DIRECTORY_SEPARATOR && \strlen($directory) > 234) {
throw new InvalidArgumentException(sprintf('Cache directory too long (%s).', $directory));
}
$this->directory = $directory;
}
protected function doClear(string $namespace): bool
{
$ok = true;
foreach ($this->scanHashDir($this->directory) as $file) {
if ('' !== $namespace && !str_starts_with($this->getFileKey($file), $namespace)) {
continue;
}
$ok = ($this->doUnlink($file) || !file_exists($file)) && $ok;
}
return $ok;
}
protected function doDelete(array $ids): bool
{
$ok = true;
foreach ($ids as $id) {
$file = $this->getFile($id);
$ok = (!is_file($file) || $this->doUnlink($file) || !file_exists($file)) && $ok;
}
return $ok;
}
protected function doUnlink(string $file): bool
{
return @unlink($file);
}
private function write(string $file, string $data, ?int $expiresAt = null): bool
{
$unlink = false;
set_error_handler(static fn ($type, $message, $file, $line) => throw new \ErrorException($message, 0, $type, $file, $line));
try {
$tmp = $this->directory.$this->tmpSuffix ??= str_replace('/', '-', base64_encode(random_bytes(6)));
try {
$h = fopen($tmp, 'x');
} catch (\ErrorException $e) {
if (!str_contains($e->getMessage(), 'File exists')) {
throw $e;
}
$tmp = $this->directory.$this->tmpSuffix = str_replace('/', '-', base64_encode(random_bytes(6)));
$h = fopen($tmp, 'x');
}
fwrite($h, $data);
fclose($h);
$unlink = true;
if (null !== $expiresAt) {
touch($tmp, $expiresAt ?: time() + 31556952); // 1 year in seconds
}
if ('\\' === \DIRECTORY_SEPARATOR) {
$success = copy($tmp, $file);
$unlink = true;
} else {
$success = rename($tmp, $file);
$unlink = !$success;
}
return $success;
} finally {
restore_error_handler();
if ($unlink) {
@unlink($tmp);
}
}
}
private function getFile(string $id, bool $mkdir = false, ?string $directory = null): string
{
// Use xxh128 to favor speed over security, which is not an issue here
$hash = str_replace('/', '-', base64_encode(hash('xxh128', static::class.$id, true)));
$dir = ($directory ?? $this->directory).strtoupper($hash[0].\DIRECTORY_SEPARATOR.$hash[1].\DIRECTORY_SEPARATOR);
if ($mkdir && !is_dir($dir)) {
@mkdir($dir, 0777, true);
}
return $dir.substr($hash, 2, 20);
}
private function getFileKey(string $file): string
{
return '';
}
private function scanHashDir(string $directory): \Generator
{
if (!is_dir($directory)) {
return;
}
$chars = '+-ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
for ($i = 0; $i < 38; ++$i) {
if (!is_dir($directory.$chars[$i])) {
continue;
}
for ($j = 0; $j < 38; ++$j) {
if (!is_dir($dir = $directory.$chars[$i].\DIRECTORY_SEPARATOR.$chars[$j])) {
continue;
}
foreach (@scandir($dir, \SCANDIR_SORT_NONE) ?: [] as $file) {
if ('.' !== $file && '..' !== $file) {
yield $dir.\DIRECTORY_SEPARATOR.$file;
}
}
}
}
}
public function __sleep(): array
{
throw new \BadMethodCallException('Cannot serialize '.__CLASS__);
}
public function __wakeup(): void
{
throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);
}
public function __destruct()
{
if (method_exists(parent::class, '__destruct')) {
parent::__destruct();
}
if (isset($this->tmpSuffix) && is_file($this->directory.$this->tmpSuffix)) {
unlink($this->directory.$this->tmpSuffix);
}
}
}
+113
View File
@@ -0,0 +1,113 @@
<?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\Traits;
use Symfony\Component\Cache\Exception\CacheException;
use Symfony\Component\Cache\Marshaller\MarshallerInterface;
/**
* @author Nicolas Grekas <p@tchwork.com>
* @author Rob Frawley 2nd <rmf@src.run>
*
* @internal
*/
trait FilesystemTrait
{
use FilesystemCommonTrait;
private MarshallerInterface $marshaller;
public function prune(): bool
{
$time = time();
$pruned = true;
foreach ($this->scanHashDir($this->directory) as $file) {
if (!$h = @fopen($file, 'r')) {
continue;
}
if (($expiresAt = (int) fgets($h)) && $time >= $expiresAt) {
fclose($h);
$pruned = (@unlink($file) || !file_exists($file)) && $pruned;
} else {
fclose($h);
}
}
return $pruned;
}
protected function doFetch(array $ids): iterable
{
$values = [];
$now = time();
foreach ($ids as $id) {
$file = $this->getFile($id);
if (!is_file($file) || !$h = @fopen($file, 'r')) {
continue;
}
if (($expiresAt = (int) fgets($h)) && $now >= $expiresAt) {
fclose($h);
@unlink($file);
} else {
$i = rawurldecode(rtrim(fgets($h)));
$value = stream_get_contents($h);
fclose($h);
if ($i === $id) {
$values[$id] = $this->marshaller->unmarshall($value);
}
}
}
return $values;
}
protected function doHave(string $id): bool
{
$file = $this->getFile($id);
return is_file($file) && (@filemtime($file) > time() || $this->doFetch([$id]));
}
protected function doSave(array $values, int $lifetime): array|bool
{
$expiresAt = $lifetime ? (time() + $lifetime) : 0;
$values = $this->marshaller->marshall($values, $failed);
foreach ($values as $id => $value) {
if (!$this->write($this->getFile($id, true), $expiresAt."\n".rawurlencode($id)."\n".$value, $expiresAt)) {
$failed[] = $id;
}
}
if ($failed && !is_writable($this->directory)) {
throw new CacheException(sprintf('Cache directory is not writable (%s).', $this->directory));
}
return $failed;
}
private function getFileKey(string $file): string
{
if (!$h = @fopen($file, 'r')) {
return '';
}
fgets($h); // expiry
$encodedKey = fgets($h);
fclose($h);
return rawurldecode(rtrim($encodedKey));
}
}
+37
View File
@@ -0,0 +1,37 @@
<?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\Traits;
use Symfony\Component\Cache\PruneableInterface;
use Symfony\Contracts\Service\ResetInterface;
/**
* @author Nicolas Grekas <p@tchwork.com>
*
* @internal
*/
trait ProxyTrait
{
private object $pool;
public function prune(): bool
{
return $this->pool instanceof PruneableInterface && $this->pool->prune();
}
public function reset(): void
{
if ($this->pool instanceof ResetInterface) {
$this->pool->reset();
}
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+81
View File
@@ -0,0 +1,81 @@
<?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\Traits;
if (version_compare(phpversion('redis'), '6.1.0', '>=')) {
/**
* @internal
*/
trait Redis6ProxyTrait
{
public function dump($key): \Redis|string|false
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->dump(...\func_get_args());
}
public function hRandField($key, $options = null): \Redis|array|string|false
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->hRandField(...\func_get_args());
}
public function hSet($key, $fields_and_vals): \Redis|false|int
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->hSet(...\func_get_args());
}
public function mget($keys): \Redis|array|false
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->mget(...\func_get_args());
}
public function sRandMember($key, $count = 0): mixed
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->sRandMember(...\func_get_args());
}
public function waitaof($numlocal, $numreplicas, $timeout): \Redis|array|false
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->waitaof(...\func_get_args());
}
}
} else {
/**
* @internal
*/
trait Redis6ProxyTrait
{
public function dump($key): \Redis|string
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->dump(...\func_get_args());
}
public function hRandField($key, $options = null): \Redis|array|string
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->hRandField(...\func_get_args());
}
public function hSet($key, $member, $value): \Redis|false|int
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->hSet(...\func_get_args());
}
public function mget($keys): \Redis|array
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->mget(...\func_get_args());
}
public function sRandMember($key, $count = 0): \Redis|array|false|string
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->sRandMember(...\func_get_args());
}
}
}
+983
View File
@@ -0,0 +1,983 @@
<?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\Traits;
use Symfony\Component\VarExporter\LazyObjectInterface;
use Symfony\Component\VarExporter\LazyProxyTrait;
use Symfony\Contracts\Service\ResetInterface;
// Help opcache.preload discover always-needed symbols
class_exists(\Symfony\Component\VarExporter\Internal\Hydrator::class);
class_exists(\Symfony\Component\VarExporter\Internal\LazyObjectRegistry::class);
class_exists(\Symfony\Component\VarExporter\Internal\LazyObjectState::class);
/**
* @internal
*/
class RedisCluster5Proxy extends \RedisCluster implements ResetInterface, LazyObjectInterface
{
use LazyProxyTrait {
resetLazyObject as reset;
}
private const LAZY_OBJECT_PROPERTY_SCOPES = [];
public function __construct($name, $seeds = null, $timeout = null, $read_timeout = null, $persistent = null, #[\SensitiveParameter] $auth = null)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->__construct(...\func_get_args());
}
public function _masters()
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->_masters(...\func_get_args());
}
public function _prefix($key)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->_prefix(...\func_get_args());
}
public function _redir()
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->_redir(...\func_get_args());
}
public function _serialize($value)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->_serialize(...\func_get_args());
}
public function _unserialize($value)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->_unserialize(...\func_get_args());
}
public function _compress($value)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->_compress(...\func_get_args());
}
public function _uncompress($value)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->_uncompress(...\func_get_args());
}
public function _pack($value)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->_pack(...\func_get_args());
}
public function _unpack($value)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->_unpack(...\func_get_args());
}
public function acl($key_or_address, $subcmd, ...$args)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->acl(...\func_get_args());
}
public function append($key, $value)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->append(...\func_get_args());
}
public function bgrewriteaof($key_or_address)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->bgrewriteaof(...\func_get_args());
}
public function bgsave($key_or_address)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->bgsave(...\func_get_args());
}
public function bitcount($key)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->bitcount(...\func_get_args());
}
public function bitop($operation, $ret_key, $key, ...$other_keys)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->bitop(...\func_get_args());
}
public function bitpos($key, $bit, $start = null, $end = null)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->bitpos(...\func_get_args());
}
public function blpop($key, $timeout_or_key, ...$extra_args)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->blpop(...\func_get_args());
}
public function brpop($key, $timeout_or_key, ...$extra_args)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->brpop(...\func_get_args());
}
public function brpoplpush($src, $dst, $timeout)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->brpoplpush(...\func_get_args());
}
public function clearlasterror()
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->clearlasterror(...\func_get_args());
}
public function bzpopmax($key, $timeout_or_key, ...$extra_args)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->bzpopmax(...\func_get_args());
}
public function bzpopmin($key, $timeout_or_key, ...$extra_args)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->bzpopmin(...\func_get_args());
}
public function client($key_or_address, $arg = null, ...$other_args)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->client(...\func_get_args());
}
public function close()
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->close(...\func_get_args());
}
public function cluster($key_or_address, $arg = null, ...$other_args)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->cluster(...\func_get_args());
}
public function command(...$args)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->command(...\func_get_args());
}
public function config($key_or_address, $arg = null, ...$other_args)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->config(...\func_get_args());
}
public function dbsize($key_or_address)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->dbsize(...\func_get_args());
}
public function decr($key)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->decr(...\func_get_args());
}
public function decrby($key, $value)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->decrby(...\func_get_args());
}
public function del($key, ...$other_keys)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->del(...\func_get_args());
}
public function discard()
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->discard(...\func_get_args());
}
public function dump($key)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->dump(...\func_get_args());
}
public function echo($msg)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->echo(...\func_get_args());
}
public function eval($script, $args = null, $num_keys = null)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->eval(...\func_get_args());
}
public function evalsha($script_sha, $args = null, $num_keys = null)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->evalsha(...\func_get_args());
}
public function exec()
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->exec(...\func_get_args());
}
public function exists($key)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->exists(...\func_get_args());
}
public function expire($key, $timeout)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->expire(...\func_get_args());
}
public function expireat($key, $timestamp)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->expireat(...\func_get_args());
}
public function flushall($key_or_address, $async = null)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->flushall(...\func_get_args());
}
public function flushdb($key_or_address, $async = null)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->flushdb(...\func_get_args());
}
public function geoadd($key, $lng, $lat, $member, ...$other_triples)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->geoadd(...\func_get_args());
}
public function geodist($key, $src, $dst, $unit = null)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->geodist(...\func_get_args());
}
public function geohash($key, $member, ...$other_members)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->geohash(...\func_get_args());
}
public function geopos($key, $member, ...$other_members)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->geopos(...\func_get_args());
}
public function georadius($key, $lng, $lan, $radius, $unit, $opts = null)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->georadius(...\func_get_args());
}
public function georadius_ro($key, $lng, $lan, $radius, $unit, $opts = null)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->georadius_ro(...\func_get_args());
}
public function georadiusbymember($key, $member, $radius, $unit, $opts = null)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->georadiusbymember(...\func_get_args());
}
public function georadiusbymember_ro($key, $member, $radius, $unit, $opts = null)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->georadiusbymember_ro(...\func_get_args());
}
public function get($key)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->get(...\func_get_args());
}
public function getbit($key, $offset)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->getbit(...\func_get_args());
}
public function getlasterror()
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->getlasterror(...\func_get_args());
}
public function getmode()
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->getmode(...\func_get_args());
}
public function getoption($option)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->getoption(...\func_get_args());
}
public function getrange($key, $start, $end)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->getrange(...\func_get_args());
}
public function getset($key, $value)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->getset(...\func_get_args());
}
public function hdel($key, $member, ...$other_members)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->hdel(...\func_get_args());
}
public function hexists($key, $member)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->hexists(...\func_get_args());
}
public function hget($key, $member)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->hget(...\func_get_args());
}
public function hgetall($key)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->hgetall(...\func_get_args());
}
public function hincrby($key, $member, $value)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->hincrby(...\func_get_args());
}
public function hincrbyfloat($key, $member, $value)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->hincrbyfloat(...\func_get_args());
}
public function hkeys($key)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->hkeys(...\func_get_args());
}
public function hlen($key)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->hlen(...\func_get_args());
}
public function hmget($key, $keys)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->hmget(...\func_get_args());
}
public function hmset($key, $pairs)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->hmset(...\func_get_args());
}
public function hscan($str_key, &$i_iterator, $str_pattern = null, $i_count = null)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->hscan($str_key, $i_iterator, ...\array_slice(\func_get_args(), 2));
}
public function hset($key, $member, $value)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->hset(...\func_get_args());
}
public function hsetnx($key, $member, $value)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->hsetnx(...\func_get_args());
}
public function hstrlen($key, $member)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->hstrlen(...\func_get_args());
}
public function hvals($key)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->hvals(...\func_get_args());
}
public function incr($key)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->incr(...\func_get_args());
}
public function incrby($key, $value)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->incrby(...\func_get_args());
}
public function incrbyfloat($key, $value)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->incrbyfloat(...\func_get_args());
}
public function info($key_or_address, $option = null)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->info(...\func_get_args());
}
public function keys($pattern)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->keys(...\func_get_args());
}
public function lastsave($key_or_address)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->lastsave(...\func_get_args());
}
public function lget($key, $index)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->lget(...\func_get_args());
}
public function lindex($key, $index)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->lindex(...\func_get_args());
}
public function linsert($key, $position, $pivot, $value)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->linsert(...\func_get_args());
}
public function llen($key)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->llen(...\func_get_args());
}
public function lpop($key)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->lpop(...\func_get_args());
}
public function lpush($key, $value)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->lpush(...\func_get_args());
}
public function lpushx($key, $value)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->lpushx(...\func_get_args());
}
public function lrange($key, $start, $end)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->lrange(...\func_get_args());
}
public function lrem($key, $value)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->lrem(...\func_get_args());
}
public function lset($key, $index, $value)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->lset(...\func_get_args());
}
public function ltrim($key, $start, $stop)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->ltrim(...\func_get_args());
}
public function mget($keys)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->mget(...\func_get_args());
}
public function mset($pairs)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->mset(...\func_get_args());
}
public function msetnx($pairs)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->msetnx(...\func_get_args());
}
public function multi()
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->multi(...\func_get_args());
}
public function object($field, $key)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->object(...\func_get_args());
}
public function persist($key)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->persist(...\func_get_args());
}
public function pexpire($key, $timestamp)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->pexpire(...\func_get_args());
}
public function pexpireat($key, $timestamp)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->pexpireat(...\func_get_args());
}
public function pfadd($key, $elements)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->pfadd(...\func_get_args());
}
public function pfcount($key)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->pfcount(...\func_get_args());
}
public function pfmerge($dstkey, $keys)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->pfmerge(...\func_get_args());
}
public function ping($key_or_address)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->ping(...\func_get_args());
}
public function psetex($key, $expire, $value)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->psetex(...\func_get_args());
}
public function psubscribe($patterns, $callback)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->psubscribe(...\func_get_args());
}
public function pttl($key)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->pttl(...\func_get_args());
}
public function publish($channel, $message)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->publish(...\func_get_args());
}
public function pubsub($key_or_address, $arg = null, ...$other_args)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->pubsub(...\func_get_args());
}
public function punsubscribe($pattern, ...$other_patterns)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->punsubscribe(...\func_get_args());
}
public function randomkey($key_or_address)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->randomkey(...\func_get_args());
}
public function rawcommand($cmd, ...$args)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->rawcommand(...\func_get_args());
}
public function rename($key, $newkey)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->rename(...\func_get_args());
}
public function renamenx($key, $newkey)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->renamenx(...\func_get_args());
}
public function restore($ttl, $key, $value)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->restore(...\func_get_args());
}
public function role()
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->role(...\func_get_args());
}
public function rpop($key)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->rpop(...\func_get_args());
}
public function rpoplpush($src, $dst)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->rpoplpush(...\func_get_args());
}
public function rpush($key, $value)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->rpush(...\func_get_args());
}
public function rpushx($key, $value)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->rpushx(...\func_get_args());
}
public function sadd($key, $value)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->sadd(...\func_get_args());
}
public function saddarray($key, $options)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->saddarray(...\func_get_args());
}
public function save($key_or_address)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->save(...\func_get_args());
}
public function scan(&$i_iterator, $str_node, $str_pattern = null, $i_count = null)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->scan($i_iterator, ...\array_slice(\func_get_args(), 1));
}
public function scard($key)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->scard(...\func_get_args());
}
public function script($key_or_address, $arg = null, ...$other_args)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->script(...\func_get_args());
}
public function sdiff($key, ...$other_keys)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->sdiff(...\func_get_args());
}
public function sdiffstore($dst, $key, ...$other_keys)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->sdiffstore(...\func_get_args());
}
public function set($key, $value, $opts = null)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->set(...\func_get_args());
}
public function setbit($key, $offset, $value)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->setbit(...\func_get_args());
}
public function setex($key, $expire, $value)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->setex(...\func_get_args());
}
public function setnx($key, $value)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->setnx(...\func_get_args());
}
public function setoption($option, $value)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->setoption(...\func_get_args());
}
public function setrange($key, $offset, $value)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->setrange(...\func_get_args());
}
public function sinter($key, ...$other_keys)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->sinter(...\func_get_args());
}
public function sinterstore($dst, $key, ...$other_keys)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->sinterstore(...\func_get_args());
}
public function sismember($key, $value)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->sismember(...\func_get_args());
}
public function slowlog($key_or_address, $arg = null, ...$other_args)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->slowlog(...\func_get_args());
}
public function smembers($key)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->smembers(...\func_get_args());
}
public function smove($src, $dst, $value)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->smove(...\func_get_args());
}
public function sort($key, $options = null)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->sort(...\func_get_args());
}
public function spop($key)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->spop(...\func_get_args());
}
public function srandmember($key, $count = null)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->srandmember(...\func_get_args());
}
public function srem($key, $value)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->srem(...\func_get_args());
}
public function sscan($str_key, &$i_iterator, $str_pattern = null, $i_count = null)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->sscan($str_key, $i_iterator, ...\array_slice(\func_get_args(), 2));
}
public function strlen($key)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->strlen(...\func_get_args());
}
public function subscribe($channels, $callback)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->subscribe(...\func_get_args());
}
public function sunion($key, ...$other_keys)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->sunion(...\func_get_args());
}
public function sunionstore($dst, $key, ...$other_keys)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->sunionstore(...\func_get_args());
}
public function time()
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->time(...\func_get_args());
}
public function ttl($key)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->ttl(...\func_get_args());
}
public function type($key)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->type(...\func_get_args());
}
public function unsubscribe($channel, ...$other_channels)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->unsubscribe(...\func_get_args());
}
public function unlink($key, ...$other_keys)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->unlink(...\func_get_args());
}
public function unwatch()
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->unwatch(...\func_get_args());
}
public function watch($key, ...$other_keys)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->watch(...\func_get_args());
}
public function xack($str_key, $str_group, $arr_ids)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->xack(...\func_get_args());
}
public function xadd($str_key, $str_id, $arr_fields, $i_maxlen = null, $boo_approximate = null)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->xadd(...\func_get_args());
}
public function xclaim($str_key, $str_group, $str_consumer, $i_min_idle, $arr_ids, $arr_opts = null)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->xclaim(...\func_get_args());
}
public function xdel($str_key, $arr_ids)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->xdel(...\func_get_args());
}
public function xgroup($str_operation, $str_key = null, $str_arg1 = null, $str_arg2 = null, $str_arg3 = null)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->xgroup(...\func_get_args());
}
public function xinfo($str_cmd, $str_key = null, $str_group = null)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->xinfo(...\func_get_args());
}
public function xlen($key)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->xlen(...\func_get_args());
}
public function xpending($str_key, $str_group, $str_start = null, $str_end = null, $i_count = null, $str_consumer = null)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->xpending(...\func_get_args());
}
public function xrange($str_key, $str_start, $str_end, $i_count = null)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->xrange(...\func_get_args());
}
public function xread($arr_streams, $i_count = null, $i_block = null)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->xread(...\func_get_args());
}
public function xreadgroup($str_group, $str_consumer, $arr_streams, $i_count = null, $i_block = null)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->xreadgroup(...\func_get_args());
}
public function xrevrange($str_key, $str_start, $str_end, $i_count = null)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->xrevrange(...\func_get_args());
}
public function xtrim($str_key, $i_maxlen, $boo_approximate = null)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->xtrim(...\func_get_args());
}
public function zadd($key, $score, $value, ...$extra_args)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->zadd(...\func_get_args());
}
public function zcard($key)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->zcard(...\func_get_args());
}
public function zcount($key, $min, $max)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->zcount(...\func_get_args());
}
public function zincrby($key, $value, $member)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->zincrby(...\func_get_args());
}
public function zinterstore($key, $keys, $weights = null, $aggregate = null)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->zinterstore(...\func_get_args());
}
public function zlexcount($key, $min, $max)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->zlexcount(...\func_get_args());
}
public function zpopmax($key)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->zpopmax(...\func_get_args());
}
public function zpopmin($key)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->zpopmin(...\func_get_args());
}
public function zrange($key, $start, $end, $scores = null)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->zrange(...\func_get_args());
}
public function zrangebylex($key, $min, $max, $offset = null, $limit = null)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->zrangebylex(...\func_get_args());
}
public function zrangebyscore($key, $start, $end, $options = null)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->zrangebyscore(...\func_get_args());
}
public function zrank($key, $member)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->zrank(...\func_get_args());
}
public function zrem($key, $member, ...$other_members)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->zrem(...\func_get_args());
}
public function zremrangebylex($key, $min, $max)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->zremrangebylex(...\func_get_args());
}
public function zremrangebyrank($key, $min, $max)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->zremrangebyrank(...\func_get_args());
}
public function zremrangebyscore($key, $min, $max)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->zremrangebyscore(...\func_get_args());
}
public function zrevrange($key, $start, $end, $scores = null)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->zrevrange(...\func_get_args());
}
public function zrevrangebylex($key, $min, $max, $offset = null, $limit = null)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->zrevrangebylex(...\func_get_args());
}
public function zrevrangebyscore($key, $start, $end, $options = null)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->zrevrangebyscore(...\func_get_args());
}
public function zrevrank($key, $member)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->zrevrank(...\func_get_args());
}
public function zscan($str_key, &$i_iterator, $str_pattern = null, $i_count = null)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->zscan($str_key, $i_iterator, ...\array_slice(\func_get_args(), 2));
}
public function zscore($key, $member)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->zscore(...\func_get_args());
}
public function zunionstore($key, $keys, $weights = null, $aggregate = null)
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->zunionstore(...\func_get_args());
}
}
File diff suppressed because it is too large Load Diff
+46
View File
@@ -0,0 +1,46 @@
<?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\Traits;
if (version_compare(phpversion('redis'), '6.1.0', '>')) {
/**
* @internal
*/
trait RedisCluster6ProxyTrait
{
public function getex($key, $options = []): \RedisCluster|string|false
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->getex(...\func_get_args());
}
public function publish($channel, $message): \RedisCluster|bool|int
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->publish(...\func_get_args());
}
public function waitaof($key_or_address, $numlocal, $numreplicas, $timeout): \RedisCluster|array|false
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->waitaof(...\func_get_args());
}
}
} else {
/**
* @internal
*/
trait RedisCluster6ProxyTrait
{
public function publish($channel, $message): \RedisCluster|bool
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->publish(...\func_get_args());
}
}
}
+47
View File
@@ -0,0 +1,47 @@
<?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\Traits;
/**
* This file acts as a wrapper to the \RedisCluster implementation so it can accept the same type of calls as
* individual \Redis objects.
*
* Calls are made to individual nodes via: RedisCluster->{method}($host, ...args)'
* according to https://github.com/phpredis/phpredis/blob/develop/cluster.markdown#directed-node-commands
*
* @author Jack Thomas <jack.thomas@solidalpha.com>
*
* @internal
*/
class RedisClusterNodeProxy
{
public function __construct(
private array $host,
private \RedisCluster $redis,
) {
}
public function __call(string $method, array $args)
{
return $this->redis->{$method}($this->host, ...$args);
}
public function scan(&$iIterator, $strPattern = null, $iCount = null)
{
return $this->redis->scan($iIterator, $this->host, $strPattern, $iCount);
}
public function getOption($name)
{
return $this->redis->getOption($name);
}
}
+23
View File
@@ -0,0 +1,23 @@
<?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\Traits;
class_alias(6.0 <= (float) phpversion('redis') ? RedisCluster6Proxy::class : RedisCluster5Proxy::class, RedisClusterProxy::class);
if (false) {
/**
* @internal
*/
class RedisClusterProxy extends \RedisCluster
{
}
}
+23
View File
@@ -0,0 +1,23 @@
<?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\Traits;
class_alias(6.0 <= (float) phpversion('redis') ? Redis6Proxy::class : Redis5Proxy::class, RedisProxy::class);
if (false) {
/**
* @internal
*/
class RedisProxy extends \Redis
{
}
}
+682
View File
@@ -0,0 +1,682 @@
<?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\Traits;
use Predis\Command\Redis\UNLINK;
use Predis\Connection\Aggregate\ClusterInterface;
use Predis\Connection\Aggregate\RedisCluster;
use Predis\Connection\Aggregate\ReplicationInterface;
use Predis\Connection\Cluster\ClusterInterface as Predis2ClusterInterface;
use Predis\Connection\Cluster\RedisCluster as Predis2RedisCluster;
use Predis\Response\ErrorInterface;
use Predis\Response\Status;
use Relay\Relay;
use Relay\Sentinel;
use Symfony\Component\Cache\Exception\CacheException;
use Symfony\Component\Cache\Exception\InvalidArgumentException;
use Symfony\Component\Cache\Marshaller\DefaultMarshaller;
use Symfony\Component\Cache\Marshaller\MarshallerInterface;
/**
* @author Aurimas Niekis <aurimas@niekis.lt>
* @author Nicolas Grekas <p@tchwork.com>
*
* @internal
*/
trait RedisTrait
{
private static array $defaultConnectionOptions = [
'class' => null,
'persistent' => 0,
'persistent_id' => null,
'timeout' => 30,
'read_timeout' => 0,
'retry_interval' => 0,
'tcp_keepalive' => 0,
'lazy' => null,
'redis_cluster' => false,
'redis_sentinel' => null,
'dbindex' => 0,
'failover' => 'none',
'ssl' => null, // see https://php.net/context.ssl
];
private \Redis|Relay|\RedisArray|\RedisCluster|\Predis\ClientInterface $redis;
private MarshallerInterface $marshaller;
private function init(\Redis|Relay|\RedisArray|\RedisCluster|\Predis\ClientInterface $redis, string $namespace, int $defaultLifetime, ?MarshallerInterface $marshaller): void
{
parent::__construct($namespace, $defaultLifetime);
if (preg_match('#[^-+_.A-Za-z0-9]#', $namespace, $match)) {
throw new InvalidArgumentException(sprintf('RedisAdapter namespace contains "%s" but only characters in [-+_.A-Za-z0-9] are allowed.', $match[0]));
}
if ($redis instanceof \Predis\ClientInterface && $redis->getOptions()->exceptions) {
$options = clone $redis->getOptions();
\Closure::bind(function () { $this->options['exceptions'] = false; }, $options, $options)();
$redis = new $redis($redis->getConnection(), $options);
}
$this->redis = $redis;
$this->marshaller = $marshaller ?? new DefaultMarshaller();
}
/**
* Creates a Redis connection using a DSN configuration.
*
* Example DSN:
* - redis://localhost
* - redis://example.com:1234
* - redis://secret@example.com/13
* - redis:///var/run/redis.sock
* - redis://secret@/var/run/redis.sock/13
*
* @param array $options See self::$defaultConnectionOptions
*
* @throws InvalidArgumentException when the DSN is invalid
*/
public static function createConnection(#[\SensitiveParameter] string $dsn, array $options = []): \Redis|\RedisArray|\RedisCluster|\Predis\ClientInterface|Relay
{
if (str_starts_with($dsn, 'redis:')) {
$scheme = 'redis';
} elseif (str_starts_with($dsn, 'rediss:')) {
$scheme = 'rediss';
} else {
throw new InvalidArgumentException('Invalid Redis DSN: it does not start with "redis[s]:".');
}
if (!\extension_loaded('redis') && !class_exists(\Predis\Client::class)) {
throw new CacheException('Cannot find the "redis" extension nor the "predis/predis" package.');
}
$params = preg_replace_callback('#^'.$scheme.':(//)?(?:(?:(?<user>[^:@]*+):)?(?<password>[^@]*+)@)?#', function ($m) use (&$auth) {
if (isset($m['password'])) {
if (\in_array($m['user'], ['', 'default'], true)) {
$auth = rawurldecode($m['password']);
} else {
$auth = [rawurldecode($m['user']), rawurldecode($m['password'])];
}
if ('' === $auth) {
$auth = null;
}
}
return 'file:'.($m[1] ?? '');
}, $dsn);
if (false === $params = parse_url($params)) {
throw new InvalidArgumentException('Invalid Redis DSN.');
}
$query = $hosts = [];
$tls = 'rediss' === $scheme;
$tcpScheme = $tls ? 'tls' : 'tcp';
if (isset($params['query'])) {
parse_str($params['query'], $query);
if (isset($query['host'])) {
if (!\is_array($hosts = $query['host'])) {
throw new InvalidArgumentException('Invalid Redis DSN: query parameter "host" must be an array.');
}
foreach ($hosts as $host => $parameters) {
if (\is_string($parameters)) {
parse_str($parameters, $parameters);
}
if (false === $i = strrpos($host, ':')) {
$hosts[$host] = ['scheme' => $tcpScheme, 'host' => $host, 'port' => 6379] + $parameters;
} elseif ($port = (int) substr($host, 1 + $i)) {
$hosts[$host] = ['scheme' => $tcpScheme, 'host' => substr($host, 0, $i), 'port' => $port] + $parameters;
} else {
$hosts[$host] = ['scheme' => 'unix', 'path' => substr($host, 0, $i)] + $parameters;
}
}
$hosts = array_values($hosts);
}
}
if (isset($params['host']) || isset($params['path'])) {
if (!isset($params['dbindex']) && isset($params['path'])) {
if (preg_match('#/(\d+)?$#', $params['path'], $m)) {
$params['dbindex'] = $m[1] ?? $query['dbindex'] ?? '0';
$params['path'] = substr($params['path'], 0, -\strlen($m[0]));
} elseif (isset($params['host'])) {
throw new InvalidArgumentException('Invalid Redis DSN: parameter "dbindex" must be a number.');
}
}
if (isset($params['host'])) {
array_unshift($hosts, ['scheme' => $tcpScheme, 'host' => $params['host'], 'port' => $params['port'] ?? 6379]);
} else {
array_unshift($hosts, ['scheme' => 'unix', 'path' => $params['path']]);
}
}
if (!$hosts) {
throw new InvalidArgumentException('Invalid Redis DSN: missing host.');
}
if (isset($params['dbindex'], $query['dbindex']) && $params['dbindex'] !== $query['dbindex']) {
throw new InvalidArgumentException('Invalid Redis DSN: path and query "dbindex" parameters mismatch.');
}
$params += $query + $options + self::$defaultConnectionOptions;
if (isset($params['redis_sentinel']) && isset($params['sentinel_master'])) {
throw new InvalidArgumentException('Cannot use both "redis_sentinel" and "sentinel_master" at the same time.');
}
$params['redis_sentinel'] ??= $params['sentinel_master'] ?? null;
if (isset($params['redis_sentinel']) && !class_exists(\Predis\Client::class) && !class_exists(\RedisSentinel::class) && !class_exists(Sentinel::class)) {
throw new CacheException('Redis Sentinel support requires one of: "predis/predis", "ext-redis >= 5.2", "ext-relay".');
}
if (isset($params['lazy'])) {
$params['lazy'] = filter_var($params['lazy'], \FILTER_VALIDATE_BOOLEAN);
}
$params['redis_cluster'] = filter_var($params['redis_cluster'], \FILTER_VALIDATE_BOOLEAN);
if ($params['redis_cluster'] && isset($params['redis_sentinel'])) {
throw new InvalidArgumentException('Cannot use both "redis_cluster" and "redis_sentinel" at the same time.');
}
$class = $params['class'] ?? match (true) {
$params['redis_cluster'] => \extension_loaded('redis') ? \RedisCluster::class : \Predis\Client::class,
isset($params['redis_sentinel']) => match (true) {
\extension_loaded('redis') => \Redis::class,
\extension_loaded('relay') => Relay::class,
default => \Predis\Client::class,
},
1 < \count($hosts) && \extension_loaded('redis') => 1 < \count($hosts) ? \RedisArray::class : \Redis::class,
\extension_loaded('redis') => \Redis::class,
\extension_loaded('relay') => Relay::class,
default => \Predis\Client::class,
};
if (isset($params['redis_sentinel']) && !is_a($class, \Predis\Client::class, true) && !class_exists(\RedisSentinel::class) && !class_exists(Sentinel::class)) {
throw new CacheException(sprintf('Cannot use Redis Sentinel: class "%s" does not extend "Predis\Client" and neither ext-redis >= 5.2 nor ext-relay have been found.', $class));
}
$isRedisExt = is_a($class, \Redis::class, true);
$isRelayExt = !$isRedisExt && is_a($class, Relay::class, true);
if ($isRedisExt || $isRelayExt) {
$connect = $params['persistent'] || $params['persistent_id'] ? 'pconnect' : 'connect';
$initializer = static function () use ($class, $isRedisExt, $connect, $params, $auth, $hosts, $tls) {
$sentinelClass = $isRedisExt ? \RedisSentinel::class : Sentinel::class;
$redis = new $class();
$hostIndex = 0;
do {
$host = $hosts[$hostIndex]['host'] ?? $hosts[$hostIndex]['path'];
$port = $hosts[$hostIndex]['port'] ?? 0;
$passAuth = isset($params['auth']) && (!$isRedisExt || \defined('Redis::OPT_NULL_MULTIBULK_AS_NULL'));
$address = false;
if (isset($hosts[$hostIndex]['host']) && $tls) {
$host = 'tls://'.$host;
}
if (!isset($params['redis_sentinel'])) {
break;
}
try {
if (version_compare(phpversion('redis'), '6.0.0', '>=') && $isRedisExt) {
$options = [
'host' => $host,
'port' => $port,
'connectTimeout' => $params['timeout'],
'persistent' => $params['persistent_id'],
'retryInterval' => $params['retry_interval'],
'readTimeout' => $params['read_timeout'],
];
if ($passAuth) {
$options['auth'] = $params['auth'];
}
$sentinel = new \RedisSentinel($options);
} else {
$extra = $passAuth ? [$params['auth']] : [];
$sentinel = new $sentinelClass($host, $port, $params['timeout'], (string) $params['persistent_id'], $params['retry_interval'], $params['read_timeout'], ...$extra);
}
if ($address = $sentinel->getMasterAddrByName($params['redis_sentinel'])) {
[$host, $port] = $address;
}
} catch (\RedisException|\Relay\Exception $redisException) {
}
} while (++$hostIndex < \count($hosts) && !$address);
if (isset($params['redis_sentinel']) && !$address) {
throw new InvalidArgumentException(sprintf('Failed to retrieve master information from sentinel "%s".', $params['redis_sentinel']), previous: $redisException ?? null);
}
try {
$extra = [
'stream' => $params['ssl'] ?? null,
];
$booleanStreamOptions = [
'allow_self_signed',
'capture_peer_cert',
'capture_peer_cert_chain',
'disable_compression',
'SNI_enabled',
'verify_peer',
'verify_peer_name',
];
foreach ($extra['stream'] ?? [] as $streamOption => $value) {
if (\in_array($streamOption, $booleanStreamOptions, true) && \is_string($value)) {
$extra['stream'][$streamOption] = filter_var($value, \FILTER_VALIDATE_BOOL);
}
}
if (isset($params['auth'])) {
$extra['auth'] = $params['auth'];
}
@$redis->{$connect}($host, $port, (float) $params['timeout'], (string) $params['persistent_id'], $params['retry_interval'], $params['read_timeout'], ...\defined('Redis::SCAN_PREFIX') || !$isRedisExt ? [$extra] : []);
set_error_handler(function ($type, $msg) use (&$error) { $error = $msg; });
try {
$isConnected = $redis->isConnected();
} finally {
restore_error_handler();
}
if (!$isConnected) {
$error = preg_match('/^Redis::p?connect\(\): (.*)/', $error ?? $redis->getLastError() ?? '', $error) ? sprintf(' (%s)', $error[1]) : '';
throw new InvalidArgumentException('Redis connection failed: '.$error.'.');
}
if ((null !== $auth && !$redis->auth($auth))
// Due to a bug in phpredis we must always select the dbindex if persistent pooling is enabled
// @see https://github.com/phpredis/phpredis/issues/1920
// @see https://github.com/symfony/symfony/issues/51578
|| (($params['dbindex'] || ('pconnect' === $connect && '0' !== \ini_get('redis.pconnect.pooling_enabled'))) && !$redis->select($params['dbindex']))
) {
$e = preg_replace('/^ERR /', '', $redis->getLastError());
throw new InvalidArgumentException('Redis connection failed: '.$e.'.');
}
if (0 < $params['tcp_keepalive'] && (!$isRedisExt || \defined('Redis::OPT_TCP_KEEPALIVE'))) {
$redis->setOption($isRedisExt ? \Redis::OPT_TCP_KEEPALIVE : Relay::OPT_TCP_KEEPALIVE, $params['tcp_keepalive']);
}
} catch (\RedisException|\Relay\Exception $e) {
throw new InvalidArgumentException('Redis connection failed: '.$e->getMessage());
}
return $redis;
};
if ($params['lazy']) {
$redis = $isRedisExt ? RedisProxy::createLazyProxy($initializer) : RelayProxy::createLazyProxy($initializer);
} else {
$redis = $initializer();
}
} elseif (is_a($class, \RedisArray::class, true)) {
foreach ($hosts as $i => $host) {
$hosts[$i] = match ($host['scheme']) {
'tcp' => $host['host'].':'.$host['port'],
'tls' => 'tls://'.$host['host'].':'.$host['port'],
default => $host['path'],
};
}
$params['lazy_connect'] = $params['lazy'] ?? true;
$params['connect_timeout'] = $params['timeout'];
try {
$redis = new $class($hosts, $params);
} catch (\RedisClusterException $e) {
throw new InvalidArgumentException('Redis connection failed: '.$e->getMessage());
}
if (0 < $params['tcp_keepalive'] && (!$isRedisExt || \defined('Redis::OPT_TCP_KEEPALIVE'))) {
$redis->setOption($isRedisExt ? \Redis::OPT_TCP_KEEPALIVE : Relay::OPT_TCP_KEEPALIVE, $params['tcp_keepalive']);
}
} elseif (is_a($class, \RedisCluster::class, true)) {
$initializer = static function () use ($isRedisExt, $class, $params, $hosts) {
foreach ($hosts as $i => $host) {
$hosts[$i] = match ($host['scheme']) {
'tcp' => $host['host'].':'.$host['port'],
'tls' => 'tls://'.$host['host'].':'.$host['port'],
default => $host['path'],
};
}
try {
$redis = new $class(null, $hosts, $params['timeout'], $params['read_timeout'], (bool) $params['persistent'], $params['auth'] ?? '', ...\defined('Redis::SCAN_PREFIX') ? [$params['ssl'] ?? null] : []);
} catch (\RedisClusterException $e) {
throw new InvalidArgumentException('Redis connection failed: '.$e->getMessage());
}
if (0 < $params['tcp_keepalive'] && (!$isRedisExt || \defined('Redis::OPT_TCP_KEEPALIVE'))) {
$redis->setOption($isRedisExt ? \Redis::OPT_TCP_KEEPALIVE : Relay::OPT_TCP_KEEPALIVE, $params['tcp_keepalive']);
}
$redis->setOption(\RedisCluster::OPT_SLAVE_FAILOVER, match ($params['failover']) {
'error' => \RedisCluster::FAILOVER_ERROR,
'distribute' => \RedisCluster::FAILOVER_DISTRIBUTE,
'slaves' => \RedisCluster::FAILOVER_DISTRIBUTE_SLAVES,
'none' => \RedisCluster::FAILOVER_NONE,
});
return $redis;
};
$redis = $params['lazy'] ? RedisClusterProxy::createLazyProxy($initializer) : $initializer();
} elseif (is_a($class, \Predis\ClientInterface::class, true)) {
if ($params['redis_cluster']) {
$params['cluster'] = 'redis';
} elseif (isset($params['redis_sentinel'])) {
$params['replication'] = 'sentinel';
$params['service'] = $params['redis_sentinel'];
}
$params += ['parameters' => []];
$params['parameters'] += [
'persistent' => $params['persistent'],
'timeout' => $params['timeout'],
'read_write_timeout' => $params['read_timeout'],
'tcp_nodelay' => true,
];
if ($params['dbindex']) {
$params['parameters']['database'] = $params['dbindex'];
}
if (null !== $auth) {
if (\is_array($auth)) {
// ACL
$params['parameters']['username'] = $auth[0];
$params['parameters']['password'] = $auth[1];
} else {
$params['parameters']['password'] = $auth;
}
}
if (isset($params['ssl'])) {
foreach ($hosts as $i => $host) {
$hosts[$i]['ssl'] ??= $params['ssl'];
}
}
if (1 === \count($hosts) && !($params['redis_cluster'] || $params['redis_sentinel'])) {
$hosts = $hosts[0];
} elseif (\in_array($params['failover'], ['slaves', 'distribute'], true) && !isset($params['replication'])) {
$params['replication'] = true;
$hosts[0] += ['alias' => 'master'];
}
$params['exceptions'] = false;
$redis = new $class($hosts, array_diff_key($params, self::$defaultConnectionOptions));
if (isset($params['redis_sentinel'])) {
$redis->getConnection()->setSentinelTimeout($params['timeout']);
}
} elseif (class_exists($class, false)) {
throw new InvalidArgumentException(sprintf('"%s" is not a subclass of "Redis", "RedisArray", "RedisCluster", "Relay\Relay" nor "Predis\ClientInterface".', $class));
} else {
throw new InvalidArgumentException(sprintf('Class "%s" does not exist.', $class));
}
return $redis;
}
protected function doFetch(array $ids): iterable
{
if (!$ids) {
return [];
}
$result = [];
if ($this->redis instanceof \Predis\ClientInterface && ($this->redis->getConnection() instanceof ClusterInterface || $this->redis->getConnection() instanceof Predis2ClusterInterface)) {
$values = $this->pipeline(function () use ($ids) {
foreach ($ids as $id) {
yield 'get' => [$id];
}
});
} else {
$values = $this->redis->mget($ids);
if (!\is_array($values) || \count($values) !== \count($ids)) {
return [];
}
$values = array_combine($ids, $values);
}
foreach ($values as $id => $v) {
if ($v) {
$result[$id] = $this->marshaller->unmarshall($v);
}
}
return $result;
}
protected function doHave(string $id): bool
{
return (bool) $this->redis->exists($id);
}
protected function doClear(string $namespace): bool
{
if ($this->redis instanceof \Predis\ClientInterface) {
$prefix = $this->redis->getOptions()->prefix ? $this->redis->getOptions()->prefix->getPrefix() : '';
$prefixLen = \strlen($prefix ?? '');
}
$cleared = true;
$hosts = $this->getHosts();
$host = reset($hosts);
if ($host instanceof \Predis\Client && $host->getConnection() instanceof ReplicationInterface) {
// Predis supports info command only on the master in replication environments
$hosts = [$host->getClientFor('master')];
}
foreach ($hosts as $host) {
if (!isset($namespace[0])) {
$cleared = $host->flushDb() && $cleared;
continue;
}
$info = $host->info('Server');
$info = !$info instanceof ErrorInterface ? $info['Server'] ?? $info : ['redis_version' => '2.0'];
if ($host instanceof Relay) {
$prefix = Relay::SCAN_PREFIX & $host->getOption(Relay::OPT_SCAN) ? '' : $host->getOption(Relay::OPT_PREFIX);
$prefixLen = \strlen($host->getOption(Relay::OPT_PREFIX) ?? '');
} elseif (!$host instanceof \Predis\ClientInterface) {
$prefix = \defined('Redis::SCAN_PREFIX') && (\Redis::SCAN_PREFIX & $host->getOption(\Redis::OPT_SCAN)) ? '' : $host->getOption(\Redis::OPT_PREFIX);
$prefixLen = \strlen($host->getOption(\Redis::OPT_PREFIX) ?? '');
}
$pattern = $prefix.$namespace.'*';
if (!version_compare($info['redis_version'], '2.8', '>=')) {
// As documented in Redis documentation (http://redis.io/commands/keys) using KEYS
// can hang your server when it is executed against large databases (millions of items).
// Whenever you hit this scale, you should really consider upgrading to Redis 2.8 or above.
$unlink = version_compare($info['redis_version'], '4.0', '>=') ? 'UNLINK' : 'DEL';
$args = $this->redis instanceof \Predis\ClientInterface ? [0, $pattern] : [[$pattern], 0];
$cleared = $host->eval("local keys=redis.call('KEYS',ARGV[1]) for i=1,#keys,5000 do redis.call('$unlink',unpack(keys,i,math.min(i+4999,#keys))) end return 1", $args[0], $args[1]) && $cleared;
continue;
}
$cursor = null;
do {
$keys = $host instanceof \Predis\ClientInterface ? $host->scan($cursor, 'MATCH', $pattern, 'COUNT', 1000) : $host->scan($cursor, $pattern, 1000);
if (isset($keys[1]) && \is_array($keys[1])) {
$cursor = $keys[0];
$keys = $keys[1];
}
if ($keys) {
if ($prefixLen) {
foreach ($keys as $i => $key) {
$keys[$i] = substr($key, $prefixLen);
}
}
$this->doDelete($keys);
}
} while ($cursor);
}
return $cleared;
}
protected function doDelete(array $ids): bool
{
if (!$ids) {
return true;
}
if ($this->redis instanceof \Predis\ClientInterface && ($this->redis->getConnection() instanceof ClusterInterface || $this->redis->getConnection() instanceof Predis2ClusterInterface)) {
static $del;
$del ??= (class_exists(UNLINK::class) ? 'unlink' : 'del');
$this->pipeline(function () use ($ids, $del) {
foreach ($ids as $id) {
yield $del => [$id];
}
})->rewind();
} else {
static $unlink = true;
if ($unlink) {
try {
$unlink = false !== $this->redis->unlink($ids);
} catch (\Throwable) {
$unlink = false;
}
}
if (!$unlink) {
$this->redis->del($ids);
}
}
return true;
}
protected function doSave(array $values, int $lifetime): array|bool
{
if (!$values = $this->marshaller->marshall($values, $failed)) {
return $failed;
}
$results = $this->pipeline(function () use ($values, $lifetime) {
foreach ($values as $id => $value) {
if (0 >= $lifetime) {
yield 'set' => [$id, $value];
} else {
yield 'setEx' => [$id, $lifetime, $value];
}
}
});
foreach ($results as $id => $result) {
if (true !== $result && (!$result instanceof Status || Status::get('OK') !== $result)) {
$failed[] = $id;
}
}
return $failed;
}
private function pipeline(\Closure $generator, ?object $redis = null): \Generator
{
$ids = [];
$redis ??= $this->redis;
if ($redis instanceof \RedisCluster || ($redis instanceof \Predis\ClientInterface && ($redis->getConnection() instanceof RedisCluster || $redis->getConnection() instanceof Predis2RedisCluster))) {
// phpredis & predis don't support pipelining with RedisCluster
// see https://github.com/phpredis/phpredis/blob/develop/cluster.markdown#pipelining
// see https://github.com/nrk/predis/issues/267#issuecomment-123781423
$results = [];
foreach ($generator() as $command => $args) {
$results[] = $redis->{$command}(...$args);
$ids[] = 'eval' === $command ? ($redis instanceof \Predis\ClientInterface ? $args[2] : $args[1][0]) : $args[0];
}
} elseif ($redis instanceof \Predis\ClientInterface) {
$results = $redis->pipeline(static function ($redis) use ($generator, &$ids) {
foreach ($generator() as $command => $args) {
$redis->{$command}(...$args);
$ids[] = 'eval' === $command ? $args[2] : $args[0];
}
});
} elseif ($redis instanceof \RedisArray) {
$connections = $results = $ids = [];
foreach ($generator() as $command => $args) {
$id = 'eval' === $command ? $args[1][0] : $args[0];
if (!isset($connections[$h = $redis->_target($id)])) {
$connections[$h] = [$redis->_instance($h), -1];
$connections[$h][0]->multi(\Redis::PIPELINE);
}
$connections[$h][0]->{$command}(...$args);
$results[] = [$h, ++$connections[$h][1]];
$ids[] = $id;
}
foreach ($connections as $h => $c) {
$connections[$h] = $c[0]->exec();
}
foreach ($results as $k => [$h, $c]) {
$results[$k] = $connections[$h][$c];
}
} else {
$redis->multi($redis instanceof Relay ? Relay::PIPELINE : \Redis::PIPELINE);
foreach ($generator() as $command => $args) {
$redis->{$command}(...$args);
$ids[] = 'eval' === $command ? $args[1][0] : $args[0];
}
$results = $redis->exec();
}
if (!$redis instanceof \Predis\ClientInterface && 'eval' === $command && $redis->getLastError()) {
$e = $redis instanceof Relay ? new \Relay\Exception($redis->getLastError()) : new \RedisException($redis->getLastError());
$results = array_map(fn ($v) => false === $v ? $e : $v, (array) $results);
}
if (\is_bool($results)) {
return;
}
foreach ($ids as $k => $id) {
yield $id => $results[$k];
}
}
private function getHosts(): array
{
$hosts = [$this->redis];
if ($this->redis instanceof \Predis\ClientInterface) {
$connection = $this->redis->getConnection();
if (($connection instanceof ClusterInterface || $connection instanceof Predis2ClusterInterface) && $connection instanceof \Traversable) {
$hosts = [];
foreach ($connection as $c) {
$hosts[] = new \Predis\Client($c);
}
}
} elseif ($this->redis instanceof \RedisArray) {
$hosts = [];
foreach ($this->redis->_hosts() as $host) {
$hosts[] = $this->redis->_instance($host);
}
} elseif ($this->redis instanceof \RedisCluster) {
$hosts = [];
foreach ($this->redis->_masters() as $host) {
$hosts[] = new RedisClusterNodeProxy($host, $this->redis);
}
}
return $hosts;
}
}
File diff suppressed because it is too large Load Diff
+156
View File
@@ -0,0 +1,156 @@
<?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\Traits;
if (version_compare(phpversion('relay'), '0.8.1', '>=')) {
/**
* @internal
*/
trait RelayProxyTrait
{
public function copy($src, $dst, $options = null): \Relay\Relay|bool
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->copy(...\func_get_args());
}
public function jsonArrAppend($key, $value_or_array, $path = null): \Relay\Relay|array|false
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->jsonArrAppend(...\func_get_args());
}
public function jsonArrIndex($key, $path, $value, $start = 0, $stop = -1): \Relay\Relay|array|false
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->jsonArrIndex(...\func_get_args());
}
public function jsonArrInsert($key, $path, $index, $value, ...$other_values): \Relay\Relay|array|false
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->jsonArrInsert(...\func_get_args());
}
public function jsonArrLen($key, $path = null): \Relay\Relay|array|false
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->jsonArrLen(...\func_get_args());
}
public function jsonArrPop($key, $path = null, $index = -1): \Relay\Relay|array|false
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->jsonArrPop(...\func_get_args());
}
public function jsonArrTrim($key, $path, $start, $stop): \Relay\Relay|array|false
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->jsonArrTrim(...\func_get_args());
}
public function jsonClear($key, $path = null): \Relay\Relay|false|int
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->jsonClear(...\func_get_args());
}
public function jsonDebug($command, $key, $path = null): \Relay\Relay|false|int
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->jsonDebug(...\func_get_args());
}
public function jsonDel($key, $path = null): \Relay\Relay|false|int
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->jsonDel(...\func_get_args());
}
public function jsonForget($key, $path = null): \Relay\Relay|false|int
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->jsonForget(...\func_get_args());
}
public function jsonGet($key, $options = [], ...$paths): mixed
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->jsonGet(...\func_get_args());
}
public function jsonMerge($key, $path, $value): \Relay\Relay|bool
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->jsonMerge(...\func_get_args());
}
public function jsonMget($key_or_array, $path): \Relay\Relay|array|false
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->jsonMget(...\func_get_args());
}
public function jsonMset($key, $path, $value, ...$other_triples): \Relay\Relay|bool
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->jsonMset(...\func_get_args());
}
public function jsonNumIncrBy($key, $path, $value): \Relay\Relay|array|false
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->jsonNumIncrBy(...\func_get_args());
}
public function jsonNumMultBy($key, $path, $value): \Relay\Relay|array|false
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->jsonNumMultBy(...\func_get_args());
}
public function jsonObjKeys($key, $path = null): \Relay\Relay|array|false
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->jsonObjKeys(...\func_get_args());
}
public function jsonObjLen($key, $path = null): \Relay\Relay|array|false
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->jsonObjLen(...\func_get_args());
}
public function jsonResp($key, $path = null): \Relay\Relay|array|false|int|string
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->jsonResp(...\func_get_args());
}
public function jsonSet($key, $path, $value, $condition = null): \Relay\Relay|bool
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->jsonSet(...\func_get_args());
}
public function jsonStrAppend($key, $value, $path = null): \Relay\Relay|array|false
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->jsonStrAppend(...\func_get_args());
}
public function jsonStrLen($key, $path = null): \Relay\Relay|array|false
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->jsonStrLen(...\func_get_args());
}
public function jsonToggle($key, $path): \Relay\Relay|array|false
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->jsonToggle(...\func_get_args());
}
public function jsonType($key, $path = null): \Relay\Relay|array|false
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->jsonType(...\func_get_args());
}
}
} else {
/**
* @internal
*/
trait RelayProxyTrait
{
public function copy($src, $dst, $options = null): \Relay\Relay|false|int
{
return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->copy(...\func_get_args());
}
}
}
+81
View File
@@ -0,0 +1,81 @@
<?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.
*/
/**
* A short namespace-less class to serialize items with metadata.
*
* @author Nicolas Grekas <p@tchwork.com>
*
* @internal
*/
class ©
{
private const EXPIRY_OFFSET = 1648206727;
private const INT32_MAX = 2147483647;
public readonly mixed $value;
public readonly array $metadata;
public function __construct(mixed $value, array $metadata)
{
$this->value = $value;
$this->metadata = $metadata;
}
public function __serialize(): array
{
// pack 31-bits ctime into 14bits
$c = $this->metadata['ctime'] ?? 0;
$c = match (true) {
$c > self::INT32_MAX - 2 => self::INT32_MAX,
$c > 0 => 1 + $c,
default => 1,
};
$e = 0;
while (!(0x40000000 & $c)) {
$c <<= 1;
++$e;
}
$c = (0x7FE0 & ($c >> 16)) | $e;
$pack = pack('Vn', (int) (0.1 + ($this->metadata['expiry'] ?: self::INT32_MAX + self::EXPIRY_OFFSET) - self::EXPIRY_OFFSET), $c);
if (isset($this->metadata['tags'])) {
$pack[4] = $pack[4] | "\x80";
}
return [$pack => $this->value] + ($this->metadata['tags'] ?? []);
}
public function __unserialize(array $data): void
{
$pack = array_key_first($data);
$this->value = $data[$pack];
if ($hasTags = "\x80" === ($pack[4] & "\x80")) {
unset($data[$pack]);
$pack[4] = $pack[4] & "\x7F";
}
$metadata = unpack('Vexpiry/nctime', $pack);
$metadata['expiry'] += self::EXPIRY_OFFSET;
if (!$metadata['ctime'] = ((0x4000 | $metadata['ctime']) << 16 >> (0x1F & $metadata['ctime'])) - 1) {
unset($metadata['ctime']);
}
if ($hasTags) {
$metadata['tags'] = $data;
}
$this->metadata = $metadata;
}
}
+60
View File
@@ -0,0 +1,60 @@
{
"name": "symfony/cache",
"type": "library",
"description": "Provides extended PSR-6, PSR-16 (and tags) implementations",
"keywords": ["caching", "psr6"],
"homepage": "https://symfony.com",
"license": "MIT",
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"provide": {
"psr/cache-implementation": "2.0|3.0",
"psr/simple-cache-implementation": "1.0|2.0|3.0",
"symfony/cache-implementation": "1.1|2.0|3.0"
},
"require": {
"php": ">=8.2",
"psr/cache": "^2.0|^3.0",
"psr/log": "^1.1|^2|^3",
"symfony/cache-contracts": "^2.5|^3",
"symfony/deprecation-contracts": "^2.5|^3.0",
"symfony/service-contracts": "^2.5|^3",
"symfony/var-exporter": "^6.4|^7.0"
},
"require-dev": {
"cache/integration-tests": "dev-master",
"doctrine/dbal": "^3.6|^4",
"predis/predis": "^1.1|^2.0",
"psr/simple-cache": "^1.0|^2.0|^3.0",
"symfony/config": "^6.4|^7.0",
"symfony/dependency-injection": "^6.4|^7.0",
"symfony/filesystem": "^6.4|^7.0",
"symfony/http-kernel": "^6.4|^7.0",
"symfony/messenger": "^6.4|^7.0",
"symfony/var-dumper": "^6.4|^7.0"
},
"conflict": {
"doctrine/dbal": "<3.6",
"symfony/dependency-injection": "<6.4",
"symfony/http-kernel": "<6.4",
"symfony/var-dumper": "<6.4"
},
"autoload": {
"psr-4": { "Symfony\\Component\\Cache\\": "" },
"classmap": [
"Traits/ValueWrapper.php"
],
"exclude-from-classmap": [
"/Tests/"
]
},
"minimum-stability": "dev"
}
+25
View File
@@ -0,0 +1,25 @@
CHANGELOG
=========
7.1
---
* Add `DatePoint::getMicrosecond()` and `DatePoint::setMicrosecond()`
6.4
---
* Add `DatePoint`: an immutable DateTime implementation with stricter error handling and return types
* Throw `DateMalformedStringException`/`DateInvalidTimeZoneException` when appropriate
* Add `$modifier` argument to the `now()` helper
6.3
---
* Add `ClockAwareTrait` to help write time-sensitive classes
* Add `Clock` class and `now()` function
6.2
---
* Add the component
+89
View File
@@ -0,0 +1,89 @@
<?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\Clock;
use Psr\Clock\ClockInterface as PsrClockInterface;
/**
* A global clock.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
final class Clock implements ClockInterface
{
private static ClockInterface $globalClock;
public function __construct(
private readonly ?PsrClockInterface $clock = null,
private ?\DateTimeZone $timezone = null,
) {
}
/**
* Returns the current global clock.
*
* Note that you should prefer injecting a ClockInterface or using
* ClockAwareTrait when possible instead of using this method.
*/
public static function get(): ClockInterface
{
return self::$globalClock ??= new NativeClock();
}
public static function set(PsrClockInterface $clock): void
{
self::$globalClock = $clock instanceof ClockInterface ? $clock : new self($clock);
}
public function now(): DatePoint
{
$now = ($this->clock ?? self::get())->now();
if (!$now instanceof DatePoint) {
$now = DatePoint::createFromInterface($now);
}
return isset($this->timezone) ? $now->setTimezone($this->timezone) : $now;
}
public function sleep(float|int $seconds): void
{
$clock = $this->clock ?? self::get();
if ($clock instanceof ClockInterface) {
$clock->sleep($seconds);
} else {
(new NativeClock())->sleep($seconds);
}
}
/**
* @throws \DateInvalidTimeZoneException When $timezone is invalid
*/
public function withTimeZone(\DateTimeZone|string $timezone): static
{
if (\PHP_VERSION_ID >= 80300 && \is_string($timezone)) {
$timezone = new \DateTimeZone($timezone);
} elseif (\is_string($timezone)) {
try {
$timezone = new \DateTimeZone($timezone);
} catch (\Exception $e) {
throw new \DateInvalidTimeZoneException($e->getMessage(), $e->getCode(), $e);
}
}
$clone = clone $this;
$clone->timezone = $timezone;
return $clone;
}
}
+38
View File
@@ -0,0 +1,38 @@
<?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\Clock;
use Psr\Clock\ClockInterface;
use Symfony\Contracts\Service\Attribute\Required;
/**
* A trait to help write time-sensitive classes.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
trait ClockAwareTrait
{
private readonly ClockInterface $clock;
#[Required]
public function setClock(ClockInterface $clock): void
{
$this->clock = $clock;
}
protected function now(): DatePoint
{
$now = ($this->clock ??= new Clock())->now();
return $now instanceof DatePoint ? $now : DatePoint::createFromInterface($now);
}
}
+24
View File
@@ -0,0 +1,24 @@
<?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\Clock;
use Psr\Clock\ClockInterface as PsrClockInterface;
/**
* @author Nicolas Grekas <p@tchwork.com>
*/
interface ClockInterface extends PsrClockInterface
{
public function sleep(float|int $seconds): void;
public function withTimeZone(\DateTimeZone|string $timezone): static;
}
+169
View File
@@ -0,0 +1,169 @@
<?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\Clock;
/**
* An immmutable DateTime with stricter error handling and return types than the native one.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
final class DatePoint extends \DateTimeImmutable
{
/**
* @throws \DateMalformedStringException When $datetime is invalid
*/
public function __construct(string $datetime = 'now', ?\DateTimeZone $timezone = null, ?parent $reference = null)
{
$now = $reference ?? Clock::get()->now();
if ('now' !== $datetime) {
if (!$now instanceof static) {
$now = static::createFromInterface($now);
}
if (\PHP_VERSION_ID < 80300) {
try {
$builtInDate = new parent($datetime, $timezone ?? $now->getTimezone());
$timezone = $builtInDate->getTimezone();
} catch (\Exception $e) {
throw new \DateMalformedStringException($e->getMessage(), $e->getCode(), $e);
}
} else {
$builtInDate = new parent($datetime, $timezone ?? $now->getTimezone());
$timezone = $builtInDate->getTimezone();
}
$now = $now->setTimezone($timezone)->modify($datetime);
if ('00:00:00.000000' === $builtInDate->format('H:i:s.u')) {
$now = $now->setTime(0, 0);
}
} elseif (null !== $timezone) {
$now = $now->setTimezone($timezone);
}
$this->__unserialize((array) $now);
}
/**
* @throws \DateMalformedStringException When $format or $datetime are invalid
*/
public static function createFromFormat(string $format, string $datetime, ?\DateTimeZone $timezone = null): static
{
return parent::createFromFormat($format, $datetime, $timezone) ?: throw new \DateMalformedStringException(static::getLastErrors()['errors'][0] ?? 'Invalid date string or format.');
}
public static function createFromInterface(\DateTimeInterface $object): static
{
return parent::createFromInterface($object);
}
public static function createFromMutable(\DateTime $object): static
{
return parent::createFromMutable($object);
}
public static function createFromTimestamp(int|float $timestamp): static
{
if (\PHP_VERSION_ID >= 80400) {
return parent::createFromTimestamp($timestamp);
}
if (\is_int($timestamp) || !$ms = (int) $timestamp - $timestamp) {
return static::createFromFormat('U', (string) $timestamp);
}
if (!is_finite($timestamp) || \PHP_INT_MAX + 1.0 <= $timestamp || \PHP_INT_MIN > $timestamp) {
throw new \DateRangeError(sprintf('DateTimeImmutable::createFromTimestamp(): Argument #1 ($timestamp) must be a finite number between %s and %s.999999, %s given', \PHP_INT_MIN, \PHP_INT_MAX, $timestamp));
}
if ($timestamp < 0) {
$timestamp = (int) $timestamp - 2.0 + $ms;
}
return static::createFromFormat('U.u', sprintf('%.6F', $timestamp));
}
public function add(\DateInterval $interval): static
{
return parent::add($interval);
}
public function sub(\DateInterval $interval): static
{
return parent::sub($interval);
}
/**
* @throws \DateMalformedStringException When $modifier is invalid
*/
public function modify(string $modifier): static
{
if (\PHP_VERSION_ID < 80300) {
return @parent::modify($modifier) ?: throw new \DateMalformedStringException(error_get_last()['message'] ?? sprintf('Invalid modifier: "%s".', $modifier));
}
return parent::modify($modifier);
}
public function setTimestamp(int $value): static
{
return parent::setTimestamp($value);
}
public function setDate(int $year, int $month, int $day): static
{
return parent::setDate($year, $month, $day);
}
public function setISODate(int $year, int $week, int $day = 1): static
{
return parent::setISODate($year, $week, $day);
}
public function setTime(int $hour, int $minute, int $second = 0, int $microsecond = 0): static
{
return parent::setTime($hour, $minute, $second, $microsecond);
}
public function setTimezone(\DateTimeZone $timezone): static
{
return parent::setTimezone($timezone);
}
public function getTimezone(): \DateTimeZone
{
return parent::getTimezone() ?: throw new \DateInvalidTimeZoneException('The DatePoint object has no timezone.');
}
public function setMicrosecond(int $microsecond): static
{
if ($microsecond < 0 || $microsecond > 999999) {
throw new \DateRangeError('DatePoint::setMicrosecond(): Argument #1 ($microsecond) must be between 0 and 999999, '.$microsecond.' given');
}
if (\PHP_VERSION_ID < 80400) {
return $this->setTime(...explode('.', $this->format('H.i.s.'.$microsecond)));
}
return parent::setMicrosecond($microsecond);
}
public function getMicrosecond(): int
{
if (\PHP_VERSION_ID >= 80400) {
return parent::getMicrosecond();
}
return $this->format('u');
}
}
+19
View File
@@ -0,0 +1,19 @@
Copyright (c) 2022-present Fabien Potencier
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
+98
View File
@@ -0,0 +1,98 @@
<?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\Clock;
/**
* A clock that always returns the same date, suitable for testing time-sensitive logic.
*
* Consider using ClockSensitiveTrait in your test cases instead of using this class directly.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
final class MockClock implements ClockInterface
{
private DatePoint $now;
/**
* @throws \DateMalformedStringException When $now is invalid
* @throws \DateInvalidTimeZoneException When $timezone is invalid
*/
public function __construct(\DateTimeImmutable|string $now = 'now', \DateTimeZone|string|null $timezone = null)
{
if (\PHP_VERSION_ID >= 80300 && \is_string($timezone)) {
$timezone = new \DateTimeZone($timezone);
} elseif (\is_string($timezone)) {
try {
$timezone = new \DateTimeZone($timezone);
} catch (\Exception $e) {
throw new \DateInvalidTimeZoneException($e->getMessage(), $e->getCode(), $e);
}
}
if (\is_string($now)) {
$now = new DatePoint($now, $timezone ?? new \DateTimeZone('UTC'));
} elseif (!$now instanceof DatePoint) {
$now = DatePoint::createFromInterface($now);
}
$this->now = null !== $timezone ? $now->setTimezone($timezone) : $now;
}
public function now(): DatePoint
{
return clone $this->now;
}
public function sleep(float|int $seconds): void
{
$now = (float) $this->now->format('Uu') + $seconds * 1e6;
$now = substr_replace(sprintf('@%07.0F', $now), '.', -6, 0);
$timezone = $this->now->getTimezone();
$this->now = DatePoint::createFromInterface(new \DateTimeImmutable($now, $timezone))->setTimezone($timezone);
}
/**
* @throws \DateMalformedStringException When $modifier is invalid
*/
public function modify(string $modifier): void
{
if (\PHP_VERSION_ID < 80300) {
$this->now = @$this->now->modify($modifier) ?: throw new \DateMalformedStringException(error_get_last()['message'] ?? sprintf('Invalid modifier: "%s". Could not modify MockClock.', $modifier));
return;
}
$this->now = $this->now->modify($modifier);
}
/**
* @throws \DateInvalidTimeZoneException When the timezone name is invalid
*/
public function withTimeZone(\DateTimeZone|string $timezone): static
{
if (\PHP_VERSION_ID >= 80300 && \is_string($timezone)) {
$timezone = new \DateTimeZone($timezone);
} elseif (\is_string($timezone)) {
try {
$timezone = new \DateTimeZone($timezone);
} catch (\Exception $e) {
throw new \DateInvalidTimeZoneException($e->getMessage(), $e->getCode(), $e);
}
}
$clone = clone $this;
$clone->now = $clone->now->setTimezone($timezone);
return $clone;
}
}
+93
View File
@@ -0,0 +1,93 @@
<?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\Clock;
/**
* A monotonic clock suitable for performance profiling.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
final class MonotonicClock implements ClockInterface
{
private int $sOffset;
private int $usOffset;
private \DateTimeZone $timezone;
/**
* @throws \DateInvalidTimeZoneException When $timezone is invalid
*/
public function __construct(\DateTimeZone|string|null $timezone = null)
{
if (false === $offset = hrtime()) {
throw new \RuntimeException('hrtime() returned false: the runtime environment does not provide access to a monotonic timer.');
}
$time = explode(' ', microtime(), 2);
$this->sOffset = $time[1] - $offset[0];
$this->usOffset = (int) ($time[0] * 1000000) - (int) ($offset[1] / 1000);
$this->timezone = \is_string($timezone ??= date_default_timezone_get()) ? $this->withTimeZone($timezone)->timezone : $timezone;
}
public function now(): DatePoint
{
[$s, $us] = hrtime();
if (1000000 <= $us = (int) ($us / 1000) + $this->usOffset) {
++$s;
$us -= 1000000;
} elseif (0 > $us) {
--$s;
$us += 1000000;
}
if (6 !== \strlen($now = (string) $us)) {
$now = str_pad($now, 6, '0', \STR_PAD_LEFT);
}
$now = '@'.($s + $this->sOffset).'.'.$now;
return DatePoint::createFromInterface(new \DateTimeImmutable($now, $this->timezone))->setTimezone($this->timezone);
}
public function sleep(float|int $seconds): void
{
if (0 < $s = (int) $seconds) {
sleep($s);
}
if (0 < $us = $seconds - $s) {
usleep((int) ($us * 1E6));
}
}
/**
* @throws \DateInvalidTimeZoneException When $timezone is invalid
*/
public function withTimeZone(\DateTimeZone|string $timezone): static
{
if (\PHP_VERSION_ID >= 80300 && \is_string($timezone)) {
$timezone = new \DateTimeZone($timezone);
} elseif (\is_string($timezone)) {
try {
$timezone = new \DateTimeZone($timezone);
} catch (\Exception $e) {
throw new \DateInvalidTimeZoneException($e->getMessage(), $e->getCode(), $e);
}
}
$clone = clone $this;
$clone->timezone = $timezone;
return $clone;
}
}
+67
View File
@@ -0,0 +1,67 @@
<?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\Clock;
/**
* A clock that relies the system time.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
final class NativeClock implements ClockInterface
{
private \DateTimeZone $timezone;
/**
* @throws \DateInvalidTimeZoneException When $timezone is invalid
*/
public function __construct(\DateTimeZone|string|null $timezone = null)
{
$this->timezone = \is_string($timezone ??= date_default_timezone_get()) ? $this->withTimeZone($timezone)->timezone : $timezone;
}
public function now(): DatePoint
{
return DatePoint::createFromInterface(new \DateTimeImmutable('now', $this->timezone));
}
public function sleep(float|int $seconds): void
{
if (0 < $s = (int) $seconds) {
sleep($s);
}
if (0 < $us = $seconds - $s) {
usleep((int) ($us * 1E6));
}
}
/**
* @throws \DateInvalidTimeZoneException When $timezone is invalid
*/
public function withTimeZone(\DateTimeZone|string $timezone): static
{
if (\PHP_VERSION_ID >= 80300 && \is_string($timezone)) {
$timezone = new \DateTimeZone($timezone);
} elseif (\is_string($timezone)) {
try {
$timezone = new \DateTimeZone($timezone);
} catch (\Exception $e) {
throw new \DateInvalidTimeZoneException($e->getMessage(), $e->getCode(), $e);
}
}
$clone = clone $this;
$clone->timezone = $timezone;
return $clone;
}
}
+47
View File
@@ -0,0 +1,47 @@
Clock Component
===============
Symfony Clock decouples applications from the system clock.
Getting Started
---------------
```bash
composer require symfony/clock
```
```php
use Symfony\Component\Clock\NativeClock;
use Symfony\Component\Clock\ClockInterface;
class MyClockSensitiveClass
{
public function __construct(
private ClockInterface $clock,
) {
// Only if you need to force a timezone:
//$this->clock = $clock->withTimeZone('UTC');
}
public function doSomething()
{
$now = $this->clock->now();
// [...] do something with $now, which is a \DateTimeImmutable object
$this->clock->sleep(2.5); // Pause execution for 2.5 seconds
}
}
$clock = new NativeClock();
$service = new MyClockSensitiveClass($clock);
$service->doSomething();
```
Resources
---------
* [Documentation](https://symfony.com/doc/current/components/clock.html)
* [Contributing](https://symfony.com/doc/current/contributing/index.html)
* [Report issues](https://github.com/symfony/symfony/issues) and
[send Pull Requests](https://github.com/symfony/symfony/pulls)
in the [main Symfony repository](https://github.com/symfony/symfony)
+28
View File
@@ -0,0 +1,28 @@
<?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\Clock;
if (!\function_exists(now::class)) {
/**
* @throws \DateMalformedStringException When the modifier is invalid
*/
function now(string $modifier = 'now'): DatePoint
{
if ('now' !== $modifier) {
return new DatePoint($modifier);
}
$now = Clock::get()->now();
return $now instanceof DatePoint ? $now : DatePoint::createFromInterface($now);
}
}
+77
View File
@@ -0,0 +1,77 @@
<?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\Clock\Test;
use PHPUnit\Framework\Attributes\After;
use PHPUnit\Framework\Attributes\Before;
use PHPUnit\Framework\Attributes\BeforeClass;
use Symfony\Component\Clock\Clock;
use Symfony\Component\Clock\ClockInterface;
use Symfony\Component\Clock\MockClock;
use function Symfony\Component\Clock\now;
/**
* Helps with mocking the time in your test cases.
*
* This trait provides one self::mockTime() method that freezes the time.
* It restores the global clock after each test case.
* self::mockTime() accepts either a string (eg '+1 days' or '2022-12-22'),
* a DateTimeImmutable, or a boolean (to freeze/restore the global clock).
*
* @author Nicolas Grekas <p@tchwork.com>
*/
trait ClockSensitiveTrait
{
public static function mockTime(string|\DateTimeImmutable|bool $when = true): ClockInterface
{
Clock::set(match (true) {
false === $when => self::saveClockBeforeTest(false),
true === $when => new MockClock(),
$when instanceof \DateTimeImmutable => new MockClock($when),
default => new MockClock(now($when)),
});
return Clock::get();
}
/**
* @beforeClass
*
* @before
*
* @internal
*/
#[Before]
#[BeforeClass]
public static function saveClockBeforeTest(bool $save = true): ClockInterface
{
static $originalClock;
if ($save && $originalClock) {
self::restoreClockAfterTest();
}
return $save ? $originalClock = Clock::get() : $originalClock;
}
/**
* @after
*
* @internal
*/
#[After]
protected static function restoreClockAfterTest(): void
{
Clock::set(self::saveClockBeforeTest(false));
}
}
+34
View File
@@ -0,0 +1,34 @@
{
"name": "symfony/clock",
"type": "library",
"description": "Decouples applications from the system clock",
"keywords": ["clock", "time", "psr20"],
"homepage": "https://symfony.com",
"license": "MIT",
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"provide": {
"psr/clock-implementation": "1.0"
},
"require": {
"php": ">=8.2",
"psr/clock": "^1.0",
"symfony/polyfill-php83": "^1.28"
},
"autoload": {
"files": [ "Resources/now.php" ],
"psr-4": { "Symfony\\Component\\Clock\\": "" },
"exclude-from-classmap": [
"/Tests/"
]
},
"minimum-stability": "dev"
}
+171
View File
@@ -0,0 +1,171 @@
<?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\Config\Builder;
/**
* Build PHP classes to generate config.
*
* @internal
*
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
*/
class ClassBuilder
{
private string $name;
/** @var Property[] */
private array $properties = [];
/** @var Method[] */
private array $methods = [];
private array $require = [];
private array $use = [];
private array $implements = [];
private bool $allowExtraKeys = false;
public function __construct(
private string $namespace,
string $name,
) {
$this->name = ucfirst($this->camelCase($name)).'Config';
}
public function getDirectory(): string
{
return str_replace('\\', \DIRECTORY_SEPARATOR, $this->namespace);
}
public function getFilename(): string
{
return $this->name.'.php';
}
public function build(): string
{
$rootPath = explode(\DIRECTORY_SEPARATOR, $this->getDirectory());
$require = '';
foreach ($this->require as $class) {
// figure out relative path.
$path = explode(\DIRECTORY_SEPARATOR, $class->getDirectory());
$path[] = $class->getFilename();
foreach ($rootPath as $key => $value) {
if ($path[$key] !== $value) {
break;
}
unset($path[$key]);
}
$require .= sprintf('require_once __DIR__.\DIRECTORY_SEPARATOR.\'%s\';', implode('\'.\DIRECTORY_SEPARATOR.\'', $path))."\n";
}
$use = $require ? "\n" : '';
foreach (array_keys($this->use) as $statement) {
$use .= sprintf('use %s;', $statement)."\n";
}
$implements = [] === $this->implements ? '' : 'implements '.implode(', ', $this->implements);
$body = '';
foreach ($this->properties as $property) {
$body .= ' '.$property->getContent()."\n";
}
foreach ($this->methods as $method) {
$lines = explode("\n", $method->getContent());
foreach ($lines as $line) {
$body .= ($line ? ' '.$line : '')."\n";
}
}
$content = strtr('<?php
namespace NAMESPACE;
REQUIREUSE
/**
* This class is automatically generated to help in creating a config.
*/
class CLASS IMPLEMENTS
{
BODY
}
', ['NAMESPACE' => $this->namespace, 'REQUIRE' => $require, 'USE' => $use, 'CLASS' => $this->getName(), 'IMPLEMENTS' => $implements, 'BODY' => $body]);
return $content;
}
public function addRequire(self $class): void
{
$this->require[] = $class;
}
public function addUse(string $class): void
{
$this->use[$class] = true;
}
public function addImplements(string $interface): void
{
$this->implements[] = '\\'.ltrim($interface, '\\');
}
public function addMethod(string $name, string $body, array $params = []): void
{
$this->methods[] = new Method(strtr($body, ['NAME' => $this->camelCase($name)] + $params));
}
public function addProperty(string $name, ?string $classType = null, ?string $defaultValue = null): Property
{
$property = new Property($name, '_' !== $name[0] ? $this->camelCase($name) : $name);
if (null !== $classType) {
$property->setType($classType);
}
$this->properties[] = $property;
$defaultValue = null !== $defaultValue ? sprintf(' = %s', $defaultValue) : '';
$property->setContent(sprintf('private $%s%s;', $property->getName(), $defaultValue));
return $property;
}
public function getProperties(): array
{
return $this->properties;
}
private function camelCase(string $input): string
{
$output = lcfirst(str_replace(' ', '', ucwords(str_replace('_', ' ', $input))));
return preg_replace('#\W#', '', $output);
}
public function getName(): string
{
return $this->name;
}
public function getNamespace(): string
{
return $this->namespace;
}
public function getFqcn(): string
{
return '\\'.$this->namespace.'\\'.$this->name;
}
public function setAllowExtraKeys(bool $allowExtraKeys): void
{
$this->allowExtraKeys = $allowExtraKeys;
}
public function shouldAllowExtraKeys(): bool
{
return $this->allowExtraKeys;
}
}
+599
View File
@@ -0,0 +1,599 @@
<?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\Config\Builder;
use Symfony\Component\Config\Definition\ArrayNode;
use Symfony\Component\Config\Definition\BaseNode;
use Symfony\Component\Config\Definition\BooleanNode;
use Symfony\Component\Config\Definition\Builder\ExprBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;
use Symfony\Component\Config\Definition\EnumNode;
use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
use Symfony\Component\Config\Definition\FloatNode;
use Symfony\Component\Config\Definition\IntegerNode;
use Symfony\Component\Config\Definition\NodeInterface;
use Symfony\Component\Config\Definition\PrototypedArrayNode;
use Symfony\Component\Config\Definition\ScalarNode;
use Symfony\Component\Config\Definition\VariableNode;
use Symfony\Component\Config\Loader\ParamConfigurator;
/**
* Generate ConfigBuilders to help create valid config.
*
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
*/
class ConfigBuilderGenerator implements ConfigBuilderGeneratorInterface
{
/**
* @var ClassBuilder[]
*/
private array $classes = [];
public function __construct(
private string $outputDir,
) {
}
/**
* @return \Closure that will return the root config class
*/
public function build(ConfigurationInterface $configuration): \Closure
{
$this->classes = [];
$rootNode = $configuration->getConfigTreeBuilder()->buildTree();
$rootClass = new ClassBuilder('Symfony\\Config', $rootNode->getName());
$path = $this->getFullPath($rootClass);
if (!is_file($path)) {
// Generate the class if the file not exists
$this->classes[] = $rootClass;
$this->buildNode($rootNode, $rootClass, $this->getSubNamespace($rootClass));
$rootClass->addImplements(ConfigBuilderInterface::class);
$rootClass->addMethod('getExtensionAlias', '
public function NAME(): string
{
return \'ALIAS\';
}', ['ALIAS' => $rootNode->getPath()]);
$this->writeClasses();
}
return function () use ($path, $rootClass) {
require_once $path;
$className = $rootClass->getFqcn();
return new $className();
};
}
private function getFullPath(ClassBuilder $class): string
{
$directory = $this->outputDir.\DIRECTORY_SEPARATOR.$class->getDirectory();
if (!is_dir($directory)) {
@mkdir($directory, 0777, true);
}
return $directory.\DIRECTORY_SEPARATOR.$class->getFilename();
}
private function writeClasses(): void
{
foreach ($this->classes as $class) {
$this->buildConstructor($class);
$this->buildToArray($class);
if ($class->getProperties()) {
$class->addProperty('_usedProperties', null, '[]');
}
$this->buildSetExtraKey($class);
file_put_contents($this->getFullPath($class), $class->build());
}
$this->classes = [];
}
private function buildNode(NodeInterface $node, ClassBuilder $class, string $namespace): void
{
if (!$node instanceof ArrayNode) {
throw new \LogicException('The node was expected to be an ArrayNode. This Configuration includes an edge case not supported yet.');
}
foreach ($node->getChildren() as $child) {
match (true) {
$child instanceof ScalarNode => $this->handleScalarNode($child, $class),
$child instanceof PrototypedArrayNode => $this->handlePrototypedArrayNode($child, $class, $namespace),
$child instanceof VariableNode => $this->handleVariableNode($child, $class),
$child instanceof ArrayNode => $this->handleArrayNode($child, $class, $namespace),
default => throw new \RuntimeException(sprintf('Unknown node "%s".', $child::class)),
};
}
}
private function handleArrayNode(ArrayNode $node, ClassBuilder $class, string $namespace): void
{
$childClass = new ClassBuilder($namespace, $node->getName());
$childClass->setAllowExtraKeys($node->shouldIgnoreExtraKeys());
$class->addRequire($childClass);
$this->classes[] = $childClass;
$hasNormalizationClosures = $this->hasNormalizationClosures($node);
$comment = $this->getComment($node);
if ($hasNormalizationClosures) {
$comment = sprintf(" * @template TValue\n * @param TValue \$value\n%s", $comment);
$comment .= sprintf(' * @return %s|$this'."\n", $childClass->getFqcn());
$comment .= sprintf(' * @psalm-return (TValue is array ? %s : static)'."\n ", $childClass->getFqcn());
}
if ('' !== $comment) {
$comment = "/**\n$comment*/\n";
}
$property = $class->addProperty(
$node->getName(),
$this->getType($childClass->getFqcn(), $hasNormalizationClosures)
);
$nodeTypes = $this->getParameterTypes($node);
$body = $hasNormalizationClosures ? '
COMMENTpublic function NAME(PARAM_TYPE $value = []): CLASS|static
{
if (!\is_array($value)) {
$this->_usedProperties[\'PROPERTY\'] = true;
$this->PROPERTY = $value;
return $this;
}
if (!$this->PROPERTY instanceof CLASS) {
$this->_usedProperties[\'PROPERTY\'] = true;
$this->PROPERTY = new CLASS($value);
} elseif (0 < \func_num_args()) {
throw new InvalidConfigurationException(\'The node created by "NAME()" has already been initialized. You cannot pass values the second time you call NAME().\');
}
return $this->PROPERTY;
}' : '
COMMENTpublic function NAME(array $value = []): CLASS
{
if (null === $this->PROPERTY) {
$this->_usedProperties[\'PROPERTY\'] = true;
$this->PROPERTY = new CLASS($value);
} elseif (0 < \func_num_args()) {
throw new InvalidConfigurationException(\'The node created by "NAME()" has already been initialized. You cannot pass values the second time you call NAME().\');
}
return $this->PROPERTY;
}';
$class->addUse(InvalidConfigurationException::class);
$class->addMethod($node->getName(), $body, [
'COMMENT' => $comment,
'PROPERTY' => $property->getName(),
'CLASS' => $childClass->getFqcn(),
'PARAM_TYPE' => \in_array('mixed', $nodeTypes, true) ? 'mixed' : implode('|', $nodeTypes),
]);
$this->buildNode($node, $childClass, $this->getSubNamespace($childClass));
}
private function handleVariableNode(VariableNode $node, ClassBuilder $class): void
{
$comment = $this->getComment($node);
$property = $class->addProperty($node->getName());
$class->addUse(ParamConfigurator::class);
$body = '
/**
COMMENT *
* @return $this
*/
public function NAME(mixed $valueDEFAULT): static
{
$this->_usedProperties[\'PROPERTY\'] = true;
$this->PROPERTY = $value;
return $this;
}';
$class->addMethod($node->getName(), $body, [
'PROPERTY' => $property->getName(),
'COMMENT' => $comment,
'DEFAULT' => $node->hasDefaultValue() ? ' = '.var_export($node->getDefaultValue(), true) : '',
]);
}
private function handlePrototypedArrayNode(PrototypedArrayNode $node, ClassBuilder $class, string $namespace): void
{
$name = $this->getSingularName($node);
$prototype = $node->getPrototype();
$methodName = $name;
$hasNormalizationClosures = $this->hasNormalizationClosures($node) || $this->hasNormalizationClosures($prototype);
$nodeParameterTypes = $this->getParameterTypes($node);
$prototypeParameterTypes = $this->getParameterTypes($prototype);
if (!$prototype instanceof ArrayNode || ($prototype instanceof PrototypedArrayNode && $prototype->getPrototype() instanceof ScalarNode)) {
$class->addUse(ParamConfigurator::class);
$property = $class->addProperty($node->getName());
if (null === $key = $node->getKeyAttribute()) {
// This is an array of values; don't use singular name
$nodeTypesWithoutArray = array_filter($nodeParameterTypes, static fn ($type) => 'array' !== $type);
$body = '
/**
* @param ParamConfigurator|list<ParamConfigurator|PROTOTYPE_TYPE>EXTRA_TYPE $value
*
* @return $this
*/
public function NAME(PARAM_TYPE $value): static
{
$this->_usedProperties[\'PROPERTY\'] = true;
$this->PROPERTY = $value;
return $this;
}';
$class->addMethod($node->getName(), $body, [
'PROPERTY' => $property->getName(),
'PROTOTYPE_TYPE' => implode('|', $prototypeParameterTypes),
'EXTRA_TYPE' => $nodeTypesWithoutArray ? '|'.implode('|', $nodeTypesWithoutArray) : '',
'PARAM_TYPE' => \in_array('mixed', $nodeParameterTypes, true) ? 'mixed' : 'ParamConfigurator|'.implode('|', $nodeParameterTypes),
]);
} else {
$body = '
/**
* @return $this
*/
public function NAME(string $VAR, TYPE $VALUE): static
{
$this->_usedProperties[\'PROPERTY\'] = true;
$this->PROPERTY[$VAR] = $VALUE;
return $this;
}';
$class->addMethod($methodName, $body, [
'PROPERTY' => $property->getName(),
'TYPE' => \in_array('mixed', $prototypeParameterTypes, true) ? 'mixed' : 'ParamConfigurator|'.implode('|', $prototypeParameterTypes),
'VAR' => '' === $key ? 'key' : $key,
'VALUE' => 'value' === $key ? 'data' : 'value',
]);
}
return;
}
$childClass = new ClassBuilder($namespace, $name);
if ($prototype instanceof ArrayNode) {
$childClass->setAllowExtraKeys($prototype->shouldIgnoreExtraKeys());
}
$class->addRequire($childClass);
$this->classes[] = $childClass;
$property = $class->addProperty(
$node->getName(),
$this->getType($childClass->getFqcn().'[]', $hasNormalizationClosures)
);
$comment = $this->getComment($node);
if ($hasNormalizationClosures) {
$comment = sprintf(" * @template TValue\n * @param TValue \$value\n%s", $comment);
$comment .= sprintf(' * @return %s|$this'."\n", $childClass->getFqcn());
$comment .= sprintf(' * @psalm-return (TValue is array ? %s : static)'."\n ", $childClass->getFqcn());
}
if ('' !== $comment) {
$comment = "/**\n$comment*/\n";
}
if (null === $key = $node->getKeyAttribute()) {
$body = $hasNormalizationClosures ? '
COMMENTpublic function NAME(PARAM_TYPE $value = []): CLASS|static
{
$this->_usedProperties[\'PROPERTY\'] = true;
if (!\is_array($value)) {
$this->PROPERTY[] = $value;
return $this;
}
return $this->PROPERTY[] = new CLASS($value);
}' : '
COMMENTpublic function NAME(array $value = []): CLASS
{
$this->_usedProperties[\'PROPERTY\'] = true;
return $this->PROPERTY[] = new CLASS($value);
}';
$class->addMethod($methodName, $body, [
'COMMENT' => $comment,
'PROPERTY' => $property->getName(),
'CLASS' => $childClass->getFqcn(),
'PARAM_TYPE' => \in_array('mixed', $nodeParameterTypes, true) ? 'mixed' : implode('|', $nodeParameterTypes),
]);
} else {
$body = $hasNormalizationClosures ? '
COMMENTpublic function NAME(string $VAR, PARAM_TYPE $VALUE = []): CLASS|static
{
if (!\is_array($VALUE)) {
$this->_usedProperties[\'PROPERTY\'] = true;
$this->PROPERTY[$VAR] = $VALUE;
return $this;
}
if (!isset($this->PROPERTY[$VAR]) || !$this->PROPERTY[$VAR] instanceof CLASS) {
$this->_usedProperties[\'PROPERTY\'] = true;
$this->PROPERTY[$VAR] = new CLASS($VALUE);
} elseif (1 < \func_num_args()) {
throw new InvalidConfigurationException(\'The node created by "NAME()" has already been initialized. You cannot pass values the second time you call NAME().\');
}
return $this->PROPERTY[$VAR];
}' : '
COMMENTpublic function NAME(string $VAR, array $VALUE = []): CLASS
{
if (!isset($this->PROPERTY[$VAR])) {
$this->_usedProperties[\'PROPERTY\'] = true;
$this->PROPERTY[$VAR] = new CLASS($VALUE);
} elseif (1 < \func_num_args()) {
throw new InvalidConfigurationException(\'The node created by "NAME()" has already been initialized. You cannot pass values the second time you call NAME().\');
}
return $this->PROPERTY[$VAR];
}';
$class->addUse(InvalidConfigurationException::class);
$class->addMethod($methodName, str_replace('$value', '$VAR', $body), [
'COMMENT' => $comment, 'PROPERTY' => $property->getName(),
'CLASS' => $childClass->getFqcn(),
'VAR' => '' === $key ? 'key' : $key,
'VALUE' => 'value' === $key ? 'data' : 'value',
'PARAM_TYPE' => \in_array('mixed', $prototypeParameterTypes, true) ? 'mixed' : implode('|', $prototypeParameterTypes),
]);
}
$this->buildNode($prototype, $childClass, $namespace.'\\'.$childClass->getName());
}
private function handleScalarNode(ScalarNode $node, ClassBuilder $class): void
{
$comment = $this->getComment($node);
$property = $class->addProperty($node->getName());
$class->addUse(ParamConfigurator::class);
$body = '
/**
COMMENT * @return $this
*/
public function NAME($value): static
{
$this->_usedProperties[\'PROPERTY\'] = true;
$this->PROPERTY = $value;
return $this;
}';
$class->addMethod($node->getName(), $body, ['PROPERTY' => $property->getName(), 'COMMENT' => $comment]);
}
private function getParameterTypes(NodeInterface $node): array
{
$paramTypes = [];
if ($node instanceof BaseNode) {
$types = $node->getNormalizedTypes();
if (\in_array(ExprBuilder::TYPE_ANY, $types, true)) {
$paramTypes[] = 'mixed';
}
if (\in_array(ExprBuilder::TYPE_STRING, $types, true)) {
$paramTypes[] = 'string';
}
}
if ($node instanceof BooleanNode) {
$paramTypes[] = 'bool';
} elseif ($node instanceof IntegerNode) {
$paramTypes[] = 'int';
} elseif ($node instanceof FloatNode) {
$paramTypes[] = 'float';
} elseif ($node instanceof EnumNode) {
$paramTypes[] = 'mixed';
} elseif ($node instanceof ArrayNode) {
$paramTypes[] = 'array';
} elseif ($node instanceof VariableNode) {
$paramTypes[] = 'mixed';
}
return array_unique($paramTypes);
}
private function getComment(BaseNode $node): string
{
$comment = '';
if ('' !== $info = (string) $node->getInfo()) {
$comment .= ' * '.$info."\n";
}
if (!$node instanceof ArrayNode) {
foreach ((array) ($node->getExample() ?? []) as $example) {
$comment .= ' * @example '.$example."\n";
}
if ('' !== $default = $node->getDefaultValue()) {
$comment .= ' * @default '.(null === $default ? 'null' : var_export($default, true))."\n";
}
if ($node instanceof EnumNode) {
$comment .= sprintf(' * @param ParamConfigurator|%s $value', implode('|', array_unique(array_map(fn ($a) => !$a instanceof \UnitEnum ? var_export($a, true) : '\\'.ltrim(var_export($a, true), '\\'), $node->getValues()))))."\n";
} else {
$parameterTypes = $this->getParameterTypes($node);
$comment .= ' * @param ParamConfigurator|'.implode('|', $parameterTypes).' $value'."\n";
}
} else {
foreach ((array) ($node->getExample() ?? []) as $example) {
$comment .= ' * @example '.json_encode($example)."\n";
}
if ($node->hasDefaultValue() && [] != $default = $node->getDefaultValue()) {
$comment .= ' * @default '.json_encode($default)."\n";
}
}
if ($node->isDeprecated()) {
$comment .= ' * @deprecated '.$node->getDeprecation($node->getName(), $node->getParent()->getName())['message']."\n";
}
return $comment;
}
/**
* Pick a good singular name.
*/
private function getSingularName(PrototypedArrayNode $node): string
{
$name = $node->getName();
if (!str_ends_with($name, 's')) {
return $name;
}
$parent = $node->getParent();
$mappings = $parent instanceof ArrayNode ? $parent->getXmlRemappings() : [];
foreach ($mappings as $map) {
if ($map[1] === $name) {
$name = $map[0];
break;
}
}
return $name;
}
private function buildToArray(ClassBuilder $class): void
{
$body = '$output = [];';
foreach ($class->getProperties() as $p) {
$code = '$this->PROPERTY';
if (null !== $p->getType()) {
if ($p->isArray()) {
$code = $p->areScalarsAllowed()
? 'array_map(fn ($v) => $v instanceof CLASS ? $v->toArray() : $v, $this->PROPERTY)'
: 'array_map(fn ($v) => $v->toArray(), $this->PROPERTY)'
;
} else {
$code = $p->areScalarsAllowed()
? '$this->PROPERTY instanceof CLASS ? $this->PROPERTY->toArray() : $this->PROPERTY'
: '$this->PROPERTY->toArray()'
;
}
}
$body .= strtr('
if (isset($this->_usedProperties[\'PROPERTY\'])) {
$output[\'ORG_NAME\'] = '.$code.';
}', ['PROPERTY' => $p->getName(), 'ORG_NAME' => $p->getOriginalName(), 'CLASS' => $p->getType()]);
}
$extraKeys = $class->shouldAllowExtraKeys() ? ' + $this->_extraKeys' : '';
$class->addMethod('toArray', '
public function NAME(): array
{
'.$body.'
return $output'.$extraKeys.';
}');
}
private function buildConstructor(ClassBuilder $class): void
{
$body = '';
foreach ($class->getProperties() as $p) {
$code = '$value[\'ORG_NAME\']';
if (null !== $p->getType()) {
if ($p->isArray()) {
$code = $p->areScalarsAllowed()
? 'array_map(fn ($v) => \is_array($v) ? new '.$p->getType().'($v) : $v, $value[\'ORG_NAME\'])'
: 'array_map(fn ($v) => new '.$p->getType().'($v), $value[\'ORG_NAME\'])'
;
} else {
$code = $p->areScalarsAllowed()
? '\is_array($value[\'ORG_NAME\']) ? new '.$p->getType().'($value[\'ORG_NAME\']) : $value[\'ORG_NAME\']'
: 'new '.$p->getType().'($value[\'ORG_NAME\'])'
;
}
}
$body .= strtr('
if (array_key_exists(\'ORG_NAME\', $value)) {
$this->_usedProperties[\'PROPERTY\'] = true;
$this->PROPERTY = '.$code.';
unset($value[\'ORG_NAME\']);
}
', ['PROPERTY' => $p->getName(), 'ORG_NAME' => $p->getOriginalName()]);
}
if ($class->shouldAllowExtraKeys()) {
$body .= '
$this->_extraKeys = $value;
';
} else {
$body .= '
if ([] !== $value) {
throw new InvalidConfigurationException(sprintf(\'The following keys are not supported by "%s": \', __CLASS__).implode(\', \', array_keys($value)));
}';
$class->addUse(InvalidConfigurationException::class);
}
$class->addMethod('__construct', '
public function __construct(array $value = [])
{'.$body.'
}');
}
private function buildSetExtraKey(ClassBuilder $class): void
{
if (!$class->shouldAllowExtraKeys()) {
return;
}
$class->addUse(ParamConfigurator::class);
$class->addProperty('_extraKeys');
$class->addMethod('set', '
/**
* @param ParamConfigurator|mixed $value
*
* @return $this
*/
public function NAME(string $key, mixed $value): static
{
$this->_extraKeys[$key] = $value;
return $this;
}');
}
private function getSubNamespace(ClassBuilder $rootClass): string
{
return sprintf('%s\\%s', $rootClass->getNamespace(), substr($rootClass->getName(), 0, -6));
}
private function hasNormalizationClosures(NodeInterface $node): bool
{
try {
$r = new \ReflectionProperty($node, 'normalizationClosures');
} catch (\ReflectionException) {
return false;
}
return [] !== $r->getValue($node);
}
private function getType(string $classType, bool $hasNormalizationClosures): string
{
return $classType.($hasNormalizationClosures ? '|scalar' : '');
}
}
@@ -0,0 +1,27 @@
<?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\Config\Builder;
use Symfony\Component\Config\Definition\ConfigurationInterface;
/**
* Generates ConfigBuilders to help create valid config.
*
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
*/
interface ConfigBuilderGeneratorInterface
{
/**
* @return \Closure that will return the root config class
*/
public function build(ConfigurationInterface $configuration): \Closure;
}
@@ -0,0 +1,30 @@
<?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\Config\Builder;
/**
* A ConfigBuilder provides helper methods to build a large complex array.
*
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
*/
interface ConfigBuilderInterface
{
/**
* Gets all configuration represented as an array.
*/
public function toArray(): array;
/**
* Gets the alias for the extension which config we are building.
*/
public function getExtensionAlias(): string;
}
+32
View File
@@ -0,0 +1,32 @@
<?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\Config\Builder;
/**
* Represents a method when building classes.
*
* @internal
*
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
*/
class Method
{
public function __construct(
private string $content,
) {
}
public function getContent(): string
{
return $this->content;
}
}
+84
View File
@@ -0,0 +1,84 @@
<?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\Config\Builder;
/**
* Represents a property when building classes.
*
* @internal
*
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
*/
class Property
{
private bool $array = false;
private bool $scalarsAllowed = false;
private ?string $type = null;
private ?string $content = null;
public function __construct(
private string $originalName,
private string $name,
) {
}
public function getName(): string
{
return $this->name;
}
public function getOriginalName(): string
{
return $this->originalName;
}
public function setType(string $type): void
{
$this->array = false;
$this->type = $type;
if (str_ends_with($type, '|scalar')) {
$this->scalarsAllowed = true;
$this->type = $type = substr($type, 0, -7);
}
if (str_ends_with($type, '[]')) {
$this->array = true;
$this->type = substr($type, 0, -2);
}
}
public function getType(): ?string
{
return $this->type;
}
public function getContent(): ?string
{
return $this->content;
}
public function setContent(string $content): void
{
$this->content = $content;
}
public function isArray(): bool
{
return $this->array;
}
public function areScalarsAllowed(): bool
{
return $this->scalarsAllowed;
}
}
+164
View File
@@ -0,0 +1,164 @@
CHANGELOG
=========
7.1
---
* Allow custom meta location in `ResourceCheckerConfigCache`
* Allow custom meta location in `ConfigCache`
7.0
---
* Require explicit argument when calling `NodeBuilder::setParent()`
6.3
---
* Allow enum values in `EnumNode`
6.2
---
* Deprecate calling `NodeBuilder::setParent()` without any arguments
* Add a more accurate typehint in generated PHP config
6.1
---
* Allow using environment variables in `EnumNode`
* Add Node's information in generated Config
* Add `DefinitionFileLoader` class to load a TreeBuilder definition from an external file
* Add `DefinitionConfigurator` helper
6.0
---
* Remove `BaseNode::getDeprecationMessage()`
5.3.0
-----
* Add support for generating `ConfigBuilder` for extensions
5.1.0
-----
* updated the signature of method `NodeDefinition::setDeprecated()` to `NodeDefinition::setDeprecation(string $package, string $version, string $message)`
* updated the signature of method `BaseNode::setDeprecated()` to `BaseNode::setDeprecation(string $package, string $version, string $message)`
* deprecated passing a null message to `BaseNode::setDeprecated()` to un-deprecate a node
* deprecated `BaseNode::getDeprecationMessage()`, use `BaseNode::getDeprecation()` instead
5.0.0
-----
* Dropped support for constructing a `TreeBuilder` without passing root node information.
* Removed the `root()` method in `TreeBuilder`, pass the root node information to the constructor instead
* Added method `getChildNodeDefinitions()` to ParentNodeDefinitionInterface
* Removed `FileLoaderLoadException`, use `LoaderLoadException` instead
4.4.0
-----
* added a way to exclude patterns of resources from being imported by the `import()` method
4.3.0
-----
* deprecated using environment variables with `cannotBeEmpty()` if the value is validated with `validate()`
* made `Resource\*` classes final and not implement `Serializable` anymore
* deprecated the `root()` method in `TreeBuilder`, pass the root node information to the constructor instead
4.2.0
-----
* deprecated constructing a `TreeBuilder` without passing root node information
* renamed `FileLoaderLoadException` to `LoaderLoadException`
4.1.0
-----
* added `setPathSeparator` method to `NodeBuilder` class
* added third `$pathSeparator` constructor argument to `BaseNode`
* the `Processor` class has been made final
4.0.0
-----
* removed `ConfigCachePass`
3.4.0
-----
* added `setDeprecated()` method to indicate a deprecated node
* added `XmlUtils::parse()` method to parse an XML string
* deprecated `ConfigCachePass`
3.3.0
-----
* added `ReflectionClassResource` class
* added second `$exists` constructor argument to `ClassExistenceResource`
* made `ClassExistenceResource` work with interfaces and traits
* added `ConfigCachePass` (originally in FrameworkBundle)
* added `castToArray()` helper to turn any config value into an array
3.0.0
-----
* removed `ReferenceDumper` class
* removed the `ResourceInterface::isFresh()` method
* removed `BCResourceInterfaceChecker` class
* removed `ResourceInterface::getResource()` method
2.8.0
-----
The edge case of defining just one value for nodes of type Enum is now allowed:
```php
$rootNode
->children()
->enumNode('variable')
->values(['value'])
->end()
->end()
;
```
Before: `InvalidArgumentException` (variable must contain at least two
distinct elements).
After: the code will work as expected and it will restrict the values of the
`variable` option to just `value`.
* deprecated the `ResourceInterface::isFresh()` method. If you implement custom resource types and they
can be validated that way, make them implement the new `SelfCheckingResourceInterface`.
* deprecated the getResource() method in ResourceInterface. You can still call this method
on concrete classes implementing the interface, but it does not make sense at the interface
level as you need to know about the particular type of resource at hand to understand the
semantics of the returned value.
2.7.0
-----
* added `ConfigCacheInterface`, `ConfigCacheFactoryInterface` and a basic `ConfigCacheFactory`
implementation to delegate creation of ConfigCache instances
2.2.0
-----
* added `ArrayNodeDefinition::canBeEnabled()` and `ArrayNodeDefinition::canBeDisabled()`
to ease configuration when some sections are respectively disabled / enabled
by default.
* added a `normalizeKeys()` method for array nodes (to avoid key normalization)
* added numerical type handling for config definitions
* added convenience methods for optional configuration sections to `ArrayNodeDefinition`
* added a utils class for XML manipulations
2.1.0
-----
* added a way to add documentation on configuration
* implemented `Serializable` on resources
* `LoaderResolverInterface` is now used instead of `LoaderResolver` for type
hinting
+60
View File
@@ -0,0 +1,60 @@
<?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\Config;
use Symfony\Component\Config\Resource\SelfCheckingResourceChecker;
/**
* ConfigCache caches arbitrary content in files on disk.
*
* When in debug mode, those metadata resources that implement
* \Symfony\Component\Config\Resource\SelfCheckingResourceInterface will
* be used to check cache freshness.
*
* @author Fabien Potencier <fabien@symfony.com>
* @author Matthias Pigulla <mp@webfactory.de>
*/
class ConfigCache extends ResourceCheckerConfigCache
{
/**
* @param string $file The absolute cache path
* @param bool $debug Whether debugging is enabled or not
* @param string|null $metaFile The absolute path to the meta file
*/
public function __construct(
string $file,
private bool $debug,
?string $metaFile = null,
) {
$checkers = [];
if (true === $this->debug) {
$checkers = [new SelfCheckingResourceChecker()];
}
parent::__construct($file, $checkers, $metaFile);
}
/**
* Checks if the cache is still fresh.
*
* This implementation always returns true when debug is off and the
* cache file exists.
*/
public function isFresh(): bool
{
if (!$this->debug && is_file($this->getPath())) {
return true;
}
return parent::isFresh();
}
}
+42
View File
@@ -0,0 +1,42 @@
<?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\Config;
/**
* Basic implementation of ConfigCacheFactoryInterface that
* creates an instance of the default ConfigCache.
*
* This factory and/or cache <em>do not</em> support cache validation
* by means of ResourceChecker instances (that is, service-based).
*
* @author Matthias Pigulla <mp@webfactory.de>
*/
class ConfigCacheFactory implements ConfigCacheFactoryInterface
{
/**
* @param bool $debug The debug flag to pass to ConfigCache
*/
public function __construct(
private bool $debug,
) {
}
public function cache(string $file, callable $callback): ConfigCacheInterface
{
$cache = new ConfigCache($file, $this->debug);
if (!$cache->isFresh()) {
$callback($cache);
}
return $cache;
}
}
+30
View File
@@ -0,0 +1,30 @@
<?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\Config;
/**
* Interface for a ConfigCache factory. This factory creates
* an instance of ConfigCacheInterface and initializes the
* cache if necessary.
*
* @author Matthias Pigulla <mp@webfactory.de>
*/
interface ConfigCacheFactoryInterface
{
/**
* Creates a cache instance and (re-)initializes it if necessary.
*
* @param string $file The absolute cache file path
* @param callable $callable The callable to be executed when the cache needs to be filled (i. e. is not fresh). The cache will be passed as the only parameter to this callback
*/
public function cache(string $file, callable $callable): ConfigCacheInterface;
}

Some files were not shown because too many files have changed in this diff Show More