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