155 changed files with 14957 additions and 0 deletions
@ -0,0 +1,57 @@ |
|||
<?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. |
|||
* |
|||
* @param string $key The key of the item to retrieve from the cache |
|||
* @param callable|CallbackInterface $callback Should return the computed value for the given key/item |
|||
* @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 mixed The value corresponding to the provided key |
|||
* |
|||
* @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); |
|||
|
|||
/** |
|||
* Removes an item from the pool. |
|||
* |
|||
* @param string $key The key to delete |
|||
* |
|||
* @throws InvalidArgumentException When $key is not valid |
|||
* |
|||
* @return bool True if the item was successfully removed, false if there was any error |
|||
*/ |
|||
public function delete(string $key): bool; |
|||
} |
|||
@ -0,0 +1,76 @@ |
|||
<?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; |
|||
|
|||
/** |
|||
* An implementation of CacheInterface for PSR-6 CacheItemPoolInterface classes. |
|||
* |
|||
* @author Nicolas Grekas <p@tchwork.com> |
|||
*/ |
|||
trait CacheTrait |
|||
{ |
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function get(string $key, callable $callback, float $beta = null, array &$metadata = null) |
|||
{ |
|||
return $this->doGet($this, $key, $callback, $beta, $metadata); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
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) |
|||
{ |
|||
if (0 > $beta = $beta ?? 1.0) { |
|||
throw new class(sprintf('Argument "$beta" provided to "%s::get()" must be a positive number, %f given.', \get_class($this), $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); |
|||
$this->logger && $this->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(); |
|||
} |
|||
} |
|||
@ -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\Contracts\Cache; |
|||
|
|||
use Psr\Cache\CacheItemInterface; |
|||
|
|||
/** |
|||
* Computes and returns the cached value of an item. |
|||
* |
|||
* @author Nicolas Grekas <p@tchwork.com> |
|||
*/ |
|||
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 mixed The computed value for the passed item |
|||
*/ |
|||
public function __invoke(CacheItemInterface $item, bool &$save); |
|||
} |
|||
@ -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\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. |
|||
*/ |
|||
const METADATA_EXPIRY = 'expiry'; |
|||
|
|||
/** |
|||
* References the time the item took to be created, in milliseconds. |
|||
*/ |
|||
const METADATA_CTIME = 'ctime'; |
|||
|
|||
/** |
|||
* References the list of tags that were assigned to the item, as string[]. |
|||
*/ |
|||
const METADATA_TAGS = 'tags'; |
|||
|
|||
/** |
|||
* 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($tags): self; |
|||
|
|||
/** |
|||
* 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; |
|||
} |
|||
@ -0,0 +1,19 @@ |
|||
Copyright (c) 2018-2019 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. |
|||
@ -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/master/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); |
|||
} |
|||
@ -0,0 +1,34 @@ |
|||
{ |
|||
"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": "^7.1.3", |
|||
"psr/cache": "^1.0" |
|||
}, |
|||
"suggest": { |
|||
"symfony/cache-implementation": "" |
|||
}, |
|||
"autoload": { |
|||
"psr-4": { "Symfony\\Contracts\\Cache\\": "" } |
|||
}, |
|||
"minimum-stability": "dev", |
|||
"extra": { |
|||
"branch-alias": { |
|||
"dev-master": "1.1-dev" |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,3 @@ |
|||
composer.lock |
|||
phpunit.xml |
|||
vendor/ |
|||
@ -0,0 +1,201 @@ |
|||
<?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 Psr\Log\NullLogger; |
|||
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 |
|||
{ |
|||
/** |
|||
* @internal |
|||
*/ |
|||
protected const NS_SEPARATOR = ':'; |
|||
|
|||
use AbstractAdapterTrait; |
|||
use ContractsTrait; |
|||
|
|||
private static $apcuSupported; |
|||
private static $phpFilesSupported; |
|||
|
|||
protected function __construct(string $namespace = '', int $defaultLifetime = 0) |
|||
{ |
|||
$this->namespace = '' === $namespace ? '' : CacheItem::validateKey($namespace).static::NS_SEPARATOR; |
|||
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)); |
|||
} |
|||
$this->createCacheItem = \Closure::bind( |
|||
static function ($key, $value, $isHit) use ($defaultLifetime) { |
|||
$item = new CacheItem(); |
|||
$item->key = $key; |
|||
$item->value = $v = $value; |
|||
$item->isHit = $isHit; |
|||
$item->defaultLifetime = $defaultLifetime; |
|||
// Detect wrapped values that encode for their expiry and creation duration |
|||
// For compactness, these values are packed in the key of an array using |
|||
// magic numbers in the form 9D-..-..-..-..-00-..-..-..-5F |
|||
if (\is_array($v) && 1 === \count($v) && 10 === \strlen($k = key($v)) && "\x9D" === $k[0] && "\0" === $k[5] && "\x5F" === $k[9]) { |
|||
$item->value = $v[$k]; |
|||
$v = unpack('Ve/Nc', substr($k, 1, -1)); |
|||
$item->metadata[CacheItem::METADATA_EXPIRY] = $v['e'] + CacheItem::METADATA_EXPIRY_OFFSET; |
|||
$item->metadata[CacheItem::METADATA_CTIME] = $v['c']; |
|||
} |
|||
|
|||
return $item; |
|||
}, |
|||
null, |
|||
CacheItem::class |
|||
); |
|||
$getId = \Closure::fromCallable([$this, 'getId']); |
|||
$this->mergeByLifetime = \Closure::bind( |
|||
static function ($deferred, $namespace, &$expiredIds) use ($getId) { |
|||
$byLifetime = []; |
|||
$now = microtime(true); |
|||
$expiredIds = []; |
|||
|
|||
foreach ($deferred as $key => $item) { |
|||
$key = (string) $key; |
|||
if (null === $item->expiry) { |
|||
$ttl = 0 < $item->defaultLifetime ? $item->defaultLifetime : 0; |
|||
} elseif (0 >= $ttl = (int) ($item->expiry - $now)) { |
|||
$expiredIds[] = $getId($key); |
|||
continue; |
|||
} |
|||
if (isset(($metadata = $item->newMetadata)[CacheItem::METADATA_TAGS])) { |
|||
unset($metadata[CacheItem::METADATA_TAGS]); |
|||
} |
|||
// For compactness, expiry and creation duration are packed in the key of an array, using magic numbers as separators |
|||
$byLifetime[$ttl][$getId($key)] = $metadata ? ["\x9D".pack('VN', (int) $metadata[CacheItem::METADATA_EXPIRY] - CacheItem::METADATA_EXPIRY_OFFSET, $metadata[CacheItem::METADATA_CTIME])."\x5F" => $item->value] : $item->value; |
|||
} |
|||
|
|||
return $byLifetime; |
|||
}, |
|||
null, |
|||
CacheItem::class |
|||
); |
|||
} |
|||
|
|||
/** |
|||
* Returns the best possible adapter that your runtime supports. |
|||
* |
|||
* Using ApcuAdapter makes system caches compatible with read-only filesystems. |
|||
* |
|||
* @param string $namespace |
|||
* @param int $defaultLifetime |
|||
* @param string $version |
|||
* @param string $directory |
|||
* |
|||
* @return AdapterInterface |
|||
*/ |
|||
public static function createSystemCache($namespace, $defaultLifetime, $version, $directory, LoggerInterface $logger = null) |
|||
{ |
|||
$opcache = new PhpFilesAdapter($namespace, $defaultLifetime, $directory, true); |
|||
if (null !== $logger) { |
|||
$opcache->setLogger($logger); |
|||
} |
|||
|
|||
if (!self::$apcuSupported = self::$apcuSupported ?? ApcuAdapter::isSupported()) { |
|||
return $opcache; |
|||
} |
|||
|
|||
$apcu = new ApcuAdapter($namespace, (int) $defaultLifetime / 5, $version); |
|||
if ('cli' === \PHP_SAPI && !filter_var(ini_get('apc.enable_cli'), FILTER_VALIDATE_BOOLEAN)) { |
|||
$apcu->setLogger(new NullLogger()); |
|||
} elseif (null !== $logger) { |
|||
$apcu->setLogger($logger); |
|||
} |
|||
|
|||
return new ChainAdapter([$apcu, $opcache]); |
|||
} |
|||
|
|||
public static function createConnection($dsn, array $options = []) |
|||
{ |
|||
if (!\is_string($dsn)) { |
|||
throw new InvalidArgumentException(sprintf('The %s() method expect argument #1 to be string, %s given.', __METHOD__, \gettype($dsn))); |
|||
} |
|||
if (0 === strpos($dsn, 'redis:') || 0 === strpos($dsn, 'rediss:')) { |
|||
return RedisAdapter::createConnection($dsn, $options); |
|||
} |
|||
if (0 === strpos($dsn, 'memcached:')) { |
|||
return MemcachedAdapter::createConnection($dsn, $options); |
|||
} |
|||
|
|||
throw new InvalidArgumentException(sprintf('Unsupported DSN: %s.', $dsn)); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function commit() |
|||
{ |
|||
$ok = true; |
|||
$byLifetime = $this->mergeByLifetime; |
|||
$byLifetime = $byLifetime($this->deferred, $this->namespace, $expiredIds); |
|||
$retry = $this->deferred = []; |
|||
|
|||
if ($expiredIds) { |
|||
$this->doDelete($expiredIds); |
|||
} |
|||
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 = \is_object($v) ? \get_class($v) : \gettype($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]); |
|||
} |
|||
} 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 = \is_object($v) ? \get_class($v) : \gettype($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]); |
|||
} |
|||
} |
|||
|
|||
return $ok; |
|||
} |
|||
} |
|||
@ -0,0 +1,305 @@ |
|||
<?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 |
|||
* @experimental in 4.3 |
|||
*/ |
|||
abstract class AbstractTagAwareAdapter implements TagAwareAdapterInterface, TagAwareCacheInterface, LoggerAwareInterface, ResettableInterface |
|||
{ |
|||
use AbstractAdapterTrait; |
|||
use ContractsTrait; |
|||
|
|||
private const TAGS_PREFIX = "\0tags\0"; |
|||
|
|||
protected function __construct(string $namespace = '', int $defaultLifetime = 0) |
|||
{ |
|||
$this->namespace = '' === $namespace ? '' : CacheItem::validateKey($namespace).':'; |
|||
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)); |
|||
} |
|||
$this->createCacheItem = \Closure::bind( |
|||
static function ($key, $value, $isHit) use ($defaultLifetime) { |
|||
$item = new CacheItem(); |
|||
$item->key = $key; |
|||
$item->defaultLifetime = $defaultLifetime; |
|||
$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] = $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 |
|||
); |
|||
$getId = \Closure::fromCallable([$this, 'getId']); |
|||
$tagPrefix = self::TAGS_PREFIX; |
|||
$this->mergeByLifetime = \Closure::bind( |
|||
static function ($deferred, &$expiredIds) use ($getId, $tagPrefix) { |
|||
$byLifetime = []; |
|||
$now = microtime(true); |
|||
$expiredIds = []; |
|||
|
|||
foreach ($deferred as $key => $item) { |
|||
$key = (string) $key; |
|||
if (null === $item->expiry) { |
|||
$ttl = 0 < $item->defaultLifetime ? $item->defaultLifetime : 0; |
|||
} elseif (0 >= $ttl = (int) ($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) $metadata[CacheItem::METADATA_EXPIRY] - CacheItem::METADATA_EXPIRY_OFFSET, $metadata[CacheItem::METADATA_CTIME]); |
|||
} |
|||
|
|||
// Extract tag changes, these should be removed from values in doSave() |
|||
$value['tag-operations'] = ['add' => [], 'remove' => []]; |
|||
$oldTags = $item->metadata[CacheItem::METADATA_TAGS] ?? []; |
|||
foreach (array_diff($value['tags'], $oldTags) as $addedTag) { |
|||
$value['tag-operations']['add'][] = $getId($tagPrefix.$addedTag); |
|||
} |
|||
foreach (array_diff($oldTags, $value['tags']) as $removedTag) { |
|||
$value['tag-operations']['remove'][] = $getId($tagPrefix.$removedTag); |
|||
} |
|||
|
|||
$byLifetime[$ttl][$getId($key)] = $value; |
|||
} |
|||
|
|||
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 |
|||
* @param array $tagData Optional array of tag identifiers => key identifiers that should be removed from the pool |
|||
* |
|||
* @return bool True if the items were successfully removed, false otherwise |
|||
*/ |
|||
abstract protected function doDelete(array $ids, 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 |
|||
* |
|||
* @return bool True on success |
|||
*/ |
|||
abstract protected function doInvalidate(array $tagIds): bool; |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function commit() |
|||
{ |
|||
$ok = true; |
|||
$byLifetime = $this->mergeByLifetime; |
|||
$byLifetime = $byLifetime($this->deferred, $expiredIds); |
|||
$retry = $this->deferred = []; |
|||
|
|||
if ($expiredIds) { |
|||
// Tags are not cleaned up in this case, however that is done on invalidateTags(). |
|||
$this->doDelete($expiredIds); |
|||
} |
|||
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 = \is_object($v) ? \get_class($v) : \gettype($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]); |
|||
} |
|||
} 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 = \is_object($v) ? \get_class($v) : \gettype($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]); |
|||
} |
|||
} |
|||
|
|||
return $ok; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
* |
|||
* Overloaded in order to deal with tags for adjusted doDelete() signature. |
|||
*/ |
|||
public function deleteItems(array $keys) |
|||
{ |
|||
if (!$keys) { |
|||
return true; |
|||
} |
|||
|
|||
$ids = []; |
|||
$tagData = []; |
|||
|
|||
foreach ($keys as $key) { |
|||
$ids[$key] = $this->getId($key); |
|||
unset($this->deferred[$key]); |
|||
} |
|||
|
|||
foreach ($this->doFetch($ids) as $id => $value) { |
|||
foreach ($value['tags'] ?? [] as $tag) { |
|||
$tagData[$this->getId(self::TAGS_PREFIX.$tag)][] = $id; |
|||
} |
|||
} |
|||
|
|||
try { |
|||
if ($this->doDelete(array_values($ids), $tagData)) { |
|||
return true; |
|||
} |
|||
} catch (\Exception $e) { |
|||
} |
|||
|
|||
$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]); |
|||
$ok = false; |
|||
} |
|||
|
|||
return $ok; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function invalidateTags(array $tags) |
|||
{ |
|||
if (empty($tags)) { |
|||
return false; |
|||
} |
|||
|
|||
$tagIds = []; |
|||
foreach (array_unique($tags) as $tag) { |
|||
$tagIds[] = $this->getId(self::TAGS_PREFIX.$tag); |
|||
} |
|||
|
|||
if ($this->doInvalidate($tagIds)) { |
|||
return true; |
|||
} |
|||
|
|||
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; |
|||
} |
|||
} |
|||
@ -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\Adapter; |
|||
|
|||
use Psr\Cache\CacheItemPoolInterface; |
|||
use Symfony\Component\Cache\CacheItem; |
|||
|
|||
/** |
|||
* Interface for adapters managing instances of Symfony's CacheItem. |
|||
* |
|||
* @author Kévin Dunglas <dunglas@gmail.com> |
|||
*/ |
|||
interface AdapterInterface extends CacheItemPoolInterface |
|||
{ |
|||
/** |
|||
* {@inheritdoc} |
|||
* |
|||
* @return CacheItem |
|||
*/ |
|||
public function getItem($key); |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
* |
|||
* @return \Traversable|CacheItem[] |
|||
*/ |
|||
public function getItems(array $keys = []); |
|||
} |
|||
@ -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\Cache\Adapter; |
|||
|
|||
use Symfony\Component\Cache\Traits\ApcuTrait; |
|||
|
|||
class ApcuAdapter extends AbstractAdapter |
|||
{ |
|||
use ApcuTrait; |
|||
|
|||
/** |
|||
* @throws CacheException if APCu is not enabled |
|||
*/ |
|||
public function __construct(string $namespace = '', int $defaultLifetime = 0, string $version = null) |
|||
{ |
|||
$this->init($namespace, $defaultLifetime, $version); |
|||
} |
|||
} |
|||
@ -0,0 +1,163 @@ |
|||
<?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 Symfony\Component\Cache\CacheItem; |
|||
use Symfony\Component\Cache\ResettableInterface; |
|||
use Symfony\Component\Cache\Traits\ArrayTrait; |
|||
use Symfony\Contracts\Cache\CacheInterface; |
|||
|
|||
/** |
|||
* @author Nicolas Grekas <p@tchwork.com> |
|||
*/ |
|||
class ArrayAdapter implements AdapterInterface, CacheInterface, LoggerAwareInterface, ResettableInterface |
|||
{ |
|||
use ArrayTrait; |
|||
|
|||
private $createCacheItem; |
|||
|
|||
/** |
|||
* @param bool $storeSerialized Disabling serialization can lead to cache corruptions when storing mutable values but increases performance otherwise |
|||
*/ |
|||
public function __construct(int $defaultLifetime = 0, bool $storeSerialized = true) |
|||
{ |
|||
$this->storeSerialized = $storeSerialized; |
|||
$this->createCacheItem = \Closure::bind( |
|||
static function ($key, $value, $isHit) use ($defaultLifetime) { |
|||
$item = new CacheItem(); |
|||
$item->key = $key; |
|||
$item->value = $value; |
|||
$item->isHit = $isHit; |
|||
$item->defaultLifetime = $defaultLifetime; |
|||
|
|||
return $item; |
|||
}, |
|||
null, |
|||
CacheItem::class |
|||
); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function get(string $key, callable $callback, float $beta = null, array &$metadata = null) |
|||
{ |
|||
$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; |
|||
$this->save($item->set($callback($item, $save))); |
|||
} |
|||
|
|||
return $item->get(); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function getItem($key) |
|||
{ |
|||
if (!$isHit = $this->hasItem($key)) { |
|||
$this->values[$key] = $value = null; |
|||
} else { |
|||
$value = $this->storeSerialized ? $this->unfreeze($key, $isHit) : $this->values[$key]; |
|||
} |
|||
$f = $this->createCacheItem; |
|||
|
|||
return $f($key, $value, $isHit); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function getItems(array $keys = []) |
|||
{ |
|||
foreach ($keys as $key) { |
|||
if (!\is_string($key) || !isset($this->expiries[$key])) { |
|||
CacheItem::validateKey($key); |
|||
} |
|||
} |
|||
|
|||
return $this->generateItems($keys, microtime(true), $this->createCacheItem); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function deleteItems(array $keys) |
|||
{ |
|||
foreach ($keys as $key) { |
|||
$this->deleteItem($key); |
|||
} |
|||
|
|||
return true; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function save(CacheItemInterface $item) |
|||
{ |
|||
if (!$item instanceof CacheItem) { |
|||
return false; |
|||
} |
|||
$item = (array) $item; |
|||
$key = $item["\0*\0key"]; |
|||
$value = $item["\0*\0value"]; |
|||
$expiry = $item["\0*\0expiry"]; |
|||
|
|||
if (null !== $expiry && $expiry <= microtime(true)) { |
|||
$this->deleteItem($key); |
|||
|
|||
return true; |
|||
} |
|||
if ($this->storeSerialized && null === $value = $this->freeze($value, $key)) { |
|||
return false; |
|||
} |
|||
if (null === $expiry && 0 < $item["\0*\0defaultLifetime"]) { |
|||
$expiry = microtime(true) + $item["\0*\0defaultLifetime"]; |
|||
} |
|||
|
|||
$this->values[$key] = $value; |
|||
$this->expiries[$key] = null !== $expiry ? $expiry : PHP_INT_MAX; |
|||
|
|||
return true; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function saveDeferred(CacheItemInterface $item) |
|||
{ |
|||
return $this->save($item); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function commit() |
|||
{ |
|||
return true; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function delete(string $key): bool |
|||
{ |
|||
return $this->deleteItem($key); |
|||
} |
|||
} |
|||
@ -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 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 $adapters = []; |
|||
private $adapterCount; |
|||
private $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, 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_class($adapter), CacheItemPoolInterface::class)); |
|||
} |
|||
|
|||
if ($adapter instanceof AdapterInterface) { |
|||
$this->adapters[] = $adapter; |
|||
} else { |
|||
$this->adapters[] = new ProxyAdapter($adapter); |
|||
} |
|||
} |
|||
$this->adapterCount = \count($this->adapters); |
|||
|
|||
$this->syncItem = \Closure::bind( |
|||
static function ($sourceItem, $item) use ($defaultLifetime) { |
|||
$item->value = $sourceItem->value; |
|||
$item->expiry = $sourceItem->expiry; |
|||
$item->isHit = $sourceItem->isHit; |
|||
$item->metadata = $sourceItem->metadata; |
|||
|
|||
$sourceItem->isTaggable = false; |
|||
unset($sourceItem->metadata[CacheItem::METADATA_TAGS]); |
|||
|
|||
if (0 < $sourceItem->defaultLifetime && $sourceItem->defaultLifetime < $defaultLifetime) { |
|||
$defaultLifetime = $sourceItem->defaultLifetime; |
|||
} |
|||
if (0 < $defaultLifetime && ($item->defaultLifetime <= 0 || $defaultLifetime < $item->defaultLifetime)) { |
|||
$item->defaultLifetime = $defaultLifetime; |
|||
} |
|||
|
|||
return $item; |
|||
}, |
|||
null, |
|||
CacheItem::class |
|||
); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function get(string $key, callable $callback, float $beta = null, array &$metadata = null) |
|||
{ |
|||
$lastItem = null; |
|||
$i = 0; |
|||
$wrap = function (CacheItem $item = null) use ($key, $callback, $beta, &$wrap, &$i, &$lastItem, &$metadata) { |
|||
$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) { |
|||
($this->syncItem)($lastItem = $lastItem ?? $item, $item); |
|||
} |
|||
|
|||
return $value; |
|||
}; |
|||
|
|||
return $wrap(); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function getItem($key) |
|||
{ |
|||
$syncItem = $this->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])); |
|||
} |
|||
|
|||
return $item; |
|||
} |
|||
|
|||
$misses[$i] = $item; |
|||
} |
|||
|
|||
return $item; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function getItems(array $keys = []) |
|||
{ |
|||
return $this->generateItems($this->adapters[0]->getItems($keys), 0); |
|||
} |
|||
|
|||
private function generateItems($items, $adapterIndex) |
|||
{ |
|||
$missing = []; |
|||
$misses = []; |
|||
$nextAdapterIndex = $adapterIndex + 1; |
|||
$nextAdapter = isset($this->adapters[$nextAdapterIndex]) ? $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 = $this->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])); |
|||
} |
|||
|
|||
yield $k => $item; |
|||
} |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function hasItem($key) |
|||
{ |
|||
foreach ($this->adapters as $adapter) { |
|||
if ($adapter->hasItem($key)) { |
|||
return true; |
|||
} |
|||
} |
|||
|
|||
return false; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function clear() |
|||
{ |
|||
$cleared = true; |
|||
$i = $this->adapterCount; |
|||
|
|||
while ($i--) { |
|||
$cleared = $this->adapters[$i]->clear() && $cleared; |
|||
} |
|||
|
|||
return $cleared; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function deleteItem($key) |
|||
{ |
|||
$deleted = true; |
|||
$i = $this->adapterCount; |
|||
|
|||
while ($i--) { |
|||
$deleted = $this->adapters[$i]->deleteItem($key) && $deleted; |
|||
} |
|||
|
|||
return $deleted; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function deleteItems(array $keys) |
|||
{ |
|||
$deleted = true; |
|||
$i = $this->adapterCount; |
|||
|
|||
while ($i--) { |
|||
$deleted = $this->adapters[$i]->deleteItems($keys) && $deleted; |
|||
} |
|||
|
|||
return $deleted; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function save(CacheItemInterface $item) |
|||
{ |
|||
$saved = true; |
|||
$i = $this->adapterCount; |
|||
|
|||
while ($i--) { |
|||
$saved = $this->adapters[$i]->save($item) && $saved; |
|||
} |
|||
|
|||
return $saved; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function saveDeferred(CacheItemInterface $item) |
|||
{ |
|||
$saved = true; |
|||
$i = $this->adapterCount; |
|||
|
|||
while ($i--) { |
|||
$saved = $this->adapters[$i]->saveDeferred($item) && $saved; |
|||
} |
|||
|
|||
return $saved; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function commit() |
|||
{ |
|||
$committed = true; |
|||
$i = $this->adapterCount; |
|||
|
|||
while ($i--) { |
|||
$committed = $this->adapters[$i]->commit() && $committed; |
|||
} |
|||
|
|||
return $committed; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function prune() |
|||
{ |
|||
$pruned = true; |
|||
|
|||
foreach ($this->adapters as $adapter) { |
|||
if ($adapter instanceof PruneableInterface) { |
|||
$pruned = $adapter->prune() && $pruned; |
|||
} |
|||
} |
|||
|
|||
return $pruned; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function reset() |
|||
{ |
|||
foreach ($this->adapters as $adapter) { |
|||
if ($adapter instanceof ResetInterface) { |
|||
$adapter->reset(); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -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\Cache\Adapter; |
|||
|
|||
use Doctrine\Common\Cache\CacheProvider; |
|||
use Symfony\Component\Cache\Traits\DoctrineTrait; |
|||
|
|||
class DoctrineAdapter extends AbstractAdapter |
|||
{ |
|||
use DoctrineTrait; |
|||
|
|||
public function __construct(CacheProvider $provider, string $namespace = '', int $defaultLifetime = 0) |
|||
{ |
|||
parent::__construct('', $defaultLifetime); |
|||
$this->provider = $provider; |
|||
$provider->setNamespace($namespace); |
|||
} |
|||
} |
|||
@ -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,149 @@ |
|||
<?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\LogicException; |
|||
use Symfony\Component\Cache\Marshaller\DefaultMarshaller; |
|||
use Symfony\Component\Cache\Marshaller\MarshallerInterface; |
|||
use Symfony\Component\Cache\PruneableInterface; |
|||
use Symfony\Component\Cache\Traits\FilesystemTrait; |
|||
use Symfony\Component\Filesystem\Filesystem; |
|||
|
|||
/** |
|||
* 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> |
|||
* |
|||
* @experimental in 4.3 |
|||
*/ |
|||
class FilesystemTagAwareAdapter extends AbstractTagAwareAdapter implements PruneableInterface |
|||
{ |
|||
use FilesystemTrait { |
|||
doSave as doSaveCache; |
|||
doDelete as doDeleteCache; |
|||
} |
|||
|
|||
/** |
|||
* Folder used for tag symlinks. |
|||
*/ |
|||
private const TAG_FOLDER = 'tags'; |
|||
|
|||
/** |
|||
* @var Filesystem|null |
|||
*/ |
|||
private $fs; |
|||
|
|||
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); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
protected function doSave(array $values, ?int $lifetime, array $addTagData = [], array $removeTagData = []): array |
|||
{ |
|||
$failed = $this->doSaveCache($values, $lifetime); |
|||
|
|||
$fs = $this->getFilesystem(); |
|||
// 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); |
|||
$fs->symlink($file, $this->getFile($id, true, $tagFolder)); |
|||
} |
|||
} |
|||
|
|||
// Unlink removed Tags |
|||
$files = []; |
|||
foreach ($removeTagData as $tagId => $ids) { |
|||
$tagFolder = $this->getTagFolder($tagId); |
|||
foreach ($ids as $id) { |
|||
if ($failed && \in_array($id, $failed, true)) { |
|||
continue; |
|||
} |
|||
|
|||
$files[] = $this->getFile($id, false, $tagFolder); |
|||
} |
|||
} |
|||
$fs->remove($files); |
|||
|
|||
return $failed; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
protected function doDelete(array $ids, array $tagData = []): bool |
|||
{ |
|||
$ok = $this->doDeleteCache($ids); |
|||
|
|||
// Remove tags |
|||
$files = []; |
|||
$fs = $this->getFilesystem(); |
|||
foreach ($tagData as $tagId => $idMap) { |
|||
$tagFolder = $this->getTagFolder($tagId); |
|||
foreach ($idMap as $id) { |
|||
$files[] = $this->getFile($id, false, $tagFolder); |
|||
} |
|||
} |
|||
$fs->remove($files); |
|||
|
|||
return $ok; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
protected function doInvalidate(array $tagIds): bool |
|||
{ |
|||
foreach ($tagIds as $tagId) { |
|||
$tagsFolder = $this->getTagFolder($tagId); |
|||
if (!file_exists($tagsFolder)) { |
|||
continue; |
|||
} |
|||
|
|||
foreach (new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($tagsFolder, \FilesystemIterator::SKIP_DOTS)) as $itemLink) { |
|||
if (!$itemLink->isLink()) { |
|||
throw new LogicException('Expected a (sym)link when iterating over tag folder, non link found: '.$itemLink); |
|||
} |
|||
|
|||
$valueFile = $itemLink->getRealPath(); |
|||
if ($valueFile && file_exists($valueFile)) { |
|||
@unlink($valueFile); |
|||
} |
|||
|
|||
@unlink((string) $itemLink); |
|||
} |
|||
} |
|||
|
|||
return true; |
|||
} |
|||
|
|||
private function getFilesystem(): Filesystem |
|||
{ |
|||
return $this->fs ?? $this->fs = new Filesystem(); |
|||
} |
|||
|
|||
private function getTagFolder(string $tagId): string |
|||
{ |
|||
return $this->getFile($tagId, false, $this->directory.self::TAG_FOLDER.\DIRECTORY_SEPARATOR).\DIRECTORY_SEPARATOR; |
|||
} |
|||
} |
|||
@ -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\Adapter; |
|||
|
|||
use Symfony\Component\Cache\Marshaller\MarshallerInterface; |
|||
use Symfony\Component\Cache\Traits\MemcachedTrait; |
|||
|
|||
class MemcachedAdapter extends AbstractAdapter |
|||
{ |
|||
use MemcachedTrait; |
|||
|
|||
protected $maxIdLength = 250; |
|||
|
|||
/** |
|||
* 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) |
|||
{ |
|||
$this->init($client, $namespace, $defaultLifetime, $marshaller); |
|||
} |
|||
} |
|||
@ -0,0 +1,140 @@ |
|||
<?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 $createCacheItem; |
|||
|
|||
public function __construct() |
|||
{ |
|||
$this->createCacheItem = \Closure::bind( |
|||
function ($key) { |
|||
$item = new CacheItem(); |
|||
$item->key = $key; |
|||
$item->isHit = false; |
|||
|
|||
return $item; |
|||
}, |
|||
$this, |
|||
CacheItem::class |
|||
); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function get(string $key, callable $callback, float $beta = null, array &$metadata = null) |
|||
{ |
|||
$save = true; |
|||
|
|||
return $callback(($this->createCacheItem)($key), $save); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function getItem($key) |
|||
{ |
|||
$f = $this->createCacheItem; |
|||
|
|||
return $f($key); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function getItems(array $keys = []) |
|||
{ |
|||
return $this->generateItems($keys); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function hasItem($key) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function clear() |
|||
{ |
|||
return true; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function deleteItem($key) |
|||
{ |
|||
return true; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function deleteItems(array $keys) |
|||
{ |
|||
return true; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function save(CacheItemInterface $item) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function saveDeferred(CacheItemInterface $item) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function commit() |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function delete(string $key): bool |
|||
{ |
|||
return $this->deleteItem($key); |
|||
} |
|||
|
|||
private function generateItems(array $keys) |
|||
{ |
|||
$f = $this->createCacheItem; |
|||
|
|||
foreach ($keys as $key) { |
|||
yield $key => $f($key); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,54 @@ |
|||
<?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\Connection; |
|||
use Symfony\Component\Cache\Exception\InvalidArgumentException; |
|||
use Symfony\Component\Cache\Marshaller\MarshallerInterface; |
|||
use Symfony\Component\Cache\PruneableInterface; |
|||
use Symfony\Component\Cache\Traits\PdoTrait; |
|||
|
|||
class PdoAdapter extends AbstractAdapter implements PruneableInterface |
|||
{ |
|||
use PdoTrait; |
|||
|
|||
protected $maxIdLength = 255; |
|||
|
|||
/** |
|||
* You can either pass an existing database connection as PDO instance or |
|||
* a Doctrine DBAL Connection or a DSN string that will be used to |
|||
* lazy-connect to the database when the cache is actually used. |
|||
* |
|||
* When a Doctrine DBAL Connection is passed, 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] |
|||
* * 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: []] |
|||
* |
|||
* @param \PDO|Connection|string $connOrDsn a \PDO or Connection instance or DSN string or null |
|||
* |
|||
* @throws InvalidArgumentException When first argument is not PDO nor Connection nor string |
|||
* @throws InvalidArgumentException When PDO error mode is not PDO::ERRMODE_EXCEPTION |
|||
* @throws InvalidArgumentException When namespace contains invalid characters |
|||
*/ |
|||
public function __construct($connOrDsn, string $namespace = '', int $defaultLifetime = 0, array $options = [], MarshallerInterface $marshaller = null) |
|||
{ |
|||
$this->init($connOrDsn, $namespace, $defaultLifetime, $options, $marshaller); |
|||
} |
|||
} |
|||
@ -0,0 +1,325 @@ |
|||
<?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\PhpArrayTrait; |
|||
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 PhpArrayTrait; |
|||
use ContractsTrait; |
|||
|
|||
private $createCacheItem; |
|||
|
|||
/** |
|||
* @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(string $file, AdapterInterface $fallbackPool) |
|||
{ |
|||
$this->file = $file; |
|||
$this->pool = $fallbackPool; |
|||
$this->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 Fallback when opcache is disabled |
|||
* |
|||
* @return CacheItemPoolInterface |
|||
*/ |
|||
public static function create($file, CacheItemPoolInterface $fallbackPool) |
|||
{ |
|||
// Shared memory is available in PHP 7.0+ with OPCache enabled |
|||
if (filter_var(ini_get('opcache.enable'), FILTER_VALIDATE_BOOLEAN)) { |
|||
if (!$fallbackPool instanceof AdapterInterface) { |
|||
$fallbackPool = new ProxyAdapter($fallbackPool); |
|||
} |
|||
|
|||
return new static($file, $fallbackPool); |
|||
} |
|||
|
|||
return $fallbackPool; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function get(string $key, callable $callback, float $beta = null, array &$metadata = null) |
|||
{ |
|||
if (null === $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 $e) { |
|||
unset($this->keys[$key]); |
|||
goto get_from_pool; |
|||
} |
|||
|
|||
return $value; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function getItem($key) |
|||
{ |
|||
if (!\is_string($key)) { |
|||
throw new InvalidArgumentException(sprintf('Cache key must be string, "%s" given.', \is_object($key) ? \get_class($key) : \gettype($key))); |
|||
} |
|||
if (null === $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 $e) { |
|||
$value = null; |
|||
$isHit = false; |
|||
} |
|||
} |
|||
|
|||
$f = $this->createCacheItem; |
|||
|
|||
return $f($key, $value, $isHit); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function getItems(array $keys = []) |
|||
{ |
|||
foreach ($keys as $key) { |
|||
if (!\is_string($key)) { |
|||
throw new InvalidArgumentException(sprintf('Cache key must be string, "%s" given.', \is_object($key) ? \get_class($key) : \gettype($key))); |
|||
} |
|||
} |
|||
if (null === $this->values) { |
|||
$this->initialize(); |
|||
} |
|||
|
|||
return $this->generateItems($keys); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function hasItem($key) |
|||
{ |
|||
if (!\is_string($key)) { |
|||
throw new InvalidArgumentException(sprintf('Cache key must be string, "%s" given.', \is_object($key) ? \get_class($key) : \gettype($key))); |
|||
} |
|||
if (null === $this->values) { |
|||
$this->initialize(); |
|||
} |
|||
|
|||
return isset($this->keys[$key]) || $this->pool->hasItem($key); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function deleteItem($key) |
|||
{ |
|||
if (!\is_string($key)) { |
|||
throw new InvalidArgumentException(sprintf('Cache key must be string, "%s" given.', \is_object($key) ? \get_class($key) : \gettype($key))); |
|||
} |
|||
if (null === $this->values) { |
|||
$this->initialize(); |
|||
} |
|||
|
|||
return !isset($this->keys[$key]) && $this->pool->deleteItem($key); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function deleteItems(array $keys) |
|||
{ |
|||
$deleted = true; |
|||
$fallbackKeys = []; |
|||
|
|||
foreach ($keys as $key) { |
|||
if (!\is_string($key)) { |
|||
throw new InvalidArgumentException(sprintf('Cache key must be string, "%s" given.', \is_object($key) ? \get_class($key) : \gettype($key))); |
|||
} |
|||
|
|||
if (isset($this->keys[$key])) { |
|||
$deleted = false; |
|||
} else { |
|||
$fallbackKeys[] = $key; |
|||
} |
|||
} |
|||
if (null === $this->values) { |
|||
$this->initialize(); |
|||
} |
|||
|
|||
if ($fallbackKeys) { |
|||
$deleted = $this->pool->deleteItems($fallbackKeys) && $deleted; |
|||
} |
|||
|
|||
return $deleted; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function save(CacheItemInterface $item) |
|||
{ |
|||
if (null === $this->values) { |
|||
$this->initialize(); |
|||
} |
|||
|
|||
return !isset($this->keys[$item->getKey()]) && $this->pool->save($item); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function saveDeferred(CacheItemInterface $item) |
|||
{ |
|||
if (null === $this->values) { |
|||
$this->initialize(); |
|||
} |
|||
|
|||
return !isset($this->keys[$item->getKey()]) && $this->pool->saveDeferred($item); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function commit() |
|||
{ |
|||
return $this->pool->commit(); |
|||
} |
|||
|
|||
private function generateItems(array $keys): \Generator |
|||
{ |
|||
$f = $this->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 $e) { |
|||
yield $key => $f($key, null, false); |
|||
} |
|||
} else { |
|||
yield $key => $f($key, $value, true); |
|||
} |
|||
} else { |
|||
$fallbackKeys[] = $key; |
|||
} |
|||
} |
|||
|
|||
if ($fallbackKeys) { |
|||
yield from $this->pool->getItems($fallbackKeys); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* @throws \ReflectionException When $class is not found and is required |
|||
* |
|||
* @internal to be removed in Symfony 5.0 |
|||
*/ |
|||
public static function throwOnRequiredClass($class) |
|||
{ |
|||
$e = new \ReflectionException("Class $class does not exist"); |
|||
$trace = $e->getTrace(); |
|||
$autoloadFrame = [ |
|||
'function' => 'spl_autoload_call', |
|||
'args' => [$class], |
|||
]; |
|||
$i = 1 + array_search($autoloadFrame, $trace, true); |
|||
|
|||
if (isset($trace[$i]['function']) && !isset($trace[$i]['class'])) { |
|||
switch ($trace[$i]['function']) { |
|||
case 'get_class_methods': |
|||
case 'get_class_vars': |
|||
case 'get_parent_class': |
|||
case 'is_a': |
|||
case 'is_subclass_of': |
|||
case 'class_exists': |
|||
case 'class_implements': |
|||
case 'class_parents': |
|||
case 'trait_exists': |
|||
case 'defined': |
|||
case 'interface_exists': |
|||
case 'method_exists': |
|||
case 'property_exists': |
|||
case 'is_callable': |
|||
return; |
|||
} |
|||
} |
|||
|
|||
throw $e; |
|||
} |
|||
} |
|||
@ -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\Adapter; |
|||
|
|||
use Symfony\Component\Cache\Exception\CacheException; |
|||
use Symfony\Component\Cache\PruneableInterface; |
|||
use Symfony\Component\Cache\Traits\PhpFilesTrait; |
|||
|
|||
class PhpFilesAdapter extends AbstractAdapter implements PruneableInterface |
|||
{ |
|||
use PhpFilesTrait; |
|||
|
|||
/** |
|||
* @param $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, bool $appendOnly = false) |
|||
{ |
|||
$this->appendOnly = $appendOnly; |
|||
self::$startTime = 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); |
|||
}; |
|||
} |
|||
} |
|||
@ -0,0 +1,247 @@ |
|||
<?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 ProxyTrait; |
|||
use ContractsTrait; |
|||
|
|||
private $namespace; |
|||
private $namespaceLen; |
|||
private $createCacheItem; |
|||
private $setInnerItem; |
|||
private $poolHash; |
|||
|
|||
public function __construct(CacheItemPoolInterface $pool, string $namespace = '', int $defaultLifetime = 0) |
|||
{ |
|||
$this->pool = $pool; |
|||
$this->poolHash = $poolHash = spl_object_hash($pool); |
|||
$this->namespace = '' === $namespace ? '' : CacheItem::validateKey($namespace); |
|||
$this->namespaceLen = \strlen($namespace); |
|||
$this->createCacheItem = \Closure::bind( |
|||
static function ($key, $innerItem) use ($defaultLifetime, $poolHash) { |
|||
$item = new CacheItem(); |
|||
$item->key = $key; |
|||
|
|||
if (null === $innerItem) { |
|||
return $item; |
|||
} |
|||
|
|||
$item->value = $v = $innerItem->get(); |
|||
$item->isHit = $innerItem->isHit(); |
|||
$item->innerItem = $innerItem; |
|||
$item->defaultLifetime = $defaultLifetime; |
|||
$item->poolHash = $poolHash; |
|||
|
|||
// Detect wrapped values that encode for their expiry and creation duration |
|||
// For compactness, these values are packed in the key of an array using |
|||
// magic numbers in the form 9D-..-..-..-..-00-..-..-..-5F |
|||
if (\is_array($v) && 1 === \count($v) && 10 === \strlen($k = key($v)) && "\x9D" === $k[0] && "\0" === $k[5] && "\x5F" === $k[9]) { |
|||
$item->value = $v[$k]; |
|||
$v = unpack('Ve/Nc', substr($k, 1, -1)); |
|||
$item->metadata[CacheItem::METADATA_EXPIRY] = $v['e'] + CacheItem::METADATA_EXPIRY_OFFSET; |
|||
$item->metadata[CacheItem::METADATA_CTIME] = $v['c']; |
|||
} elseif ($innerItem instanceof CacheItem) { |
|||
$item->metadata = $innerItem->metadata; |
|||
} |
|||
$innerItem->set(null); |
|||
|
|||
return $item; |
|||
}, |
|||
null, |
|||
CacheItem::class |
|||
); |
|||
$this->setInnerItem = \Closure::bind( |
|||
/** |
|||
* @param array $item A CacheItem cast to (array); accessing protected properties requires adding the "\0*\0" PHP prefix |
|||
*/ |
|||
static function (CacheItemInterface $innerItem, array $item) { |
|||
// Tags are stored separately, no need to account for them when considering this item's newly set metadata |
|||
if (isset(($metadata = $item["\0*\0newMetadata"])[CacheItem::METADATA_TAGS])) { |
|||
unset($metadata[CacheItem::METADATA_TAGS]); |
|||
} |
|||
if ($metadata) { |
|||
// For compactness, expiry and creation duration are packed in the key of an array, using magic numbers as separators |
|||
$item["\0*\0value"] = ["\x9D".pack('VN', (int) $metadata[CacheItem::METADATA_EXPIRY] - CacheItem::METADATA_EXPIRY_OFFSET, $metadata[CacheItem::METADATA_CTIME])."\x5F" => $item["\0*\0value"]]; |
|||
} |
|||
$innerItem->set($item["\0*\0value"]); |
|||
$innerItem->expiresAt(null !== $item["\0*\0expiry"] ? \DateTime::createFromFormat('U.u', sprintf('%.6f', $item["\0*\0expiry"])) : null); |
|||
}, |
|||
null, |
|||
CacheItem::class |
|||
); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function get(string $key, callable $callback, float $beta = null, array &$metadata = null) |
|||
{ |
|||
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 = ($this->createCacheItem)($key, $innerItem); |
|||
$item->set($value = $callback($item, $save)); |
|||
($this->setInnerItem)($innerItem, (array) $item); |
|||
|
|||
return $value; |
|||
}, $beta, $metadata); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function getItem($key) |
|||
{ |
|||
$f = $this->createCacheItem; |
|||
$item = $this->pool->getItem($this->getId($key)); |
|||
|
|||
return $f($key, $item); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function getItems(array $keys = []) |
|||
{ |
|||
if ($this->namespaceLen) { |
|||
foreach ($keys as $i => $key) { |
|||
$keys[$i] = $this->getId($key); |
|||
} |
|||
} |
|||
|
|||
return $this->generateItems($this->pool->getItems($keys)); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function hasItem($key) |
|||
{ |
|||
return $this->pool->hasItem($this->getId($key)); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function clear() |
|||
{ |
|||
return $this->pool->clear(); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function deleteItem($key) |
|||
{ |
|||
return $this->pool->deleteItem($this->getId($key)); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function deleteItems(array $keys) |
|||
{ |
|||
if ($this->namespaceLen) { |
|||
foreach ($keys as $i => $key) { |
|||
$keys[$i] = $this->getId($key); |
|||
} |
|||
} |
|||
|
|||
return $this->pool->deleteItems($keys); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function save(CacheItemInterface $item) |
|||
{ |
|||
return $this->doSave($item, __FUNCTION__); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function saveDeferred(CacheItemInterface $item) |
|||
{ |
|||
return $this->doSave($item, __FUNCTION__); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function commit() |
|||
{ |
|||
return $this->pool->commit(); |
|||
} |
|||
|
|||
private function doSave(CacheItemInterface $item, $method) |
|||
{ |
|||
if (!$item instanceof CacheItem) { |
|||
return false; |
|||
} |
|||
$item = (array) $item; |
|||
if (null === $item["\0*\0expiry"] && 0 < $item["\0*\0defaultLifetime"]) { |
|||
$item["\0*\0expiry"] = microtime(true) + $item["\0*\0defaultLifetime"]; |
|||
} |
|||
|
|||
if ($item["\0*\0poolHash"] === $this->poolHash && $item["\0*\0innerItem"]) { |
|||
$innerItem = $item["\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 |
|||
$f = $this->createCacheItem; |
|||
$innerItem = $f($this->namespace.$item["\0*\0key"], null); |
|||
} else { |
|||
$innerItem = $this->pool->getItem($this->namespace.$item["\0*\0key"]); |
|||
} |
|||
|
|||
($this->setInnerItem)($innerItem, $item); |
|||
|
|||
return $this->pool->$method($innerItem); |
|||
} |
|||
|
|||
private function generateItems($items) |
|||
{ |
|||
$f = $this->createCacheItem; |
|||
|
|||
foreach ($items as $key => $item) { |
|||
if ($this->namespaceLen) { |
|||
$key = substr($key, $this->namespaceLen); |
|||
} |
|||
|
|||
yield $key => $f($key, $item); |
|||
} |
|||
} |
|||
|
|||
private function getId($key) |
|||
{ |
|||
CacheItem::validateKey($key); |
|||
|
|||
return $this->namespace.$key; |
|||
} |
|||
} |
|||
@ -0,0 +1,86 @@ |
|||
<?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 |
|||
{ |
|||
/** |
|||
* @internal |
|||
*/ |
|||
protected const NS_SEPARATOR = '_'; |
|||
|
|||
use ProxyTrait; |
|||
|
|||
private $miss; |
|||
|
|||
public function __construct(CacheInterface $pool, string $namespace = '', int $defaultLifetime = 0) |
|||
{ |
|||
parent::__construct($namespace, $defaultLifetime); |
|||
|
|||
$this->pool = $pool; |
|||
$this->miss = new \stdClass(); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
protected function doFetch(array $ids) |
|||
{ |
|||
foreach ($this->pool->getMultiple($ids, $this->miss) as $key => $value) { |
|||
if ($this->miss !== $value) { |
|||
yield $key => $value; |
|||
} |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
protected function doHave($id) |
|||
{ |
|||
return $this->pool->has($id); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
protected function doClear($namespace) |
|||
{ |
|||
return $this->pool->clear(); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
protected function doDelete(array $ids) |
|||
{ |
|||
return $this->pool->deleteMultiple($ids); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
protected function doSave(array $values, $lifetime) |
|||
{ |
|||
return $this->pool->setMultiple($values, 0 === $lifetime ? null : $lifetime); |
|||
} |
|||
} |
|||
@ -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\Cache\Adapter; |
|||
|
|||
use Symfony\Component\Cache\Marshaller\MarshallerInterface; |
|||
use Symfony\Component\Cache\Traits\RedisTrait; |
|||
|
|||
class RedisAdapter extends AbstractAdapter |
|||
{ |
|||
use RedisTrait; |
|||
|
|||
/** |
|||
* @param \Redis|\RedisArray|\RedisCluster|\Predis\Client $redisClient The redis client |
|||
* @param string $namespace The default namespace |
|||
* @param int $defaultLifetime The default lifetime |
|||
*/ |
|||
public function __construct($redisClient, string $namespace = '', int $defaultLifetime = 0, MarshallerInterface $marshaller = null) |
|||
{ |
|||
$this->init($redisClient, $namespace, $defaultLifetime, $marshaller); |
|||
} |
|||
} |
|||
@ -0,0 +1,208 @@ |
|||
<?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; |
|||
use Predis\Connection\Aggregate\ClusterInterface; |
|||
use Predis\Response\Status; |
|||
use Symfony\Component\Cache\CacheItem; |
|||
use Symfony\Component\Cache\Exception\LogicException; |
|||
use Symfony\Component\Cache\Marshaller\MarshallerInterface; |
|||
use Symfony\Component\Cache\Traits\RedisTrait; |
|||
|
|||
/** |
|||
* Stores tag id <> cache id relationship as a Redis Set, lookup on invalidation using sPOP. |
|||
* |
|||
* 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). |
|||
* |
|||
* Requirements: |
|||
* - Server: Redis 3.2+ |
|||
* - Client: PHP Redis 3.1.3+ OR Predis |
|||
* - Redis Server(s) configured with any `volatile-*` eviction policy, OR `noeviction` if it will NEVER fill up memory |
|||
* |
|||
* Design limitations: |
|||
* - Max 2 billion cache keys per cache tag |
|||
* E.g. If you use a "all" items tag for expiry instead of clear(), that limits you to 2 billion cache items as well |
|||
* |
|||
* @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. |
|||
* @see https://redis.io/commands/spop Documentation for sPOP operation, capable of retriving AND emptying a Set at once. |
|||
* |
|||
* @author Nicolas Grekas <p@tchwork.com> |
|||
* @author André Rømcke <andre.romcke+symfony@gmail.com> |
|||
* |
|||
* @experimental in 4.3 |
|||
*/ |
|||
class RedisTagAwareAdapter extends AbstractTagAwareAdapter |
|||
{ |
|||
use RedisTrait; |
|||
|
|||
/** |
|||
* Redis "Set" can hold more than 4 billion members, here we limit ourselves to PHP's > 2 billion max int (32Bit). |
|||
*/ |
|||
private const POP_MAX_LIMIT = 2147483647 - 1; |
|||
|
|||
/** |
|||
* Limits for how many keys are deleted in batch. |
|||
*/ |
|||
private const BULK_DELETE_LIMIT = 10000; |
|||
|
|||
/** |
|||
* 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; |
|||
|
|||
/** |
|||
* @var bool|null |
|||
*/ |
|||
private $redisServerSupportSPOP = null; |
|||
|
|||
/** |
|||
* @param \Redis|\RedisArray|\RedisCluster|\Predis\Client $redisClient The redis client |
|||
* @param string $namespace The default namespace |
|||
* @param int $defaultLifetime The default lifetime |
|||
* |
|||
* @throws \Symfony\Component\Cache\Exception\LogicException If phpredis with version lower than 3.1.3. |
|||
*/ |
|||
public function __construct($redisClient, string $namespace = '', int $defaultLifetime = 0, MarshallerInterface $marshaller = null) |
|||
{ |
|||
$this->init($redisClient, $namespace, $defaultLifetime, $marshaller); |
|||
|
|||
// Make sure php-redis is 3.1.3 or higher configured for Redis classes |
|||
if (!$this->redis instanceof Predis\Client && version_compare(phpversion('redis'), '3.1.3', '<')) { |
|||
throw new LogicException('RedisTagAwareAdapter requires php-redis 3.1.3 or higher, alternatively use predis/predis'); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
protected function doSave(array $values, ?int $lifetime, array $addTagData = [], array $delTagData = []): array |
|||
{ |
|||
// 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) { |
|||
// 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) { |
|||
yield 'sAdd' => array_merge([$tagId], $ids); |
|||
} |
|||
|
|||
foreach ($delTagData as $tagId => $ids) { |
|||
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 || $result !== Status::get('OK'))) { |
|||
$failed[] = $id; |
|||
} |
|||
} |
|||
|
|||
return $failed; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
protected function doDelete(array $ids, array $tagData = []): bool |
|||
{ |
|||
if (!$ids) { |
|||
return true; |
|||
} |
|||
|
|||
$predisCluster = $this->redis instanceof \Predis\Client && $this->redis->getConnection() instanceof ClusterInterface; |
|||
$this->pipeline(static function () use ($ids, $tagData, $predisCluster) { |
|||
if ($predisCluster) { |
|||
foreach ($ids as $id) { |
|||
yield 'del' => [$id]; |
|||
} |
|||
} else { |
|||
yield 'del' => $ids; |
|||
} |
|||
|
|||
foreach ($tagData as $tagId => $idList) { |
|||
yield 'sRem' => array_merge([$tagId], $idList); |
|||
} |
|||
})->rewind(); |
|||
|
|||
return true; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
protected function doInvalidate(array $tagIds): bool |
|||
{ |
|||
if (!$this->redisServerSupportSPOP()) { |
|||
return false; |
|||
} |
|||
|
|||
// Pop all tag info at once to avoid race conditions |
|||
$tagIdSets = $this->pipeline(static function () use ($tagIds) { |
|||
foreach ($tagIds as $tagId) { |
|||
// Client: Predis or PHP Redis 3.1.3+ (https://github.com/phpredis/phpredis/commit/d2e203a6) |
|||
// Server: Redis 3.2 or higher (https://redis.io/commands/spop) |
|||
yield 'sPop' => [$tagId, self::POP_MAX_LIMIT]; |
|||
} |
|||
}); |
|||
|
|||
// Flatten generator result from pipeline, ignore keys (tag ids) |
|||
$ids = array_unique(array_merge(...iterator_to_array($tagIdSets, false))); |
|||
|
|||
// Delete cache in chunks to avoid overloading the connection |
|||
foreach (array_chunk($ids, self::BULK_DELETE_LIMIT) as $chunkIds) { |
|||
$this->doDelete($chunkIds); |
|||
} |
|||
|
|||
return true; |
|||
} |
|||
|
|||
private function redisServerSupportSPOP(): bool |
|||
{ |
|||
if (null !== $this->redisServerSupportSPOP) { |
|||
return $this->redisServerSupportSPOP; |
|||
} |
|||
|
|||
foreach ($this->getHosts() as $host) { |
|||
$info = $host->info('Server'); |
|||
$info = isset($info['Server']) ? $info['Server'] : $info; |
|||
if (version_compare($info['redis_version'], '3.2', '<')) { |
|||
CacheItem::log($this->logger, 'Redis server needs to be version 3.2 or higher, your Redis server was detected as '.$info['redis_version']); |
|||
|
|||
return $this->redisServerSupportSPOP = false; |
|||
} |
|||
} |
|||
|
|||
return $this->redisServerSupportSPOP = true; |
|||
} |
|||
} |
|||
@ -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\Adapter; |
|||
|
|||
@trigger_error(sprintf('The "%s" class is @deprecated since Symfony 4.3, use "Psr16Adapter" instead.', SimpleCacheAdapter::class), E_USER_DEPRECATED); |
|||
|
|||
/** |
|||
* @deprecated since Symfony 4.3, use Psr16Adapter instead. |
|||
*/ |
|||
class SimpleCacheAdapter extends Psr16Adapter |
|||
{ |
|||
} |
|||
@ -0,0 +1,379 @@ |
|||
<?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 Symfony\Component\Cache\CacheItem; |
|||
use Symfony\Component\Cache\PruneableInterface; |
|||
use Symfony\Component\Cache\ResettableInterface; |
|||
use Symfony\Component\Cache\Traits\ContractsTrait; |
|||
use Symfony\Component\Cache\Traits\ProxyTrait; |
|||
use Symfony\Contracts\Cache\TagAwareCacheInterface; |
|||
|
|||
/** |
|||
* @author Nicolas Grekas <p@tchwork.com> |
|||
*/ |
|||
class TagAwareAdapter implements TagAwareAdapterInterface, TagAwareCacheInterface, PruneableInterface, ResettableInterface |
|||
{ |
|||
const TAGS_PREFIX = "\0tags\0"; |
|||
|
|||
use ProxyTrait; |
|||
use ContractsTrait; |
|||
|
|||
private $deferred = []; |
|||
private $createCacheItem; |
|||
private $setCacheItemTags; |
|||
private $getTagsByKey; |
|||
private $invalidateTags; |
|||
private $tags; |
|||
private $knownTagVersions = []; |
|||
private $knownTagVersionsTtl; |
|||
|
|||
public function __construct(AdapterInterface $itemsPool, AdapterInterface $tagsPool = null, float $knownTagVersionsTtl = 0.15) |
|||
{ |
|||
$this->pool = $itemsPool; |
|||
$this->tags = $tagsPool ?: $itemsPool; |
|||
$this->knownTagVersionsTtl = $knownTagVersionsTtl; |
|||
$this->createCacheItem = \Closure::bind( |
|||
static function ($key, $value, CacheItem $protoItem) { |
|||
$item = new CacheItem(); |
|||
$item->key = $key; |
|||
$item->value = $value; |
|||
$item->defaultLifetime = $protoItem->defaultLifetime; |
|||
$item->expiry = $protoItem->expiry; |
|||
$item->poolHash = $protoItem->poolHash; |
|||
|
|||
return $item; |
|||
}, |
|||
null, |
|||
CacheItem::class |
|||
); |
|||
$this->setCacheItemTags = \Closure::bind( |
|||
static function (CacheItem $item, $key, array &$itemTags) { |
|||
$item->isTaggable = true; |
|||
if (!$item->isHit) { |
|||
return $item; |
|||
} |
|||
if (isset($itemTags[$key])) { |
|||
foreach ($itemTags[$key] as $tag => $version) { |
|||
$item->metadata[CacheItem::METADATA_TAGS][$tag] = $tag; |
|||
} |
|||
unset($itemTags[$key]); |
|||
} else { |
|||
$item->value = null; |
|||
$item->isHit = false; |
|||
} |
|||
|
|||
return $item; |
|||
}, |
|||
null, |
|||
CacheItem::class |
|||
); |
|||
$this->getTagsByKey = \Closure::bind( |
|||
static function ($deferred) { |
|||
$tagsByKey = []; |
|||
foreach ($deferred as $key => $item) { |
|||
$tagsByKey[$key] = $item->newMetadata[CacheItem::METADATA_TAGS] ?? []; |
|||
} |
|||
|
|||
return $tagsByKey; |
|||
}, |
|||
null, |
|||
CacheItem::class |
|||
); |
|||
$this->invalidateTags = \Closure::bind( |
|||
static function (AdapterInterface $tagsAdapter, array $tags) { |
|||
foreach ($tags as $v) { |
|||
$v->defaultLifetime = 0; |
|||
$v->expiry = null; |
|||
$tagsAdapter->saveDeferred($v); |
|||
} |
|||
|
|||
return $tagsAdapter->commit(); |
|||
}, |
|||
null, |
|||
CacheItem::class |
|||
); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function invalidateTags(array $tags) |
|||
{ |
|||
$ok = true; |
|||
$tagsByKey = []; |
|||
$invalidatedTags = []; |
|||
foreach ($tags as $tag) { |
|||
CacheItem::validateKey($tag); |
|||
$invalidatedTags[$tag] = 0; |
|||
} |
|||
|
|||
if ($this->deferred) { |
|||
$items = $this->deferred; |
|||
foreach ($items as $key => $item) { |
|||
if (!$this->pool->saveDeferred($item)) { |
|||
unset($this->deferred[$key]); |
|||
$ok = false; |
|||
} |
|||
} |
|||
|
|||
$f = $this->getTagsByKey; |
|||
$tagsByKey = $f($items); |
|||
$this->deferred = []; |
|||
} |
|||
|
|||
$tagVersions = $this->getTagVersions($tagsByKey, $invalidatedTags); |
|||
$f = $this->createCacheItem; |
|||
|
|||
foreach ($tagsByKey as $key => $tags) { |
|||
$this->pool->saveDeferred($f(static::TAGS_PREFIX.$key, array_intersect_key($tagVersions, $tags), $items[$key])); |
|||
} |
|||
$ok = $this->pool->commit() && $ok; |
|||
|
|||
if ($invalidatedTags) { |
|||
$f = $this->invalidateTags; |
|||
$ok = $f($this->tags, $invalidatedTags) && $ok; |
|||
} |
|||
|
|||
return $ok; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function hasItem($key) |
|||
{ |
|||
if ($this->deferred) { |
|||
$this->commit(); |
|||
} |
|||
if (!$this->pool->hasItem($key)) { |
|||
return false; |
|||
} |
|||
if (!$itemTags = $this->pool->getItem(static::TAGS_PREFIX.$key)->get()) { |
|||
return true; |
|||
} |
|||
|
|||
foreach ($this->getTagVersions([$itemTags]) as $tag => $version) { |
|||
if ($itemTags[$tag] !== $version && 1 !== $itemTags[$tag] - $version) { |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
return true; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function getItem($key) |
|||
{ |
|||
foreach ($this->getItems([$key]) as $item) { |
|||
return $item; |
|||
} |
|||
|
|||
return null; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function getItems(array $keys = []) |
|||
{ |
|||
if ($this->deferred) { |
|||
$this->commit(); |
|||
} |
|||
$tagKeys = []; |
|||
|
|||
foreach ($keys as $key) { |
|||
if ('' !== $key && \is_string($key)) { |
|||
$key = static::TAGS_PREFIX.$key; |
|||
$tagKeys[$key] = $key; |
|||
} |
|||
} |
|||
|
|||
try { |
|||
$items = $this->pool->getItems($tagKeys + $keys); |
|||
} catch (InvalidArgumentException $e) { |
|||
$this->pool->getItems($keys); // Should throw an exception |
|||
|
|||
throw $e; |
|||
} |
|||
|
|||
return $this->generateItems($items, $tagKeys); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function clear() |
|||
{ |
|||
$this->deferred = []; |
|||
|
|||
return $this->pool->clear(); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function deleteItem($key) |
|||
{ |
|||
return $this->deleteItems([$key]); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function deleteItems(array $keys) |
|||
{ |
|||
foreach ($keys as $key) { |
|||
if ('' !== $key && \is_string($key)) { |
|||
$keys[] = static::TAGS_PREFIX.$key; |
|||
} |
|||
} |
|||
|
|||
return $this->pool->deleteItems($keys); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function save(CacheItemInterface $item) |
|||
{ |
|||
if (!$item instanceof CacheItem) { |
|||
return false; |
|||
} |
|||
$this->deferred[$item->getKey()] = $item; |
|||
|
|||
return $this->commit(); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function saveDeferred(CacheItemInterface $item) |
|||
{ |
|||
if (!$item instanceof CacheItem) { |
|||
return false; |
|||
} |
|||
$this->deferred[$item->getKey()] = $item; |
|||
|
|||
return true; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function commit() |
|||
{ |
|||
return $this->invalidateTags([]); |
|||
} |
|||
|
|||
public function __destruct() |
|||
{ |
|||
$this->commit(); |
|||
} |
|||
|
|||
private function generateItems($items, array $tagKeys) |
|||
{ |
|||
$bufferedItems = $itemTags = []; |
|||
$f = $this->setCacheItemTags; |
|||
|
|||
foreach ($items as $key => $item) { |
|||
if (!$tagKeys) { |
|||
yield $key => $f($item, static::TAGS_PREFIX.$key, $itemTags); |
|||
continue; |
|||
} |
|||
if (!isset($tagKeys[$key])) { |
|||
$bufferedItems[$key] = $item; |
|||
continue; |
|||
} |
|||
|
|||
unset($tagKeys[$key]); |
|||
$itemTags[$key] = $item->get() ?: []; |
|||
|
|||
if (!$tagKeys) { |
|||
$tagVersions = $this->getTagVersions($itemTags); |
|||
|
|||
foreach ($itemTags as $key => $tags) { |
|||
foreach ($tags as $tag => $version) { |
|||
if ($tagVersions[$tag] !== $version && 1 !== $version - $tagVersions[$tag]) { |
|||
unset($itemTags[$key]); |
|||
continue 2; |
|||
} |
|||
} |
|||
} |
|||
$tagVersions = $tagKeys = null; |
|||
|
|||
foreach ($bufferedItems as $key => $item) { |
|||
yield $key => $f($item, static::TAGS_PREFIX.$key, $itemTags); |
|||
} |
|||
$bufferedItems = null; |
|||
} |
|||
} |
|||
} |
|||
|
|||
private function getTagVersions(array $tagsByKey, array &$invalidatedTags = []) |
|||
{ |
|||
$tagVersions = $invalidatedTags; |
|||
|
|||
foreach ($tagsByKey as $tags) { |
|||
$tagVersions += $tags; |
|||
} |
|||
|
|||
if (!$tagVersions) { |
|||
return []; |
|||
} |
|||
|
|||
if (!$fetchTagVersions = 1 !== \func_num_args()) { |
|||
foreach ($tagsByKey as $tags) { |
|||
foreach ($tags as $tag => $version) { |
|||
if ($tagVersions[$tag] > $version) { |
|||
$tagVersions[$tag] = $version; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
$now = microtime(true); |
|||
$tags = []; |
|||
foreach ($tagVersions as $tag => $version) { |
|||
$tags[$tag.static::TAGS_PREFIX] = $tag; |
|||
if ($fetchTagVersions || !isset($this->knownTagVersions[$tag])) { |
|||
$fetchTagVersions = true; |
|||
continue; |
|||
} |
|||
$version -= $this->knownTagVersions[$tag][1]; |
|||
if ((0 !== $version && 1 !== $version) || $this->knownTagVersionsTtl > $now - $this->knownTagVersions[$tag][0]) { |
|||
// reuse previously fetched tag versions up to the ttl, unless we are storing items or a potential miss arises |
|||
$fetchTagVersions = true; |
|||
} else { |
|||
$this->knownTagVersions[$tag][1] += $version; |
|||
} |
|||
} |
|||
|
|||
if (!$fetchTagVersions) { |
|||
return $tagVersions; |
|||
} |
|||
|
|||
foreach ($this->tags->getItems(array_keys($tags)) as $tag => $version) { |
|||
$tagVersions[$tag = $tags[$tag]] = $version->get() ?: 0; |
|||
if (isset($invalidatedTags[$tag])) { |
|||
$invalidatedTags[$tag] = $version->set(++$tagVersions[$tag]); |
|||
} |
|||
$this->knownTagVersions[$tag] = [$now, $tagVersions[$tag]]; |
|||
} |
|||
|
|||
return $tagVersions; |
|||
} |
|||
} |
|||
@ -0,0 +1,33 @@ |
|||
<?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 |
|||
* |
|||
* @return bool True on success |
|||
* |
|||
* @throws InvalidArgumentException When $tags is not valid |
|||
*/ |
|||
public function invalidateTags(array $tags); |
|||
} |
|||
@ -0,0 +1,281 @@ |
|||
<?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 $pool; |
|||
private $calls = []; |
|||
|
|||
public function __construct(AdapterInterface $pool) |
|||
{ |
|||
$this->pool = $pool; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function get(string $key, callable $callback, float $beta = null, array &$metadata = null) |
|||
{ |
|||
if (!$this->pool instanceof CacheInterface) { |
|||
throw new \BadMethodCallException(sprintf('Cannot call "%s::get()": this class doesn\'t implement "%s".', \get_class($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] = \is_object($value) ? \get_class($value) : \gettype($value); |
|||
} finally { |
|||
$event->end = microtime(true); |
|||
} |
|||
if ($isHit) { |
|||
++$event->hits; |
|||
} else { |
|||
++$event->misses; |
|||
} |
|||
|
|||
return $value; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function getItem($key) |
|||
{ |
|||
$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; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function hasItem($key) |
|||
{ |
|||
$event = $this->start(__FUNCTION__); |
|||
try { |
|||
return $event->result[$key] = $this->pool->hasItem($key); |
|||
} finally { |
|||
$event->end = microtime(true); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function deleteItem($key) |
|||
{ |
|||
$event = $this->start(__FUNCTION__); |
|||
try { |
|||
return $event->result[$key] = $this->pool->deleteItem($key); |
|||
} finally { |
|||
$event->end = microtime(true); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function save(CacheItemInterface $item) |
|||
{ |
|||
$event = $this->start(__FUNCTION__); |
|||
try { |
|||
return $event->result[$item->getKey()] = $this->pool->save($item); |
|||
} finally { |
|||
$event->end = microtime(true); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function saveDeferred(CacheItemInterface $item) |
|||
{ |
|||
$event = $this->start(__FUNCTION__); |
|||
try { |
|||
return $event->result[$item->getKey()] = $this->pool->saveDeferred($item); |
|||
} finally { |
|||
$event->end = microtime(true); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function getItems(array $keys = []) |
|||
{ |
|||
$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(); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function clear() |
|||
{ |
|||
$event = $this->start(__FUNCTION__); |
|||
try { |
|||
return $event->result = $this->pool->clear(); |
|||
} finally { |
|||
$event->end = microtime(true); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function deleteItems(array $keys) |
|||
{ |
|||
$event = $this->start(__FUNCTION__); |
|||
$event->result['keys'] = $keys; |
|||
try { |
|||
return $event->result['result'] = $this->pool->deleteItems($keys); |
|||
} finally { |
|||
$event->end = microtime(true); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function commit() |
|||
{ |
|||
$event = $this->start(__FUNCTION__); |
|||
try { |
|||
return $event->result = $this->pool->commit(); |
|||
} finally { |
|||
$event->end = microtime(true); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function prune() |
|||
{ |
|||
if (!$this->pool instanceof PruneableInterface) { |
|||
return false; |
|||
} |
|||
$event = $this->start(__FUNCTION__); |
|||
try { |
|||
return $event->result = $this->pool->prune(); |
|||
} finally { |
|||
$event->end = microtime(true); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function reset() |
|||
{ |
|||
if (!$this->pool instanceof ResetInterface) { |
|||
return; |
|||
} |
|||
$event = $this->start(__FUNCTION__); |
|||
try { |
|||
$this->pool->reset(); |
|||
} finally { |
|||
$event->end = microtime(true); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
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() |
|||
{ |
|||
return $this->calls; |
|||
} |
|||
|
|||
public function clearCalls() |
|||
{ |
|||
$this->calls = []; |
|||
} |
|||
|
|||
protected function start($name) |
|||
{ |
|||
$this->calls[] = $event = new TraceableAdapterEvent(); |
|||
$event->name = $name; |
|||
$event->start = microtime(true); |
|||
|
|||
return $event; |
|||
} |
|||
} |
|||
|
|||
class TraceableAdapterEvent |
|||
{ |
|||
public $name; |
|||
public $start; |
|||
public $end; |
|||
public $result; |
|||
public $hits = 0; |
|||
public $misses = 0; |
|||
} |
|||
@ -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\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); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function invalidateTags(array $tags) |
|||
{ |
|||
$event = $this->start(__FUNCTION__); |
|||
try { |
|||
return $event->result = $this->pool->invalidateTags($tags); |
|||
} finally { |
|||
$event->end = microtime(true); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,61 @@ |
|||
CHANGELOG |
|||
========= |
|||
|
|||
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 |
|||
@ -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; |
|||
|
|||
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; |
|||
|
|||
protected $key; |
|||
protected $value; |
|||
protected $isHit = false; |
|||
protected $expiry; |
|||
protected $defaultLifetime; |
|||
protected $metadata = []; |
|||
protected $newMetadata = []; |
|||
protected $innerItem; |
|||
protected $poolHash; |
|||
protected $isTaggable = false; |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function getKey() |
|||
{ |
|||
return $this->key; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function get() |
|||
{ |
|||
return $this->value; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function isHit() |
|||
{ |
|||
return $this->isHit; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
* |
|||
* @return $this |
|||
*/ |
|||
public function set($value) |
|||
{ |
|||
$this->value = $value; |
|||
|
|||
return $this; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
* |
|||
* @return $this |
|||
*/ |
|||
public function expiresAt($expiration) |
|||
{ |
|||
if (null === $expiration) { |
|||
$this->expiry = $this->defaultLifetime > 0 ? microtime(true) + $this->defaultLifetime : null; |
|||
} elseif ($expiration instanceof \DateTimeInterface) { |
|||
$this->expiry = (float) $expiration->format('U.u'); |
|||
} else { |
|||
throw new InvalidArgumentException(sprintf('Expiration date must implement DateTimeInterface or be null, "%s" given', \is_object($expiration) ? \get_class($expiration) : \gettype($expiration))); |
|||
} |
|||
|
|||
return $this; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
* |
|||
* @return $this |
|||
*/ |
|||
public function expiresAfter($time) |
|||
{ |
|||
if (null === $time) { |
|||
$this->expiry = $this->defaultLifetime > 0 ? microtime(true) + $this->defaultLifetime : null; |
|||
} elseif ($time instanceof \DateInterval) { |
|||
$this->expiry = microtime(true) + \DateTime::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', \is_object($time) ? \get_class($time) : \gettype($time))); |
|||
} |
|||
|
|||
return $this; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function tag($tags): ItemInterface |
|||
{ |
|||
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_iterable($tags)) { |
|||
$tags = [$tags]; |
|||
} |
|||
foreach ($tags as $tag) { |
|||
if (!\is_string($tag)) { |
|||
throw new InvalidArgumentException(sprintf('Cache tag must be string, "%s" given', \is_object($tag) ? \get_class($tag) : \gettype($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, '{}()/\@:')) { |
|||
throw new InvalidArgumentException(sprintf('Cache tag "%s" contains reserved characters {}()/\@:', $tag)); |
|||
} |
|||
$this->newMetadata[self::METADATA_TAGS][$tag] = $tag; |
|||
} |
|||
|
|||
return $this; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function getMetadata(): array |
|||
{ |
|||
return $this->metadata; |
|||
} |
|||
|
|||
/** |
|||
* Returns the list of tags bound to the value coming from the pool storage if any. |
|||
* |
|||
* @return array |
|||
* |
|||
* @deprecated since Symfony 4.2, use the "getMetadata()" method instead. |
|||
*/ |
|||
public function getPreviousTags() |
|||
{ |
|||
@trigger_error(sprintf('The "%s()" method is deprecated since Symfony 4.2, use the "getMetadata()" method instead.', __METHOD__), E_USER_DEPRECATED); |
|||
|
|||
return $this->metadata[self::METADATA_TAGS] ?? []; |
|||
} |
|||
|
|||
/** |
|||
* Validates a cache key according to PSR-6. |
|||
* |
|||
* @param string $key The key to validate |
|||
* |
|||
* @return string |
|||
* |
|||
* @throws InvalidArgumentException When $key is not valid |
|||
*/ |
|||
public static function validateKey($key) |
|||
{ |
|||
if (!\is_string($key)) { |
|||
throw new InvalidArgumentException(sprintf('Cache key must be string, "%s" given', \is_object($key) ? \get_class($key) : \gettype($key))); |
|||
} |
|||
if ('' === $key) { |
|||
throw new InvalidArgumentException('Cache key length must be greater than zero'); |
|||
} |
|||
if (false !== strpbrk($key, '{}()/\@:')) { |
|||
throw new InvalidArgumentException(sprintf('Cache key "%s" contains reserved characters {}()/\@:', $key)); |
|||
} |
|||
|
|||
return $key; |
|||
} |
|||
|
|||
/** |
|||
* Internal logging helper. |
|||
* |
|||
* @internal |
|||
*/ |
|||
public static function log(LoggerInterface $logger = null, $message, $context = []) |
|||
{ |
|||
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); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,190 @@ |
|||
<?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> |
|||
*/ |
|||
class CacheDataCollector extends DataCollector implements LateDataCollectorInterface |
|||
{ |
|||
/** |
|||
* @var TraceableAdapter[] |
|||
*/ |
|||
private $instances = []; |
|||
|
|||
/** |
|||
* @param string $name |
|||
*/ |
|||
public function addInstance($name, TraceableAdapter $instance) |
|||
{ |
|||
$this->instances[$name] = $instance; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function collect(Request $request, Response $response, \Exception $exception = null) |
|||
{ |
|||
$empty = ['calls' => [], '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']['statistics'] = $this->calculateStatistics(); |
|||
$this->data['total']['statistics'] = $this->calculateTotalStatistics(); |
|||
} |
|||
|
|||
public function reset() |
|||
{ |
|||
$this->data = []; |
|||
foreach ($this->instances as $instance) { |
|||
$instance->clearCalls(); |
|||
} |
|||
} |
|||
|
|||
public function lateCollect() |
|||
{ |
|||
$this->data = $this->cloneVar($this->data); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function getName() |
|||
{ |
|||
return 'cache'; |
|||
} |
|||
|
|||
/** |
|||
* Method returns amount of logged Cache reads: "get" calls. |
|||
* |
|||
* @return array |
|||
*/ |
|||
public function getStatistics() |
|||
{ |
|||
return $this->data['instances']['statistics']; |
|||
} |
|||
|
|||
/** |
|||
* Method returns the statistic totals. |
|||
* |
|||
* @return array |
|||
*/ |
|||
public function getTotals() |
|||
{ |
|||
return $this->data['total']['statistics']; |
|||
} |
|||
|
|||
/** |
|||
* Method returns all logged Cache call objects. |
|||
* |
|||
* @return mixed |
|||
*/ |
|||
public function getCalls() |
|||
{ |
|||
return $this->data['instances']['calls']; |
|||
} |
|||
|
|||
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 - $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']; |
|||
if (false === $call->result) { |
|||
++$statistics[$name]['misses']; |
|||
} else { |
|||
++$statistics[$name]['hits']; |
|||
} |
|||
} 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,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\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 |
|||
{ |
|||
private $dataCollectorCacheId; |
|||
private $cachePoolTag; |
|||
private $cachePoolRecorderInnerSuffix; |
|||
|
|||
public function __construct(string $dataCollectorCacheId = 'data_collector.cache', string $cachePoolTag = 'cache.pool', string $cachePoolRecorderInnerSuffix = '.recorder_inner') |
|||
{ |
|||
$this->dataCollectorCacheId = $dataCollectorCacheId; |
|||
$this->cachePoolTag = $cachePoolTag; |
|||
$this->cachePoolRecorderInnerSuffix = $cachePoolRecorderInnerSuffix; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function process(ContainerBuilder $container) |
|||
{ |
|||
if (!$container->hasDefinition($this->dataCollectorCacheId)) { |
|||
return; |
|||
} |
|||
|
|||
$collectorDefinition = $container->getDefinition($this->dataCollectorCacheId); |
|||
foreach ($container->findTaggedServiceIds($this->cachePoolTag) as $id => $attributes) { |
|||
$definition = $container->getDefinition($id); |
|||
if ($definition->isAbstract()) { |
|||
continue; |
|||
} |
|||
|
|||
$recorder = new Definition(is_subclass_of($definition->getClass(), TagAwareAdapterInterface::class) ? TraceableTagAwareAdapter::class : TraceableAdapter::class); |
|||
$recorder->setTags($definition->getTags()); |
|||
$recorder->setPublic($definition->isPublic()); |
|||
$recorder->setArguments([new Reference($innerId = $id.$this->cachePoolRecorderInnerSuffix)]); |
|||
|
|||
$definition->setTags([]); |
|||
$definition->setPublic(false); |
|||
|
|||
$container->setDefinition($innerId, $definition); |
|||
$container->setDefinition($id, $recorder); |
|||
|
|||
// Tell the collector to add the new instance |
|||
$collectorDefinition->addMethodCall('addInstance', [$id, new Reference($id)]); |
|||
$collectorDefinition->setPublic(false); |
|||
} |
|||
} |
|||
} |
|||
@ -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\DependencyInjection\Compiler\CompilerPassInterface; |
|||
use Symfony\Component\DependencyInjection\ContainerBuilder; |
|||
use Symfony\Component\DependencyInjection\Reference; |
|||
|
|||
/** |
|||
* @author Nicolas Grekas <p@tchwork.com> |
|||
*/ |
|||
class CachePoolClearerPass implements CompilerPassInterface |
|||
{ |
|||
private $cachePoolClearerTag; |
|||
|
|||
public function __construct(string $cachePoolClearerTag = 'cache.pool.clearer') |
|||
{ |
|||
$this->cachePoolClearerTag = $cachePoolClearerTag; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function process(ContainerBuilder $container) |
|||
{ |
|||
$container->getParameterBag()->remove('cache.prefix.seed'); |
|||
|
|||
foreach ($container->findTaggedServiceIds($this->cachePoolClearerTag) 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,177 @@ |
|||
<?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\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 |
|||
{ |
|||
private $cachePoolTag; |
|||
private $kernelResetTag; |
|||
private $cacheClearerId; |
|||
private $cachePoolClearerTag; |
|||
private $cacheSystemClearerId; |
|||
private $cacheSystemClearerTag; |
|||
|
|||
public function __construct(string $cachePoolTag = 'cache.pool', string $kernelResetTag = 'kernel.reset', string $cacheClearerId = 'cache.global_clearer', string $cachePoolClearerTag = 'cache.pool.clearer', string $cacheSystemClearerId = 'cache.system_clearer', string $cacheSystemClearerTag = 'kernel.cache_clearer') |
|||
{ |
|||
$this->cachePoolTag = $cachePoolTag; |
|||
$this->kernelResetTag = $kernelResetTag; |
|||
$this->cacheClearerId = $cacheClearerId; |
|||
$this->cachePoolClearerTag = $cachePoolClearerTag; |
|||
$this->cacheSystemClearerId = $cacheSystemClearerId; |
|||
$this->cacheSystemClearerTag = $cacheSystemClearerTag; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function process(ContainerBuilder $container) |
|||
{ |
|||
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'); |
|||
|
|||
$pools = []; |
|||
$clearers = []; |
|||
$attributes = [ |
|||
'provider', |
|||
'name', |
|||
'namespace', |
|||
'default_lifetime', |
|||
'reset', |
|||
]; |
|||
foreach ($container->findTaggedServiceIds($this->cachePoolTag) 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($this->cachePoolTag)) { |
|||
$tags[0] += $t[0]; |
|||
} |
|||
} |
|||
$name = $tags[0]['name'] ?? $id; |
|||
if (!isset($tags[0]['namespace'])) { |
|||
if (null !== $class) { |
|||
$seed .= '.'.$class; |
|||
} |
|||
|
|||
$tags[0]['namespace'] = $this->getNamespace($seed, $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'])); |
|||
} |
|||
$i = 0; |
|||
foreach ($attributes as $attr) { |
|||
if (!isset($tags[0][$attr])) { |
|||
// no-op |
|||
} elseif ('reset' === $attr) { |
|||
if ($tags[0][$attr]) { |
|||
$pool->addTag($this->kernelResetTag, ['method' => $tags[0][$attr]]); |
|||
} |
|||
} elseif ('namespace' !== $attr || ArrayAdapter::class !== $adapter->getClass()) { |
|||
$pool->replaceArgument($i++, $tags[0][$attr]); |
|||
} |
|||
unset($tags[0][$attr]); |
|||
} |
|||
if (!empty($tags[0])) { |
|||
throw new InvalidArgumentException(sprintf('Invalid "%s" tag for service "%s": accepted attributes are "clearer", "provider", "name", "namespace", "default_lifetime" and "reset", found "%s".', $this->cachePoolTag, $id, implode('", "', array_keys($tags[0])))); |
|||
} |
|||
|
|||
if (null !== $clearer) { |
|||
$clearers[$clearer][$name] = new Reference($id, $container::IGNORE_ON_UNINITIALIZED_REFERENCE); |
|||
} |
|||
|
|||
$pools[$name] = new Reference($id, $container::IGNORE_ON_UNINITIALIZED_REFERENCE); |
|||
} |
|||
|
|||
$notAliasedCacheClearerId = $this->cacheClearerId; |
|||
while ($container->hasAlias($this->cacheClearerId)) { |
|||
$this->cacheClearerId = (string) $container->getAlias($this->cacheClearerId); |
|||
} |
|||
if ($container->hasDefinition($this->cacheClearerId)) { |
|||
$clearers[$notAliasedCacheClearerId] = $pools; |
|||
} |
|||
|
|||
foreach ($clearers as $id => $pools) { |
|||
$clearer = $container->getDefinition($id); |
|||
if ($clearer instanceof ChildDefinition) { |
|||
$clearer->replaceArgument(0, $pools); |
|||
} else { |
|||
$clearer->setArgument(0, $pools); |
|||
} |
|||
$clearer->addTag($this->cachePoolClearerTag); |
|||
|
|||
if ($this->cacheSystemClearerId === $id) { |
|||
$clearer->addTag($this->cacheSystemClearerTag); |
|||
} |
|||
} |
|||
|
|||
if ($container->hasDefinition('console.command.cache_pool_list')) { |
|||
$container->getDefinition('console.command.cache_pool_list')->replaceArgument(0, array_keys($pools)); |
|||
} |
|||
} |
|||
|
|||
private function getNamespace($seed, $id) |
|||
{ |
|||
return substr(str_replace('/', '-', base64_encode(hash('sha256', $id.$seed, true))), 0, 10); |
|||
} |
|||
|
|||
/** |
|||
* @internal |
|||
*/ |
|||
public static function getServiceProvider(ContainerBuilder $container, $name) |
|||
{ |
|||
$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->setPublic(false); |
|||
$definition->setFactory([AbstractAdapter::class, 'createConnection']); |
|||
$definition->setArguments([$dsn, ['lazy' => true]]); |
|||
$container->setDefinition($name, $definition); |
|||
} |
|||
} |
|||
|
|||
return $name; |
|||
} |
|||
} |
|||
@ -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\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 |
|||
{ |
|||
private $cacheCommandServiceId; |
|||
private $cachePoolTag; |
|||
|
|||
public function __construct(string $cacheCommandServiceId = 'console.command.cache_pool_prune', string $cachePoolTag = 'cache.pool') |
|||
{ |
|||
$this->cacheCommandServiceId = $cacheCommandServiceId; |
|||
$this->cachePoolTag = $cachePoolTag; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function process(ContainerBuilder $container) |
|||
{ |
|||
if (!$container->hasDefinition($this->cacheCommandServiceId)) { |
|||
return; |
|||
} |
|||
|
|||
$services = []; |
|||
|
|||
foreach ($container->findTaggedServiceIds($this->cachePoolTag) 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($this->cacheCommandServiceId)->replaceArgument(0, new IteratorArgument($services)); |
|||
} |
|||
} |
|||
@ -0,0 +1,104 @@ |
|||
<?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 Doctrine\Common\Cache\CacheProvider; |
|||
use Psr\Cache\CacheItemPoolInterface; |
|||
use Symfony\Contracts\Service\ResetInterface; |
|||
|
|||
/** |
|||
* @author Nicolas Grekas <p@tchwork.com> |
|||
*/ |
|||
class DoctrineProvider extends CacheProvider implements PruneableInterface, ResettableInterface |
|||
{ |
|||
private $pool; |
|||
|
|||
public function __construct(CacheItemPoolInterface $pool) |
|||
{ |
|||
$this->pool = $pool; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function prune() |
|||
{ |
|||
return $this->pool instanceof PruneableInterface && $this->pool->prune(); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function reset() |
|||
{ |
|||
if ($this->pool instanceof ResetInterface) { |
|||
$this->pool->reset(); |
|||
} |
|||
$this->setNamespace($this->getNamespace()); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
protected function doFetch($id) |
|||
{ |
|||
$item = $this->pool->getItem(rawurlencode($id)); |
|||
|
|||
return $item->isHit() ? $item->get() : false; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
protected function doContains($id) |
|||
{ |
|||
return $this->pool->hasItem(rawurlencode($id)); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
protected function doSave($id, $data, $lifeTime = 0) |
|||
{ |
|||
$item = $this->pool->getItem(rawurlencode($id)); |
|||
|
|||
if (0 < $lifeTime) { |
|||
$item->expiresAfter($lifeTime); |
|||
} |
|||
|
|||
return $this->pool->save($item->set($data)); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
protected function doDelete($id) |
|||
{ |
|||
return $this->pool->deleteItem(rawurlencode($id)); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
protected function doFlush() |
|||
{ |
|||
return $this->pool->clear(); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
protected function doGetStats() |
|||
{ |
|||
return null; |
|||
} |
|||
} |
|||
@ -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 |
|||
{ |
|||
} |
|||
} |
|||
@ -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 |
|||
{ |
|||
} |
|||
} |
|||
@ -0,0 +1,19 @@ |
|||
Copyright (c) 2016-2019 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. |
|||
@ -0,0 +1,150 @@ |
|||
<?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 $openedFiles = []; |
|||
private static $lockedFiles = []; |
|||
|
|||
/** |
|||
* The number of items in this list controls the max number of concurrent processes. |
|||
*/ |
|||
private static $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.'DoctrineAdapter.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.'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.'SimpleCacheAdapter.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) |
|||
{ |
|||
$key = self::$files ? crc32($item->getKey()) % \count(self::$files) : -1; |
|||
|
|||
if ($key < 0 || (self::$lockedFiles[$key] ?? false) || !$lock = self::open($key)) { |
|||
return $callback($item, $save); |
|||
} |
|||
|
|||
while (true) { |
|||
try { |
|||
// race to get the lock in non-blocking mode |
|||
if (flock($lock, LOCK_EX | LOCK_NB)) { |
|||
$logger && $logger->info('Lock acquired, now computing item "{key}"', ['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 && $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]); |
|||
} |
|||
static $signalingException, $signalingCallback; |
|||
$signalingException = $signalingException ?? unserialize("O:9:\"Exception\":1:{s:16:\"\0Exception\0trace\";a:0:{}}"); |
|||
$signalingCallback = $signalingCallback ?? function () use ($signalingException) { throw $signalingException; }; |
|||
|
|||
try { |
|||
$value = $pool->get($item->getKey(), $signalingCallback, 0); |
|||
$logger && $logger->info('Item "{key}" retrieved after lock was released', ['key' => $item->getKey()]); |
|||
$save = false; |
|||
|
|||
return $value; |
|||
} catch (\Exception $e) { |
|||
if ($signalingException !== $e) { |
|||
throw $e; |
|||
} |
|||
$logger && $logger->info('Item "{key}" not found while lock was released, now retrying', ['key' => $item->getKey()]); |
|||
} |
|||
} |
|||
} |
|||
|
|||
private static function open(int $key) |
|||
{ |
|||
if (null !== $h = self::$openedFiles[$key] ?? null) { |
|||
return $h; |
|||
} |
|||
set_error_handler(function () {}); |
|||
try { |
|||
$h = fopen(self::$files[$key], 'r+'); |
|||
} finally { |
|||
restore_error_handler(); |
|||
} |
|||
|
|||
return self::$openedFiles[$key] = $h ?: @fopen(self::$files[$key], 'r'); |
|||
} |
|||
} |
|||
@ -0,0 +1,99 @@ |
|||
<?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 $useIgbinarySerialize = true; |
|||
|
|||
public function __construct(bool $useIgbinarySerialize = null) |
|||
{ |
|||
if (null === $useIgbinarySerialize) { |
|||
$useIgbinarySerialize = \extension_loaded('igbinary'); |
|||
} elseif ($useIgbinarySerialize && !\extension_loaded('igbinary')) { |
|||
throw new CacheException('The "igbinary" PHP extension is not loaded.'); |
|||
} |
|||
$this->useIgbinarySerialize = $useIgbinarySerialize; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
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) { |
|||
$failed[] = $id; |
|||
} |
|||
} |
|||
|
|||
return $serialized; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function unmarshall(string $value) |
|||
{ |
|||
if ('b:0;' === $value) { |
|||
return false; |
|||
} |
|||
if ('N;' === $value) { |
|||
return null; |
|||
} |
|||
static $igbinaryNull; |
|||
if ($value === ($igbinaryNull ?? $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($class) |
|||
{ |
|||
throw new \DomainException('Class not found: '.$class); |
|||
} |
|||
} |
|||
@ -0,0 +1,40 @@ |
|||
<?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. |
|||
* |
|||
* @return mixed |
|||
* |
|||
* @throws \Exception Whenever unserialization fails |
|||
*/ |
|||
public function unmarshall(string $value); |
|||
} |
|||
@ -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; |
|||
|
|||
/** |
|||
* Interface extends psr-6 and psr-16 caches to allow for pruning (deletion) of all expired cache items. |
|||
*/ |
|||
interface PruneableInterface |
|||
{ |
|||
/** |
|||
* @return bool |
|||
*/ |
|||
public function prune(); |
|||
} |
|||
@ -0,0 +1,263 @@ |
|||
<?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 const METADATA_EXPIRY_OFFSET = 1527506807; |
|||
|
|||
private $createCacheItem; |
|||
private $cacheItemPrototype; |
|||
|
|||
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->key = $allowInt && \is_int($key) ? (string) $key : CacheItem::validateKey($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); |
|||
}; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function get($key, $default = null) |
|||
{ |
|||
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; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function set($key, $value, $ttl = null) |
|||
{ |
|||
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); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function delete($key) |
|||
{ |
|||
try { |
|||
return $this->pool->deleteItem($key); |
|||
} catch (SimpleCacheException $e) { |
|||
throw $e; |
|||
} catch (Psr6CacheException $e) { |
|||
throw new InvalidArgumentException($e->getMessage(), $e->getCode(), $e); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function clear() |
|||
{ |
|||
return $this->pool->clear(); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function getMultiple($keys, $default = null) |
|||
{ |
|||
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', \is_object($keys) ? \get_class($keys) : \gettype($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) { |
|||
if (!$item->isHit()) { |
|||
$values[$key] = $default; |
|||
continue; |
|||
} |
|||
$values[$key] = $item->get(); |
|||
|
|||
if (!$metadata = $item->getMetadata()) { |
|||
continue; |
|||
} |
|||
unset($metadata[CacheItem::METADATA_TAGS]); |
|||
|
|||
if ($metadata) { |
|||
$values[$key] = ["\x9D".pack('VN', (int) $metadata[CacheItem::METADATA_EXPIRY] - self::METADATA_EXPIRY_OFFSET, $metadata[CacheItem::METADATA_CTIME])."\x5F" => $values[$key]]; |
|||
} |
|||
} |
|||
|
|||
return $values; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function setMultiple($values, $ttl = null) |
|||
{ |
|||
$valuesIsArray = \is_array($values); |
|||
if (!$valuesIsArray && !$values instanceof \Traversable) { |
|||
throw new InvalidArgumentException(sprintf('Cache values must be array or Traversable, "%s" given', \is_object($values) ? \get_class($values) : \gettype($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; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function deleteMultiple($keys) |
|||
{ |
|||
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', \is_object($keys) ? \get_class($keys) : \gettype($keys))); |
|||
} |
|||
|
|||
try { |
|||
return $this->pool->deleteItems($keys); |
|||
} catch (SimpleCacheException $e) { |
|||
throw $e; |
|||
} catch (Psr6CacheException $e) { |
|||
throw new InvalidArgumentException($e->getMessage(), $e->getCode(), $e); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function has($key) |
|||
{ |
|||
try { |
|||
return $this->pool->hasItem($key); |
|||
} catch (SimpleCacheException $e) { |
|||
throw $e; |
|||
} catch (Psr6CacheException $e) { |
|||
throw new InvalidArgumentException($e->getMessage(), $e->getCode(), $e); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,18 @@ |
|||
Symfony PSR-6 implementation for caching |
|||
======================================== |
|||
|
|||
This component provides an extended [PSR-6](http://www.php-fig.org/psr/psr-6/) |
|||
implementation for adding cache to your applications. It is designed to have a |
|||
low overhead so that caching is fastest. It ships with a few caching adapters |
|||
for the most widespread and suited to caching backends. It also provides a |
|||
`doctrine/cache` proxy adapter to cover more advanced caching needs and a proxy |
|||
adapter for greater interoperability between PSR-6 implementations. |
|||
|
|||
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) |
|||
@ -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 |
|||
{ |
|||
} |
|||
@ -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\Simple; |
|||
|
|||
use Psr\Log\LoggerAwareInterface; |
|||
use Psr\SimpleCache\CacheInterface as Psr16CacheInterface; |
|||
use Symfony\Component\Cache\Adapter\AbstractAdapter; |
|||
use Symfony\Component\Cache\CacheItem; |
|||
use Symfony\Component\Cache\Exception\InvalidArgumentException; |
|||
use Symfony\Component\Cache\ResettableInterface; |
|||
use Symfony\Component\Cache\Traits\AbstractTrait; |
|||
use Symfony\Contracts\Cache\CacheInterface; |
|||
|
|||
@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.3, use "%s" and type-hint for "%s" instead.', AbstractCache::class, AbstractAdapter::class, CacheInterface::class), E_USER_DEPRECATED); |
|||
|
|||
/** |
|||
* @deprecated since Symfony 4.3, use AbstractAdapter and type-hint for CacheInterface instead. |
|||
*/ |
|||
abstract class AbstractCache implements Psr16CacheInterface, LoggerAwareInterface, ResettableInterface |
|||
{ |
|||
/** |
|||
* @internal |
|||
*/ |
|||
protected const NS_SEPARATOR = ':'; |
|||
|
|||
use AbstractTrait { |
|||
deleteItems as private; |
|||
AbstractTrait::deleteItem as delete; |
|||
AbstractTrait::hasItem as has; |
|||
} |
|||
|
|||
private $defaultLifetime; |
|||
|
|||
protected function __construct(string $namespace = '', int $defaultLifetime = 0) |
|||
{ |
|||
$this->defaultLifetime = max(0, $defaultLifetime); |
|||
$this->namespace = '' === $namespace ? '' : CacheItem::validateKey($namespace).':'; |
|||
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)); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function get($key, $default = null) |
|||
{ |
|||
$id = $this->getId($key); |
|||
|
|||
try { |
|||
foreach ($this->doFetch([$id]) as $value) { |
|||
return $value; |
|||
} |
|||
} catch (\Exception $e) { |
|||
CacheItem::log($this->logger, 'Failed to fetch key "{key}": '.$e->getMessage(), ['key' => $key, 'exception' => $e]); |
|||
} |
|||
|
|||
return $default; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function set($key, $value, $ttl = null) |
|||
{ |
|||
CacheItem::validateKey($key); |
|||
|
|||
return $this->setMultiple([$key => $value], $ttl); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function getMultiple($keys, $default = null) |
|||
{ |
|||
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', \is_object($keys) ? \get_class($keys) : \gettype($keys))); |
|||
} |
|||
$ids = []; |
|||
|
|||
foreach ($keys as $key) { |
|||
$ids[] = $this->getId($key); |
|||
} |
|||
try { |
|||
$values = $this->doFetch($ids); |
|||
} catch (\Exception $e) { |
|||
CacheItem::log($this->logger, 'Failed to fetch values: '.$e->getMessage(), ['keys' => $keys, 'exception' => $e]); |
|||
$values = []; |
|||
} |
|||
$ids = array_combine($ids, $keys); |
|||
|
|||
return $this->generateValues($values, $ids, $default); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function setMultiple($values, $ttl = null) |
|||
{ |
|||
if (!\is_array($values) && !$values instanceof \Traversable) { |
|||
throw new InvalidArgumentException(sprintf('Cache values must be array or Traversable, "%s" given', \is_object($values) ? \get_class($values) : \gettype($values))); |
|||
} |
|||
$valuesById = []; |
|||
|
|||
foreach ($values as $key => $value) { |
|||
if (\is_int($key)) { |
|||
$key = (string) $key; |
|||
} |
|||
$valuesById[$this->getId($key)] = $value; |
|||
} |
|||
if (false === $ttl = $this->normalizeTtl($ttl)) { |
|||
return $this->doDelete(array_keys($valuesById)); |
|||
} |
|||
|
|||
try { |
|||
$e = $this->doSave($valuesById, $ttl); |
|||
} catch (\Exception $e) { |
|||
} |
|||
if (true === $e || [] === $e) { |
|||
return true; |
|||
} |
|||
$keys = []; |
|||
foreach (\is_array($e) ? $e : array_keys($valuesById) as $id) { |
|||
$keys[] = substr($id, \strlen($this->namespace)); |
|||
} |
|||
$message = 'Failed to save values'.($e instanceof \Exception ? ': '.$e->getMessage() : '.'); |
|||
CacheItem::log($this->logger, $message, ['keys' => $keys, 'exception' => $e instanceof \Exception ? $e : null]); |
|||
|
|||
return false; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function deleteMultiple($keys) |
|||
{ |
|||
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', \is_object($keys) ? \get_class($keys) : \gettype($keys))); |
|||
} |
|||
|
|||
return $this->deleteItems($keys); |
|||
} |
|||
|
|||
private function normalizeTtl($ttl) |
|||
{ |
|||
if (null === $ttl) { |
|||
return $this->defaultLifetime; |
|||
} |
|||
if ($ttl instanceof \DateInterval) { |
|||
$ttl = (int) \DateTime::createFromFormat('U', 0)->add($ttl)->format('U'); |
|||
} |
|||
if (\is_int($ttl)) { |
|||
return 0 < $ttl ? $ttl : false; |
|||
} |
|||
|
|||
throw new InvalidArgumentException(sprintf('Expiration date must be an integer, a DateInterval or null, "%s" given', \is_object($ttl) ? \get_class($ttl) : \gettype($ttl))); |
|||
} |
|||
|
|||
private function generateValues($values, &$keys, $default) |
|||
{ |
|||
try { |
|||
foreach ($values as $id => $value) { |
|||
if (!isset($keys[$id])) { |
|||
$id = key($keys); |
|||
} |
|||
$key = $keys[$id]; |
|||
unset($keys[$id]); |
|||
yield $key => $value; |
|||
} |
|||
} catch (\Exception $e) { |
|||
CacheItem::log($this->logger, 'Failed to fetch values: '.$e->getMessage(), ['keys' => array_values($keys), 'exception' => $e]); |
|||
} |
|||
|
|||
foreach ($keys as $key) { |
|||
yield $key => $default; |
|||
} |
|||
} |
|||
} |
|||
@ -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\Simple; |
|||
|
|||
use Symfony\Component\Cache\Traits\ApcuTrait; |
|||
|
|||
@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.3, use "%s" and type-hint for "%s" instead.', ApcuCache::class, ApcuAdapter::class, CacheInterface::class), E_USER_DEPRECATED); |
|||
|
|||
/** |
|||
* @deprecated since Symfony 4.3, use ApcuAdapter and type-hint for CacheInterface instead. |
|||
*/ |
|||
class ApcuCache extends AbstractCache |
|||
{ |
|||
use ApcuTrait; |
|||
|
|||
public function __construct(string $namespace = '', int $defaultLifetime = 0, string $version = null) |
|||
{ |
|||
$this->init($namespace, $defaultLifetime, $version); |
|||
} |
|||
} |
|||
@ -0,0 +1,159 @@ |
|||
<?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\Simple; |
|||
|
|||
use Psr\Log\LoggerAwareInterface; |
|||
use Psr\SimpleCache\CacheInterface as Psr16CacheInterface; |
|||
use Symfony\Component\Cache\Adapter\ArrayAdapter; |
|||
use Symfony\Component\Cache\CacheItem; |
|||
use Symfony\Component\Cache\Exception\InvalidArgumentException; |
|||
use Symfony\Component\Cache\ResettableInterface; |
|||
use Symfony\Component\Cache\Traits\ArrayTrait; |
|||
use Symfony\Contracts\Cache\CacheInterface; |
|||
|
|||
@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.3, use "%s" and type-hint for "%s" instead.', ArrayCache::class, ArrayAdapter::class, CacheInterface::class), E_USER_DEPRECATED); |
|||
|
|||
/** |
|||
* @deprecated since Symfony 4.3, use ArrayAdapter and type-hint for CacheInterface instead. |
|||
*/ |
|||
class ArrayCache implements Psr16CacheInterface, LoggerAwareInterface, ResettableInterface |
|||
{ |
|||
use ArrayTrait { |
|||
ArrayTrait::deleteItem as delete; |
|||
ArrayTrait::hasItem as has; |
|||
} |
|||
|
|||
private $defaultLifetime; |
|||
|
|||
/** |
|||
* @param bool $storeSerialized Disabling serialization can lead to cache corruptions when storing mutable values but increases performance otherwise |
|||
*/ |
|||
public function __construct(int $defaultLifetime = 0, bool $storeSerialized = true) |
|||
{ |
|||
$this->defaultLifetime = $defaultLifetime; |
|||
$this->storeSerialized = $storeSerialized; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function get($key, $default = null) |
|||
{ |
|||
if (!\is_string($key) || !isset($this->expiries[$key])) { |
|||
CacheItem::validateKey($key); |
|||
} |
|||
if (!$isHit = isset($this->expiries[$key]) && ($this->expiries[$key] > microtime(true) || !$this->delete($key))) { |
|||
$this->values[$key] = null; |
|||
|
|||
return $default; |
|||
} |
|||
if (!$this->storeSerialized) { |
|||
return $this->values[$key]; |
|||
} |
|||
$value = $this->unfreeze($key, $isHit); |
|||
|
|||
return $isHit ? $value : $default; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function getMultiple($keys, $default = null) |
|||
{ |
|||
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', \is_object($keys) ? \get_class($keys) : \gettype($keys))); |
|||
} |
|||
foreach ($keys as $key) { |
|||
if (!\is_string($key) || !isset($this->expiries[$key])) { |
|||
CacheItem::validateKey($key); |
|||
} |
|||
} |
|||
|
|||
return $this->generateItems($keys, microtime(true), function ($k, $v, $hit) use ($default) { return $hit ? $v : $default; }); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function deleteMultiple($keys) |
|||
{ |
|||
if (!\is_array($keys) && !$keys instanceof \Traversable) { |
|||
throw new InvalidArgumentException(sprintf('Cache keys must be array or Traversable, "%s" given', \is_object($keys) ? \get_class($keys) : \gettype($keys))); |
|||
} |
|||
foreach ($keys as $key) { |
|||
$this->delete($key); |
|||
} |
|||
|
|||
return true; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function set($key, $value, $ttl = null) |
|||
{ |
|||
if (!\is_string($key)) { |
|||
CacheItem::validateKey($key); |
|||
} |
|||
|
|||
return $this->setMultiple([$key => $value], $ttl); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function setMultiple($values, $ttl = null) |
|||
{ |
|||
if (!\is_array($values) && !$values instanceof \Traversable) { |
|||
throw new InvalidArgumentException(sprintf('Cache values must be array or Traversable, "%s" given', \is_object($values) ? \get_class($values) : \gettype($values))); |
|||
} |
|||
$valuesArray = []; |
|||
|
|||
foreach ($values as $key => $value) { |
|||
if (!\is_int($key) && !(\is_string($key) && isset($this->expiries[$key]))) { |
|||
CacheItem::validateKey($key); |
|||
} |
|||
$valuesArray[$key] = $value; |
|||
} |
|||
if (false === $ttl = $this->normalizeTtl($ttl)) { |
|||
return $this->deleteMultiple(array_keys($valuesArray)); |
|||
} |
|||
$expiry = 0 < $ttl ? microtime(true) + $ttl : PHP_INT_MAX; |
|||
|
|||
foreach ($valuesArray as $key => $value) { |
|||
if ($this->storeSerialized && null === $value = $this->freeze($value, $key)) { |
|||
return false; |
|||
} |
|||
$this->values[$key] = $value; |
|||
$this->expiries[$key] = $expiry; |
|||
} |
|||
|
|||
return true; |
|||
} |
|||
|
|||
private function normalizeTtl($ttl) |
|||
{ |
|||
if (null === $ttl) { |
|||
return $this->defaultLifetime; |
|||
} |
|||
if ($ttl instanceof \DateInterval) { |
|||
$ttl = (int) \DateTime::createFromFormat('U', 0)->add($ttl)->format('U'); |
|||
} |
|||
if (\is_int($ttl)) { |
|||
return 0 < $ttl ? $ttl : false; |
|||
} |
|||
|
|||
throw new InvalidArgumentException(sprintf('Expiration date must be an integer, a DateInterval or null, "%s" given', \is_object($ttl) ? \get_class($ttl) : \gettype($ttl))); |
|||
} |
|||
} |
|||
@ -0,0 +1,257 @@ |
|||
<?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\Simple; |
|||
|
|||
use Psr\SimpleCache\CacheInterface as Psr16CacheInterface; |
|||
use Symfony\Component\Cache\Adapter\ChainAdapter; |
|||
use Symfony\Component\Cache\Exception\InvalidArgumentException; |
|||
use Symfony\Component\Cache\PruneableInterface; |
|||
use Symfony\Component\Cache\ResettableInterface; |
|||
use Symfony\Contracts\Cache\CacheInterface; |
|||
use Symfony\Contracts\Service\ResetInterface; |
|||
|
|||
@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.3, use "%s" and type-hint for "%s" instead.', ChainCache::class, ChainAdapter::class, CacheInterface::class), E_USER_DEPRECATED); |
|||
|
|||
/** |
|||
* Chains several caches together. |
|||
* |
|||
* Cached items are fetched from the first cache having them in its data store. |
|||
* They are saved and deleted in all caches at once. |
|||
* |
|||
* @deprecated since Symfony 4.3, use ChainAdapter and type-hint for CacheInterface instead. |
|||
*/ |
|||
class ChainCache implements Psr16CacheInterface, PruneableInterface, ResettableInterface |
|||
{ |
|||
private $miss; |
|||
private $caches = []; |
|||
private $defaultLifetime; |
|||
private $cacheCount; |
|||
|
|||
/** |
|||
* @param Psr16CacheInterface[] $caches The ordered list of caches used to fetch cached items |
|||
* @param int $defaultLifetime The lifetime of items propagated from lower caches to upper ones |
|||
*/ |
|||
public function __construct(array $caches, int $defaultLifetime = 0) |
|||
{ |
|||
if (!$caches) { |
|||
throw new InvalidArgumentException('At least one cache must be specified.'); |
|||
} |
|||
|
|||
foreach ($caches as $cache) { |
|||
if (!$cache instanceof Psr16CacheInterface) { |
|||
throw new InvalidArgumentException(sprintf('The class "%s" does not implement the "%s" interface.', \get_class($cache), Psr16CacheInterface::class)); |
|||
} |
|||
} |
|||
|
|||
$this->miss = new \stdClass(); |
|||
$this->caches = array_values($caches); |
|||
$this->cacheCount = \count($this->caches); |
|||
$this->defaultLifetime = 0 < $defaultLifetime ? $defaultLifetime : null; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function get($key, $default = null) |
|||
{ |
|||
$miss = null !== $default && \is_object($default) ? $default : $this->miss; |
|||
|
|||
foreach ($this->caches as $i => $cache) { |
|||
$value = $cache->get($key, $miss); |
|||
|
|||
if ($miss !== $value) { |
|||
while (0 <= --$i) { |
|||
$this->caches[$i]->set($key, $value, $this->defaultLifetime); |
|||
} |
|||
|
|||
return $value; |
|||
} |
|||
} |
|||
|
|||
return $default; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function getMultiple($keys, $default = null) |
|||
{ |
|||
$miss = null !== $default && \is_object($default) ? $default : $this->miss; |
|||
|
|||
return $this->generateItems($this->caches[0]->getMultiple($keys, $miss), 0, $miss, $default); |
|||
} |
|||
|
|||
private function generateItems($values, $cacheIndex, $miss, $default) |
|||
{ |
|||
$missing = []; |
|||
$nextCacheIndex = $cacheIndex + 1; |
|||
$nextCache = isset($this->caches[$nextCacheIndex]) ? $this->caches[$nextCacheIndex] : null; |
|||
|
|||
foreach ($values as $k => $value) { |
|||
if ($miss !== $value) { |
|||
yield $k => $value; |
|||
} elseif (!$nextCache) { |
|||
yield $k => $default; |
|||
} else { |
|||
$missing[] = $k; |
|||
} |
|||
} |
|||
|
|||
if ($missing) { |
|||
$cache = $this->caches[$cacheIndex]; |
|||
$values = $this->generateItems($nextCache->getMultiple($missing, $miss), $nextCacheIndex, $miss, $default); |
|||
|
|||
foreach ($values as $k => $value) { |
|||
if ($miss !== $value) { |
|||
$cache->set($k, $value, $this->defaultLifetime); |
|||
yield $k => $value; |
|||
} else { |
|||
yield $k => $default; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function has($key) |
|||
{ |
|||
foreach ($this->caches as $cache) { |
|||
if ($cache->has($key)) { |
|||
return true; |
|||
} |
|||
} |
|||
|
|||
return false; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function clear() |
|||
{ |
|||
$cleared = true; |
|||
$i = $this->cacheCount; |
|||
|
|||
while ($i--) { |
|||
$cleared = $this->caches[$i]->clear() && $cleared; |
|||
} |
|||
|
|||
return $cleared; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function delete($key) |
|||
{ |
|||
$deleted = true; |
|||
$i = $this->cacheCount; |
|||
|
|||
while ($i--) { |
|||
$deleted = $this->caches[$i]->delete($key) && $deleted; |
|||
} |
|||
|
|||
return $deleted; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function deleteMultiple($keys) |
|||
{ |
|||
if ($keys instanceof \Traversable) { |
|||
$keys = iterator_to_array($keys, false); |
|||
} |
|||
$deleted = true; |
|||
$i = $this->cacheCount; |
|||
|
|||
while ($i--) { |
|||
$deleted = $this->caches[$i]->deleteMultiple($keys) && $deleted; |
|||
} |
|||
|
|||
return $deleted; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function set($key, $value, $ttl = null) |
|||
{ |
|||
$saved = true; |
|||
$i = $this->cacheCount; |
|||
|
|||
while ($i--) { |
|||
$saved = $this->caches[$i]->set($key, $value, $ttl) && $saved; |
|||
} |
|||
|
|||
return $saved; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function setMultiple($values, $ttl = null) |
|||
{ |
|||
if ($values instanceof \Traversable) { |
|||
$valuesIterator = $values; |
|||
$values = function () use ($valuesIterator, &$values) { |
|||
$generatedValues = []; |
|||
|
|||
foreach ($valuesIterator as $key => $value) { |
|||
yield $key => $value; |
|||
$generatedValues[$key] = $value; |
|||
} |
|||
|
|||
$values = $generatedValues; |
|||
}; |
|||
$values = $values(); |
|||
} |
|||
$saved = true; |
|||
$i = $this->cacheCount; |
|||
|
|||
while ($i--) { |
|||
$saved = $this->caches[$i]->setMultiple($values, $ttl) && $saved; |
|||
} |
|||
|
|||
return $saved; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function prune() |
|||
{ |
|||
$pruned = true; |
|||
|
|||
foreach ($this->caches as $cache) { |
|||
if ($cache instanceof PruneableInterface) { |
|||
$pruned = $cache->prune() && $pruned; |
|||
} |
|||
} |
|||
|
|||
return $pruned; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function reset() |
|||
{ |
|||
foreach ($this->caches as $cache) { |
|||
if ($cache instanceof ResetInterface) { |
|||
$cache->reset(); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,34 @@ |
|||
<?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\Simple; |
|||
|
|||
use Doctrine\Common\Cache\CacheProvider; |
|||
use Symfony\Component\Cache\Adapter\DoctrineAdapter; |
|||
use Symfony\Component\Cache\Traits\DoctrineTrait; |
|||
use Symfony\Contracts\Cache\CacheInterface; |
|||
|
|||
@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.3, use "%s" and type-hint for "%s" instead.', DoctrineCache::class, DoctrineAdapter::class, CacheInterface::class), E_USER_DEPRECATED); |
|||
|
|||
/** |
|||
* @deprecated since Symfony 4.3, use DoctrineAdapter and type-hint for CacheInterface instead. |
|||
*/ |
|||
class DoctrineCache extends AbstractCache |
|||
{ |
|||
use DoctrineTrait; |
|||
|
|||
public function __construct(CacheProvider $provider, string $namespace = '', int $defaultLifetime = 0) |
|||
{ |
|||
parent::__construct('', $defaultLifetime); |
|||
$this->provider = $provider; |
|||
$provider->setNamespace($namespace); |
|||
} |
|||
} |
|||
@ -0,0 +1,36 @@ |
|||
<?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\Simple; |
|||
|
|||
use Symfony\Component\Cache\Adapter\FilesystemAdapter; |
|||
use Symfony\Component\Cache\Marshaller\DefaultMarshaller; |
|||
use Symfony\Component\Cache\Marshaller\MarshallerInterface; |
|||
use Symfony\Component\Cache\PruneableInterface; |
|||
use Symfony\Component\Cache\Traits\FilesystemTrait; |
|||
use Symfony\Contracts\Cache\CacheInterface; |
|||
|
|||
@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.3, use "%s" and type-hint for "%s" instead.', FilesystemCache::class, FilesystemAdapter::class, CacheInterface::class), E_USER_DEPRECATED); |
|||
|
|||
/** |
|||
* @deprecated since Symfony 4.3, use FilesystemAdapter and type-hint for CacheInterface instead. |
|||
*/ |
|||
class FilesystemCache extends AbstractCache 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,34 @@ |
|||
<?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\Simple; |
|||
|
|||
use Symfony\Component\Cache\Adapter\MemcachedAdapter; |
|||
use Symfony\Component\Cache\Marshaller\MarshallerInterface; |
|||
use Symfony\Component\Cache\Traits\MemcachedTrait; |
|||
use Symfony\Contracts\Cache\CacheInterface; |
|||
|
|||
@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.3, use "%s" and type-hint for "%s" instead.', MemcachedCache::class, MemcachedAdapter::class, CacheInterface::class), E_USER_DEPRECATED); |
|||
|
|||
/** |
|||
* @deprecated since Symfony 4.3, use MemcachedAdapter and type-hint for CacheInterface instead. |
|||
*/ |
|||
class MemcachedCache extends AbstractCache |
|||
{ |
|||
use MemcachedTrait; |
|||
|
|||
protected $maxIdLength = 250; |
|||
|
|||
public function __construct(\Memcached $client, string $namespace = '', int $defaultLifetime = 0, MarshallerInterface $marshaller = null) |
|||
{ |
|||
$this->init($client, $namespace, $defaultLifetime, $marshaller); |
|||
} |
|||
} |
|||
@ -0,0 +1,90 @@ |
|||
<?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\Simple; |
|||
|
|||
use Psr\SimpleCache\CacheInterface as Psr16CacheInterface; |
|||
use Symfony\Components\Cache\Adapter\NullAdapter; |
|||
use Symfony\Contracts\Cache\CacheInterface; |
|||
|
|||
@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.3, use "%s" and type-hint for "%s" instead.', NullCache::class, NullAdapter::class, CacheInterface::class), E_USER_DEPRECATED); |
|||
|
|||
/** |
|||
* @deprecated since Symfony 4.3, use NullAdapter and type-hint for CacheInterface instead. |
|||
*/ |
|||
class NullCache implements Psr16CacheInterface |
|||
{ |
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function get($key, $default = null) |
|||
{ |
|||
return $default; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function getMultiple($keys, $default = null) |
|||
{ |
|||
foreach ($keys as $key) { |
|||
yield $key => $default; |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function has($key) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function clear() |
|||
{ |
|||
return true; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function delete($key) |
|||
{ |
|||
return true; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function deleteMultiple($keys) |
|||
{ |
|||
return true; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function set($key, $value, $ttl = null) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function setMultiple($values, $ttl = null) |
|||
{ |
|||
return false; |
|||
} |
|||
} |
|||
@ -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\Component\Cache\Simple; |
|||
|
|||
use Symfony\Component\Cache\Adapter\PdoAdapter; |
|||
use Symfony\Component\Cache\Marshaller\MarshallerInterface; |
|||
use Symfony\Component\Cache\PruneableInterface; |
|||
use Symfony\Component\Cache\Traits\PdoTrait; |
|||
use Symfony\Contracts\Cache\CacheInterface; |
|||
|
|||
@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.3, use "%s" and type-hint for "%s" instead.', PdoCache::class, PdoAdapter::class, CacheInterface::class), E_USER_DEPRECATED); |
|||
|
|||
/** |
|||
* @deprecated since Symfony 4.3, use PdoAdapter and type-hint for CacheInterface instead. |
|||
*/ |
|||
class PdoCache extends AbstractCache implements PruneableInterface |
|||
{ |
|||
use PdoTrait; |
|||
|
|||
protected $maxIdLength = 255; |
|||
|
|||
/** |
|||
* You can either pass an existing database connection as PDO instance or |
|||
* a Doctrine DBAL Connection or a DSN string that will be used to |
|||
* lazy-connect to the database when the cache is actually used. |
|||
* |
|||
* When a Doctrine DBAL Connection is passed, 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] |
|||
* * 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: []] |
|||
* |
|||
* @param \PDO|Connection|string $connOrDsn a \PDO or Connection instance or DSN string or null |
|||
* |
|||
* @throws InvalidArgumentException When first argument is not PDO nor Connection nor string |
|||
* @throws InvalidArgumentException When PDO error mode is not PDO::ERRMODE_EXCEPTION |
|||
* @throws InvalidArgumentException When namespace contains invalid characters |
|||
*/ |
|||
public function __construct($connOrDsn, string $namespace = '', int $defaultLifetime = 0, array $options = [], MarshallerInterface $marshaller = null) |
|||
{ |
|||
$this->init($connOrDsn, $namespace, $defaultLifetime, $options, $marshaller); |
|||
} |
|||
} |
|||
@ -0,0 +1,248 @@ |
|||
<?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\Simple; |
|||
|
|||
use Psr\SimpleCache\CacheInterface as Psr16CacheInterface; |
|||
use Symfony\Component\Cache\Adapter\PhpArrayAdapter; |
|||
use Symfony\Component\Cache\Exception\InvalidArgumentException; |
|||
use Symfony\Component\Cache\PruneableInterface; |
|||
use Symfony\Component\Cache\ResettableInterface; |
|||
use Symfony\Component\Cache\Traits\PhpArrayTrait; |
|||
use Symfony\Contracts\Cache\CacheInterface; |
|||
|
|||
@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.3, use "%s" and type-hint for "%s" instead.', PhpArrayCache::class, PhpArrayAdapter::class, CacheInterface::class), E_USER_DEPRECATED); |
|||
|
|||
/** |
|||
* @deprecated since Symfony 4.3, use PhpArrayAdapter and type-hint for CacheInterface instead. |
|||
*/ |
|||
class PhpArrayCache implements Psr16CacheInterface, PruneableInterface, ResettableInterface |
|||
{ |
|||
use PhpArrayTrait; |
|||
|
|||
/** |
|||
* @param string $file The PHP file were values are cached |
|||
* @param Psr16CacheInterface $fallbackPool A pool to fallback on when an item is not hit |
|||
*/ |
|||
public function __construct(string $file, Psr16CacheInterface $fallbackPool) |
|||
{ |
|||
$this->file = $file; |
|||
$this->pool = $fallbackPool; |
|||
} |
|||
|
|||
/** |
|||
* This adapter takes advantage of how PHP stores arrays in its latest versions. |
|||
* |
|||
* @param string $file The PHP file were values are cached |
|||
* |
|||
* @return Psr16CacheInterface |
|||
*/ |
|||
public static function create($file, Psr16CacheInterface $fallbackPool) |
|||
{ |
|||
// Shared memory is available in PHP 7.0+ with OPCache enabled |
|||
if (filter_var(ini_get('opcache.enable'), FILTER_VALIDATE_BOOLEAN)) { |
|||
return new static($file, $fallbackPool); |
|||
} |
|||
|
|||
return $fallbackPool; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function get($key, $default = null) |
|||
{ |
|||
if (!\is_string($key)) { |
|||
throw new InvalidArgumentException(sprintf('Cache key must be string, "%s" given.', \is_object($key) ? \get_class($key) : \gettype($key))); |
|||
} |
|||
if (null === $this->values) { |
|||
$this->initialize(); |
|||
} |
|||
if (!isset($this->keys[$key])) { |
|||
return $this->pool->get($key, $default); |
|||
} |
|||
$value = $this->values[$this->keys[$key]]; |
|||
|
|||
if ('N;' === $value) { |
|||
return null; |
|||
} |
|||
if ($value instanceof \Closure) { |
|||
try { |
|||
return $value(); |
|||
} catch (\Throwable $e) { |
|||
return $default; |
|||
} |
|||
} |
|||
|
|||
return $value; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function getMultiple($keys, $default = null) |
|||
{ |
|||
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', \is_object($keys) ? \get_class($keys) : \gettype($keys))); |
|||
} |
|||
foreach ($keys as $key) { |
|||
if (!\is_string($key)) { |
|||
throw new InvalidArgumentException(sprintf('Cache key must be string, "%s" given.', \is_object($key) ? \get_class($key) : \gettype($key))); |
|||
} |
|||
} |
|||
if (null === $this->values) { |
|||
$this->initialize(); |
|||
} |
|||
|
|||
return $this->generateItems($keys, $default); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function has($key) |
|||
{ |
|||
if (!\is_string($key)) { |
|||
throw new InvalidArgumentException(sprintf('Cache key must be string, "%s" given.', \is_object($key) ? \get_class($key) : \gettype($key))); |
|||
} |
|||
if (null === $this->values) { |
|||
$this->initialize(); |
|||
} |
|||
|
|||
return isset($this->keys[$key]) || $this->pool->has($key); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function delete($key) |
|||
{ |
|||
if (!\is_string($key)) { |
|||
throw new InvalidArgumentException(sprintf('Cache key must be string, "%s" given.', \is_object($key) ? \get_class($key) : \gettype($key))); |
|||
} |
|||
if (null === $this->values) { |
|||
$this->initialize(); |
|||
} |
|||
|
|||
return !isset($this->keys[$key]) && $this->pool->delete($key); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function deleteMultiple($keys) |
|||
{ |
|||
if (!\is_array($keys) && !$keys instanceof \Traversable) { |
|||
throw new InvalidArgumentException(sprintf('Cache keys must be array or Traversable, "%s" given', \is_object($keys) ? \get_class($keys) : \gettype($keys))); |
|||
} |
|||
|
|||
$deleted = true; |
|||
$fallbackKeys = []; |
|||
|
|||
foreach ($keys as $key) { |
|||
if (!\is_string($key)) { |
|||
throw new InvalidArgumentException(sprintf('Cache key must be string, "%s" given.', \is_object($key) ? \get_class($key) : \gettype($key))); |
|||
} |
|||
|
|||
if (isset($this->keys[$key])) { |
|||
$deleted = false; |
|||
} else { |
|||
$fallbackKeys[] = $key; |
|||
} |
|||
} |
|||
if (null === $this->values) { |
|||
$this->initialize(); |
|||
} |
|||
|
|||
if ($fallbackKeys) { |
|||
$deleted = $this->pool->deleteMultiple($fallbackKeys) && $deleted; |
|||
} |
|||
|
|||
return $deleted; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function set($key, $value, $ttl = null) |
|||
{ |
|||
if (!\is_string($key)) { |
|||
throw new InvalidArgumentException(sprintf('Cache key must be string, "%s" given.', \is_object($key) ? \get_class($key) : \gettype($key))); |
|||
} |
|||
if (null === $this->values) { |
|||
$this->initialize(); |
|||
} |
|||
|
|||
return !isset($this->keys[$key]) && $this->pool->set($key, $value, $ttl); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function setMultiple($values, $ttl = null) |
|||
{ |
|||
if (!\is_array($values) && !$values instanceof \Traversable) { |
|||
throw new InvalidArgumentException(sprintf('Cache values must be array or Traversable, "%s" given', \is_object($values) ? \get_class($values) : \gettype($values))); |
|||
} |
|||
|
|||
$saved = true; |
|||
$fallbackValues = []; |
|||
|
|||
foreach ($values as $key => $value) { |
|||
if (!\is_string($key) && !\is_int($key)) { |
|||
throw new InvalidArgumentException(sprintf('Cache key must be string, "%s" given.', \is_object($key) ? \get_class($key) : \gettype($key))); |
|||
} |
|||
|
|||
if (isset($this->keys[$key])) { |
|||
$saved = false; |
|||
} else { |
|||
$fallbackValues[$key] = $value; |
|||
} |
|||
} |
|||
|
|||
if ($fallbackValues) { |
|||
$saved = $this->pool->setMultiple($fallbackValues, $ttl) && $saved; |
|||
} |
|||
|
|||
return $saved; |
|||
} |
|||
|
|||
private function generateItems(array $keys, $default) |
|||
{ |
|||
$fallbackKeys = []; |
|||
|
|||
foreach ($keys as $key) { |
|||
if (isset($this->keys[$key])) { |
|||
$value = $this->values[$this->keys[$key]]; |
|||
|
|||
if ('N;' === $value) { |
|||
yield $key => null; |
|||
} elseif ($value instanceof \Closure) { |
|||
try { |
|||
yield $key => $value(); |
|||
} catch (\Throwable $e) { |
|||
yield $key => $default; |
|||
} |
|||
} else { |
|||
yield $key => $value; |
|||
} |
|||
} else { |
|||
$fallbackKeys[] = $key; |
|||
} |
|||
} |
|||
|
|||
if ($fallbackKeys) { |
|||
yield from $this->pool->getMultiple($fallbackKeys, $default); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,45 @@ |
|||
<?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\Simple; |
|||
|
|||
use Symfony\Component\Cache\Adapter\PhpFilesAdapter; |
|||
use Symfony\Component\Cache\Exception\CacheException; |
|||
use Symfony\Component\Cache\PruneableInterface; |
|||
use Symfony\Component\Cache\Traits\PhpFilesTrait; |
|||
use Symfony\Contracts\Cache\CacheInterface; |
|||
|
|||
@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.3, use "%s" and type-hint for "%s" instead.', PhpFilesCache::class, PhpFilesAdapter::class, CacheInterface::class), E_USER_DEPRECATED); |
|||
|
|||
/** |
|||
* @deprecated since Symfony 4.3, use PhpFilesAdapter and type-hint for CacheInterface instead. |
|||
*/ |
|||
class PhpFilesCache extends AbstractCache implements PruneableInterface |
|||
{ |
|||
use PhpFilesTrait; |
|||
|
|||
/** |
|||
* @param $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, bool $appendOnly = false) |
|||
{ |
|||
$this->appendOnly = $appendOnly; |
|||
self::$startTime = 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); |
|||
}; |
|||
} |
|||
} |
|||
@ -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\Simple; |
|||
|
|||
use Symfony\Component\Cache\Psr16Cache; |
|||
|
|||
@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.3, use "%s" instead.', Psr6Cache::class, Psr16Cache::class), E_USER_DEPRECATED); |
|||
|
|||
/** |
|||
* @deprecated since Symfony 4.3, use Psr16Cache instead. |
|||
*/ |
|||
class Psr6Cache extends Psr16Cache |
|||
{ |
|||
} |
|||
@ -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\Simple; |
|||
|
|||
use Symfony\Component\Cache\Adapter\RedisAdapter; |
|||
use Symfony\Component\Cache\Marshaller\MarshallerInterface; |
|||
use Symfony\Component\Cache\Traits\RedisTrait; |
|||
use Symfony\Contracts\Cache\CacheInterface; |
|||
|
|||
@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.3, use "%s" and type-hint for "%s" instead.', RedisCache::class, RedisAdapter::class, CacheInterface::class), E_USER_DEPRECATED); |
|||
|
|||
/** |
|||
* @deprecated since Symfony 4.3, use RedisAdapter and type-hint for CacheInterface instead. |
|||
*/ |
|||
class RedisCache extends AbstractCache |
|||
{ |
|||
use RedisTrait; |
|||
|
|||
/** |
|||
* @param \Redis|\RedisArray|\RedisCluster|\Predis\Client $redisClient |
|||
*/ |
|||
public function __construct($redisClient, string $namespace = '', int $defaultLifetime = 0, MarshallerInterface $marshaller = null) |
|||
{ |
|||
$this->init($redisClient, $namespace, $defaultLifetime, $marshaller); |
|||
} |
|||
} |
|||
@ -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\Simple; |
|||
|
|||
use Psr\SimpleCache\CacheInterface as Psr16CacheInterface; |
|||
use Symfony\Component\Cache\PruneableInterface; |
|||
use Symfony\Component\Cache\ResettableInterface; |
|||
use Symfony\Contracts\Cache\CacheInterface; |
|||
use Symfony\Contracts\Service\ResetInterface; |
|||
|
|||
@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.3, use "%s" and type-hint for "%s" instead.', TraceableCache::class, TraceableAdapter::class, CacheInterface::class), E_USER_DEPRECATED); |
|||
|
|||
/** |
|||
* @deprecated since Symfony 4.3, use TraceableAdapter and type-hint for CacheInterface instead. |
|||
*/ |
|||
class TraceableCache implements Psr16CacheInterface, PruneableInterface, ResettableInterface |
|||
{ |
|||
private $pool; |
|||
private $miss; |
|||
private $calls = []; |
|||
|
|||
public function __construct(Psr16CacheInterface $pool) |
|||
{ |
|||
$this->pool = $pool; |
|||
$this->miss = new \stdClass(); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function get($key, $default = null) |
|||
{ |
|||
$miss = null !== $default && \is_object($default) ? $default : $this->miss; |
|||
$event = $this->start(__FUNCTION__); |
|||
try { |
|||
$value = $this->pool->get($key, $miss); |
|||
} finally { |
|||
$event->end = microtime(true); |
|||
} |
|||
if ($event->result[$key] = $miss !== $value) { |
|||
++$event->hits; |
|||
} else { |
|||
++$event->misses; |
|||
$value = $default; |
|||
} |
|||
|
|||
return $value; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function has($key) |
|||
{ |
|||
$event = $this->start(__FUNCTION__); |
|||
try { |
|||
return $event->result[$key] = $this->pool->has($key); |
|||
} finally { |
|||
$event->end = microtime(true); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function delete($key) |
|||
{ |
|||
$event = $this->start(__FUNCTION__); |
|||
try { |
|||
return $event->result[$key] = $this->pool->delete($key); |
|||
} finally { |
|||
$event->end = microtime(true); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function set($key, $value, $ttl = null) |
|||
{ |
|||
$event = $this->start(__FUNCTION__); |
|||
try { |
|||
return $event->result[$key] = $this->pool->set($key, $value, $ttl); |
|||
} finally { |
|||
$event->end = microtime(true); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function setMultiple($values, $ttl = null) |
|||
{ |
|||
$event = $this->start(__FUNCTION__); |
|||
$event->result['keys'] = []; |
|||
|
|||
if ($values instanceof \Traversable) { |
|||
$values = function () use ($values, $event) { |
|||
foreach ($values as $k => $v) { |
|||
$event->result['keys'][] = $k; |
|||
yield $k => $v; |
|||
} |
|||
}; |
|||
$values = $values(); |
|||
} elseif (\is_array($values)) { |
|||
$event->result['keys'] = array_keys($values); |
|||
} |
|||
|
|||
try { |
|||
return $event->result['result'] = $this->pool->setMultiple($values, $ttl); |
|||
} finally { |
|||
$event->end = microtime(true); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function getMultiple($keys, $default = null) |
|||
{ |
|||
$miss = null !== $default && \is_object($default) ? $default : $this->miss; |
|||
$event = $this->start(__FUNCTION__); |
|||
try { |
|||
$result = $this->pool->getMultiple($keys, $miss); |
|||
} finally { |
|||
$event->end = microtime(true); |
|||
} |
|||
$f = function () use ($result, $event, $miss, $default) { |
|||
$event->result = []; |
|||
foreach ($result as $key => $value) { |
|||
if ($event->result[$key] = $miss !== $value) { |
|||
++$event->hits; |
|||
} else { |
|||
++$event->misses; |
|||
$value = $default; |
|||
} |
|||
yield $key => $value; |
|||
} |
|||
}; |
|||
|
|||
return $f(); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function clear() |
|||
{ |
|||
$event = $this->start(__FUNCTION__); |
|||
try { |
|||
return $event->result = $this->pool->clear(); |
|||
} finally { |
|||
$event->end = microtime(true); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function deleteMultiple($keys) |
|||
{ |
|||
$event = $this->start(__FUNCTION__); |
|||
if ($keys instanceof \Traversable) { |
|||
$keys = $event->result['keys'] = iterator_to_array($keys, false); |
|||
} else { |
|||
$event->result['keys'] = $keys; |
|||
} |
|||
try { |
|||
return $event->result['result'] = $this->pool->deleteMultiple($keys); |
|||
} finally { |
|||
$event->end = microtime(true); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function prune() |
|||
{ |
|||
if (!$this->pool instanceof PruneableInterface) { |
|||
return false; |
|||
} |
|||
$event = $this->start(__FUNCTION__); |
|||
try { |
|||
return $event->result = $this->pool->prune(); |
|||
} finally { |
|||
$event->end = microtime(true); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function reset() |
|||
{ |
|||
if (!$this->pool instanceof ResetInterface) { |
|||
return; |
|||
} |
|||
$event = $this->start(__FUNCTION__); |
|||
try { |
|||
$this->pool->reset(); |
|||
} finally { |
|||
$event->end = microtime(true); |
|||
} |
|||
} |
|||
|
|||
public function getCalls() |
|||
{ |
|||
try { |
|||
return $this->calls; |
|||
} finally { |
|||
$this->calls = []; |
|||
} |
|||
} |
|||
|
|||
private function start($name) |
|||
{ |
|||
$this->calls[] = $event = new TraceableCacheEvent(); |
|||
$event->name = $name; |
|||
$event->start = microtime(true); |
|||
|
|||
return $event; |
|||
} |
|||
} |
|||
|
|||
class TraceableCacheEvent |
|||
{ |
|||
public $name; |
|||
public $start; |
|||
public $end; |
|||
public $result; |
|||
public $hits = 0; |
|||
public $misses = 0; |
|||
} |
|||
@ -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\Tests\Adapter; |
|||
|
|||
use Symfony\Component\Cache\Adapter\RedisAdapter; |
|||
|
|||
abstract class AbstractRedisAdapterTest extends AdapterTestCase |
|||
{ |
|||
protected $skippedTests = [ |
|||
'testExpiration' => 'Testing expiration slows down the test suite', |
|||
'testHasItemReturnsFalseWhenDeferredItemIsExpired' => 'Testing expiration slows down the test suite', |
|||
'testDefaultLifeTime' => 'Testing expiration slows down the test suite', |
|||
]; |
|||
|
|||
protected static $redis; |
|||
|
|||
public function createCachePool($defaultLifetime = 0) |
|||
{ |
|||
return new RedisAdapter(self::$redis, str_replace('\\', '.', __CLASS__), $defaultLifetime); |
|||
} |
|||
|
|||
public static function setUpBeforeClass(): void |
|||
{ |
|||
if (!\extension_loaded('redis')) { |
|||
self::markTestSkipped('Extension redis required.'); |
|||
} |
|||
if (!@((new \Redis())->connect(getenv('REDIS_HOST')))) { |
|||
$e = error_get_last(); |
|||
self::markTestSkipped($e['message']); |
|||
} |
|||
} |
|||
|
|||
public static function tearDownAfterClass(): void |
|||
{ |
|||
self::$redis = null; |
|||
} |
|||
} |
|||
@ -0,0 +1,273 @@ |
|||
<?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\Tests\Adapter; |
|||
|
|||
use Cache\IntegrationTests\CachePoolTest; |
|||
use PHPUnit\Framework\Assert; |
|||
use Psr\Cache\CacheItemInterface; |
|||
use Psr\Cache\CacheItemPoolInterface; |
|||
use Symfony\Component\Cache\CacheItem; |
|||
use Symfony\Component\Cache\PruneableInterface; |
|||
use Symfony\Contracts\Cache\CallbackInterface; |
|||
|
|||
abstract class AdapterTestCase extends CachePoolTest |
|||
{ |
|||
protected function setUp(): void |
|||
{ |
|||
parent::setUp(); |
|||
|
|||
if (!\array_key_exists('testPrune', $this->skippedTests) && !$this->createCachePool() instanceof PruneableInterface) { |
|||
$this->skippedTests['testPrune'] = 'Not a pruneable cache pool.'; |
|||
} |
|||
} |
|||
|
|||
public function testGet() |
|||
{ |
|||
if (isset($this->skippedTests[__FUNCTION__])) { |
|||
$this->markTestSkipped($this->skippedTests[__FUNCTION__]); |
|||
} |
|||
|
|||
$cache = $this->createCachePool(); |
|||
$cache->clear(); |
|||
|
|||
$value = mt_rand(); |
|||
|
|||
$this->assertSame($value, $cache->get('foo', function (CacheItem $item) use ($value) { |
|||
$this->assertSame('foo', $item->getKey()); |
|||
|
|||
return $value; |
|||
})); |
|||
|
|||
$item = $cache->getItem('foo'); |
|||
$this->assertSame($value, $item->get()); |
|||
|
|||
$isHit = true; |
|||
$this->assertSame($value, $cache->get('foo', function (CacheItem $item) use (&$isHit) { $isHit = false; }, 0)); |
|||
$this->assertTrue($isHit); |
|||
|
|||
$this->assertNull($cache->get('foo', function (CacheItem $item) use (&$isHit, $value) { |
|||
$isHit = false; |
|||
$this->assertTrue($item->isHit()); |
|||
$this->assertSame($value, $item->get()); |
|||
}, INF)); |
|||
$this->assertFalse($isHit); |
|||
|
|||
$this->assertSame($value, $cache->get('bar', new class($value) implements CallbackInterface { |
|||
private $value; |
|||
|
|||
public function __construct(int $value) |
|||
{ |
|||
$this->value = $value; |
|||
} |
|||
|
|||
public function __invoke(CacheItemInterface $item, bool &$save) |
|||
{ |
|||
Assert::assertSame('bar', $item->getKey()); |
|||
|
|||
return $this->value; |
|||
} |
|||
})); |
|||
} |
|||
|
|||
public function testRecursiveGet() |
|||
{ |
|||
if (isset($this->skippedTests[__FUNCTION__])) { |
|||
$this->markTestSkipped($this->skippedTests[__FUNCTION__]); |
|||
} |
|||
|
|||
$cache = $this->createCachePool(0, __FUNCTION__); |
|||
|
|||
$v = $cache->get('k1', function () use (&$counter, $cache) { |
|||
$v = $cache->get('k2', function () use (&$counter) { return ++$counter; }); |
|||
$v = $cache->get('k2', function () use (&$counter) { return ++$counter; }); |
|||
|
|||
return $v; |
|||
}); |
|||
|
|||
$this->assertSame(1, $counter); |
|||
$this->assertSame(1, $v); |
|||
$this->assertSame(1, $cache->get('k2', function () { return 2; })); |
|||
} |
|||
|
|||
public function testGetMetadata() |
|||
{ |
|||
if (isset($this->skippedTests[__FUNCTION__])) { |
|||
$this->markTestSkipped($this->skippedTests[__FUNCTION__]); |
|||
} |
|||
|
|||
$cache = $this->createCachePool(0, __FUNCTION__); |
|||
|
|||
$cache->deleteItem('foo'); |
|||
$cache->get('foo', function ($item) { |
|||
$item->expiresAfter(10); |
|||
sleep(1); |
|||
|
|||
return 'bar'; |
|||
}); |
|||
|
|||
$item = $cache->getItem('foo'); |
|||
|
|||
$expected = [ |
|||
CacheItem::METADATA_EXPIRY => 9.5 + time(), |
|||
CacheItem::METADATA_CTIME => 1000, |
|||
]; |
|||
$this->assertEqualsWithDelta($expected, $item->getMetadata(), .6, 'Item metadata should embed expiry and ctime.'); |
|||
} |
|||
|
|||
public function testDefaultLifeTime() |
|||
{ |
|||
if (isset($this->skippedTests[__FUNCTION__])) { |
|||
$this->markTestSkipped($this->skippedTests[__FUNCTION__]); |
|||
} |
|||
|
|||
$cache = $this->createCachePool(2); |
|||
|
|||
$item = $cache->getItem('key.dlt'); |
|||
$item->set('value'); |
|||
$cache->save($item); |
|||
sleep(1); |
|||
|
|||
$item = $cache->getItem('key.dlt'); |
|||
$this->assertTrue($item->isHit()); |
|||
|
|||
sleep(2); |
|||
$item = $cache->getItem('key.dlt'); |
|||
$this->assertFalse($item->isHit()); |
|||
} |
|||
|
|||
public function testExpiration() |
|||
{ |
|||
if (isset($this->skippedTests[__FUNCTION__])) { |
|||
$this->markTestSkipped($this->skippedTests[__FUNCTION__]); |
|||
} |
|||
|
|||
$cache = $this->createCachePool(); |
|||
$cache->save($cache->getItem('k1')->set('v1')->expiresAfter(2)); |
|||
$cache->save($cache->getItem('k2')->set('v2')->expiresAfter(366 * 86400)); |
|||
|
|||
sleep(3); |
|||
$item = $cache->getItem('k1'); |
|||
$this->assertFalse($item->isHit()); |
|||
$this->assertNull($item->get(), "Item's value must be null when isHit() is false."); |
|||
|
|||
$item = $cache->getItem('k2'); |
|||
$this->assertTrue($item->isHit()); |
|||
$this->assertSame('v2', $item->get()); |
|||
} |
|||
|
|||
public function testNotUnserializable() |
|||
{ |
|||
if (isset($this->skippedTests[__FUNCTION__])) { |
|||
$this->markTestSkipped($this->skippedTests[__FUNCTION__]); |
|||
} |
|||
|
|||
$cache = $this->createCachePool(); |
|||
|
|||
$item = $cache->getItem('foo'); |
|||
$cache->save($item->set(new NotUnserializable())); |
|||
|
|||
$item = $cache->getItem('foo'); |
|||
$this->assertFalse($item->isHit()); |
|||
|
|||
foreach ($cache->getItems(['foo']) as $item) { |
|||
} |
|||
$cache->save($item->set(new NotUnserializable())); |
|||
|
|||
foreach ($cache->getItems(['foo']) as $item) { |
|||
} |
|||
$this->assertFalse($item->isHit()); |
|||
} |
|||
|
|||
public function testPrune() |
|||
{ |
|||
if (isset($this->skippedTests[__FUNCTION__])) { |
|||
$this->markTestSkipped($this->skippedTests[__FUNCTION__]); |
|||
} |
|||
|
|||
if (!method_exists($this, 'isPruned')) { |
|||
$this->fail('Test classes for pruneable caches must implement `isPruned($cache, $name)` method.'); |
|||
} |
|||
|
|||
/** @var PruneableInterface|CacheItemPoolInterface $cache */ |
|||
$cache = $this->createCachePool(); |
|||
|
|||
$doSet = function ($name, $value, \DateInterval $expiresAfter = null) use ($cache) { |
|||
$item = $cache->getItem($name); |
|||
$item->set($value); |
|||
|
|||
if ($expiresAfter) { |
|||
$item->expiresAfter($expiresAfter); |
|||
} |
|||
|
|||
$cache->save($item); |
|||
}; |
|||
|
|||
$doSet('foo', 'foo-val', new \DateInterval('PT05S')); |
|||
$doSet('bar', 'bar-val', new \DateInterval('PT10S')); |
|||
$doSet('baz', 'baz-val', new \DateInterval('PT15S')); |
|||
$doSet('qux', 'qux-val', new \DateInterval('PT20S')); |
|||
|
|||
sleep(30); |
|||
$cache->prune(); |
|||
$this->assertTrue($this->isPruned($cache, 'foo')); |
|||
$this->assertTrue($this->isPruned($cache, 'bar')); |
|||
$this->assertTrue($this->isPruned($cache, 'baz')); |
|||
$this->assertTrue($this->isPruned($cache, 'qux')); |
|||
|
|||
$doSet('foo', 'foo-val'); |
|||
$doSet('bar', 'bar-val', new \DateInterval('PT20S')); |
|||
$doSet('baz', 'baz-val', new \DateInterval('PT40S')); |
|||
$doSet('qux', 'qux-val', new \DateInterval('PT80S')); |
|||
|
|||
$cache->prune(); |
|||
$this->assertFalse($this->isPruned($cache, 'foo')); |
|||
$this->assertFalse($this->isPruned($cache, 'bar')); |
|||
$this->assertFalse($this->isPruned($cache, 'baz')); |
|||
$this->assertFalse($this->isPruned($cache, 'qux')); |
|||
|
|||
sleep(30); |
|||
$cache->prune(); |
|||
$this->assertFalse($this->isPruned($cache, 'foo')); |
|||
$this->assertTrue($this->isPruned($cache, 'bar')); |
|||
$this->assertFalse($this->isPruned($cache, 'baz')); |
|||
$this->assertFalse($this->isPruned($cache, 'qux')); |
|||
|
|||
sleep(30); |
|||
$cache->prune(); |
|||
$this->assertFalse($this->isPruned($cache, 'foo')); |
|||
$this->assertTrue($this->isPruned($cache, 'baz')); |
|||
$this->assertFalse($this->isPruned($cache, 'qux')); |
|||
|
|||
sleep(30); |
|||
$cache->prune(); |
|||
$this->assertFalse($this->isPruned($cache, 'foo')); |
|||
$this->assertTrue($this->isPruned($cache, 'qux')); |
|||
} |
|||
|
|||
/** |
|||
* @group issue-32995 |
|||
* |
|||
* @runInSeparateProcess https://github.com/symfony/symfony/issues/32995 |
|||
*/ |
|||
public function testSavingObject() |
|||
{ |
|||
parent::testSavingObject(); |
|||
} |
|||
} |
|||
|
|||
class NotUnserializable |
|||
{ |
|||
public function __wakeup() |
|||
{ |
|||
throw new \Exception(__CLASS__); |
|||
} |
|||
} |
|||
@ -0,0 +1,124 @@ |
|||
<?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\Tests\Adapter; |
|||
|
|||
use Psr\Log\NullLogger; |
|||
use Symfony\Component\Cache\Adapter\ApcuAdapter; |
|||
|
|||
class ApcuAdapterTest extends AdapterTestCase |
|||
{ |
|||
protected $skippedTests = [ |
|||
'testExpiration' => 'Testing expiration slows down the test suite', |
|||
'testHasItemReturnsFalseWhenDeferredItemIsExpired' => 'Testing expiration slows down the test suite', |
|||
'testDefaultLifeTime' => 'Testing expiration slows down the test suite', |
|||
]; |
|||
|
|||
public function createCachePool($defaultLifetime = 0) |
|||
{ |
|||
if (!\function_exists('apcu_fetch') || !filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN)) { |
|||
$this->markTestSkipped('APCu extension is required.'); |
|||
} |
|||
if ('cli' === \PHP_SAPI && !filter_var(ini_get('apc.enable_cli'), FILTER_VALIDATE_BOOLEAN)) { |
|||
if ('testWithCliSapi' !== $this->getName()) { |
|||
$this->markTestSkipped('apc.enable_cli=1 is required.'); |
|||
} |
|||
} |
|||
if ('\\' === \DIRECTORY_SEPARATOR) { |
|||
$this->markTestSkipped('Fails transiently on Windows.'); |
|||
} |
|||
|
|||
return new ApcuAdapter(str_replace('\\', '.', __CLASS__), $defaultLifetime); |
|||
} |
|||
|
|||
public function testUnserializable() |
|||
{ |
|||
$pool = $this->createCachePool(); |
|||
|
|||
$item = $pool->getItem('foo'); |
|||
$item->set(function () {}); |
|||
|
|||
$this->assertFalse($pool->save($item)); |
|||
|
|||
$item = $pool->getItem('foo'); |
|||
$this->assertFalse($item->isHit()); |
|||
} |
|||
|
|||
public function testVersion() |
|||
{ |
|||
$namespace = str_replace('\\', '.', \get_class($this)); |
|||
|
|||
$pool1 = new ApcuAdapter($namespace, 0, 'p1'); |
|||
|
|||
$item = $pool1->getItem('foo'); |
|||
$this->assertFalse($item->isHit()); |
|||
$this->assertTrue($pool1->save($item->set('bar'))); |
|||
|
|||
$item = $pool1->getItem('foo'); |
|||
$this->assertTrue($item->isHit()); |
|||
$this->assertSame('bar', $item->get()); |
|||
|
|||
$pool2 = new ApcuAdapter($namespace, 0, 'p2'); |
|||
|
|||
$item = $pool2->getItem('foo'); |
|||
$this->assertFalse($item->isHit()); |
|||
$this->assertNull($item->get()); |
|||
|
|||
$item = $pool1->getItem('foo'); |
|||
$this->assertFalse($item->isHit()); |
|||
$this->assertNull($item->get()); |
|||
} |
|||
|
|||
public function testNamespace() |
|||
{ |
|||
$namespace = str_replace('\\', '.', \get_class($this)); |
|||
|
|||
$pool1 = new ApcuAdapter($namespace.'_1', 0, 'p1'); |
|||
|
|||
$item = $pool1->getItem('foo'); |
|||
$this->assertFalse($item->isHit()); |
|||
$this->assertTrue($pool1->save($item->set('bar'))); |
|||
|
|||
$item = $pool1->getItem('foo'); |
|||
$this->assertTrue($item->isHit()); |
|||
$this->assertSame('bar', $item->get()); |
|||
|
|||
$pool2 = new ApcuAdapter($namespace.'_2', 0, 'p1'); |
|||
|
|||
$item = $pool2->getItem('foo'); |
|||
$this->assertFalse($item->isHit()); |
|||
$this->assertNull($item->get()); |
|||
|
|||
$item = $pool1->getItem('foo'); |
|||
$this->assertTrue($item->isHit()); |
|||
$this->assertSame('bar', $item->get()); |
|||
} |
|||
|
|||
public function testWithCliSapi() |
|||
{ |
|||
try { |
|||
// disable PHPUnit error handler to mimic a production environment |
|||
$isCalled = false; |
|||
set_error_handler(function () use (&$isCalled) { |
|||
$isCalled = true; |
|||
}); |
|||
$pool = new ApcuAdapter(str_replace('\\', '.', __CLASS__)); |
|||
$pool->setLogger(new NullLogger()); |
|||
|
|||
$item = $pool->getItem('foo'); |
|||
$item->isHit(); |
|||
$pool->save($item->set('bar')); |
|||
$this->assertFalse($isCalled); |
|||
} finally { |
|||
restore_error_handler(); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,57 @@ |
|||
<?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\Tests\Adapter; |
|||
|
|||
use Symfony\Component\Cache\Adapter\ArrayAdapter; |
|||
|
|||
/** |
|||
* @group time-sensitive |
|||
*/ |
|||
class ArrayAdapterTest extends AdapterTestCase |
|||
{ |
|||
protected $skippedTests = [ |
|||
'testGetMetadata' => 'ArrayAdapter does not keep metadata.', |
|||
'testDeferredSaveWithoutCommit' => 'Assumes a shared cache which ArrayAdapter is not.', |
|||
'testSaveWithoutExpire' => 'Assumes a shared cache which ArrayAdapter is not.', |
|||
]; |
|||
|
|||
public function createCachePool($defaultLifetime = 0) |
|||
{ |
|||
return new ArrayAdapter($defaultLifetime); |
|||
} |
|||
|
|||
public function testGetValuesHitAndMiss() |
|||
{ |
|||
/** @var ArrayAdapter $cache */ |
|||
$cache = $this->createCachePool(); |
|||
|
|||
// Hit |
|||
$item = $cache->getItem('foo'); |
|||
$item->set('::4711'); |
|||
$cache->save($item); |
|||
|
|||
$fooItem = $cache->getItem('foo'); |
|||
$this->assertTrue($fooItem->isHit()); |
|||
$this->assertEquals('::4711', $fooItem->get()); |
|||
|
|||
// Miss (should be present as NULL in $values) |
|||
$cache->getItem('bar'); |
|||
|
|||
$values = $cache->getValues(); |
|||
|
|||
$this->assertCount(2, $values); |
|||
$this->assertArrayHasKey('foo', $values); |
|||
$this->assertSame(serialize('::4711'), $values['foo']); |
|||
$this->assertArrayHasKey('bar', $values); |
|||
$this->assertNull($values['bar']); |
|||
} |
|||
} |
|||
@ -0,0 +1,119 @@ |
|||
<?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\Tests\Adapter; |
|||
|
|||
use PHPUnit\Framework\MockObject\MockObject; |
|||
use Symfony\Component\Cache\Adapter\AdapterInterface; |
|||
use Symfony\Component\Cache\Adapter\ArrayAdapter; |
|||
use Symfony\Component\Cache\Adapter\ChainAdapter; |
|||
use Symfony\Component\Cache\Adapter\FilesystemAdapter; |
|||
use Symfony\Component\Cache\PruneableInterface; |
|||
use Symfony\Component\Cache\Tests\Fixtures\ExternalAdapter; |
|||
|
|||
/** |
|||
* @author Kévin Dunglas <dunglas@gmail.com> |
|||
* @group time-sensitive |
|||
*/ |
|||
class ChainAdapterTest extends AdapterTestCase |
|||
{ |
|||
public function createCachePool($defaultLifetime = 0, $testMethod = null) |
|||
{ |
|||
if ('testGetMetadata' === $testMethod) { |
|||
return new ChainAdapter([new FilesystemAdapter('', $defaultLifetime)], $defaultLifetime); |
|||
} |
|||
|
|||
return new ChainAdapter([new ArrayAdapter($defaultLifetime), new ExternalAdapter(), new FilesystemAdapter('', $defaultLifetime)], $defaultLifetime); |
|||
} |
|||
|
|||
public function testEmptyAdaptersException() |
|||
{ |
|||
$this->expectException('Symfony\Component\Cache\Exception\InvalidArgumentException'); |
|||
$this->expectExceptionMessage('At least one adapter must be specified.'); |
|||
new ChainAdapter([]); |
|||
} |
|||
|
|||
public function testInvalidAdapterException() |
|||
{ |
|||
$this->expectException('Symfony\Component\Cache\Exception\InvalidArgumentException'); |
|||
$this->expectExceptionMessage('The class "stdClass" does not implement'); |
|||
new ChainAdapter([new \stdClass()]); |
|||
} |
|||
|
|||
public function testPrune() |
|||
{ |
|||
if (isset($this->skippedTests[__FUNCTION__])) { |
|||
$this->markTestSkipped($this->skippedTests[__FUNCTION__]); |
|||
} |
|||
|
|||
$cache = new ChainAdapter([ |
|||
$this->getPruneableMock(), |
|||
$this->getNonPruneableMock(), |
|||
$this->getPruneableMock(), |
|||
]); |
|||
$this->assertTrue($cache->prune()); |
|||
|
|||
$cache = new ChainAdapter([ |
|||
$this->getPruneableMock(), |
|||
$this->getFailingPruneableMock(), |
|||
$this->getPruneableMock(), |
|||
]); |
|||
$this->assertFalse($cache->prune()); |
|||
} |
|||
|
|||
/** |
|||
* @return MockObject|PruneableCacheInterface |
|||
*/ |
|||
private function getPruneableMock() |
|||
{ |
|||
$pruneable = $this |
|||
->getMockBuilder(PruneableCacheInterface::class) |
|||
->getMock(); |
|||
|
|||
$pruneable |
|||
->expects($this->atLeastOnce()) |
|||
->method('prune') |
|||
->willReturn(true); |
|||
|
|||
return $pruneable; |
|||
} |
|||
|
|||
/** |
|||
* @return MockObject|PruneableCacheInterface |
|||
*/ |
|||
private function getFailingPruneableMock() |
|||
{ |
|||
$pruneable = $this |
|||
->getMockBuilder(PruneableCacheInterface::class) |
|||
->getMock(); |
|||
|
|||
$pruneable |
|||
->expects($this->atLeastOnce()) |
|||
->method('prune') |
|||
->willReturn(false); |
|||
|
|||
return $pruneable; |
|||
} |
|||
|
|||
/** |
|||
* @return MockObject|AdapterInterface |
|||
*/ |
|||
private function getNonPruneableMock() |
|||
{ |
|||
return $this |
|||
->getMockBuilder(AdapterInterface::class) |
|||
->getMock(); |
|||
} |
|||
} |
|||
|
|||
interface PruneableCacheInterface extends PruneableInterface, AdapterInterface |
|||
{ |
|||
} |
|||
@ -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\Cache\Tests\Adapter; |
|||
|
|||
use Symfony\Component\Cache\Adapter\DoctrineAdapter; |
|||
use Symfony\Component\Cache\Tests\Fixtures\ArrayCache; |
|||
|
|||
/** |
|||
* @group time-sensitive |
|||
*/ |
|||
class DoctrineAdapterTest extends AdapterTestCase |
|||
{ |
|||
protected $skippedTests = [ |
|||
'testDeferredSaveWithoutCommit' => 'Assumes a shared cache which ArrayCache is not.', |
|||
'testSaveWithoutExpire' => 'Assumes a shared cache which ArrayCache is not.', |
|||
'testNotUnserializable' => 'ArrayCache does not use serialize/unserialize', |
|||
]; |
|||
|
|||
public function createCachePool($defaultLifetime = 0) |
|||
{ |
|||
return new DoctrineAdapter(new ArrayCache($defaultLifetime), '', $defaultLifetime); |
|||
} |
|||
} |
|||
@ -0,0 +1,61 @@ |
|||
<?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\Tests\Adapter; |
|||
|
|||
use Psr\Cache\CacheItemPoolInterface; |
|||
use Symfony\Component\Cache\Adapter\FilesystemAdapter; |
|||
|
|||
/** |
|||
* @group time-sensitive |
|||
*/ |
|||
class FilesystemAdapterTest extends AdapterTestCase |
|||
{ |
|||
public function createCachePool($defaultLifetime = 0) |
|||
{ |
|||
return new FilesystemAdapter('', $defaultLifetime); |
|||
} |
|||
|
|||
public static function tearDownAfterClass(): void |
|||
{ |
|||
self::rmdir(sys_get_temp_dir().'/symfony-cache'); |
|||
} |
|||
|
|||
public static function rmdir($dir) |
|||
{ |
|||
if (!file_exists($dir)) { |
|||
return; |
|||
} |
|||
if (!$dir || 0 !== strpos(\dirname($dir), sys_get_temp_dir())) { |
|||
throw new \Exception(__METHOD__."() operates only on subdirs of system's temp dir"); |
|||
} |
|||
$children = new \RecursiveIteratorIterator( |
|||
new \RecursiveDirectoryIterator($dir, \RecursiveDirectoryIterator::SKIP_DOTS), |
|||
\RecursiveIteratorIterator::CHILD_FIRST |
|||
); |
|||
foreach ($children as $child) { |
|||
if ($child->isDir()) { |
|||
rmdir($child); |
|||
} else { |
|||
unlink($child); |
|||
} |
|||
} |
|||
rmdir($dir); |
|||
} |
|||
|
|||
protected function isPruned(CacheItemPoolInterface $cache, $name) |
|||
{ |
|||
$getFileMethod = (new \ReflectionObject($cache))->getMethod('getFile'); |
|||
$getFileMethod->setAccessible(true); |
|||
|
|||
return !file_exists($getFileMethod->invoke($cache, $name)); |
|||
} |
|||
} |
|||
@ -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\Cache\Tests\Adapter; |
|||
|
|||
use Symfony\Component\Cache\Adapter\FilesystemTagAwareAdapter; |
|||
use Symfony\Component\Cache\Tests\Traits\TagAwareTestTrait; |
|||
|
|||
/** |
|||
* @group time-sensitive |
|||
*/ |
|||
class FilesystemTagAwareAdapterTest extends FilesystemAdapterTest |
|||
{ |
|||
use TagAwareTestTrait; |
|||
|
|||
public function createCachePool($defaultLifetime = 0) |
|||
{ |
|||
return new FilesystemTagAwareAdapter('', $defaultLifetime); |
|||
} |
|||
} |
|||
@ -0,0 +1,87 @@ |
|||
<?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\Tests\Adapter; |
|||
|
|||
use PHPUnit\Framework\TestCase; |
|||
use Symfony\Component\Cache\Adapter\AbstractAdapter; |
|||
|
|||
class MaxIdLengthAdapterTest extends TestCase |
|||
{ |
|||
public function testLongKey() |
|||
{ |
|||
$cache = $this->getMockBuilder(MaxIdLengthAdapter::class) |
|||
->setConstructorArgs([str_repeat('-', 10)]) |
|||
->setMethods(['doHave', 'doFetch', 'doDelete', 'doSave', 'doClear']) |
|||
->getMock(); |
|||
|
|||
$cache->expects($this->exactly(2)) |
|||
->method('doHave') |
|||
->withConsecutive( |
|||
[$this->equalTo('----------:nWfzGiCgLczv3SSUzXL3kg:')], |
|||
[$this->equalTo('----------:---------------------------------------')] |
|||
); |
|||
|
|||
$cache->hasItem(str_repeat('-', 40)); |
|||
$cache->hasItem(str_repeat('-', 39)); |
|||
} |
|||
|
|||
public function testLongKeyVersioning() |
|||
{ |
|||
$cache = $this->getMockBuilder(MaxIdLengthAdapter::class) |
|||
->setConstructorArgs([str_repeat('-', 26)]) |
|||
->getMock(); |
|||
|
|||
$cache |
|||
->method('doFetch') |
|||
->willReturn(['2:']); |
|||
|
|||
$reflectionClass = new \ReflectionClass(AbstractAdapter::class); |
|||
|
|||
$reflectionMethod = $reflectionClass->getMethod('getId'); |
|||
$reflectionMethod->setAccessible(true); |
|||
|
|||
// No versioning enabled |
|||
$this->assertEquals('--------------------------:------------', $reflectionMethod->invokeArgs($cache, [str_repeat('-', 12)])); |
|||
$this->assertLessThanOrEqual(50, \strlen($reflectionMethod->invokeArgs($cache, [str_repeat('-', 12)]))); |
|||
$this->assertLessThanOrEqual(50, \strlen($reflectionMethod->invokeArgs($cache, [str_repeat('-', 23)]))); |
|||
$this->assertLessThanOrEqual(50, \strlen($reflectionMethod->invokeArgs($cache, [str_repeat('-', 40)]))); |
|||
|
|||
$reflectionProperty = $reflectionClass->getProperty('versioningIsEnabled'); |
|||
$reflectionProperty->setAccessible(true); |
|||
$reflectionProperty->setValue($cache, true); |
|||
|
|||
// Versioning enabled |
|||
$this->assertEquals('--------------------------:2:------------', $reflectionMethod->invokeArgs($cache, [str_repeat('-', 12)])); |
|||
$this->assertLessThanOrEqual(50, \strlen($reflectionMethod->invokeArgs($cache, [str_repeat('-', 12)]))); |
|||
$this->assertLessThanOrEqual(50, \strlen($reflectionMethod->invokeArgs($cache, [str_repeat('-', 23)]))); |
|||
$this->assertLessThanOrEqual(50, \strlen($reflectionMethod->invokeArgs($cache, [str_repeat('-', 40)]))); |
|||
} |
|||
|
|||
public function testTooLongNamespace() |
|||
{ |
|||
$this->expectException('Symfony\Component\Cache\Exception\InvalidArgumentException'); |
|||
$this->expectExceptionMessage('Namespace must be 26 chars max, 40 given ("----------------------------------------")'); |
|||
$cache = $this->getMockBuilder(MaxIdLengthAdapter::class) |
|||
->setConstructorArgs([str_repeat('-', 40)]) |
|||
->getMock(); |
|||
} |
|||
} |
|||
|
|||
abstract class MaxIdLengthAdapter extends AbstractAdapter |
|||
{ |
|||
protected $maxIdLength = 50; |
|||
|
|||
public function __construct($ns) |
|||
{ |
|||
parent::__construct($ns); |
|||
} |
|||
} |
|||
@ -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\Tests\Adapter; |
|||
|
|||
use Symfony\Component\Cache\Adapter\AbstractAdapter; |
|||
use Symfony\Component\Cache\Adapter\MemcachedAdapter; |
|||
|
|||
class MemcachedAdapterTest extends AdapterTestCase |
|||
{ |
|||
protected $skippedTests = [ |
|||
'testHasItemReturnsFalseWhenDeferredItemIsExpired' => 'Testing expiration slows down the test suite', |
|||
'testDefaultLifeTime' => 'Testing expiration slows down the test suite', |
|||
]; |
|||
|
|||
protected static $client; |
|||
|
|||
public static function setUpBeforeClass(): void |
|||
{ |
|||
if (!MemcachedAdapter::isSupported()) { |
|||
self::markTestSkipped('Extension memcached >=2.2.0 required.'); |
|||
} |
|||
self::$client = AbstractAdapter::createConnection('memcached://'.getenv('MEMCACHED_HOST'), ['binary_protocol' => false]); |
|||
self::$client->get('foo'); |
|||
$code = self::$client->getResultCode(); |
|||
|
|||
if (\Memcached::RES_SUCCESS !== $code && \Memcached::RES_NOTFOUND !== $code) { |
|||
self::markTestSkipped('Memcached error: '.strtolower(self::$client->getResultMessage())); |
|||
} |
|||
} |
|||
|
|||
public function createCachePool($defaultLifetime = 0) |
|||
{ |
|||
$client = $defaultLifetime ? AbstractAdapter::createConnection('memcached://'.getenv('MEMCACHED_HOST')) : self::$client; |
|||
|
|||
return new MemcachedAdapter($client, str_replace('\\', '.', __CLASS__), $defaultLifetime); |
|||
} |
|||
|
|||
public function testOptions() |
|||
{ |
|||
$client = MemcachedAdapter::createConnection([], [ |
|||
'libketama_compatible' => false, |
|||
'distribution' => 'modula', |
|||
'compression' => true, |
|||
'serializer' => 'php', |
|||
'hash' => 'md5', |
|||
]); |
|||
|
|||
$this->assertSame(\Memcached::SERIALIZER_PHP, $client->getOption(\Memcached::OPT_SERIALIZER)); |
|||
$this->assertSame(\Memcached::HASH_MD5, $client->getOption(\Memcached::OPT_HASH)); |
|||
$this->assertTrue($client->getOption(\Memcached::OPT_COMPRESSION)); |
|||
$this->assertSame(0, $client->getOption(\Memcached::OPT_LIBKETAMA_COMPATIBLE)); |
|||
$this->assertSame(\Memcached::DISTRIBUTION_MODULA, $client->getOption(\Memcached::OPT_DISTRIBUTION)); |
|||
} |
|||
|
|||
/** |
|||
* @dataProvider provideBadOptions |
|||
*/ |
|||
public function testBadOptions($name, $value) |
|||
{ |
|||
$this->expectException('ErrorException'); |
|||
$this->expectExceptionMessage('constant(): Couldn\'t find constant Memcached::'); |
|||
MemcachedAdapter::createConnection([], [$name => $value]); |
|||
} |
|||
|
|||
public function provideBadOptions() |
|||
{ |
|||
return [ |
|||
['foo', 'bar'], |
|||
['hash', 'zyx'], |
|||
['serializer', 'zyx'], |
|||
['distribution', 'zyx'], |
|||
]; |
|||
} |
|||
|
|||
public function testDefaultOptions() |
|||
{ |
|||
$this->assertTrue(MemcachedAdapter::isSupported()); |
|||
|
|||
$client = MemcachedAdapter::createConnection([]); |
|||
|
|||
$this->assertTrue($client->getOption(\Memcached::OPT_COMPRESSION)); |
|||
$this->assertSame(1, $client->getOption(\Memcached::OPT_BINARY_PROTOCOL)); |
|||
$this->assertSame(1, $client->getOption(\Memcached::OPT_TCP_NODELAY)); |
|||
$this->assertSame(1, $client->getOption(\Memcached::OPT_LIBKETAMA_COMPATIBLE)); |
|||
} |
|||
|
|||
public function testOptionSerializer() |
|||
{ |
|||
$this->expectException('Symfony\Component\Cache\Exception\CacheException'); |
|||
$this->expectExceptionMessage('MemcachedAdapter: "serializer" option must be "php" or "igbinary".'); |
|||
if (!\Memcached::HAVE_JSON) { |
|||
$this->markTestSkipped('Memcached::HAVE_JSON required'); |
|||
} |
|||
|
|||
new MemcachedAdapter(MemcachedAdapter::createConnection([], ['serializer' => 'json'])); |
|||
} |
|||
|
|||
/** |
|||
* @dataProvider provideServersSetting |
|||
*/ |
|||
public function testServersSetting($dsn, $host, $port) |
|||
{ |
|||
$client1 = MemcachedAdapter::createConnection($dsn); |
|||
$client2 = MemcachedAdapter::createConnection([$dsn]); |
|||
$client3 = MemcachedAdapter::createConnection([[$host, $port]]); |
|||
$expect = [ |
|||
'host' => $host, |
|||
'port' => $port, |
|||
]; |
|||
|
|||
$f = function ($s) { return ['host' => $s['host'], 'port' => $s['port']]; }; |
|||
$this->assertSame([$expect], array_map($f, $client1->getServerList())); |
|||
$this->assertSame([$expect], array_map($f, $client2->getServerList())); |
|||
$this->assertSame([$expect], array_map($f, $client3->getServerList())); |
|||
} |
|||
|
|||
public function provideServersSetting() |
|||
{ |
|||
yield [ |
|||
'memcached://127.0.0.1/50', |
|||
'127.0.0.1', |
|||
11211, |
|||
]; |
|||
yield [ |
|||
'memcached://localhost:11222?weight=25', |
|||
'localhost', |
|||
11222, |
|||
]; |
|||
if (filter_var(ini_get('memcached.use_sasl'), FILTER_VALIDATE_BOOLEAN)) { |
|||
yield [ |
|||
'memcached://user:password@127.0.0.1?weight=50', |
|||
'127.0.0.1', |
|||
11211, |
|||
]; |
|||
} |
|||
yield [ |
|||
'memcached:///var/run/memcached.sock?weight=25', |
|||
'/var/run/memcached.sock', |
|||
0, |
|||
]; |
|||
yield [ |
|||
'memcached:///var/local/run/memcached.socket?weight=25', |
|||
'/var/local/run/memcached.socket', |
|||
0, |
|||
]; |
|||
if (filter_var(ini_get('memcached.use_sasl'), FILTER_VALIDATE_BOOLEAN)) { |
|||
yield [ |
|||
'memcached://user:password@/var/local/run/memcached.socket?weight=25', |
|||
'/var/local/run/memcached.socket', |
|||
0, |
|||
]; |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* @dataProvider provideDsnWithOptions |
|||
*/ |
|||
public function testDsnWithOptions($dsn, array $options, array $expectedOptions) |
|||
{ |
|||
$client = MemcachedAdapter::createConnection($dsn, $options); |
|||
|
|||
foreach ($expectedOptions as $option => $expect) { |
|||
$this->assertSame($expect, $client->getOption($option)); |
|||
} |
|||
} |
|||
|
|||
public function provideDsnWithOptions() |
|||
{ |
|||
if (!class_exists('\Memcached')) { |
|||
self::markTestSkipped('Extension memcached required.'); |
|||
} |
|||
|
|||
yield [ |
|||
'memcached://localhost:11222?retry_timeout=10', |
|||
[\Memcached::OPT_RETRY_TIMEOUT => 8], |
|||
[\Memcached::OPT_RETRY_TIMEOUT => 10], |
|||
]; |
|||
yield [ |
|||
'memcached://localhost:11222?socket_recv_size=1&socket_send_size=2', |
|||
[\Memcached::OPT_RETRY_TIMEOUT => 8], |
|||
[\Memcached::OPT_SOCKET_RECV_SIZE => 1, \Memcached::OPT_SOCKET_SEND_SIZE => 2, \Memcached::OPT_RETRY_TIMEOUT => 8], |
|||
]; |
|||
} |
|||
|
|||
public function testClear() |
|||
{ |
|||
$this->assertTrue($this->createCachePool()->clear()); |
|||
} |
|||
|
|||
public function testMultiServerDsn() |
|||
{ |
|||
$dsn = 'memcached:?host[localhost]&host[localhost:12345]&host[/some/memcached.sock:]=3'; |
|||
$client = MemcachedAdapter::createConnection($dsn); |
|||
|
|||
$expected = [ |
|||
0 => [ |
|||
'host' => 'localhost', |
|||
'port' => 11211, |
|||
'type' => 'TCP', |
|||
], |
|||
1 => [ |
|||
'host' => 'localhost', |
|||
'port' => 12345, |
|||
'type' => 'TCP', |
|||
], |
|||
2 => [ |
|||
'host' => '/some/memcached.sock', |
|||
'port' => 0, |
|||
'type' => 'SOCKET', |
|||
], |
|||
]; |
|||
$this->assertSame($expected, $client->getServerList()); |
|||
|
|||
$dsn = 'memcached://localhost?host[foo.bar]=3'; |
|||
$client = MemcachedAdapter::createConnection($dsn); |
|||
|
|||
$expected = [ |
|||
0 => [ |
|||
'host' => 'localhost', |
|||
'port' => 11211, |
|||
'type' => 'TCP', |
|||
], |
|||
1 => [ |
|||
'host' => 'foo.bar', |
|||
'port' => 11211, |
|||
'type' => 'TCP', |
|||
], |
|||
]; |
|||
$this->assertSame($expected, $client->getServerList()); |
|||
} |
|||
} |
|||
@ -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\Tests\Adapter; |
|||
|
|||
use Symfony\Component\Cache\Adapter\ArrayAdapter; |
|||
use Symfony\Component\Cache\Adapter\FilesystemAdapter; |
|||
use Symfony\Component\Cache\Adapter\ProxyAdapter; |
|||
|
|||
/** |
|||
* @group time-sensitive |
|||
*/ |
|||
class NamespacedProxyAdapterTest extends ProxyAdapterTest |
|||
{ |
|||
public function createCachePool($defaultLifetime = 0, $testMethod = null) |
|||
{ |
|||
if ('testGetMetadata' === $testMethod) { |
|||
return new ProxyAdapter(new FilesystemAdapter(), 'foo', $defaultLifetime); |
|||
} |
|||
|
|||
return new ProxyAdapter(new ArrayAdapter($defaultLifetime), 'foo', $defaultLifetime); |
|||
} |
|||
} |
|||
@ -0,0 +1,141 @@ |
|||
<?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\Tests\Adapter; |
|||
|
|||
use PHPUnit\Framework\TestCase; |
|||
use Psr\Cache\CacheItemInterface; |
|||
use Symfony\Component\Cache\Adapter\NullAdapter; |
|||
|
|||
/** |
|||
* @group time-sensitive |
|||
*/ |
|||
class NullAdapterTest extends TestCase |
|||
{ |
|||
public function createCachePool() |
|||
{ |
|||
return new NullAdapter(); |
|||
} |
|||
|
|||
public function testGetItem() |
|||
{ |
|||
$adapter = $this->createCachePool(); |
|||
|
|||
$item = $adapter->getItem('key'); |
|||
$this->assertFalse($item->isHit()); |
|||
$this->assertNull($item->get(), "Item's value must be null when isHit is false."); |
|||
} |
|||
|
|||
public function testGet() |
|||
{ |
|||
$adapter = $this->createCachePool(); |
|||
|
|||
$fetched = []; |
|||
$item = $adapter->get('myKey', function ($item) use (&$fetched) { $fetched[] = $item; }); |
|||
$this->assertCount(1, $fetched); |
|||
$item = $fetched[0]; |
|||
$this->assertFalse($item->isHit()); |
|||
$this->assertNull($item->get(), "Item's value must be null when isHit is false."); |
|||
$this->assertSame('myKey', $item->getKey()); |
|||
} |
|||
|
|||
public function testHasItem() |
|||
{ |
|||
$this->assertFalse($this->createCachePool()->hasItem('key')); |
|||
} |
|||
|
|||
public function testGetItems() |
|||
{ |
|||
$adapter = $this->createCachePool(); |
|||
|
|||
$keys = ['foo', 'bar', 'baz', 'biz']; |
|||
|
|||
/** @var CacheItemInterface[] $items */ |
|||
$items = $adapter->getItems($keys); |
|||
$count = 0; |
|||
|
|||
foreach ($items as $key => $item) { |
|||
$itemKey = $item->getKey(); |
|||
|
|||
$this->assertEquals($itemKey, $key, 'Keys must be preserved when fetching multiple items'); |
|||
$this->assertContains($key, $keys, 'Cache key can not change.'); |
|||
$this->assertFalse($item->isHit()); |
|||
|
|||
// Remove $key for $keys |
|||
foreach ($keys as $k => $v) { |
|||
if ($v === $key) { |
|||
unset($keys[$k]); |
|||
} |
|||
} |
|||
|
|||
++$count; |
|||
} |
|||
|
|||
$this->assertSame(4, $count); |
|||
} |
|||
|
|||
public function testIsHit() |
|||
{ |
|||
$adapter = $this->createCachePool(); |
|||
|
|||
$item = $adapter->getItem('key'); |
|||
$this->assertFalse($item->isHit()); |
|||
} |
|||
|
|||
public function testClear() |
|||
{ |
|||
$this->assertTrue($this->createCachePool()->clear()); |
|||
} |
|||
|
|||
public function testDeleteItem() |
|||
{ |
|||
$this->assertTrue($this->createCachePool()->deleteItem('key')); |
|||
} |
|||
|
|||
public function testDeleteItems() |
|||
{ |
|||
$this->assertTrue($this->createCachePool()->deleteItems(['key', 'foo', 'bar'])); |
|||
} |
|||
|
|||
public function testSave() |
|||
{ |
|||
$adapter = $this->createCachePool(); |
|||
|
|||
$item = $adapter->getItem('key'); |
|||
$this->assertFalse($item->isHit()); |
|||
$this->assertNull($item->get(), "Item's value must be null when isHit is false."); |
|||
|
|||
$this->assertFalse($adapter->save($item)); |
|||
} |
|||
|
|||
public function testDeferredSave() |
|||
{ |
|||
$adapter = $this->createCachePool(); |
|||
|
|||
$item = $adapter->getItem('key'); |
|||
$this->assertFalse($item->isHit()); |
|||
$this->assertNull($item->get(), "Item's value must be null when isHit is false."); |
|||
|
|||
$this->assertFalse($adapter->saveDeferred($item)); |
|||
} |
|||
|
|||
public function testCommit() |
|||
{ |
|||
$adapter = $this->createCachePool(); |
|||
|
|||
$item = $adapter->getItem('key'); |
|||
$this->assertFalse($item->isHit()); |
|||
$this->assertNull($item->get(), "Item's value must be null when isHit is false."); |
|||
|
|||
$this->assertFalse($adapter->saveDeferred($item)); |
|||
$this->assertFalse($this->createCachePool()->commit()); |
|||
} |
|||
} |
|||
@ -0,0 +1,73 @@ |
|||
<?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\Tests\Adapter; |
|||
|
|||
use Symfony\Component\Cache\Adapter\PdoAdapter; |
|||
use Symfony\Component\Cache\Tests\Traits\PdoPruneableTrait; |
|||
|
|||
/** |
|||
* @group time-sensitive |
|||
*/ |
|||
class PdoAdapterTest extends AdapterTestCase |
|||
{ |
|||
use PdoPruneableTrait; |
|||
|
|||
protected static $dbFile; |
|||
|
|||
public static function setUpBeforeClass(): void |
|||
{ |
|||
if (!\extension_loaded('pdo_sqlite')) { |
|||
self::markTestSkipped('Extension pdo_sqlite required.'); |
|||
} |
|||
|
|||
self::$dbFile = tempnam(sys_get_temp_dir(), 'sf_sqlite_cache'); |
|||
|
|||
$pool = new PdoAdapter('sqlite:'.self::$dbFile); |
|||
$pool->createTable(); |
|||
} |
|||
|
|||
public static function tearDownAfterClass(): void |
|||
{ |
|||
@unlink(self::$dbFile); |
|||
} |
|||
|
|||
public function createCachePool($defaultLifetime = 0) |
|||
{ |
|||
return new PdoAdapter('sqlite:'.self::$dbFile, 'ns', $defaultLifetime); |
|||
} |
|||
|
|||
public function testCleanupExpiredItems() |
|||
{ |
|||
$pdo = new \PDO('sqlite:'.self::$dbFile); |
|||
|
|||
$getCacheItemCount = function () use ($pdo) { |
|||
return (int) $pdo->query('SELECT COUNT(*) FROM cache_items')->fetch(\PDO::FETCH_COLUMN); |
|||
}; |
|||
|
|||
$this->assertSame(0, $getCacheItemCount()); |
|||
|
|||
$cache = $this->createCachePool(); |
|||
|
|||
$item = $cache->getItem('some_nice_key'); |
|||
$item->expiresAfter(1); |
|||
$item->set(1); |
|||
|
|||
$cache->save($item); |
|||
$this->assertSame(1, $getCacheItemCount()); |
|||
|
|||
sleep(2); |
|||
|
|||
$newItem = $cache->getItem($item->getKey()); |
|||
$this->assertFalse($newItem->isHit()); |
|||
$this->assertSame(0, $getCacheItemCount(), 'PDOAdapter must clean up expired items'); |
|||
} |
|||
} |
|||
@ -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\Tests\Adapter; |
|||
|
|||
use Doctrine\DBAL\DriverManager; |
|||
use Symfony\Component\Cache\Adapter\PdoAdapter; |
|||
use Symfony\Component\Cache\Tests\Traits\PdoPruneableTrait; |
|||
|
|||
/** |
|||
* @group time-sensitive |
|||
*/ |
|||
class PdoDbalAdapterTest extends AdapterTestCase |
|||
{ |
|||
use PdoPruneableTrait; |
|||
|
|||
protected static $dbFile; |
|||
|
|||
public static function setUpBeforeClass(): void |
|||
{ |
|||
if (!\extension_loaded('pdo_sqlite')) { |
|||
self::markTestSkipped('Extension pdo_sqlite required.'); |
|||
} |
|||
|
|||
self::$dbFile = tempnam(sys_get_temp_dir(), 'sf_sqlite_cache'); |
|||
|
|||
$pool = new PdoAdapter(DriverManager::getConnection(['driver' => 'pdo_sqlite', 'path' => self::$dbFile])); |
|||
} |
|||
|
|||
public static function tearDownAfterClass(): void |
|||
{ |
|||
@unlink(self::$dbFile); |
|||
} |
|||
|
|||
public function createCachePool($defaultLifetime = 0) |
|||
{ |
|||
return new PdoAdapter(DriverManager::getConnection(['driver' => 'pdo_sqlite', 'path' => self::$dbFile]), '', $defaultLifetime); |
|||
} |
|||
} |
|||
@ -0,0 +1,160 @@ |
|||
<?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\Tests\Adapter; |
|||
|
|||
use Psr\Cache\CacheItemInterface; |
|||
use Symfony\Component\Cache\Adapter\FilesystemAdapter; |
|||
use Symfony\Component\Cache\Adapter\NullAdapter; |
|||
use Symfony\Component\Cache\Adapter\PhpArrayAdapter; |
|||
|
|||
/** |
|||
* @group time-sensitive |
|||
*/ |
|||
class PhpArrayAdapterTest extends AdapterTestCase |
|||
{ |
|||
protected $skippedTests = [ |
|||
'testGet' => 'PhpArrayAdapter is read-only.', |
|||
'testRecursiveGet' => 'PhpArrayAdapter is read-only.', |
|||
'testBasicUsage' => 'PhpArrayAdapter is read-only.', |
|||
'testBasicUsageWithLongKey' => 'PhpArrayAdapter is read-only.', |
|||
'testClear' => 'PhpArrayAdapter is read-only.', |
|||
'testClearWithDeferredItems' => 'PhpArrayAdapter is read-only.', |
|||
'testDeleteItem' => 'PhpArrayAdapter is read-only.', |
|||
'testSaveExpired' => 'PhpArrayAdapter is read-only.', |
|||
'testSaveWithoutExpire' => 'PhpArrayAdapter is read-only.', |
|||
'testDeferredSave' => 'PhpArrayAdapter is read-only.', |
|||
'testDeferredSaveWithoutCommit' => 'PhpArrayAdapter is read-only.', |
|||
'testDeleteItems' => 'PhpArrayAdapter is read-only.', |
|||
'testDeleteDeferredItem' => 'PhpArrayAdapter is read-only.', |
|||
'testCommit' => 'PhpArrayAdapter is read-only.', |
|||
'testSaveDeferredWhenChangingValues' => 'PhpArrayAdapter is read-only.', |
|||
'testSaveDeferredOverwrite' => 'PhpArrayAdapter is read-only.', |
|||
'testIsHitDeferred' => 'PhpArrayAdapter is read-only.', |
|||
|
|||
'testExpiresAt' => 'PhpArrayAdapter does not support expiration.', |
|||
'testExpiresAtWithNull' => 'PhpArrayAdapter does not support expiration.', |
|||
'testExpiresAfterWithNull' => 'PhpArrayAdapter does not support expiration.', |
|||
'testDeferredExpired' => 'PhpArrayAdapter does not support expiration.', |
|||
'testExpiration' => 'PhpArrayAdapter does not support expiration.', |
|||
|
|||
'testGetItemInvalidKeys' => 'PhpArrayAdapter does not throw exceptions on invalid key.', |
|||
'testGetItemsInvalidKeys' => 'PhpArrayAdapter does not throw exceptions on invalid key.', |
|||
'testHasItemInvalidKeys' => 'PhpArrayAdapter does not throw exceptions on invalid key.', |
|||
'testDeleteItemInvalidKeys' => 'PhpArrayAdapter does not throw exceptions on invalid key.', |
|||
'testDeleteItemsInvalidKeys' => 'PhpArrayAdapter does not throw exceptions on invalid key.', |
|||
|
|||
'testDefaultLifeTime' => 'PhpArrayAdapter does not allow configuring a default lifetime.', |
|||
'testPrune' => 'PhpArrayAdapter just proxies', |
|||
]; |
|||
|
|||
protected static $file; |
|||
|
|||
public static function setUpBeforeClass(): void |
|||
{ |
|||
self::$file = sys_get_temp_dir().'/symfony-cache/php-array-adapter-test.php'; |
|||
} |
|||
|
|||
protected function tearDown(): void |
|||
{ |
|||
if (file_exists(sys_get_temp_dir().'/symfony-cache')) { |
|||
FilesystemAdapterTest::rmdir(sys_get_temp_dir().'/symfony-cache'); |
|||
} |
|||
} |
|||
|
|||
public function createCachePool($defaultLifetime = 0, $testMethod = null) |
|||
{ |
|||
if ('testGetMetadata' === $testMethod) { |
|||
return new PhpArrayAdapter(self::$file, new FilesystemAdapter()); |
|||
} |
|||
|
|||
return new PhpArrayAdapterWrapper(self::$file, new NullAdapter()); |
|||
} |
|||
|
|||
public function testStore() |
|||
{ |
|||
$arrayWithRefs = []; |
|||
$arrayWithRefs[0] = 123; |
|||
$arrayWithRefs[1] = &$arrayWithRefs[0]; |
|||
|
|||
$object = (object) [ |
|||
'foo' => 'bar', |
|||
'foo2' => 'bar2', |
|||
]; |
|||
|
|||
$expected = [ |
|||
'null' => null, |
|||
'serializedString' => serialize($object), |
|||
'arrayWithRefs' => $arrayWithRefs, |
|||
'object' => $object, |
|||
'arrayWithObject' => ['bar' => $object], |
|||
]; |
|||
|
|||
$adapter = $this->createCachePool(); |
|||
$adapter->warmUp($expected); |
|||
|
|||
foreach ($expected as $key => $value) { |
|||
$this->assertSame(serialize($value), serialize($adapter->getItem($key)->get()), 'Warm up should create a PHP file that OPCache can load in memory'); |
|||
} |
|||
} |
|||
|
|||
public function testStoredFile() |
|||
{ |
|||
$data = [ |
|||
'integer' => 42, |
|||
'float' => 42.42, |
|||
'boolean' => true, |
|||
'array_simple' => ['foo', 'bar'], |
|||
'array_associative' => ['foo' => 'bar', 'foo2' => 'bar2'], |
|||
]; |
|||
$expected = [ |
|||
[ |
|||
'integer' => 0, |
|||
'float' => 1, |
|||
'boolean' => 2, |
|||
'array_simple' => 3, |
|||
'array_associative' => 4, |
|||
], |
|||
[ |
|||
0 => 42, |
|||
1 => 42.42, |
|||
2 => true, |
|||
3 => ['foo', 'bar'], |
|||
4 => ['foo' => 'bar', 'foo2' => 'bar2'], |
|||
], |
|||
]; |
|||
|
|||
$adapter = $this->createCachePool(); |
|||
$adapter->warmUp($data); |
|||
|
|||
$values = eval(substr(file_get_contents(self::$file), 6)); |
|||
|
|||
$this->assertSame($expected, $values, 'Warm up should create a PHP file that OPCache can load in memory'); |
|||
} |
|||
} |
|||
|
|||
class PhpArrayAdapterWrapper extends PhpArrayAdapter |
|||
{ |
|||
protected $data = []; |
|||
|
|||
public function save(CacheItemInterface $item) |
|||
{ |
|||
(\Closure::bind(function () use ($item) { |
|||
$key = $item->getKey(); |
|||
$this->keys[$key] = $id = \count($this->values); |
|||
$this->data[$key] = $this->values[$id] = $item->get(); |
|||
$this->warmUp($this->data); |
|||
list($this->keys, $this->values) = eval(substr(file_get_contents($this->file), 6)); |
|||
}, $this, PhpArrayAdapter::class))(); |
|||
|
|||
return true; |
|||
} |
|||
} |
|||
@ -0,0 +1,49 @@ |
|||
<?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\Tests\Adapter; |
|||
|
|||
use Symfony\Component\Cache\Adapter\FilesystemAdapter; |
|||
use Symfony\Component\Cache\Adapter\PhpArrayAdapter; |
|||
|
|||
/** |
|||
* @group time-sensitive |
|||
*/ |
|||
class PhpArrayAdapterWithFallbackTest extends AdapterTestCase |
|||
{ |
|||
protected $skippedTests = [ |
|||
'testGetItemInvalidKeys' => 'PhpArrayAdapter does not throw exceptions on invalid key.', |
|||
'testGetItemsInvalidKeys' => 'PhpArrayAdapter does not throw exceptions on invalid key.', |
|||
'testHasItemInvalidKeys' => 'PhpArrayAdapter does not throw exceptions on invalid key.', |
|||
'testDeleteItemInvalidKeys' => 'PhpArrayAdapter does not throw exceptions on invalid key.', |
|||
'testDeleteItemsInvalidKeys' => 'PhpArrayAdapter does not throw exceptions on invalid key.', |
|||
'testPrune' => 'PhpArrayAdapter just proxies', |
|||
]; |
|||
|
|||
protected static $file; |
|||
|
|||
public static function setUpBeforeClass(): void |
|||
{ |
|||
self::$file = sys_get_temp_dir().'/symfony-cache/php-array-adapter-test.php'; |
|||
} |
|||
|
|||
protected function tearDown(): void |
|||
{ |
|||
if (file_exists(sys_get_temp_dir().'/symfony-cache')) { |
|||
FilesystemAdapterTest::rmdir(sys_get_temp_dir().'/symfony-cache'); |
|||
} |
|||
} |
|||
|
|||
public function createCachePool($defaultLifetime = 0) |
|||
{ |
|||
return new PhpArrayAdapter(self::$file, new FilesystemAdapter('php-array-fallback', $defaultLifetime)); |
|||
} |
|||
} |
|||
@ -0,0 +1,43 @@ |
|||
<?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\Tests\Adapter; |
|||
|
|||
use Psr\Cache\CacheItemPoolInterface; |
|||
use Symfony\Component\Cache\Adapter\PhpFilesAdapter; |
|||
|
|||
/** |
|||
* @group time-sensitive |
|||
*/ |
|||
class PhpFilesAdapterTest extends AdapterTestCase |
|||
{ |
|||
protected $skippedTests = [ |
|||
'testDefaultLifeTime' => 'PhpFilesAdapter does not allow configuring a default lifetime.', |
|||
]; |
|||
|
|||
public function createCachePool() |
|||
{ |
|||
return new PhpFilesAdapter('sf-cache'); |
|||
} |
|||
|
|||
public static function tearDownAfterClass(): void |
|||
{ |
|||
FilesystemAdapterTest::rmdir(sys_get_temp_dir().'/symfony-cache'); |
|||
} |
|||
|
|||
protected function isPruned(CacheItemPoolInterface $cache, $name) |
|||
{ |
|||
$getFileMethod = (new \ReflectionObject($cache))->getMethod('getFile'); |
|||
$getFileMethod->setAccessible(true); |
|||
|
|||
return !file_exists($getFileMethod->invoke($cache, $name)); |
|||
} |
|||
} |
|||
@ -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\Tests\Adapter; |
|||
|
|||
use Predis\Connection\StreamConnection; |
|||
use Symfony\Component\Cache\Adapter\RedisAdapter; |
|||
|
|||
class PredisAdapterTest extends AbstractRedisAdapterTest |
|||
{ |
|||
public static function setUpBeforeClass(): void |
|||
{ |
|||
parent::setUpBeforeClass(); |
|||
self::$redis = new \Predis\Client(['host' => getenv('REDIS_HOST')]); |
|||
} |
|||
|
|||
public function testCreateConnection() |
|||
{ |
|||
$redisHost = getenv('REDIS_HOST'); |
|||
|
|||
$redis = RedisAdapter::createConnection('redis://'.$redisHost.'/1', ['class' => \Predis\Client::class, 'timeout' => 3]); |
|||
$this->assertInstanceOf(\Predis\Client::class, $redis); |
|||
|
|||
$connection = $redis->getConnection(); |
|||
$this->assertInstanceOf(StreamConnection::class, $connection); |
|||
|
|||
$params = [ |
|||
'scheme' => 'tcp', |
|||
'host' => $redisHost, |
|||
'port' => 6379, |
|||
'persistent' => 0, |
|||
'timeout' => 3, |
|||
'read_write_timeout' => 0, |
|||
'tcp_nodelay' => true, |
|||
'database' => '1', |
|||
]; |
|||
$this->assertSame($params, $connection->getParameters()->toArray()); |
|||
} |
|||
} |
|||
@ -0,0 +1,26 @@ |
|||
<?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\Tests\Adapter; |
|||
|
|||
class PredisClusterAdapterTest extends AbstractRedisAdapterTest |
|||
{ |
|||
public static function setUpBeforeClass(): void |
|||
{ |
|||
parent::setUpBeforeClass(); |
|||
self::$redis = new \Predis\Client([['host' => getenv('REDIS_HOST')]]); |
|||
} |
|||
|
|||
public static function tearDownAfterClass(): void |
|||
{ |
|||
self::$redis = null; |
|||
} |
|||
} |
|||
@ -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\Tests\Adapter; |
|||
|
|||
use Symfony\Component\Cache\Adapter\RedisAdapter; |
|||
|
|||
class PredisRedisClusterAdapterTest extends AbstractRedisAdapterTest |
|||
{ |
|||
public static function setUpBeforeClass(): void |
|||
{ |
|||
if (!$hosts = getenv('REDIS_CLUSTER_HOSTS')) { |
|||
self::markTestSkipped('REDIS_CLUSTER_HOSTS env var is not defined.'); |
|||
} |
|||
|
|||
self::$redis = RedisAdapter::createConnection('redis:?host['.str_replace(' ', ']&host[', $hosts).']', ['class' => \Predis\Client::class, 'redis_cluster' => true]); |
|||
} |
|||
|
|||
public static function tearDownAfterClass(): void |
|||
{ |
|||
self::$redis = null; |
|||
} |
|||
} |
|||
@ -0,0 +1,34 @@ |
|||
<?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\Tests\Adapter; |
|||
|
|||
use Symfony\Component\Cache\Adapter\RedisTagAwareAdapter; |
|||
use Symfony\Component\Cache\Tests\Traits\TagAwareTestTrait; |
|||
|
|||
class PredisTagAwareAdapterTest extends PredisAdapterTest |
|||
{ |
|||
use TagAwareTestTrait; |
|||
|
|||
protected function setUp(): void |
|||
{ |
|||
parent::setUp(); |
|||
$this->skippedTests['testTagItemExpiry'] = 'Testing expiration slows down the test suite'; |
|||
} |
|||
|
|||
public function createCachePool($defaultLifetime = 0) |
|||
{ |
|||
$this->assertInstanceOf(\Predis\Client::class, self::$redis); |
|||
$adapter = new RedisTagAwareAdapter(self::$redis, str_replace('\\', '.', __CLASS__), $defaultLifetime); |
|||
|
|||
return $adapter; |
|||
} |
|||
} |
|||
@ -0,0 +1,34 @@ |
|||
<?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\Tests\Adapter; |
|||
|
|||
use Symfony\Component\Cache\Adapter\RedisTagAwareAdapter; |
|||
use Symfony\Component\Cache\Tests\Traits\TagAwareTestTrait; |
|||
|
|||
class PredisTagAwareClusterAdapterTest extends PredisClusterAdapterTest |
|||
{ |
|||
use TagAwareTestTrait; |
|||
|
|||
protected function setUp(): void |
|||
{ |
|||
parent::setUp(); |
|||
$this->skippedTests['testTagItemExpiry'] = 'Testing expiration slows down the test suite'; |
|||
} |
|||
|
|||
public function createCachePool($defaultLifetime = 0) |
|||
{ |
|||
$this->assertInstanceOf(\Predis\Client::class, self::$redis); |
|||
$adapter = new RedisTagAwareAdapter(self::$redis, str_replace('\\', '.', __CLASS__), $defaultLifetime); |
|||
|
|||
return $adapter; |
|||
} |
|||
} |
|||
@ -0,0 +1,34 @@ |
|||
<?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\Tests\Adapter; |
|||
|
|||
use Symfony\Component\Cache\Adapter\RedisTagAwareAdapter; |
|||
use Symfony\Component\Cache\Tests\Traits\TagAwareTestTrait; |
|||
|
|||
class PredisTagAwareRedisClusterAdapterTest extends PredisRedisClusterAdapterTest |
|||
{ |
|||
use TagAwareTestTrait; |
|||
|
|||
protected function setUp(): void |
|||
{ |
|||
parent::setUp(); |
|||
$this->skippedTests['testTagItemExpiry'] = 'Testing expiration slows down the test suite'; |
|||
} |
|||
|
|||
public function createCachePool($defaultLifetime = 0) |
|||
{ |
|||
$this->assertInstanceOf(\Predis\Client::class, self::$redis); |
|||
$adapter = new RedisTagAwareAdapter(self::$redis, str_replace('\\', '.', __CLASS__), $defaultLifetime); |
|||
|
|||
return $adapter; |
|||
} |
|||
} |
|||
@ -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\Tests\Adapter; |
|||
|
|||
use Psr\Cache\CacheItemInterface; |
|||
use Symfony\Component\Cache\Adapter\ArrayAdapter; |
|||
use Symfony\Component\Cache\Adapter\FilesystemAdapter; |
|||
use Symfony\Component\Cache\Adapter\ProxyAdapter; |
|||
use Symfony\Component\Cache\CacheItem; |
|||
|
|||
/** |
|||
* @group time-sensitive |
|||
*/ |
|||
class ProxyAdapterTest extends AdapterTestCase |
|||
{ |
|||
protected $skippedTests = [ |
|||
'testDeferredSaveWithoutCommit' => 'Assumes a shared cache which ArrayAdapter is not.', |
|||
'testSaveWithoutExpire' => 'Assumes a shared cache which ArrayAdapter is not.', |
|||
'testPrune' => 'ProxyAdapter just proxies', |
|||
]; |
|||
|
|||
public function createCachePool($defaultLifetime = 0, $testMethod = null) |
|||
{ |
|||
if ('testGetMetadata' === $testMethod) { |
|||
return new ProxyAdapter(new FilesystemAdapter(), '', $defaultLifetime); |
|||
} |
|||
|
|||
return new ProxyAdapter(new ArrayAdapter(), '', $defaultLifetime); |
|||
} |
|||
|
|||
public function testProxyfiedItem() |
|||
{ |
|||
$this->expectException('Exception'); |
|||
$this->expectExceptionMessage('OK bar'); |
|||
$item = new CacheItem(); |
|||
$pool = new ProxyAdapter(new TestingArrayAdapter($item)); |
|||
|
|||
$proxyItem = $pool->getItem('foo'); |
|||
|
|||
$this->assertNotSame($item, $proxyItem); |
|||
$pool->save($proxyItem->set('bar')); |
|||
} |
|||
} |
|||
|
|||
class TestingArrayAdapter extends ArrayAdapter |
|||
{ |
|||
private $item; |
|||
|
|||
public function __construct(CacheItemInterface $item) |
|||
{ |
|||
$this->item = $item; |
|||
} |
|||
|
|||
public function getItem($key) |
|||
{ |
|||
return $this->item; |
|||
} |
|||
|
|||
public function save(CacheItemInterface $item) |
|||
{ |
|||
if ($item === $this->item) { |
|||
throw new \Exception('OK '.$item->get()); |
|||
} |
|||
} |
|||
} |
|||
@ -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\Cache\Tests\Adapter; |
|||
|
|||
use Symfony\Component\Cache\Adapter\ArrayAdapter; |
|||
use Symfony\Component\Cache\Adapter\FilesystemAdapter; |
|||
use Symfony\Component\Cache\Adapter\Psr16Adapter; |
|||
use Symfony\Component\Cache\Psr16Cache; |
|||
|
|||
/** |
|||
* @group time-sensitive |
|||
*/ |
|||
class Psr16AdapterTest extends AdapterTestCase |
|||
{ |
|||
protected $skippedTests = [ |
|||
'testPrune' => 'Psr16adapter just proxies', |
|||
]; |
|||
|
|||
public function createCachePool($defaultLifetime = 0) |
|||
{ |
|||
return new Psr16Adapter(new Psr16Cache(new FilesystemAdapter()), '', $defaultLifetime); |
|||
} |
|||
|
|||
public function testValidCacheKeyWithNamespace() |
|||
{ |
|||
$cache = new Psr16Adapter(new Psr16Cache(new ArrayAdapter()), 'some_namespace', 0); |
|||
$item = $cache->getItem('my_key'); |
|||
$item->set('someValue'); |
|||
$cache->save($item); |
|||
|
|||
$this->assertTrue($cache->getItem('my_key')->isHit(), 'Stored item is successfully retrieved.'); |
|||
} |
|||
} |
|||
@ -0,0 +1,108 @@ |
|||
<?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\Tests\Adapter; |
|||
|
|||
use Symfony\Component\Cache\Adapter\AbstractAdapter; |
|||
use Symfony\Component\Cache\Adapter\RedisAdapter; |
|||
use Symfony\Component\Cache\Traits\RedisProxy; |
|||
|
|||
class RedisAdapterTest extends AbstractRedisAdapterTest |
|||
{ |
|||
public static function setUpBeforeClass(): void |
|||
{ |
|||
parent::setUpBeforeClass(); |
|||
self::$redis = AbstractAdapter::createConnection('redis://'.getenv('REDIS_HOST'), ['lazy' => true]); |
|||
} |
|||
|
|||
public function createCachePool($defaultLifetime = 0) |
|||
{ |
|||
$adapter = parent::createCachePool($defaultLifetime); |
|||
$this->assertInstanceOf(RedisProxy::class, self::$redis); |
|||
|
|||
return $adapter; |
|||
} |
|||
|
|||
/** |
|||
* @dataProvider provideValidSchemes |
|||
*/ |
|||
public function testCreateConnection($dsnScheme) |
|||
{ |
|||
$redis = RedisAdapter::createConnection($dsnScheme.':?host[h1]&host[h2]&host[/foo:]'); |
|||
$this->assertInstanceOf(\RedisArray::class, $redis); |
|||
$this->assertSame(['h1:6379', 'h2:6379', '/foo'], $redis->_hosts()); |
|||
@$redis = null; // some versions of phpredis connect on destruct, let's silence the warning |
|||
|
|||
$redisHost = getenv('REDIS_HOST'); |
|||
|
|||
$redis = RedisAdapter::createConnection($dsnScheme.'://'.$redisHost); |
|||
$this->assertInstanceOf(\Redis::class, $redis); |
|||
$this->assertTrue($redis->isConnected()); |
|||
$this->assertSame(0, $redis->getDbNum()); |
|||
|
|||
$redis = RedisAdapter::createConnection($dsnScheme.'://'.$redisHost.'/2'); |
|||
$this->assertSame(2, $redis->getDbNum()); |
|||
|
|||
$redis = RedisAdapter::createConnection($dsnScheme.'://'.$redisHost, ['timeout' => 3]); |
|||
$this->assertEquals(3, $redis->getTimeout()); |
|||
|
|||
$redis = RedisAdapter::createConnection($dsnScheme.'://'.$redisHost.'?timeout=4'); |
|||
$this->assertEquals(4, $redis->getTimeout()); |
|||
|
|||
$redis = RedisAdapter::createConnection($dsnScheme.'://'.$redisHost, ['read_timeout' => 5]); |
|||
$this->assertEquals(5, $redis->getReadTimeout()); |
|||
} |
|||
|
|||
/** |
|||
* @dataProvider provideFailedCreateConnection |
|||
*/ |
|||
public function testFailedCreateConnection($dsn) |
|||
{ |
|||
$this->expectException('Symfony\Component\Cache\Exception\InvalidArgumentException'); |
|||
$this->expectExceptionMessage('Redis connection failed'); |
|||
RedisAdapter::createConnection($dsn); |
|||
} |
|||
|
|||
public function provideFailedCreateConnection() |
|||
{ |
|||
return [ |
|||
['redis://localhost:1234'], |
|||
['redis://foo@localhost'], |
|||
['redis://localhost/123'], |
|||
]; |
|||
} |
|||
|
|||
/** |
|||
* @dataProvider provideInvalidCreateConnection |
|||
*/ |
|||
public function testInvalidCreateConnection($dsn) |
|||
{ |
|||
$this->expectException('Symfony\Component\Cache\Exception\InvalidArgumentException'); |
|||
$this->expectExceptionMessage('Invalid Redis DSN'); |
|||
RedisAdapter::createConnection($dsn); |
|||
} |
|||
|
|||
public function provideValidSchemes() |
|||
{ |
|||
return [ |
|||
['redis'], |
|||
['rediss'], |
|||
]; |
|||
} |
|||
|
|||
public function provideInvalidCreateConnection() |
|||
{ |
|||
return [ |
|||
['foo://localhost'], |
|||
['redis://'], |
|||
]; |
|||
} |
|||
} |
|||
@ -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\Cache\Tests\Adapter; |
|||
|
|||
class RedisArrayAdapterTest extends AbstractRedisAdapterTest |
|||
{ |
|||
public static function setUpBeforeClass(): void |
|||
{ |
|||
parent::setupBeforeClass(); |
|||
if (!class_exists('RedisArray')) { |
|||
self::markTestSkipped('The RedisArray class is required.'); |
|||
} |
|||
self::$redis = new \RedisArray([getenv('REDIS_HOST')], ['lazy_connect' => true]); |
|||
} |
|||
} |
|||
@ -0,0 +1,58 @@ |
|||
<?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\Tests\Adapter; |
|||
|
|||
use Symfony\Component\Cache\Adapter\AbstractAdapter; |
|||
use Symfony\Component\Cache\Adapter\RedisAdapter; |
|||
use Symfony\Component\Cache\Traits\RedisClusterProxy; |
|||
|
|||
class RedisClusterAdapterTest extends AbstractRedisAdapterTest |
|||
{ |
|||
public static function setUpBeforeClass(): void |
|||
{ |
|||
if (!class_exists('RedisCluster')) { |
|||
self::markTestSkipped('The RedisCluster class is required.'); |
|||
} |
|||
if (!$hosts = getenv('REDIS_CLUSTER_HOSTS')) { |
|||
self::markTestSkipped('REDIS_CLUSTER_HOSTS env var is not defined.'); |
|||
} |
|||
|
|||
self::$redis = AbstractAdapter::createConnection('redis:?host['.str_replace(' ', ']&host[', $hosts).']', ['lazy' => true, 'redis_cluster' => true]); |
|||
} |
|||
|
|||
public function createCachePool($defaultLifetime = 0) |
|||
{ |
|||
$this->assertInstanceOf(RedisClusterProxy::class, self::$redis); |
|||
$adapter = new RedisAdapter(self::$redis, str_replace('\\', '.', __CLASS__), $defaultLifetime); |
|||
|
|||
return $adapter; |
|||
} |
|||
|
|||
/** |
|||
* @dataProvider provideFailedCreateConnection |
|||
*/ |
|||
public function testFailedCreateConnection($dsn) |
|||
{ |
|||
$this->expectException('Symfony\Component\Cache\Exception\InvalidArgumentException'); |
|||
$this->expectExceptionMessage('Redis connection failed'); |
|||
RedisAdapter::createConnection($dsn); |
|||
} |
|||
|
|||
public function provideFailedCreateConnection() |
|||
{ |
|||
return [ |
|||
['redis://localhost:1234?redis_cluster=1'], |
|||
['redis://foo@localhost?redis_cluster=1'], |
|||
['redis://localhost/123?redis_cluster=1'], |
|||
]; |
|||
} |
|||
} |
|||
@ -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\Tests\Adapter; |
|||
|
|||
use Symfony\Component\Cache\Adapter\RedisTagAwareAdapter; |
|||
use Symfony\Component\Cache\Tests\Traits\TagAwareTestTrait; |
|||
use Symfony\Component\Cache\Traits\RedisProxy; |
|||
|
|||
class RedisTagAwareAdapterTest extends RedisAdapterTest |
|||
{ |
|||
use TagAwareTestTrait; |
|||
|
|||
protected function setUp(): void |
|||
{ |
|||
parent::setUp(); |
|||
$this->skippedTests['testTagItemExpiry'] = 'Testing expiration slows down the test suite'; |
|||
} |
|||
|
|||
public function createCachePool($defaultLifetime = 0) |
|||
{ |
|||
$this->assertInstanceOf(RedisProxy::class, self::$redis); |
|||
$adapter = new RedisTagAwareAdapter(self::$redis, str_replace('\\', '.', __CLASS__), $defaultLifetime); |
|||
|
|||
return $adapter; |
|||
} |
|||
} |
|||
@ -0,0 +1,34 @@ |
|||
<?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\Tests\Adapter; |
|||
|
|||
use Symfony\Component\Cache\Adapter\RedisTagAwareAdapter; |
|||
use Symfony\Component\Cache\Tests\Traits\TagAwareTestTrait; |
|||
|
|||
class RedisTagAwareArrayAdapterTest extends RedisArrayAdapterTest |
|||
{ |
|||
use TagAwareTestTrait; |
|||
|
|||
protected function setUp(): void |
|||
{ |
|||
parent::setUp(); |
|||
$this->skippedTests['testTagItemExpiry'] = 'Testing expiration slows down the test suite'; |
|||
} |
|||
|
|||
public function createCachePool($defaultLifetime = 0) |
|||
{ |
|||
$this->assertInstanceOf(\RedisArray::class, self::$redis); |
|||
$adapter = new RedisTagAwareAdapter(self::$redis, str_replace('\\', '.', __CLASS__), $defaultLifetime); |
|||
|
|||
return $adapter; |
|||
} |
|||
} |
|||
@ -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\Tests\Adapter; |
|||
|
|||
use Symfony\Component\Cache\Adapter\RedisTagAwareAdapter; |
|||
use Symfony\Component\Cache\Tests\Traits\TagAwareTestTrait; |
|||
use Symfony\Component\Cache\Traits\RedisClusterProxy; |
|||
|
|||
class RedisTagAwareClusterAdapterTest extends RedisClusterAdapterTest |
|||
{ |
|||
use TagAwareTestTrait; |
|||
|
|||
protected function setUp(): void |
|||
{ |
|||
parent::setUp(); |
|||
$this->skippedTests['testTagItemExpiry'] = 'Testing expiration slows down the test suite'; |
|||
} |
|||
|
|||
public function createCachePool($defaultLifetime = 0) |
|||
{ |
|||
$this->assertInstanceOf(RedisClusterProxy::class, self::$redis); |
|||
$adapter = new RedisTagAwareAdapter(self::$redis, str_replace('\\', '.', __CLASS__), $defaultLifetime); |
|||
|
|||
return $adapter; |
|||
} |
|||
} |
|||
@ -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\Cache\Tests\Adapter; |
|||
|
|||
use Symfony\Component\Cache\Adapter\SimpleCacheAdapter; |
|||
use Symfony\Component\Cache\Simple\ArrayCache; |
|||
use Symfony\Component\Cache\Simple\FilesystemCache; |
|||
|
|||
/** |
|||
* @group time-sensitive |
|||
* @group legacy |
|||
*/ |
|||
class SimpleCacheAdapterTest extends AdapterTestCase |
|||
{ |
|||
protected $skippedTests = [ |
|||
'testPrune' => 'SimpleCache just proxies', |
|||
]; |
|||
|
|||
public function createCachePool($defaultLifetime = 0) |
|||
{ |
|||
return new SimpleCacheAdapter(new FilesystemCache(), '', $defaultLifetime); |
|||
} |
|||
|
|||
public function testValidCacheKeyWithNamespace() |
|||
{ |
|||
$cache = new SimpleCacheAdapter(new ArrayCache(), 'some_namespace', 0); |
|||
$item = $cache->getItem('my_key'); |
|||
$item->set('someValue'); |
|||
$cache->save($item); |
|||
|
|||
$this->assertTrue($cache->getItem('my_key')->isHit(), 'Stored item is successfully retrieved.'); |
|||
} |
|||
} |
|||
@ -0,0 +1,111 @@ |
|||
<?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\Tests\Adapter; |
|||
|
|||
use PHPUnit\Framework\MockObject\MockObject; |
|||
use Symfony\Component\Cache\Adapter\AdapterInterface; |
|||
use Symfony\Component\Cache\Adapter\FilesystemAdapter; |
|||
use Symfony\Component\Cache\Adapter\TagAwareAdapter; |
|||
use Symfony\Component\Cache\Tests\Traits\TagAwareTestTrait; |
|||
|
|||
/** |
|||
* @group time-sensitive |
|||
*/ |
|||
class TagAwareAdapterTest extends AdapterTestCase |
|||
{ |
|||
use TagAwareTestTrait; |
|||
|
|||
public function createCachePool($defaultLifetime = 0) |
|||
{ |
|||
return new TagAwareAdapter(new FilesystemAdapter('', $defaultLifetime)); |
|||
} |
|||
|
|||
public static function tearDownAfterClass(): void |
|||
{ |
|||
FilesystemAdapterTest::rmdir(sys_get_temp_dir().'/symfony-cache'); |
|||
} |
|||
|
|||
/** |
|||
* Test feature specific to TagAwareAdapter as it implicit needs to save deferred when also saving expiry info. |
|||
*/ |
|||
public function testInvalidateCommitsSeperatePools() |
|||
{ |
|||
$pool1 = $this->createCachePool(); |
|||
|
|||
$foo = $pool1->getItem('foo'); |
|||
$foo->tag('tag'); |
|||
|
|||
$pool1->saveDeferred($foo->set('foo')); |
|||
$pool1->invalidateTags(['tag']); |
|||
|
|||
$pool2 = $this->createCachePool(); |
|||
$foo = $pool2->getItem('foo'); |
|||
|
|||
$this->assertTrue($foo->isHit()); |
|||
} |
|||
|
|||
public function testPrune() |
|||
{ |
|||
$cache = new TagAwareAdapter($this->getPruneableMock()); |
|||
$this->assertTrue($cache->prune()); |
|||
|
|||
$cache = new TagAwareAdapter($this->getNonPruneableMock()); |
|||
$this->assertFalse($cache->prune()); |
|||
|
|||
$cache = new TagAwareAdapter($this->getFailingPruneableMock()); |
|||
$this->assertFalse($cache->prune()); |
|||
} |
|||
|
|||
/** |
|||
* @return MockObject|PruneableCacheInterface |
|||
*/ |
|||
private function getPruneableMock() |
|||
{ |
|||
$pruneable = $this |
|||
->getMockBuilder(PruneableCacheInterface::class) |
|||
->getMock(); |
|||
|
|||
$pruneable |
|||
->expects($this->atLeastOnce()) |
|||
->method('prune') |
|||
->willReturn(true); |
|||
|
|||
return $pruneable; |
|||
} |
|||
|
|||
/** |
|||
* @return MockObject|PruneableCacheInterface |
|||
*/ |
|||
private function getFailingPruneableMock() |
|||
{ |
|||
$pruneable = $this |
|||
->getMockBuilder(PruneableCacheInterface::class) |
|||
->getMock(); |
|||
|
|||
$pruneable |
|||
->expects($this->atLeastOnce()) |
|||
->method('prune') |
|||
->willReturn(false); |
|||
|
|||
return $pruneable; |
|||
} |
|||
|
|||
/** |
|||
* @return MockObject|AdapterInterface |
|||
*/ |
|||
private function getNonPruneableMock() |
|||
{ |
|||
return $this |
|||
->getMockBuilder(AdapterInterface::class) |
|||
->getMock(); |
|||
} |
|||
} |
|||
@ -0,0 +1,38 @@ |
|||
<?php |
|||
|
|||
namespace Symfony\Component\Cache\Tests\Adapter; |
|||
|
|||
use PHPUnit\Framework\TestCase; |
|||
use Psr\Cache\CacheItemPoolInterface; |
|||
use Symfony\Component\Cache\Adapter\ArrayAdapter; |
|||
use Symfony\Component\Cache\Adapter\ProxyAdapter; |
|||
use Symfony\Component\Cache\Adapter\TagAwareAdapter; |
|||
use Symfony\Component\Cache\Tests\Fixtures\ExternalAdapter; |
|||
|
|||
class TagAwareAndProxyAdapterIntegrationTest extends TestCase |
|||
{ |
|||
/** |
|||
* @dataProvider dataProvider |
|||
*/ |
|||
public function testIntegrationUsingProxiedAdapter(CacheItemPoolInterface $proxiedAdapter) |
|||
{ |
|||
$cache = new TagAwareAdapter(new ProxyAdapter($proxiedAdapter)); |
|||
|
|||
$item = $cache->getItem('foo'); |
|||
$item->tag(['tag1', 'tag2']); |
|||
$item->set('bar'); |
|||
$cache->save($item); |
|||
|
|||
$this->assertSame('bar', $cache->getItem('foo')->get()); |
|||
} |
|||
|
|||
public function dataProvider() |
|||
{ |
|||
return [ |
|||
[new ArrayAdapter()], |
|||
// also testing with a non-AdapterInterface implementation |
|||
// because the ProxyAdapter behaves slightly different for those |
|||
[new ExternalAdapter()], |
|||
]; |
|||
} |
|||
} |
|||
@ -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\Tests\Adapter; |
|||
|
|||
use Symfony\Component\Cache\Adapter\FilesystemAdapter; |
|||
use Symfony\Component\Cache\Adapter\TraceableAdapter; |
|||
|
|||
/** |
|||
* @group time-sensitive |
|||
*/ |
|||
class TraceableAdapterTest extends AdapterTestCase |
|||
{ |
|||
protected $skippedTests = [ |
|||
'testPrune' => 'TraceableAdapter just proxies', |
|||
]; |
|||
|
|||
public function createCachePool($defaultLifetime = 0) |
|||
{ |
|||
return new TraceableAdapter(new FilesystemAdapter('', $defaultLifetime)); |
|||
} |
|||
|
|||
public function testGetItemMissTrace() |
|||
{ |
|||
$pool = $this->createCachePool(); |
|||
$pool->getItem('k'); |
|||
$calls = $pool->getCalls(); |
|||
$this->assertCount(1, $calls); |
|||
|
|||
$call = $calls[0]; |
|||
$this->assertSame('getItem', $call->name); |
|||
$this->assertSame(['k' => false], $call->result); |
|||
$this->assertSame(0, $call->hits); |
|||
$this->assertSame(1, $call->misses); |
|||
$this->assertNotEmpty($call->start); |
|||
$this->assertNotEmpty($call->end); |
|||
} |
|||
|
|||
public function testGetItemHitTrace() |
|||
{ |
|||
$pool = $this->createCachePool(); |
|||
$item = $pool->getItem('k')->set('foo'); |
|||
$pool->save($item); |
|||
$pool->getItem('k'); |
|||
$calls = $pool->getCalls(); |
|||
$this->assertCount(3, $calls); |
|||
|
|||
$call = $calls[2]; |
|||
$this->assertSame(1, $call->hits); |
|||
$this->assertSame(0, $call->misses); |
|||
} |
|||
|
|||
public function testGetItemsMissTrace() |
|||
{ |
|||
$pool = $this->createCachePool(); |
|||
$arg = ['k0', 'k1']; |
|||
$items = $pool->getItems($arg); |
|||
foreach ($items as $item) { |
|||
} |
|||
$calls = $pool->getCalls(); |
|||
$this->assertCount(1, $calls); |
|||
|
|||
$call = $calls[0]; |
|||
$this->assertSame('getItems', $call->name); |
|||
$this->assertSame(['k0' => false, 'k1' => false], $call->result); |
|||
$this->assertSame(2, $call->misses); |
|||
$this->assertNotEmpty($call->start); |
|||
$this->assertNotEmpty($call->end); |
|||
} |
|||
|
|||
public function testHasItemMissTrace() |
|||
{ |
|||
$pool = $this->createCachePool(); |
|||
$pool->hasItem('k'); |
|||
$calls = $pool->getCalls(); |
|||
$this->assertCount(1, $calls); |
|||
|
|||
$call = $calls[0]; |
|||
$this->assertSame('hasItem', $call->name); |
|||
$this->assertSame(['k' => false], $call->result); |
|||
$this->assertNotEmpty($call->start); |
|||
$this->assertNotEmpty($call->end); |
|||
} |
|||
|
|||
public function testHasItemHitTrace() |
|||
{ |
|||
$pool = $this->createCachePool(); |
|||
$item = $pool->getItem('k')->set('foo'); |
|||
$pool->save($item); |
|||
$pool->hasItem('k'); |
|||
$calls = $pool->getCalls(); |
|||
$this->assertCount(3, $calls); |
|||
|
|||
$call = $calls[2]; |
|||
$this->assertSame('hasItem', $call->name); |
|||
$this->assertSame(['k' => true], $call->result); |
|||
$this->assertNotEmpty($call->start); |
|||
$this->assertNotEmpty($call->end); |
|||
} |
|||
|
|||
public function testDeleteItemTrace() |
|||
{ |
|||
$pool = $this->createCachePool(); |
|||
$pool->deleteItem('k'); |
|||
$calls = $pool->getCalls(); |
|||
$this->assertCount(1, $calls); |
|||
|
|||
$call = $calls[0]; |
|||
$this->assertSame('deleteItem', $call->name); |
|||
$this->assertSame(['k' => true], $call->result); |
|||
$this->assertSame(0, $call->hits); |
|||
$this->assertSame(0, $call->misses); |
|||
$this->assertNotEmpty($call->start); |
|||
$this->assertNotEmpty($call->end); |
|||
} |
|||
|
|||
public function testDeleteItemsTrace() |
|||
{ |
|||
$pool = $this->createCachePool(); |
|||
$arg = ['k0', 'k1']; |
|||
$pool->deleteItems($arg); |
|||
$calls = $pool->getCalls(); |
|||
$this->assertCount(1, $calls); |
|||
|
|||
$call = $calls[0]; |
|||
$this->assertSame('deleteItems', $call->name); |
|||
$this->assertSame(['keys' => $arg, 'result' => true], $call->result); |
|||
$this->assertSame(0, $call->hits); |
|||
$this->assertSame(0, $call->misses); |
|||
$this->assertNotEmpty($call->start); |
|||
$this->assertNotEmpty($call->end); |
|||
} |
|||
|
|||
public function testSaveTrace() |
|||
{ |
|||
$pool = $this->createCachePool(); |
|||
$item = $pool->getItem('k')->set('foo'); |
|||
$pool->save($item); |
|||
$calls = $pool->getCalls(); |
|||
$this->assertCount(2, $calls); |
|||
|
|||
$call = $calls[1]; |
|||
$this->assertSame('save', $call->name); |
|||
$this->assertSame(['k' => true], $call->result); |
|||
$this->assertSame(0, $call->hits); |
|||
$this->assertSame(0, $call->misses); |
|||
$this->assertNotEmpty($call->start); |
|||
$this->assertNotEmpty($call->end); |
|||
} |
|||
|
|||
public function testSaveDeferredTrace() |
|||
{ |
|||
$pool = $this->createCachePool(); |
|||
$item = $pool->getItem('k')->set('foo'); |
|||
$pool->saveDeferred($item); |
|||
$calls = $pool->getCalls(); |
|||
$this->assertCount(2, $calls); |
|||
|
|||
$call = $calls[1]; |
|||
$this->assertSame('saveDeferred', $call->name); |
|||
$this->assertSame(['k' => true], $call->result); |
|||
$this->assertSame(0, $call->hits); |
|||
$this->assertSame(0, $call->misses); |
|||
$this->assertNotEmpty($call->start); |
|||
$this->assertNotEmpty($call->end); |
|||
} |
|||
|
|||
public function testCommitTrace() |
|||
{ |
|||
$pool = $this->createCachePool(); |
|||
$pool->commit(); |
|||
$calls = $pool->getCalls(); |
|||
$this->assertCount(1, $calls); |
|||
|
|||
$call = $calls[0]; |
|||
$this->assertSame('commit', $call->name); |
|||
$this->assertTrue($call->result); |
|||
$this->assertSame(0, $call->hits); |
|||
$this->assertSame(0, $call->misses); |
|||
$this->assertNotEmpty($call->start); |
|||
$this->assertNotEmpty($call->end); |
|||
} |
|||
} |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue