diff --git a/composer.json b/composer.json
index 5fdff21937..832c87c0e1 100644
--- a/composer.json
+++ b/composer.json
@@ -21,6 +21,7 @@
"twig/twig": "~1.28|~2.0",
"psr/cache": "~1.0",
"psr/log": "~1.0",
+ "psr/simple-cache": "^1.0",
"symfony/polyfill-intl-icu": "~1.0",
"symfony/polyfill-mbstring": "~1.0",
"symfony/polyfill-php56": "~1.0",
@@ -79,7 +80,7 @@
"symfony/yaml": "self.version"
},
"require-dev": {
- "cache/integration-tests": "dev-master",
+ "cache/integration-tests": "^0.15.0",
"doctrine/cache": "~1.6",
"doctrine/data-fixtures": "1.0.*",
"doctrine/dbal": "~2.4",
@@ -99,7 +100,8 @@
"phpdocumentor/type-resolver": "<0.2.0"
},
"provide": {
- "psr/cache-implementation": "1.0"
+ "psr/cache-implementation": "1.0",
+ "psr/simple-cache-implementation": "1.0"
},
"autoload": {
"psr-4": {
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
index 336c320de9..ce219c21e2 100644
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -62,6 +62,8 @@
*/ abstract class AbstractAdapter implements AdapterInterface, LoggerAwareInterface { - use LoggerAwareTrait; + use AbstractTrait; private static $apcuSupported; private static $phpFilesSupported; - private $namespace; - private $deferred = array(); private $createCacheItem; private $mergeByLifetime; - /** - * @var int|null The maximum length to enforce for identifiers or null when no limit applies - */ - protected $maxIdLength; - protected function __construct($namespace = '', $defaultLifetime = 0) { $this->namespace = '' === $namespace ? '' : $this->getId($namespace).':'; @@ -130,52 +123,6 @@ abstract class AbstractAdapter implements AdapterInterface, LoggerAwareInterface throw new InvalidArgumentException(sprintf('Unsupported DSN: %s.', $dsn)); } - /** - * Fetches several cache items. - * - * @param array $ids The cache identifiers to fetch - * - * @return array|\Traversable The corresponding values found in the cache - */ - abstract protected function doFetch(array $ids); - - /** - * Confirms if the cache contains specified cache item. - * - * @param string $id The identifier for which to check existence - * - * @return bool True if item exists in the cache, false otherwise - */ - abstract protected function doHave($id); - - /** - * Deletes all items in the pool. - * - * @param string The prefix used for all identifiers managed by this pool - * - * @return bool True if the pool was successfully cleared, false otherwise - */ - abstract protected function doClear($namespace); - - /** - * Removes multiple items from the pool. - * - * @param array $ids An array of 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); - - /** - * Persists several cache items immediately. - * - * @param array $values The values to cache, indexed by their cache identifier - * @param int $lifetime The lifetime of the cached values, 0 for persisting until manual cleaning - * - * @return array|bool The identifiers that failed to be cached or a boolean stating if caching succeeded or not - */ - abstract protected function doSave(array $values, $lifetime); - /** * {@inheritdoc} */ @@ -225,87 +172,6 @@ abstract class AbstractAdapter implements AdapterInterface, LoggerAwareInterface return $this->generateItems($items, $ids); } - /** - * {@inheritdoc} - */ - public function hasItem($key) - { - $id = $this->getId($key); - - if (isset($this->deferred[$key])) { - $this->commit(); - } - - try { - return $this->doHave($id); - } catch (\Exception $e) { - CacheItem::log($this->logger, 'Failed to check if key "{key}" is cached', array('key' => $key, 'exception' => $e)); - - return false; - } - } - - /** - * {@inheritdoc} - */ - public function clear() - { - $this->deferred = array(); - - try { - return $this->doClear($this->namespace); - } catch (\Exception $e) { - CacheItem::log($this->logger, 'Failed to clear the cache', array('exception' => $e)); - - return false; - } - } - - /** - * {@inheritdoc} - */ - public function deleteItem($key) - { - return $this->deleteItems(array($key)); - } - - /** - * {@inheritdoc} - */ - public function deleteItems(array $keys) - { - $ids = array(); - - foreach ($keys as $key) { - $ids[$key] = $this->getId($key); - unset($this->deferred[$key]); - } - - try { - if ($this->doDelete($ids)) { - 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(array($id))) { - continue; - } - } catch (\Exception $e) { - } - CacheItem::log($this->logger, 'Failed to delete key "{key}"', array('key' => $key, 'exception' => $e)); - $ok = false; - } - - return $ok; - } - /** * {@inheritdoc} */ @@ -394,47 +260,6 @@ abstract class AbstractAdapter implements AdapterInterface, LoggerAwareInterface } } - /** - * Like the native unserialize() function but throws an exception if anything goes wrong. - * - * @param string $value - * - * @return mixed - * - * @throws \Exception - */ - protected static function unserialize($value) - { - if ('b:0;' === $value) { - return false; - } - $unserializeCallbackHandler = ini_set('unserialize_callback_func', __CLASS__.'::handleUnserializeCallback'); - try { - if (false !== $value = unserialize($value)) { - return $value; - } - throw new \DomainException('Failed to unserialize cached value'); - } catch (\Error $e) { - throw new \ErrorException($e->getMessage(), $e->getCode(), E_ERROR, $e->getFile(), $e->getLine()); - } finally { - ini_set('unserialize_callback_func', $unserializeCallbackHandler); - } - } - - private function getId($key) - { - CacheItem::validateKey($key); - - if (null === $this->maxIdLength) { - return $this->namespace.$key; - } - if (strlen($id = $this->namespace.$key) > $this->maxIdLength) { - $id = $this->namespace.substr_replace(base64_encode(hash('sha256', $key, true)), ':', -22); - } - - return $id; - } - private function generateItems($items, &$keys) { $f = $this->createCacheItem; @@ -453,12 +278,4 @@ abstract class AbstractAdapter implements AdapterInterface, LoggerAwareInterface yield $key => $f($key, null, false); } } - - /** - * @internal - */ - public static function handleUnserializeCallback($class) - { - throw new \DomainException('Class not found: '.$class); - } } diff --git a/src/Symfony/Component/Cache/Adapter/ApcuAdapter.php b/src/Symfony/Component/Cache/Adapter/ApcuAdapter.php index 67afd5c72a..713e9fd7d8 100644 --- a/src/Symfony/Component/Cache/Adapter/ApcuAdapter.php +++ b/src/Symfony/Component/Cache/Adapter/ApcuAdapter.php @@ -11,97 +11,14 @@ namespace Symfony\Component\Cache\Adapter; -use Symfony\Component\Cache\CacheItem; -use Symfony\Component\Cache\Exception\CacheException; +use Symfony\Component\Cache\Traits\ApcuTrait; -/** - * @author Nicolas Grekas
- */ class ApcuAdapter extends AbstractAdapter { - public static function isSupported() - { - return function_exists('apcu_fetch') && ini_get('apc.enabled') && !('cli' === PHP_SAPI && !ini_get('apc.enable_cli')); - } + use ApcuTrait; public function __construct($namespace = '', $defaultLifetime = 0, $version = null) { - if (!static::isSupported()) { - throw new CacheException('APCu is not enabled'); - } - if ('cli' === PHP_SAPI) { - ini_set('apc.use_request_time', 0); - } - parent::__construct($namespace, $defaultLifetime); - - if (null !== $version) { - CacheItem::validateKey($version); - - if (!apcu_exists($version.'@'.$namespace)) { - $this->clear($namespace); - apcu_add($version.'@'.$namespace, null); - } - } - } - - /** - * {@inheritdoc} - */ - protected function doFetch(array $ids) - { - try { - return apcu_fetch($ids); - } catch (\Error $e) { - throw new \ErrorException($e->getMessage(), $e->getCode(), E_ERROR, $e->getFile(), $e->getLine()); - } - } - - /** - * {@inheritdoc} - */ - protected function doHave($id) - { - return apcu_exists($id); - } - - /** - * {@inheritdoc} - */ - protected function doClear($namespace) - { - return isset($namespace[0]) && class_exists('APCuIterator', false) - ? apcu_delete(new \APCuIterator(sprintf('/^%s/', preg_quote($namespace, '/')), APC_ITER_KEY)) - : apcu_clear_cache(); - } - - /** - * {@inheritdoc} - */ - protected function doDelete(array $ids) - { - foreach ($ids as $id) { - apcu_delete($id); - } - - return true; - } - - /** - * {@inheritdoc} - */ - protected function doSave(array $values, $lifetime) - { - try { - return array_keys(apcu_store($values, null, $lifetime)); - } catch (\Error $e) { - } catch (\Exception $e) { - } - - if (1 === count($values)) { - // Workaround https://github.com/krakjoe/apcu/issues/170 - apcu_delete(key($values)); - } - - throw $e; + $this->init($namespace, $defaultLifetime, $version); } } diff --git a/src/Symfony/Component/Cache/Adapter/ArrayAdapter.php b/src/Symfony/Component/Cache/Adapter/ArrayAdapter.php index 2898ba50cd..45c19c7a6c 100644 --- a/src/Symfony/Component/Cache/Adapter/ArrayAdapter.php +++ b/src/Symfony/Component/Cache/Adapter/ArrayAdapter.php @@ -13,19 +13,16 @@ namespace Symfony\Component\Cache\Adapter; use Psr\Cache\CacheItemInterface; use Psr\Log\LoggerAwareInterface; -use Psr\Log\LoggerAwareTrait; use Symfony\Component\Cache\CacheItem; +use Symfony\Component\Cache\Traits\ArrayTrait; /** * @author Nicolas Grekas
*/ class ArrayAdapter implements AdapterInterface, LoggerAwareInterface { - use LoggerAwareTrait; + use ArrayTrait; - private $storeSerialized; - private $values = array(); - private $expiries = array(); private $createCacheItem; /** @@ -86,49 +83,7 @@ class ArrayAdapter implements AdapterInterface, LoggerAwareInterface CacheItem::validateKey($key); } - return $this->generateItems($keys, time()); - } - - /** - * Returns all cached values, with cache miss as null. - * - * @return array - */ - public function getValues() - { - return $this->values; - } - - /** - * {@inheritdoc} - */ - public function hasItem($key) - { - CacheItem::validateKey($key); - - return isset($this->expiries[$key]) && ($this->expiries[$key] >= time() || !$this->deleteItem($key)); - } - - /** - * {@inheritdoc} - */ - public function clear() - { - $this->values = $this->expiries = array(); - - return true; - } - - /** - * {@inheritdoc} - */ - public function deleteItem($key) - { - CacheItem::validateKey($key); - - unset($this->values[$key], $this->expiries[$key]); - - return true; + return $this->generateItems($keys, time(), $this->createCacheItem); } /** @@ -196,35 +151,4 @@ class ArrayAdapter implements AdapterInterface, LoggerAwareInterface { return true; } - - private function generateItems(array $keys, $now) - { - $f = $this->createCacheItem; - - foreach ($keys as $i => $key) { - try { - if (!$isHit = isset($this->expiries[$key]) && ($this->expiries[$key] >= $now || !$this->deleteItem($key))) { - $this->values[$key] = $value = null; - } elseif (!$this->storeSerialized) { - $value = $this->values[$key]; - } elseif ('b:0;' === $value = $this->values[$key]) { - $value = false; - } elseif (false === $value = unserialize($value)) { - $this->values[$key] = $value = null; - $isHit = false; - } - } catch (\Exception $e) { - CacheItem::log($this->logger, 'Failed to unserialize key "{key}"', array('key' => $key, 'exception' => $e)); - $this->values[$key] = $value = null; - $isHit = false; - } - unset($keys[$i]); - - yield $key => $f($key, $value, $isHit); - } - - foreach ($keys as $key) { - yield $key => $f($key, null, false); - } - } } diff --git a/src/Symfony/Component/Cache/Adapter/DoctrineAdapter.php b/src/Symfony/Component/Cache/Adapter/DoctrineAdapter.php index ed91bf56cd..befff7ca8e 100644 --- a/src/Symfony/Component/Cache/Adapter/DoctrineAdapter.php +++ b/src/Symfony/Component/Cache/Adapter/DoctrineAdapter.php @@ -12,13 +12,11 @@ namespace Symfony\Component\Cache\Adapter; use Doctrine\Common\Cache\CacheProvider; +use Symfony\Component\Cache\Traits\DoctrineTrait; -/** - * @author Nicolas Grekas
- */ class DoctrineAdapter extends AbstractAdapter { - private $provider; + use DoctrineTrait; public function __construct(CacheProvider $provider, $namespace = '', $defaultLifetime = 0) { @@ -26,71 +24,4 @@ class DoctrineAdapter extends AbstractAdapter $this->provider = $provider; $provider->setNamespace($namespace); } - - /** - * {@inheritdoc} - */ - protected function doFetch(array $ids) - { - $unserializeCallbackHandler = ini_set('unserialize_callback_func', parent::class.'::handleUnserializeCallback'); - try { - return $this->provider->fetchMultiple($ids); - } catch (\Error $e) { - $trace = $e->getTrace(); - - if (isset($trace[0]['function']) && !isset($trace[0]['class'])) { - switch ($trace[0]['function']) { - case 'unserialize': - case 'apcu_fetch': - case 'apc_fetch': - throw new \ErrorException($e->getMessage(), $e->getCode(), E_ERROR, $e->getFile(), $e->getLine()); - } - } - - throw $e; - } finally { - ini_set('unserialize_callback_func', $unserializeCallbackHandler); - } - } - - /** - * {@inheritdoc} - */ - protected function doHave($id) - { - return $this->provider->contains($id); - } - - /** - * {@inheritdoc} - */ - protected function doClear($namespace) - { - $namespace = $this->provider->getNamespace(); - - return isset($namespace[0]) - ? $this->provider->deleteAll() - : $this->provider->flushAll(); - } - - /** - * {@inheritdoc} - */ - protected function doDelete(array $ids) - { - $ok = true; - foreach ($ids as $id) { - $ok = $this->provider->delete($id) && $ok; - } - - return $ok; - } - - /** - * {@inheritdoc} - */ - protected function doSave(array $values, $lifetime) - { - return $this->provider->saveMultiple($values, $lifetime); - } } diff --git a/src/Symfony/Component/Cache/Adapter/FilesystemAdapter.php b/src/Symfony/Component/Cache/Adapter/FilesystemAdapter.php index 1c62641cf6..f37cde290f 100644 --- a/src/Symfony/Component/Cache/Adapter/FilesystemAdapter.php +++ b/src/Symfony/Component/Cache/Adapter/FilesystemAdapter.php @@ -11,78 +11,15 @@ namespace Symfony\Component\Cache\Adapter; -use Symfony\Component\Cache\Exception\CacheException; +use Symfony\Component\Cache\Traits\FilesystemTrait; -/** - * @author Nicolas Grekas
- */
class FilesystemAdapter extends AbstractAdapter
{
- use FilesystemAdapterTrait;
+ use FilesystemTrait;
public function __construct($namespace = '', $defaultLifetime = 0, $directory = null)
{
parent::__construct('', $defaultLifetime);
$this->init($namespace, $directory);
}
-
- /**
- * {@inheritdoc}
- */
- protected function doFetch(array $ids)
- {
- $values = array();
- $now = time();
-
- foreach ($ids as $id) {
- $file = $this->getFile($id);
- if (!file_exists($file) || !$h = @fopen($file, 'rb')) {
- continue;
- }
- if ($now >= (int) $expiresAt = fgets($h)) {
- fclose($h);
- if (isset($expiresAt[0])) {
- @unlink($file);
- }
- } else {
- $i = rawurldecode(rtrim(fgets($h)));
- $value = stream_get_contents($h);
- fclose($h);
- if ($i === $id) {
- $values[$id] = parent::unserialize($value);
- }
- }
- }
-
- return $values;
- }
-
- /**
- * {@inheritdoc}
- */
- protected function doHave($id)
- {
- $file = $this->getFile($id);
-
- return file_exists($file) && (@filemtime($file) > time() || $this->doFetch(array($id)));
- }
-
- /**
- * {@inheritdoc}
- */
- protected function doSave(array $values, $lifetime)
- {
- $ok = true;
- $expiresAt = time() + ($lifetime ?: 31557600); // 31557600s = 1 year
-
- foreach ($values as $id => $value) {
- $ok = $this->write($this->getFile($id, true), $expiresAt."\n".rawurlencode($id)."\n".serialize($value), $expiresAt) && $ok;
- }
-
- if (!$ok && !is_writable($this->directory)) {
- throw new CacheException(sprintf('Cache directory is not writable (%s)', $this->directory));
- }
-
- return $ok;
- }
}
diff --git a/src/Symfony/Component/Cache/Adapter/MemcachedAdapter.php b/src/Symfony/Component/Cache/Adapter/MemcachedAdapter.php
index 46b523f726..5c8784e69c 100644
--- a/src/Symfony/Component/Cache/Adapter/MemcachedAdapter.php
+++ b/src/Symfony/Component/Cache/Adapter/MemcachedAdapter.php
@@ -11,231 +11,16 @@
namespace Symfony\Component\Cache\Adapter;
-use Symfony\Component\Cache\Exception\CacheException;
-use Symfony\Component\Cache\Exception\InvalidArgumentException;
+use Symfony\Component\Cache\Traits\MemcachedTrait;
-/**
- * @author Rob Frawley 2nd
- */
class MemcachedAdapter extends AbstractAdapter
{
- private static $defaultClientOptions = array(
- 'persistent_id' => null,
- 'username' => null,
- 'password' => null,
- );
+ use MemcachedTrait;
protected $maxIdLength = 250;
- private $client;
-
- public static function isSupported()
- {
- return extension_loaded('memcached') && version_compare(phpversion('memcached'), '2.2.0', '>=');
- }
-
public function __construct(\Memcached $client, $namespace = '', $defaultLifetime = 0)
{
- if (!static::isSupported()) {
- throw new CacheException('Memcached >= 2.2.0 is required');
- }
- $opt = $client->getOption(\Memcached::OPT_SERIALIZER);
- if (\Memcached::SERIALIZER_PHP !== $opt && \Memcached::SERIALIZER_IGBINARY !== $opt) {
- throw new CacheException('MemcachedAdapter: "serializer" option must be "php" or "igbinary".');
- }
- $this->maxIdLength -= strlen($client->getOption(\Memcached::OPT_PREFIX_KEY));
-
- parent::__construct($namespace, $defaultLifetime);
- $this->client = $client;
- }
-
- /**
- * Creates a Memcached instance.
- *
- * By default, the binary protocol, no block, and libketama compatible options are enabled.
- *
- * Examples for servers:
- * - 'memcached://user:pass@localhost?weight=33'
- * - array(array('localhost', 11211, 33))
- *
- * @param array[]|string|string[] An array of servers, a DSN, or an array of DSNs
- * @param array An array of options
- *
- * @return \Memcached
- *
- * @throws \ErrorEception When invalid options or servers are provided
- */
- public static function createConnection($servers, array $options = array())
- {
- if (is_string($servers)) {
- $servers = array($servers);
- } elseif (!is_array($servers)) {
- throw new InvalidArgumentException(sprintf('MemcachedAdapter::createClient() expects array or string as first argument, %s given.', gettype($servers)));
- }
- set_error_handler(function ($type, $msg, $file, $line) { throw new \ErrorException($msg, 0, $type, $file, $line); });
- try {
- if (!static::isSupported()) {
- throw new trigger_error('Memcached >= 2.2.0 is required');
- }
- $options += static::$defaultClientOptions;
- $client = new \Memcached($options['persistent_id']);
- $username = $options['username'];
- $password = $options['password'];
- unset($options['persistent_id'], $options['username'], $options['password']);
- $options = array_change_key_case($options, CASE_UPPER);
-
- // set client's options
- $client->setOption(\Memcached::OPT_BINARY_PROTOCOL, true);
- $client->setOption(\Memcached::OPT_NO_BLOCK, true);
- if (!array_key_exists('LIBKETAMA_COMPATIBLE', $options) && !array_key_exists(\Memcached::OPT_LIBKETAMA_COMPATIBLE, $options)) {
- $client->setOption(\Memcached::OPT_LIBKETAMA_COMPATIBLE, true);
- }
- foreach ($options as $name => $value) {
- if (is_int($name)) {
- continue;
- }
- if ('HASH' === $name || 'SERIALIZER' === $name || 'DISTRIBUTION' === $name) {
- $value = constant('Memcached::'.$name.'_'.strtoupper($value));
- }
- $opt = constant('Memcached::OPT_'.$name);
-
- unset($options[$name]);
- $options[$opt] = $value;
- }
- $client->setOptions($options);
-
- // parse any DSN in $servers
- foreach ($servers as $i => $dsn) {
- if (is_array($dsn)) {
- continue;
- }
- if (0 !== strpos($dsn, 'memcached://')) {
- throw new InvalidArgumentException(sprintf('Invalid Memcached DSN: %s does not start with "memcached://"', $dsn));
- }
- $params = preg_replace_callback('#^memcached://(?:([^@]*+)@)?#', function ($m) use (&$username, &$password) {
- if (!empty($m[1])) {
- list($username, $password) = explode(':', $m[1], 2) + array(1 => null);
- }
-
- return 'file://';
- }, $dsn);
- if (false === $params = parse_url($params)) {
- throw new InvalidArgumentException(sprintf('Invalid Memcached DSN: %s', $dsn));
- }
- if (!isset($params['host']) && !isset($params['path'])) {
- throw new InvalidArgumentException(sprintf('Invalid Memcached DSN: %s', $dsn));
- }
- if (isset($params['path']) && preg_match('#/(\d+)$#', $params['path'], $m)) {
- $params['weight'] = $m[1];
- $params['path'] = substr($params['path'], 0, -strlen($m[0]));
- }
- $params += array(
- 'host' => isset($params['host']) ? $params['host'] : $params['path'],
- 'port' => isset($params['host']) ? 11211 : null,
- 'weight' => 0,
- );
- if (isset($params['query'])) {
- parse_str($params['query'], $query);
- $params += $query;
- }
-
- $servers[$i] = array($params['host'], $params['port'], $params['weight']);
- }
-
- // set client's servers, taking care of persistent connections
- if (!$client->isPristine()) {
- $oldServers = array();
- foreach ($client->getServerList() as $server) {
- $oldServers[] = array($server['host'], $server['port']);
- }
-
- $newServers = array();
- foreach ($servers as $server) {
- if (1 < count($server)) {
- $server = array_values($server);
- unset($server[2]);
- $server[1] = (int) $server[1];
- }
- $newServers[] = $server;
- }
-
- if ($oldServers !== $newServers) {
- // before resetting, ensure $servers is valid
- $client->addServers($servers);
- $client->resetServerList();
- }
- }
- $client->addServers($servers);
-
- if (null !== $username || null !== $password) {
- if (!method_exists($client, 'setSaslAuthData')) {
- trigger_error('Missing SASL support: the memcached extension must be compiled with --enable-memcached-sasl.');
- }
- $client->setSaslAuthData($username, $password);
- }
-
- return $client;
- } finally {
- restore_error_handler();
- }
- }
-
- /**
- * {@inheritdoc}
- */
- protected function doSave(array $values, $lifetime)
- {
- return $this->checkResultCode($this->client->setMulti($values, $lifetime));
- }
-
- /**
- * {@inheritdoc}
- */
- protected function doFetch(array $ids)
- {
- return $this->checkResultCode($this->client->getMulti($ids));
- }
-
- /**
- * {@inheritdoc}
- */
- protected function doHave($id)
- {
- return false !== $this->client->get($id) || $this->checkResultCode(\Memcached::RES_SUCCESS === $this->client->getResultCode());
- }
-
- /**
- * {@inheritdoc}
- */
- protected function doDelete(array $ids)
- {
- $ok = true;
- foreach ($this->checkResultCode($this->client->deleteMulti($ids)) as $result) {
- if (\Memcached::RES_SUCCESS !== $result && \Memcached::RES_NOTFOUND !== $result) {
- $ok = false;
- }
- }
-
- return $ok;
- }
-
- /**
- * {@inheritdoc}
- */
- protected function doClear($namespace)
- {
- return $this->checkResultCode($this->client->flush());
- }
-
- private function checkResultCode($result)
- {
- $code = $this->client->getResultCode();
-
- if (\Memcached::RES_SUCCESS === $code || \Memcached::RES_NOTFOUND === $code) {
- return $result;
- }
-
- throw new CacheException(sprintf('MemcachedAdapter client error: %s.', strtolower($this->client->getResultMessage())));
+ $this->init($client, $namespace, $defaultLifetime);
}
}
diff --git a/src/Symfony/Component/Cache/Adapter/PdoAdapter.php b/src/Symfony/Component/Cache/Adapter/PdoAdapter.php
index 3fa3a40533..832185629b 100644
--- a/src/Symfony/Component/Cache/Adapter/PdoAdapter.php
+++ b/src/Symfony/Component/Cache/Adapter/PdoAdapter.php
@@ -11,28 +11,13 @@
namespace Symfony\Component\Cache\Adapter;
-use Doctrine\DBAL\Connection;
-use Doctrine\DBAL\Driver\ServerInfoAwareConnection;
-use Doctrine\DBAL\DBALException;
-use Doctrine\DBAL\Schema\Schema;
-use Symfony\Component\Cache\Exception\InvalidArgumentException;
+use Symfony\Component\Cache\Traits\PdoTrait;
class PdoAdapter extends AbstractAdapter
{
- protected $maxIdLength = 255;
+ use PdoTrait;
- private $conn;
- private $dsn;
- private $driver;
- private $serverVersion;
- private $table = 'cache_items';
- private $idCol = 'item_id';
- private $dataCol = 'item_data';
- private $lifetimeCol = 'item_lifetime';
- private $timeCol = 'item_time';
- private $username = '';
- private $password = '';
- private $connectionOptions = array();
+ protected $maxIdLength = 255;
/**
* Constructor.
@@ -62,345 +47,6 @@ class PdoAdapter extends AbstractAdapter
*/
public function __construct($connOrDsn, $namespace = '', $defaultLifetime = 0, array $options = array())
{
- if (isset($namespace[0]) && preg_match('#[^-+.A-Za-z0-9]#', $namespace, $match)) {
- throw new InvalidArgumentException(sprintf('Namespace contains "%s" but only characters in [-+.A-Za-z0-9] are allowed.', $match[0]));
- }
-
- if ($connOrDsn instanceof \PDO) {
- if (\PDO::ERRMODE_EXCEPTION !== $connOrDsn->getAttribute(\PDO::ATTR_ERRMODE)) {
- throw new InvalidArgumentException(sprintf('"%s" requires PDO error mode attribute be set to throw Exceptions (i.e. $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION))', __CLASS__));
- }
-
- $this->conn = $connOrDsn;
- } elseif ($connOrDsn instanceof Connection) {
- $this->conn = $connOrDsn;
- } elseif (is_string($connOrDsn)) {
- $this->dsn = $connOrDsn;
- } else {
- throw new InvalidArgumentException(sprintf('"%s" requires PDO or Doctrine\DBAL\Connection instance or DSN string as first argument, "%s" given.', __CLASS__, is_object($connOrDsn) ? get_class($connOrDsn) : gettype($connOrDsn)));
- }
-
- $this->table = isset($options['db_table']) ? $options['db_table'] : $this->table;
- $this->idCol = isset($options['db_id_col']) ? $options['db_id_col'] : $this->idCol;
- $this->dataCol = isset($options['db_data_col']) ? $options['db_data_col'] : $this->dataCol;
- $this->lifetimeCol = isset($options['db_lifetime_col']) ? $options['db_lifetime_col'] : $this->lifetimeCol;
- $this->timeCol = isset($options['db_time_col']) ? $options['db_time_col'] : $this->timeCol;
- $this->username = isset($options['db_username']) ? $options['db_username'] : $this->username;
- $this->password = isset($options['db_password']) ? $options['db_password'] : $this->password;
- $this->connectionOptions = isset($options['db_connection_options']) ? $options['db_connection_options'] : $this->connectionOptions;
-
- parent::__construct($namespace, $defaultLifetime);
- }
-
- /**
- * Creates the table to store cache items which can be called once for setup.
- *
- * Cache ID are saved in a column of maximum length 255. Cache data is
- * saved in a BLOB.
- *
- * @throws \PDOException When the table already exists
- * @throws DBALException When the table already exists
- * @throws \DomainException When an unsupported PDO driver is used
- */
- public function createTable()
- {
- // connect if we are not yet
- $conn = $this->getConnection();
-
- if ($conn instanceof Connection) {
- $types = array(
- 'mysql' => 'binary',
- 'sqlite' => 'text',
- 'pgsql' => 'string',
- 'oci' => 'string',
- 'sqlsrv' => 'string',
- );
- if (!isset($types[$this->driver])) {
- throw new \DomainException(sprintf('Creating the cache table is currently not implemented for PDO driver "%s".', $this->driver));
- }
-
- $schema = new Schema();
- $table = $schema->createTable($this->table);
- $table->addColumn($this->idCol, $types[$this->driver], array('length' => 255));
- $table->addColumn($this->dataCol, 'blob', array('length' => 16777215));
- $table->addColumn($this->lifetimeCol, 'integer', array('unsigned' => true, 'notnull' => false));
- $table->addColumn($this->timeCol, 'integer', array('unsigned' => true, 'foo' => 'bar'));
- $table->setPrimaryKey(array($this->idCol));
-
- foreach ($schema->toSql($conn->getDatabasePlatform()) as $sql) {
- $conn->exec($sql);
- }
-
- return;
- }
-
- switch ($this->driver) {
- case 'mysql':
- // We use varbinary for the ID column because it prevents unwanted conversions:
- // - character set conversions between server and client
- // - trailing space removal
- // - case-insensitivity
- // - language processing like é == e
- $sql = "CREATE TABLE $this->table ($this->idCol VARBINARY(255) NOT NULL PRIMARY KEY, $this->dataCol MEDIUMBLOB NOT NULL, $this->lifetimeCol INTEGER UNSIGNED, $this->timeCol INTEGER UNSIGNED NOT NULL) COLLATE utf8_bin, ENGINE = InnoDB";
- break;
- case 'sqlite':
- $sql = "CREATE TABLE $this->table ($this->idCol TEXT NOT NULL PRIMARY KEY, $this->dataCol BLOB NOT NULL, $this->lifetimeCol INTEGER, $this->timeCol INTEGER NOT NULL)";
- break;
- case 'pgsql':
- $sql = "CREATE TABLE $this->table ($this->idCol VARCHAR(255) NOT NULL PRIMARY KEY, $this->dataCol BYTEA NOT NULL, $this->lifetimeCol INTEGER, $this->timeCol INTEGER NOT NULL)";
- break;
- case 'oci':
- $sql = "CREATE TABLE $this->table ($this->idCol VARCHAR2(255) NOT NULL PRIMARY KEY, $this->dataCol BLOB NOT NULL, $this->lifetimeCol INTEGER, $this->timeCol INTEGER NOT NULL)";
- break;
- case 'sqlsrv':
- $sql = "CREATE TABLE $this->table ($this->idCol VARCHAR(255) NOT NULL PRIMARY KEY, $this->dataCol VARBINARY(MAX) NOT NULL, $this->lifetimeCol INTEGER, $this->timeCol INTEGER NOT NULL)";
- break;
- default:
- throw new \DomainException(sprintf('Creating the cache table is currently not implemented for PDO driver "%s".', $this->driver));
- }
-
- $conn->exec($sql);
- }
-
- /**
- * {@inheritdoc}
- */
- protected function doFetch(array $ids)
- {
- $now = time();
- $expired = array();
-
- $sql = str_pad('', (count($ids) << 1) - 1, '?,');
- $sql = "SELECT $this->idCol, CASE WHEN $this->lifetimeCol IS NULL OR $this->lifetimeCol + $this->timeCol > ? THEN $this->dataCol ELSE NULL END FROM $this->table WHERE $this->idCol IN ($sql)";
- $stmt = $this->getConnection()->prepare($sql);
- $stmt->bindValue($i = 1, $now, \PDO::PARAM_INT);
- foreach ($ids as $id) {
- $stmt->bindValue(++$i, $id);
- }
- $stmt->execute();
-
- while ($row = $stmt->fetch(\PDO::FETCH_NUM)) {
- if (null === $row[1]) {
- $expired[] = $row[0];
- } else {
- yield $row[0] => parent::unserialize(is_resource($row[1]) ? stream_get_contents($row[1]) : $row[1]);
- }
- }
-
- if ($expired) {
- $sql = str_pad('', (count($expired) << 1) - 1, '?,');
- $sql = "DELETE FROM $this->table WHERE $this->lifetimeCol + $this->timeCol <= ? AND $this->idCol IN ($sql)";
- $stmt = $this->getConnection()->prepare($sql);
- $stmt->bindValue($i = 1, $now, \PDO::PARAM_INT);
- foreach ($expired as $id) {
- $stmt->bindValue(++$i, $id);
- }
- $stmt->execute($expired);
- }
- }
-
- /**
- * {@inheritdoc}
- */
- protected function doHave($id)
- {
- $sql = "SELECT 1 FROM $this->table WHERE $this->idCol = :id AND ($this->lifetimeCol IS NULL OR $this->lifetimeCol + $this->timeCol > :time)";
- $stmt = $this->getConnection()->prepare($sql);
-
- $stmt->bindValue(':id', $id);
- $stmt->bindValue(':time', time(), \PDO::PARAM_INT);
- $stmt->execute();
-
- return (bool) $stmt->fetchColumn();
- }
-
- /**
- * {@inheritdoc}
- */
- protected function doClear($namespace)
- {
- $conn = $this->getConnection();
-
- if ('' === $namespace) {
- if ('sqlite' === $this->driver) {
- $sql = "DELETE FROM $this->table";
- } else {
- $sql = "TRUNCATE TABLE $this->table";
- }
- } else {
- $sql = "DELETE FROM $this->table WHERE $this->idCol LIKE '$namespace%'";
- }
-
- $conn->exec($sql);
-
- return true;
- }
-
- /**
- * {@inheritdoc}
- */
- protected function doDelete(array $ids)
- {
- $sql = str_pad('', (count($ids) << 1) - 1, '?,');
- $sql = "DELETE FROM $this->table WHERE $this->idCol IN ($sql)";
- $stmt = $this->getConnection()->prepare($sql);
- $stmt->execute(array_values($ids));
-
- return true;
- }
-
- /**
- * {@inheritdoc}
- */
- protected function doSave(array $values, $lifetime)
- {
- $serialized = array();
- $failed = array();
-
- foreach ($values as $id => $value) {
- try {
- $serialized[$id] = serialize($value);
- } catch (\Exception $e) {
- $failed[] = $id;
- }
- }
-
- if (!$serialized) {
- return $failed;
- }
-
- $conn = $this->getConnection();
- $driver = $this->driver;
- $insertSql = "INSERT INTO $this->table ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (:id, :data, :lifetime, :time)";
-
- switch (true) {
- case 'mysql' === $driver:
- $sql = $insertSql." ON DUPLICATE KEY UPDATE $this->dataCol = VALUES($this->dataCol), $this->lifetimeCol = VALUES($this->lifetimeCol), $this->timeCol = VALUES($this->timeCol)";
- break;
- case 'oci' === $driver:
- // DUAL is Oracle specific dummy table
- $sql = "MERGE INTO $this->table USING DUAL ON ($this->idCol = ?) ".
- "WHEN NOT MATCHED THEN INSERT ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (?, ?, ?, ?) ".
- "WHEN MATCHED THEN UPDATE SET $this->dataCol = ?, $this->lifetimeCol = ?, $this->timeCol = ?";
- break;
- case 'sqlsrv' === $driver && version_compare($this->getServerVersion(), '10', '>='):
- // MERGE is only available since SQL Server 2008 and must be terminated by semicolon
- // It also requires HOLDLOCK according to http://weblogs.sqlteam.com/dang/archive/2009/01/31/UPSERT-Race-Condition-With-MERGE.aspx
- $sql = "MERGE INTO $this->table WITH (HOLDLOCK) USING (SELECT 1 AS dummy) AS src ON ($this->idCol = ?) ".
- "WHEN NOT MATCHED THEN INSERT ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (?, ?, ?, ?) ".
- "WHEN MATCHED THEN UPDATE SET $this->dataCol = ?, $this->lifetimeCol = ?, $this->timeCol = ?;";
- break;
- case 'sqlite' === $driver:
- $sql = 'INSERT OR REPLACE'.substr($insertSql, 6);
- break;
- case 'pgsql' === $driver && version_compare($this->getServerVersion(), '9.5', '>='):
- $sql = $insertSql." ON CONFLICT ($this->idCol) DO UPDATE SET ($this->dataCol, $this->lifetimeCol, $this->timeCol) = (EXCLUDED.$this->dataCol, EXCLUDED.$this->lifetimeCol, EXCLUDED.$this->timeCol)";
- break;
- default:
- $driver = null;
- $sql = "UPDATE $this->table SET $this->dataCol = :data, $this->lifetimeCol = :lifetime, $this->timeCol = :time WHERE $this->idCol = :id";
- break;
- }
-
- $now = time();
- $lifetime = $lifetime ?: null;
- $stmt = $conn->prepare($sql);
-
- if ('sqlsrv' === $driver || 'oci' === $driver) {
- $stmt->bindParam(1, $id);
- $stmt->bindParam(2, $id);
- $stmt->bindParam(3, $data, \PDO::PARAM_LOB);
- $stmt->bindValue(4, $lifetime, \PDO::PARAM_INT);
- $stmt->bindValue(5, $now, \PDO::PARAM_INT);
- $stmt->bindParam(6, $data, \PDO::PARAM_LOB);
- $stmt->bindValue(7, $lifetime, \PDO::PARAM_INT);
- $stmt->bindValue(8, $now, \PDO::PARAM_INT);
- } else {
- $stmt->bindParam(':id', $id);
- $stmt->bindParam(':data', $data, \PDO::PARAM_LOB);
- $stmt->bindValue(':lifetime', $lifetime, \PDO::PARAM_INT);
- $stmt->bindValue(':time', $now, \PDO::PARAM_INT);
- }
- if (null === $driver) {
- $insertStmt = $conn->prepare($insertSql);
-
- $insertStmt->bindParam(':id', $id);
- $insertStmt->bindParam(':data', $data, \PDO::PARAM_LOB);
- $insertStmt->bindValue(':lifetime', $lifetime, \PDO::PARAM_INT);
- $insertStmt->bindValue(':time', $now, \PDO::PARAM_INT);
- }
-
- foreach ($serialized as $id => $data) {
- $stmt->execute();
-
- if (null === $driver && !$stmt->rowCount()) {
- try {
- $insertStmt->execute();
- } catch (DBALException $e) {
- } catch (\PDOException $e) {
- // A concurrent write won, let it be
- }
- }
- }
-
- return $failed;
- }
-
- /**
- * @return \PDO|Connection
- */
- private function getConnection()
- {
- if (null === $this->conn) {
- $this->conn = new \PDO($this->dsn, $this->username, $this->password, $this->connectionOptions);
- $this->conn->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
- }
- if (null === $this->driver) {
- if ($this->conn instanceof \PDO) {
- $this->driver = $this->conn->getAttribute(\PDO::ATTR_DRIVER_NAME);
- } else {
- switch ($this->driver = $this->conn->getDriver()->getName()) {
- case 'mysqli':
- case 'pdo_mysql':
- case 'drizzle_pdo_mysql':
- $this->driver = 'mysql';
- break;
- case 'pdo_sqlite':
- $this->driver = 'sqlite';
- break;
- case 'pdo_pgsql':
- $this->driver = 'pgsql';
- break;
- case 'oci8':
- case 'pdo_oracle':
- $this->driver = 'oci';
- break;
- case 'pdo_sqlsrv':
- $this->driver = 'sqlsrv';
- break;
- }
- }
- }
-
- return $this->conn;
- }
-
- /**
- * @return string
- */
- private function getServerVersion()
- {
- if (null === $this->serverVersion) {
- $conn = $this->conn instanceof \PDO ? $this->conn : $this->conn->getWrappedConnection();
- if ($conn instanceof \PDO) {
- $this->serverVersion = $conn->getAttribute(\PDO::ATTR_SERVER_VERSION);
- } elseif ($conn instanceof ServerInfoAwareConnection) {
- $this->serverVersion = $conn->getServerVersion();
- } else {
- $this->serverVersion = '0';
- }
- }
-
- return $this->serverVersion;
+ $this->init($connOrDsn, $namespace, $defaultLifetime, $options);
}
}
diff --git a/src/Symfony/Component/Cache/Adapter/PhpArrayAdapter.php b/src/Symfony/Component/Cache/Adapter/PhpArrayAdapter.php
index e4d8ad5eea..ead0213864 100644
--- a/src/Symfony/Component/Cache/Adapter/PhpArrayAdapter.php
+++ b/src/Symfony/Component/Cache/Adapter/PhpArrayAdapter.php
@@ -15,6 +15,7 @@ use Psr\Cache\CacheItemInterface;
use Psr\Cache\CacheItemPoolInterface;
use Symfony\Component\Cache\CacheItem;
use Symfony\Component\Cache\Exception\InvalidArgumentException;
+use Symfony\Component\Cache\Traits\PhpArrayTrait;
/**
* Caches items at warm up time using a PHP array that is stored in shared memory by OPCache since PHP 7.0.
@@ -25,10 +26,9 @@ use Symfony\Component\Cache\Exception\InvalidArgumentException;
*/
class PhpArrayAdapter implements AdapterInterface
{
- private $file;
- private $values;
+ use PhpArrayTrait;
+
private $createCacheItem;
- private $fallbackPool;
/**
* @param string $file The PHP file were values are cached
@@ -75,89 +75,6 @@ class PhpArrayAdapter implements AdapterInterface
return $fallbackPool;
}
- /**
- * Store an array of cached values.
- *
- * @param array $values The cached values
- */
- public function warmUp(array $values)
- {
- if (file_exists($this->file)) {
- if (!is_file($this->file)) {
- throw new InvalidArgumentException(sprintf('Cache path exists and is not a file: %s.', $this->file));
- }
-
- if (!is_writable($this->file)) {
- throw new InvalidArgumentException(sprintf('Cache file is not writable: %s.', $this->file));
- }
- } else {
- $directory = dirname($this->file);
-
- if (!is_dir($directory) && !@mkdir($directory, 0777, true)) {
- throw new InvalidArgumentException(sprintf('Cache directory does not exist and cannot be created: %s.', $directory));
- }
-
- if (!is_writable($directory)) {
- throw new InvalidArgumentException(sprintf('Cache directory is not writable: %s.', $directory));
- }
- }
-
- $dump = <<<'EOF'
- $value) {
- CacheItem::validateKey(is_int($key) ? (string) $key : $key);
-
- if (null === $value || is_object($value)) {
- try {
- $value = serialize($value);
- } catch (\Exception $e) {
- throw new InvalidArgumentException(sprintf('Cache key "%s" has non-serializable %s value.', $key, get_class($value)), 0, $e);
- }
- } elseif (is_array($value)) {
- try {
- $serialized = serialize($value);
- $unserialized = unserialize($serialized);
- } catch (\Exception $e) {
- throw new InvalidArgumentException(sprintf('Cache key "%s" has non-serializable array value.', $key), 0, $e);
- }
- // Store arrays serialized if they contain any objects or references
- if ($unserialized !== $value || (false !== strpos($serialized, ';R:') && preg_match('/;R:[1-9]/', $serialized))) {
- $value = $serialized;
- }
- } elseif (is_string($value)) {
- // Serialize strings if they could be confused with serialized objects or arrays
- if ('N;' === $value || (isset($value[2]) && ':' === $value[1])) {
- $value = serialize($value);
- }
- } elseif (!is_scalar($value)) {
- throw new InvalidArgumentException(sprintf('Cache key "%s" has non-serializable %s value.', $key, gettype($value)));
- }
-
- $dump .= var_export($key, true).' => '.var_export($value, true).",\n";
- }
-
- $dump .= "\n);\n";
- $dump = str_replace("' . \"\\0\" . '", "\0", $dump);
-
- $tmpFile = uniqid($this->file, true);
-
- file_put_contents($tmpFile, $dump);
- @chmod($tmpFile, 0666);
- unset($serialized, $unserialized, $value, $dump);
-
- @rename($tmpFile, $this->file);
-
- $this->values = (include $this->file) ?: array();
- }
-
/**
* {@inheritdoc}
*/
@@ -228,18 +145,6 @@ EOF;
return isset($this->values[$key]) || $this->fallbackPool->hasItem($key);
}
- /**
- * {@inheritdoc}
- */
- public function clear()
- {
- $this->values = array();
-
- $cleared = @unlink($this->file) || !file_exists($this->file);
-
- return $this->fallbackPool->clear() && $cleared;
- }
-
/**
* {@inheritdoc}
*/
@@ -317,14 +222,6 @@ EOF;
return $this->fallbackPool->commit();
}
- /**
- * Load the cache file.
- */
- private function initialize()
- {
- $this->values = file_exists($this->file) ? (include $this->file ?: array()) : array();
- }
-
/**
* Generator for items.
*
diff --git a/src/Symfony/Component/Cache/Adapter/PhpFilesAdapter.php b/src/Symfony/Component/Cache/Adapter/PhpFilesAdapter.php
index befa38d8d4..12480c7436 100644
--- a/src/Symfony/Component/Cache/Adapter/PhpFilesAdapter.php
+++ b/src/Symfony/Component/Cache/Adapter/PhpFilesAdapter.php
@@ -12,22 +12,11 @@
namespace Symfony\Component\Cache\Adapter;
use Symfony\Component\Cache\Exception\CacheException;
-use Symfony\Component\Cache\Exception\InvalidArgumentException;
+use Symfony\Component\Cache\Traits\PhpFilesTrait;
-/**
- * @author Piotr Stankowski
- */
class PhpFilesAdapter extends AbstractAdapter
{
- use FilesystemAdapterTrait;
-
- private $includeHandler;
-
- public static function isSupported()
- {
- return function_exists('opcache_compile_file') && ini_get('opcache.enable');
- }
+ use PhpFilesTrait;
public function __construct($namespace = '', $defaultLifetime = 0, $directory = null)
{
@@ -40,92 +29,4 @@ class PhpFilesAdapter extends AbstractAdapter
$e = new \Exception();
$this->includeHandler = function () use ($e) { throw $e; };
}
-
- /**
- * {@inheritdoc}
- */
- protected function doFetch(array $ids)
- {
- $values = array();
- $now = time();
-
- set_error_handler($this->includeHandler);
- try {
- foreach ($ids as $id) {
- try {
- $file = $this->getFile($id);
- list($expiresAt, $values[$id]) = include $file;
- if ($now >= $expiresAt) {
- unset($values[$id]);
- }
- } catch (\Exception $e) {
- continue;
- }
- }
- } finally {
- restore_error_handler();
- }
-
- foreach ($values as $id => $value) {
- if ('N;' === $value) {
- $values[$id] = null;
- } elseif (is_string($value) && isset($value[2]) && ':' === $value[1]) {
- $values[$id] = parent::unserialize($value);
- }
- }
-
- return $values;
- }
-
- /**
- * {@inheritdoc}
- */
- protected function doHave($id)
- {
- return (bool) $this->doFetch(array($id));
- }
-
- /**
- * {@inheritdoc}
- */
- protected function doSave(array $values, $lifetime)
- {
- $ok = true;
- $data = array($lifetime ? time() + $lifetime : PHP_INT_MAX, '');
- $allowCompile = 'cli' !== PHP_SAPI || ini_get('opcache.enable_cli');
-
- foreach ($values as $key => $value) {
- if (null === $value || is_object($value)) {
- $value = serialize($value);
- } elseif (is_array($value)) {
- $serialized = serialize($value);
- $unserialized = parent::unserialize($serialized);
- // Store arrays serialized if they contain any objects or references
- if ($unserialized !== $value || (false !== strpos($serialized, ';R:') && preg_match('/;R:[1-9]/', $serialized))) {
- $value = $serialized;
- }
- } elseif (is_string($value)) {
- // Serialize strings if they could be confused with serialized objects or arrays
- if ('N;' === $value || (isset($value[2]) && ':' === $value[1])) {
- $value = serialize($value);
- }
- } elseif (!is_scalar($value)) {
- throw new InvalidArgumentException(sprintf('Cache key "%s" has non-serializable %s value.', $key, gettype($value)));
- }
-
- $data[1] = $value;
- $file = $this->getFile($key, true);
- $ok = $this->write($file, 'directory)) {
- throw new CacheException(sprintf('Cache directory is not writable (%s)', $this->directory));
- }
-
- return $ok;
- }
}
diff --git a/src/Symfony/Component/Cache/Adapter/RedisAdapter.php b/src/Symfony/Component/Cache/Adapter/RedisAdapter.php
index 7fd6921e3f..75cb764f40 100644
--- a/src/Symfony/Component/Cache/Adapter/RedisAdapter.php
+++ b/src/Symfony/Component/Cache/Adapter/RedisAdapter.php
@@ -11,301 +11,17 @@
namespace Symfony\Component\Cache\Adapter;
-use Predis\Connection\Factory;
-use Predis\Connection\Aggregate\PredisCluster;
-use Predis\Connection\Aggregate\RedisCluster;
-use Symfony\Component\Cache\Exception\InvalidArgumentException;
+use Symfony\Component\Cache\Traits\RedisTrait;
-/**
- * @author Aurimas Niekis
- */
class RedisAdapter extends AbstractAdapter
{
- private static $defaultConnectionOptions = array(
- 'class' => null,
- 'persistent' => 0,
- 'persistent_id' => null,
- 'timeout' => 30,
- 'read_timeout' => 0,
- 'retry_interval' => 0,
- );
- private $redis;
+ use RedisTrait;
/**
* @param \Redis|\RedisArray|\RedisCluster|\Predis\Client $redisClient
*/
public function __construct($redisClient, $namespace = '', $defaultLifetime = 0)
{
- parent::__construct($namespace, $defaultLifetime);
-
- if (preg_match('#[^-+_.A-Za-z0-9]#', $namespace, $match)) {
- throw new InvalidArgumentException(sprintf('RedisAdapter namespace contains "%s" but only characters in [-+_.A-Za-z0-9] are allowed.', $match[0]));
- }
- if (!$redisClient instanceof \Redis && !$redisClient instanceof \RedisArray && !$redisClient instanceof \RedisCluster && !$redisClient instanceof \Predis\Client) {
- throw new InvalidArgumentException(sprintf('%s() expects parameter 1 to be Redis, RedisArray, RedisCluster or Predis\Client, %s given', __METHOD__, is_object($redisClient) ? get_class($redisClient) : gettype($redisClient)));
- }
- $this->redis = $redisClient;
- }
-
- /**
- * Creates a Redis connection using a DSN configuration.
- *
- * Example DSN:
- * - redis://localhost
- * - redis://example.com:1234
- * - redis://secret@example.com/13
- * - redis:///var/run/redis.sock
- * - redis://secret@/var/run/redis.sock/13
- *
- * @param string $dsn
- * @param array $options See self::$defaultConnectionOptions
- *
- * @throws InvalidArgumentException When the DSN is invalid.
- *
- * @return \Redis|\Predis\Client According to the "class" option
- */
- public static function createConnection($dsn, array $options = array())
- {
- if (0 !== strpos($dsn, 'redis://')) {
- throw new InvalidArgumentException(sprintf('Invalid Redis DSN: %s does not start with "redis://"', $dsn));
- }
- $params = preg_replace_callback('#^redis://(?:(?:[^:@]*+:)?([^@]*+)@)?#', function ($m) use (&$auth) {
- if (isset($m[1])) {
- $auth = $m[1];
- }
-
- return 'file://';
- }, $dsn);
- if (false === $params = parse_url($params)) {
- throw new InvalidArgumentException(sprintf('Invalid Redis DSN: %s', $dsn));
- }
- if (!isset($params['host']) && !isset($params['path'])) {
- throw new InvalidArgumentException(sprintf('Invalid Redis DSN: %s', $dsn));
- }
- if (isset($params['path']) && preg_match('#/(\d+)$#', $params['path'], $m)) {
- $params['dbindex'] = $m[1];
- $params['path'] = substr($params['path'], 0, -strlen($m[0]));
- }
- $params += array(
- 'host' => isset($params['host']) ? $params['host'] : $params['path'],
- 'port' => isset($params['host']) ? 6379 : null,
- 'dbindex' => 0,
- );
- if (isset($params['query'])) {
- parse_str($params['query'], $query);
- $params += $query;
- }
- $params += $options + self::$defaultConnectionOptions;
- $class = null === $params['class'] ? (extension_loaded('redis') ? \Redis::class : \Predis\Client::class) : $params['class'];
-
- if (is_a($class, \Redis::class, true)) {
- $connect = $params['persistent'] || $params['persistent_id'] ? 'pconnect' : 'connect';
- $redis = new $class();
- @$redis->{$connect}($params['host'], $params['port'], $params['timeout'], $params['persistent_id'], $params['retry_interval']);
-
- if (@!$redis->isConnected()) {
- $e = ($e = error_get_last()) && preg_match('/^Redis::p?connect\(\): (.*)/', $e['message'], $e) ? sprintf(' (%s)', $e[1]) : '';
- throw new InvalidArgumentException(sprintf('Redis connection failed%s: %s', $e, $dsn));
- }
-
- if ((null !== $auth && !$redis->auth($auth))
- || ($params['dbindex'] && !$redis->select($params['dbindex']))
- || ($params['read_timeout'] && !$redis->setOption(\Redis::OPT_READ_TIMEOUT, $params['read_timeout']))
- ) {
- $e = preg_replace('/^ERR /', '', $redis->getLastError());
- throw new InvalidArgumentException(sprintf('Redis connection failed (%s): %s', $e, $dsn));
- }
- } elseif (is_a($class, \Predis\Client::class, true)) {
- $params['scheme'] = isset($params['host']) ? 'tcp' : 'unix';
- $params['database'] = $params['dbindex'] ?: null;
- $params['password'] = $auth;
- $redis = new $class((new Factory())->create($params));
- } elseif (class_exists($class, false)) {
- throw new InvalidArgumentException(sprintf('"%s" is not a subclass of "Redis" or "Predis\Client"', $class));
- } else {
- throw new InvalidArgumentException(sprintf('Class "%s" does not exist', $class));
- }
-
- return $redis;
- }
-
- /**
- * {@inheritdoc}
- */
- protected function doFetch(array $ids)
- {
- if ($ids) {
- $values = $this->redis->mGet($ids);
- $index = 0;
- foreach ($ids as $id) {
- if ($value = $values[$index++]) {
- yield $id => parent::unserialize($value);
- }
- }
- }
- }
-
- /**
- * {@inheritdoc}
- */
- protected function doHave($id)
- {
- return (bool) $this->redis->exists($id);
- }
-
- /**
- * {@inheritdoc}
- */
- protected function doClear($namespace)
- {
- // When using a native Redis cluster, clearing the cache cannot work and always returns false.
- // Clearing the cache should then be done by any other means (e.g. by restarting the cluster).
-
- $cleared = true;
- $hosts = array($this->redis);
- $evalArgs = array(array($namespace), 0);
-
- if ($this->redis instanceof \Predis\Client) {
- $evalArgs = array(0, $namespace);
-
- $connection = $this->redis->getConnection();
- if ($connection instanceof PredisCluster) {
- $hosts = array();
- foreach ($connection as $c) {
- $hosts[] = new \Predis\Client($c);
- }
- } elseif ($connection instanceof RedisCluster) {
- return false;
- }
- } elseif ($this->redis instanceof \RedisArray) {
- $hosts = array();
- foreach ($this->redis->_hosts() as $host) {
- $hosts[] = $this->redis->_instance($host);
- }
- } elseif ($this->redis instanceof \RedisCluster) {
- return false;
- }
- foreach ($hosts as $host) {
- if (!isset($namespace[0])) {
- $cleared = $host->flushDb() && $cleared;
- continue;
- }
-
- $info = $host->info('Server');
- $info = isset($info['Server']) ? $info['Server'] : $info;
-
- if (!version_compare($info['redis_version'], '2.8', '>=')) {
- // As documented in Redis documentation (http://redis.io/commands/keys) using KEYS
- // can hang your server when it is executed against large databases (millions of items).
- // Whenever you hit this scale, you should really consider upgrading to Redis 2.8 or above.
- $cleared = $host->eval("local keys=redis.call('KEYS',ARGV[1]..'*') for i=1,#keys,5000 do redis.call('DEL',unpack(keys,i,math.min(i+4999,#keys))) end return 1", $evalArgs[0], $evalArgs[1]) && $cleared;
- continue;
- }
-
- $cursor = null;
- do {
- $keys = $host instanceof \Predis\Client ? $host->scan($cursor, 'MATCH', $namespace.'*', 'COUNT', 1000) : $host->scan($cursor, $namespace.'*', 1000);
- if (isset($keys[1]) && is_array($keys[1])) {
- $cursor = $keys[0];
- $keys = $keys[1];
- }
- if ($keys) {
- $host->del($keys);
- }
- } while ($cursor = (int) $cursor);
- }
-
- return $cleared;
- }
-
- /**
- * {@inheritdoc}
- */
- protected function doDelete(array $ids)
- {
- if ($ids) {
- $this->redis->del($ids);
- }
-
- return true;
- }
-
- /**
- * {@inheritdoc}
- */
- protected function doSave(array $values, $lifetime)
- {
- $serialized = array();
- $failed = array();
-
- foreach ($values as $id => $value) {
- try {
- $serialized[$id] = serialize($value);
- } catch (\Exception $e) {
- $failed[] = $id;
- }
- }
-
- if (!$serialized) {
- return $failed;
- }
-
- if (0 >= $lifetime) {
- $this->redis->mSet($serialized);
-
- return $failed;
- }
-
- $this->pipeline(function ($pipe) use (&$serialized, $lifetime) {
- foreach ($serialized as $id => $value) {
- $pipe('setEx', $id, array($lifetime, $value));
- }
- });
-
- return $failed;
- }
-
- private function execute($command, $id, array $args, $redis = null)
- {
- array_unshift($args, $id);
- call_user_func_array(array($redis ?: $this->redis, $command), $args);
- }
-
- private function pipeline(\Closure $callback)
- {
- $redis = $this->redis;
-
- try {
- if ($redis instanceof \Predis\Client) {
- $redis->pipeline(function ($pipe) use ($callback) {
- $this->redis = $pipe;
- $callback(array($this, 'execute'));
- });
- } elseif ($redis instanceof \RedisArray) {
- $connections = array();
- $callback(function ($command, $id, $args) use (&$connections) {
- if (!isset($connections[$h = $this->redis->_target($id)])) {
- $connections[$h] = $this->redis->_instance($h);
- $connections[$h]->multi(\Redis::PIPELINE);
- }
- $this->execute($command, $id, $args, $connections[$h]);
- });
- foreach ($connections as $c) {
- $c->exec();
- }
- } else {
- $pipe = $redis->multi(\Redis::PIPELINE);
- try {
- $callback(array($this, 'execute'));
- } finally {
- if ($pipe) {
- $redis->exec();
- }
- }
- }
- } finally {
- $this->redis = $redis;
- }
+ $this->init($redisClient, $namespace, $defaultLifetime);
}
}
diff --git a/src/Symfony/Component/Cache/Adapter/SimpleCacheAdapter.php b/src/Symfony/Component/Cache/Adapter/SimpleCacheAdapter.php
new file mode 100644
index 0000000000..f176624410
--- /dev/null
+++ b/src/Symfony/Component/Cache/Adapter/SimpleCacheAdapter.php
@@ -0,0 +1,75 @@
+
+ *
+ * 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;
+
+/**
+ * @author Nicolas Grekas
+ */
+class SimpleCacheAdapter extends AbstractAdapter
+{
+ private $pool;
+ private $miss;
+
+ public function __construct(CacheInterface $pool, $namespace = '', $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);
+ }
+}
diff --git a/src/Symfony/Component/Cache/CHANGELOG.md b/src/Symfony/Component/Cache/CHANGELOG.md
index 94bbe13699..57a0780ae2 100644
--- a/src/Symfony/Component/Cache/CHANGELOG.md
+++ b/src/Symfony/Component/Cache/CHANGELOG.md
@@ -1,6 +1,14 @@
CHANGELOG
=========
+3.3.0
+-----
+
+ * 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
-----
diff --git a/src/Symfony/Component/Cache/Exception/CacheException.php b/src/Symfony/Component/Cache/Exception/CacheException.php
index d62b3e1213..e87b2db8fe 100644
--- a/src/Symfony/Component/Cache/Exception/CacheException.php
+++ b/src/Symfony/Component/Cache/Exception/CacheException.php
@@ -11,8 +11,9 @@
namespace Symfony\Component\Cache\Exception;
-use Psr\Cache\CacheException as CacheExceptionInterface;
+use Psr\Cache\CacheException as Psr6CacheInterface;
+use Psr\SimpleCache\CacheException as SimpleCacheInterface;
-class CacheException extends \Exception implements CacheExceptionInterface
+class CacheException extends \Exception implements Psr6CacheInterface, SimpleCacheInterface
{
}
diff --git a/src/Symfony/Component/Cache/Exception/InvalidArgumentException.php b/src/Symfony/Component/Cache/Exception/InvalidArgumentException.php
index 334a3c3e27..828bf3ed77 100644
--- a/src/Symfony/Component/Cache/Exception/InvalidArgumentException.php
+++ b/src/Symfony/Component/Cache/Exception/InvalidArgumentException.php
@@ -11,8 +11,9 @@
namespace Symfony\Component\Cache\Exception;
-use Psr\Cache\InvalidArgumentException as InvalidArgumentExceptionInterface;
+use Psr\Cache\InvalidArgumentException as Psr6CacheInterface;
+use Psr\SimpleCache\InvalidArgumentException as SimpleCacheInterface;
-class InvalidArgumentException extends \InvalidArgumentException implements InvalidArgumentExceptionInterface
+class InvalidArgumentException extends \InvalidArgumentException implements Psr6CacheInterface, SimpleCacheInterface
{
}
diff --git a/src/Symfony/Component/Cache/Simple/AbstractCache.php b/src/Symfony/Component/Cache/Simple/AbstractCache.php
new file mode 100644
index 0000000000..4c44b9b323
--- /dev/null
+++ b/src/Symfony/Component/Cache/Simple/AbstractCache.php
@@ -0,0 +1,177 @@
+
+ *
+ * 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;
+use Symfony\Component\Cache\CacheItem;
+use Symfony\Component\Cache\Exception\InvalidArgumentException;
+use Symfony\Component\Cache\Traits\AbstractTrait;
+
+/**
+ * @author Nicolas Grekas
+ */
+abstract class AbstractCache implements CacheInterface, LoggerAwareInterface
+{
+ use AbstractTrait {
+ deleteItems as private;
+ AbstractTrait::deleteItem as delete;
+ AbstractTrait::hasItem as has;
+ }
+
+ private $defaultLifetime;
+
+ protected function __construct($namespace = '', $defaultLifetime = 0)
+ {
+ $this->defaultLifetime = max(0, (int) $defaultLifetime);
+ $this->namespace = '' === $namespace ? '' : $this->getId($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(array($id)) as $value) {
+ return $value;
+ }
+ } catch (\Exception $e) {
+ CacheItem::log($this->logger, 'Failed to fetch key "{key}"', array('key' => $key, 'exception' => $e));
+ }
+
+ return $default;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function set($key, $value, $ttl = null)
+ {
+ CacheItem::validateKey($key);
+
+ return $this->setMultiple(array($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 = array();
+
+ foreach ($keys as $key) {
+ $ids[] = $this->getId($key);
+ }
+ try {
+ $values = $this->doFetch($ids);
+ } catch (\Exception $e) {
+ CacheItem::log($this->logger, 'Failed to fetch requested values', array('keys' => $keys, 'exception' => $e));
+ $values = array();
+ }
+ $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 = array();
+
+ 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 || array() === $e) {
+ return true;
+ }
+ $keys = array();
+ foreach (is_array($e) ? $e : array_keys($valuesById) as $id) {
+ $keys[] = substr($id, strlen($this->namespace));
+ }
+ CacheItem::log($this->logger, 'Failed to save values', array('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) {
+ $key = $keys[$id];
+ unset($keys[$id]);
+ yield $key => $value;
+ }
+ } catch (\Exception $e) {
+ CacheItem::log($this->logger, 'Failed to fetch requested values', array('keys' => array_values($keys), 'exception' => $e));
+ }
+
+ foreach ($keys as $key) {
+ yield $key => $default;
+ }
+ }
+}
diff --git a/src/Symfony/Component/Cache/Simple/ApcuCache.php b/src/Symfony/Component/Cache/Simple/ApcuCache.php
new file mode 100644
index 0000000000..16aa8661f0
--- /dev/null
+++ b/src/Symfony/Component/Cache/Simple/ApcuCache.php
@@ -0,0 +1,24 @@
+
+ *
+ * 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;
+
+class ApcuCache extends AbstractCache
+{
+ use ApcuTrait;
+
+ public function __construct($namespace = '', $defaultLifetime = 0, $version = null)
+ {
+ $this->init($namespace, $defaultLifetime, $version);
+ }
+}
diff --git a/src/Symfony/Component/Cache/Simple/ArrayCache.php b/src/Symfony/Component/Cache/Simple/ArrayCache.php
new file mode 100644
index 0000000000..a89768b0e2
--- /dev/null
+++ b/src/Symfony/Component/Cache/Simple/ArrayCache.php
@@ -0,0 +1,147 @@
+
+ *
+ * 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;
+use Symfony\Component\Cache\CacheItem;
+use Symfony\Component\Cache\Exception\InvalidArgumentException;
+use Symfony\Component\Cache\Traits\ArrayTrait;
+
+/**
+ * @author Nicolas Grekas
+ */
+class ArrayCache implements CacheInterface, LoggerAwareInterface
+{
+ use ArrayTrait {
+ ArrayTrait::deleteItem as delete;
+ ArrayTrait::hasItem as has;
+ }
+
+ private $defaultLifetime;
+
+ /**
+ * @param int $defaultLifetime
+ * @param bool $storeSerialized Disabling serialization can lead to cache corruptions when storing mutable values but increases performance otherwise
+ */
+ public function __construct($defaultLifetime = 0, $storeSerialized = true)
+ {
+ $this->defaultLifetime = (int) $defaultLifetime;
+ $this->storeSerialized = $storeSerialized;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function get($key, $default = null)
+ {
+ foreach ($this->getMultiple(array($key), $default) as $v) {
+ return $v;
+ }
+ }
+
+ /**
+ * {@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) {
+ CacheItem::validateKey($key);
+ }
+
+ return $this->generateItems($keys, time(), 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)
+ {
+ CacheItem::validateKey($key);
+
+ return $this->setMultiple(array($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 = array();
+
+ foreach ($values as $key => $value) {
+ is_int($key) || CacheItem::validateKey($key);
+ $valuesArray[$key] = $value;
+ }
+ if (false === $ttl = $this->normalizeTtl($ttl)) {
+ return $this->deleteMultiple(array_keys($valuesArray));
+ }
+ if ($this->storeSerialized) {
+ foreach ($valuesArray as $key => $value) {
+ try {
+ $valuesArray[$key] = serialize($value);
+ } catch (\Exception $e) {
+ $type = is_object($value) ? get_class($value) : gettype($value);
+ CacheItem::log($this->logger, 'Failed to save key "{key}" ({type})', array('key' => $key, 'type' => $type, 'exception' => $e));
+
+ return false;
+ }
+ }
+ }
+ $expiry = 0 < $ttl ? time() + $ttl : PHP_INT_MAX;
+
+ foreach ($valuesArray as $key => $value) {
+ $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)));
+ }
+}
diff --git a/src/Symfony/Component/Cache/Simple/ChainCache.php b/src/Symfony/Component/Cache/Simple/ChainCache.php
new file mode 100644
index 0000000000..08bb4881b4
--- /dev/null
+++ b/src/Symfony/Component/Cache/Simple/ChainCache.php
@@ -0,0 +1,222 @@
+
+ *
+ * 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;
+use Symfony\Component\Cache\Exception\InvalidArgumentException;
+
+/**
+ * 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.
+ *
+ * @author Nicolas Grekas
+ */
+class ChainCache implements CacheInterface
+{
+ private $miss;
+ private $caches = array();
+ private $defaultLifetime;
+ private $cacheCount;
+
+ /**
+ * @param CacheInterface[] $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, $defaultLifetime = 0)
+ {
+ if (!$caches) {
+ throw new InvalidArgumentException('At least one cache must be specified.');
+ }
+
+ foreach ($caches as $cache) {
+ if (!$cache instanceof CacheInterface) {
+ throw new InvalidArgumentException(sprintf('The class "%s" does not implement the "%s" interface.', get_class($cache), CacheInterface::class));
+ }
+ }
+
+ $this->miss = new \stdClass();
+ $this->caches = array_values($caches);
+ $this->cacheCount = count($this->caches);
+ $this->defaultLifetime = 0 < $defaultLifetime ? (int) $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 = array();
+ $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 = array();
+
+ 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;
+ }
+}
diff --git a/src/Symfony/Component/Cache/Simple/DoctrineCache.php b/src/Symfony/Component/Cache/Simple/DoctrineCache.php
new file mode 100644
index 0000000000..395c34dd81
--- /dev/null
+++ b/src/Symfony/Component/Cache/Simple/DoctrineCache.php
@@ -0,0 +1,27 @@
+
+ *
+ * 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\Traits\DoctrineTrait;
+
+class DoctrineCache extends AbstractCache
+{
+ use DoctrineTrait;
+
+ public function __construct(CacheProvider $provider, $namespace = '', $defaultLifetime = 0)
+ {
+ parent::__construct('', $defaultLifetime);
+ $this->provider = $provider;
+ $provider->setNamespace($namespace);
+ }
+}
diff --git a/src/Symfony/Component/Cache/Simple/FilesystemCache.php b/src/Symfony/Component/Cache/Simple/FilesystemCache.php
new file mode 100644
index 0000000000..a60312ea57
--- /dev/null
+++ b/src/Symfony/Component/Cache/Simple/FilesystemCache.php
@@ -0,0 +1,25 @@
+
+ *
+ * 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\FilesystemTrait;
+
+class FilesystemCache extends AbstractCache
+{
+ use FilesystemTrait;
+
+ public function __construct($namespace = '', $defaultLifetime = 0, $directory = null)
+ {
+ parent::__construct('', $defaultLifetime);
+ $this->init($namespace, $directory);
+ }
+}
diff --git a/src/Symfony/Component/Cache/Simple/MemcachedCache.php b/src/Symfony/Component/Cache/Simple/MemcachedCache.php
new file mode 100644
index 0000000000..1d5ee73c31
--- /dev/null
+++ b/src/Symfony/Component/Cache/Simple/MemcachedCache.php
@@ -0,0 +1,26 @@
+
+ *
+ * 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\MemcachedTrait;
+
+class MemcachedCache extends AbstractCache
+{
+ use MemcachedTrait;
+
+ protected $maxIdLength = 250;
+
+ public function __construct(\Memcached $client, $namespace = '', $defaultLifetime = 0)
+ {
+ $this->init($client, $namespace, $defaultLifetime);
+ }
+}
diff --git a/src/Symfony/Component/Cache/Simple/NullCache.php b/src/Symfony/Component/Cache/Simple/NullCache.php
new file mode 100644
index 0000000000..fa986aebd1
--- /dev/null
+++ b/src/Symfony/Component/Cache/Simple/NullCache.php
@@ -0,0 +1,86 @@
+
+ *
+ * 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;
+
+/**
+ * @author Nicolas Grekas
+ */
+class NullCache implements CacheInterface
+{
+ /**
+ * {@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;
+ }
+}
diff --git a/src/Symfony/Component/Cache/Simple/PdoCache.php b/src/Symfony/Component/Cache/Simple/PdoCache.php
new file mode 100644
index 0000000000..3e698e2f95
--- /dev/null
+++ b/src/Symfony/Component/Cache/Simple/PdoCache.php
@@ -0,0 +1,52 @@
+
+ *
+ * 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\PdoTrait;
+
+class PdoCache extends AbstractCache
+{
+ use PdoTrait;
+
+ protected $maxIdLength = 255;
+
+ /**
+ * Constructor.
+ *
+ * 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.
+ *
+ * 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: array()]
+ *
+ * @param \PDO|Connection|string $connOrDsn A \PDO or Connection instance or DSN string or null
+ * @param string $namespace
+ * @param int $defaultLifetime
+ * @param array $options An associative array of options
+ *
+ * @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, $namespace = '', $defaultLifetime = 0, array $options = array())
+ {
+ $this->init($connOrDsn, $namespace, $defaultLifetime, $options);
+ }
+}
diff --git a/src/Symfony/Component/Cache/Simple/PhpArrayCache.php b/src/Symfony/Component/Cache/Simple/PhpArrayCache.php
new file mode 100644
index 0000000000..3c61f5e8f6
--- /dev/null
+++ b/src/Symfony/Component/Cache/Simple/PhpArrayCache.php
@@ -0,0 +1,256 @@
+
+ *
+ * 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;
+use Symfony\Component\Cache\Exception\InvalidArgumentException;
+use Symfony\Component\Cache\Traits\PhpArrayTrait;
+
+/**
+ * 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
+ */
+class PhpArrayCache implements CacheInterface
+{
+ use PhpArrayTrait;
+
+ /**
+ * @param string $file The PHP file were values are cached
+ * @param CacheInterface $fallbackPool A pool to fallback on when an item is not hit
+ */
+ public function __construct($file, CacheInterface $fallbackPool)
+ {
+ $this->file = $file;
+ $this->fallbackPool = $fallbackPool;
+ }
+
+ /**
+ * This adapter should only be used on PHP 7.0+ to take advantage of how PHP
+ * stores arrays in its latest versions. This factory method decorates the given
+ * fallback pool with this adapter only if the current PHP version is supported.
+ *
+ * @param string $file The PHP file were values are cached
+ *
+ * @return CacheInterface
+ */
+ public static function create($file, CacheInterface $fallbackPool)
+ {
+ // Shared memory is available in PHP 7.0+ with OPCache enabled and in HHVM
+ if ((PHP_VERSION_ID >= 70000 && ini_get('opcache.enable')) || defined('HHVM_VERSION')) {
+ 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->values[$key])) {
+ return $this->fallbackPool->get($key, $default);
+ }
+
+ $value = $this->values[$key];
+
+ if ('N;' === $value) {
+ $value = null;
+ } elseif (is_string($value) && isset($value[2]) && ':' === $value[1]) {
+ try {
+ $e = null;
+ $value = unserialize($value);
+ } catch (\Error $e) {
+ } catch (\Exception $e) {
+ }
+ if (null !== $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->values[$key]) || $this->fallbackPool->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->values[$key]) && $this->fallbackPool->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 = array();
+
+ 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->values[$key])) {
+ $deleted = false;
+ } else {
+ $fallbackKeys[] = $key;
+ }
+ }
+ if (null === $this->values) {
+ $this->initialize();
+ }
+
+ if ($fallbackKeys) {
+ $deleted = $this->fallbackPool->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->values[$key]) && $this->fallbackPool->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 = array();
+
+ 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->values[$key])) {
+ $saved = false;
+ } else {
+ $fallbackValues[$key] = $value;
+ }
+ }
+
+ if ($fallbackValues) {
+ $saved = $this->fallbackPool->setMultiple($fallbackValues, $ttl) && $saved;
+ }
+
+ return $saved;
+ }
+
+ private function generateItems(array $keys, $default)
+ {
+ $fallbackKeys = array();
+
+ foreach ($keys as $key) {
+ if (isset($this->values[$key])) {
+ $value = $this->values[$key];
+
+ if ('N;' === $value) {
+ yield $key => null;
+ } elseif (is_string($value) && isset($value[2]) && ':' === $value[1]) {
+ try {
+ yield $key => unserialize($value);
+ } catch (\Error $e) {
+ yield $key => $default;
+ } catch (\Exception $e) {
+ yield $key => $default;
+ }
+ } else {
+ yield $key => $value;
+ }
+ } else {
+ $fallbackKeys[] = $key;
+ }
+ }
+
+ if ($fallbackKeys) {
+ foreach ($this->fallbackPool->getMultiple($fallbackKeys, $default) as $key => $item) {
+ yield $key => $item;
+ }
+ }
+ }
+}
diff --git a/src/Symfony/Component/Cache/Simple/PhpFilesCache.php b/src/Symfony/Component/Cache/Simple/PhpFilesCache.php
new file mode 100644
index 0000000000..c4d1200806
--- /dev/null
+++ b/src/Symfony/Component/Cache/Simple/PhpFilesCache.php
@@ -0,0 +1,32 @@
+
+ *
+ * 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\Exception\CacheException;
+use Symfony\Component\Cache\Traits\PhpFilesTrait;
+
+class PhpFilesCache extends AbstractCache
+{
+ use PhpFilesTrait;
+
+ public function __construct($namespace = '', $defaultLifetime = 0, $directory = null)
+ {
+ if (!static::isSupported()) {
+ throw new CacheException('OPcache is not enabled');
+ }
+ parent::__construct('', $defaultLifetime);
+ $this->init($namespace, $directory);
+
+ $e = new \Exception();
+ $this->includeHandler = function () use ($e) { throw $e; };
+ }
+}
diff --git a/src/Symfony/Component/Cache/Simple/Psr6Cache.php b/src/Symfony/Component/Cache/Simple/Psr6Cache.php
new file mode 100644
index 0000000000..d23af54069
--- /dev/null
+++ b/src/Symfony/Component/Cache/Simple/Psr6Cache.php
@@ -0,0 +1,225 @@
+
+ *
+ * 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\Cache\CacheItemPoolInterface;
+use Psr\Cache\CacheException as Psr6CacheException;
+use Psr\SimpleCache\CacheInterface;
+use Psr\SimpleCache\CacheException as SimpleCacheException;
+use Symfony\Component\Cache\CacheItem;
+use Symfony\Component\Cache\Exception\InvalidArgumentException;
+
+/**
+ * @author Nicolas Grekas
+ */
+class Psr6Cache implements CacheInterface
+{
+ private $pool;
+ private $createCacheItem;
+
+ public function __construct(CacheItemPoolInterface $pool)
+ {
+ $this->pool = $pool;
+
+ if ($pool instanceof Adapter\AdapterInterface) {
+ $this->createCacheItem = \Closure::bind(
+ function ($key, $value, $allowInt = false) {
+ if ($allowInt && is_int($key)) {
+ $key = (string) $key;
+ } else {
+ CacheItem::validateKey($key);
+ }
+ $item = new CacheItem();
+ $item->key = $key;
+ $item->value = $value;
+
+ return $item;
+ },
+ null,
+ CacheItem::class
+ );
+ }
+ }
+
+ /**
+ * {@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);
+ }
+
+ 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 = array();
+
+ foreach ($items as $key => $item) {
+ $values[$key] = $item->isHit() ? $item->get() : $default;
+ }
+
+ 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 = array();
+
+ try {
+ if (null !== $f = $this->createCacheItem) {
+ $valuesIsArray = false;
+ foreach ($values as $key => $value) {
+ $items[$key] = $f($key, $value, true);
+ }
+ } elseif ($valuesIsArray) {
+ $items = array();
+ 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);
+ }
+ }
+}
diff --git a/src/Symfony/Component/Cache/Simple/RedisCache.php b/src/Symfony/Component/Cache/Simple/RedisCache.php
new file mode 100644
index 0000000000..799a3d082f
--- /dev/null
+++ b/src/Symfony/Component/Cache/Simple/RedisCache.php
@@ -0,0 +1,27 @@
+
+ *
+ * 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\RedisTrait;
+
+class RedisCache extends AbstractCache
+{
+ use RedisTrait;
+
+ /**
+ * @param \Redis|\RedisArray|\RedisCluster|\Predis\Client $redisClient
+ */
+ public function __construct($redisClient, $namespace = '', $defaultLifetime = 0)
+ {
+ $this->init($redisClient, $namespace, $defaultLifetime);
+ }
+}
diff --git a/src/Symfony/Component/Cache/Simple/TraceableCache.php b/src/Symfony/Component/Cache/Simple/TraceableCache.php
new file mode 100644
index 0000000000..40b689dd5d
--- /dev/null
+++ b/src/Symfony/Component/Cache/Simple/TraceableCache.php
@@ -0,0 +1,190 @@
+
+ *
+ * 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;
+
+/**
+ * An adapter that collects data about all cache calls.
+ *
+ * @author Nicolas Grekas
+ */
+class TraceableCache implements CacheInterface
+{
+ private $pool;
+ private $miss;
+ private $calls = array();
+
+ public function __construct(CacheInterface $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__, compact('key', 'default'));
+ try {
+ $value = $this->pool->get($key, $miss);
+ } finally {
+ $event->end = microtime(true);
+ }
+ if ($miss !== $value) {
+ ++$event->hits;
+ } else {
+ ++$event->misses;
+ $value = $default;
+ }
+
+ return $event->result = $value;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function has($key)
+ {
+ $event = $this->start(__FUNCTION__, compact('key'));
+ try {
+ return $event->result = $this->pool->has($key);
+ } finally {
+ $event->end = microtime(true);
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function delete($key)
+ {
+ $event = $this->start(__FUNCTION__, compact('key'));
+ try {
+ return $event->result = $this->pool->delete($key);
+ } finally {
+ $event->end = microtime(true);
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function set($key, $value, $ttl = null)
+ {
+ $event = $this->start(__FUNCTION__, compact('key', 'value', 'ttl'));
+ try {
+ return $event->result = $this->pool->set($key, $value, $ttl);
+ } finally {
+ $event->end = microtime(true);
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setMultiple($values, $ttl = null)
+ {
+ $event = $this->start(__FUNCTION__, compact('values', 'ttl'));
+ try {
+ return $event->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__, compact('keys', 'default'));
+ try {
+ $result = $this->pool->getMultiple($keys, $miss);
+ } finally {
+ $event->end = microtime(true);
+ }
+ $f = function () use ($result, $event, $miss, $default) {
+ $event->result = array();
+ foreach ($result as $key => $value) {
+ if ($miss !== $value) {
+ ++$event->hits;
+ } else {
+ ++$event->misses;
+ $value = $default;
+ }
+ yield $key => $event->result[$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__, compact('keys'));
+ try {
+ return $event->result = $this->pool->deleteMultiple($keys);
+ } finally {
+ $event->end = microtime(true);
+ }
+ }
+
+ public function getCalls()
+ {
+ try {
+ return $this->calls;
+ } finally {
+ $this->calls = array();
+ }
+ }
+
+ private function start($name, array $arguments = null)
+ {
+ $this->calls[] = $event = new TraceableCacheEvent();
+ $event->name = $name;
+ $event->arguments = $arguments;
+ $event->start = microtime(true);
+
+ return $event;
+ }
+}
+
+class TraceableCacheEvent
+{
+ public $name;
+ public $arguments;
+ public $start;
+ public $end;
+ public $result;
+ public $hits = 0;
+ public $misses = 0;
+}
diff --git a/src/Symfony/Component/Cache/Tests/Adapter/ApcuAdapterTest.php b/src/Symfony/Component/Cache/Tests/Adapter/ApcuAdapterTest.php
index 50206bb278..7ebc36f0a5 100644
--- a/src/Symfony/Component/Cache/Tests/Adapter/ApcuAdapterTest.php
+++ b/src/Symfony/Component/Cache/Tests/Adapter/ApcuAdapterTest.php
@@ -48,7 +48,7 @@ class ApcuAdapterTest extends AdapterTestCase
public function testVersion()
{
- $namespace = str_replace('\\', '.', __CLASS__);
+ $namespace = str_replace('\\', '.', get_class($this));
$pool1 = new ApcuAdapter($namespace, 0, 'p1');
diff --git a/src/Symfony/Component/Cache/Tests/Adapter/MemcachedAdapterTest.php b/src/Symfony/Component/Cache/Tests/Adapter/MemcachedAdapterTest.php
index 6567740d68..82b41c3b4d 100644
--- a/src/Symfony/Component/Cache/Tests/Adapter/MemcachedAdapterTest.php
+++ b/src/Symfony/Component/Cache/Tests/Adapter/MemcachedAdapterTest.php
@@ -22,7 +22,7 @@ class MemcachedAdapterTest extends AdapterTestCase
'testDefaultLifeTime' => 'Testing expiration slows down the test suite',
);
- private static $client;
+ protected static $client;
public static function setupBeforeClass()
{
diff --git a/src/Symfony/Component/Cache/Tests/Adapter/PhpArrayAdapterTest.php b/src/Symfony/Component/Cache/Tests/Adapter/PhpArrayAdapterTest.php
index ff3351ddf6..ae0edb7d11 100644
--- a/src/Symfony/Component/Cache/Tests/Adapter/PhpArrayAdapterTest.php
+++ b/src/Symfony/Component/Cache/Tests/Adapter/PhpArrayAdapterTest.php
@@ -51,7 +51,7 @@ class PhpArrayAdapterTest extends AdapterTestCase
'testDefaultLifeTime' => 'PhpArrayAdapter does not allow configuring a default lifetime.',
);
- private static $file;
+ protected static $file;
public static function setupBeforeClass()
{
diff --git a/src/Symfony/Component/Cache/Tests/Adapter/PhpArrayAdapterWithFallbackTest.php b/src/Symfony/Component/Cache/Tests/Adapter/PhpArrayAdapterWithFallbackTest.php
index 7030c0e9c5..45a50d2323 100644
--- a/src/Symfony/Component/Cache/Tests/Adapter/PhpArrayAdapterWithFallbackTest.php
+++ b/src/Symfony/Component/Cache/Tests/Adapter/PhpArrayAdapterWithFallbackTest.php
@@ -25,10 +25,9 @@ class PhpArrayAdapterWithFallbackTest extends AdapterTestCase
'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.',
);
- private static $file;
+ protected static $file;
public static function setupBeforeClass()
{
@@ -42,8 +41,8 @@ class PhpArrayAdapterWithFallbackTest extends AdapterTestCase
}
}
- public function createCachePool()
+ public function createCachePool($defaultLifetime = 0)
{
- return new PhpArrayAdapter(self::$file, new FilesystemAdapter('php-array-fallback'));
+ return new PhpArrayAdapter(self::$file, new FilesystemAdapter('php-array-fallback', $defaultLifetime));
}
}
diff --git a/src/Symfony/Component/Cache/Tests/Adapter/SimpleCacheAdapterTest.php b/src/Symfony/Component/Cache/Tests/Adapter/SimpleCacheAdapterTest.php
new file mode 100644
index 0000000000..1e0297c69e
--- /dev/null
+++ b/src/Symfony/Component/Cache/Tests/Adapter/SimpleCacheAdapterTest.php
@@ -0,0 +1,26 @@
+
+ *
+ * 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\Simple\FilesystemCache;
+use Symfony\Component\Cache\Adapter\SimpleCacheAdapter;
+
+/**
+ * @group time-sensitive
+ */
+class SimpleCacheAdapterTest extends AdapterTestCase
+{
+ public function createCachePool($defaultLifetime = 0)
+ {
+ return new SimpleCacheAdapter(new FilesystemCache(), '', $defaultLifetime);
+ }
+}
diff --git a/src/Symfony/Component/Cache/Tests/Adapter/TraceableAdapterTest.php b/src/Symfony/Component/Cache/Tests/Adapter/TraceableAdapterTest.php
index ad55218b0d..f05fbf9cfb 100644
--- a/src/Symfony/Component/Cache/Tests/Adapter/TraceableAdapterTest.php
+++ b/src/Symfony/Component/Cache/Tests/Adapter/TraceableAdapterTest.php
@@ -32,10 +32,10 @@ class TraceableAdapterTest extends AdapterTestCase
$this->assertCount(1, $calls);
$call = $calls[0];
- $this->assertEquals('getItem', $call->name);
- $this->assertEquals('k', $call->argument);
- $this->assertEquals(0, $call->hits);
- $this->assertEquals(1, $call->misses);
+ $this->assertSame('getItem', $call->name);
+ $this->assertSame('k', $call->argument);
+ $this->assertSame(0, $call->hits);
+ $this->assertSame(1, $call->misses);
$this->assertNull($call->result);
$this->assertNotEmpty($call->start);
$this->assertNotEmpty($call->end);
@@ -51,8 +51,8 @@ class TraceableAdapterTest extends AdapterTestCase
$this->assertCount(3, $calls);
$call = $calls[2];
- $this->assertEquals(1, $call->hits);
- $this->assertEquals(0, $call->misses);
+ $this->assertSame(1, $call->hits);
+ $this->assertSame(0, $call->misses);
}
public function testGetItemsMiss()
@@ -66,9 +66,9 @@ class TraceableAdapterTest extends AdapterTestCase
$this->assertCount(1, $calls);
$call = $calls[0];
- $this->assertEquals('getItems', $call->name);
- $this->assertEquals($arg, $call->argument);
- $this->assertEquals(2, $call->misses);
+ $this->assertSame('getItems', $call->name);
+ $this->assertSame($arg, $call->argument);
+ $this->assertSame(2, $call->misses);
$this->assertNotEmpty($call->start);
$this->assertNotEmpty($call->end);
}
@@ -81,8 +81,8 @@ class TraceableAdapterTest extends AdapterTestCase
$this->assertCount(1, $calls);
$call = $calls[0];
- $this->assertEquals('hasItem', $call->name);
- $this->assertEquals('k', $call->argument);
+ $this->assertSame('hasItem', $call->name);
+ $this->assertSame('k', $call->argument);
$this->assertFalse($call->result);
$this->assertNotEmpty($call->start);
$this->assertNotEmpty($call->end);
@@ -98,8 +98,8 @@ class TraceableAdapterTest extends AdapterTestCase
$this->assertCount(3, $calls);
$call = $calls[2];
- $this->assertEquals('hasItem', $call->name);
- $this->assertEquals('k', $call->argument);
+ $this->assertSame('hasItem', $call->name);
+ $this->assertSame('k', $call->argument);
$this->assertTrue($call->result);
$this->assertNotEmpty($call->start);
$this->assertNotEmpty($call->end);
@@ -113,10 +113,10 @@ class TraceableAdapterTest extends AdapterTestCase
$this->assertCount(1, $calls);
$call = $calls[0];
- $this->assertEquals('deleteItem', $call->name);
- $this->assertEquals('k', $call->argument);
- $this->assertEquals(0, $call->hits);
- $this->assertEquals(0, $call->misses);
+ $this->assertSame('deleteItem', $call->name);
+ $this->assertSame('k', $call->argument);
+ $this->assertSame(0, $call->hits);
+ $this->assertSame(0, $call->misses);
$this->assertNotEmpty($call->start);
$this->assertNotEmpty($call->end);
}
@@ -130,10 +130,10 @@ class TraceableAdapterTest extends AdapterTestCase
$this->assertCount(1, $calls);
$call = $calls[0];
- $this->assertEquals('deleteItems', $call->name);
- $this->assertEquals($arg, $call->argument);
- $this->assertEquals(0, $call->hits);
- $this->assertEquals(0, $call->misses);
+ $this->assertSame('deleteItems', $call->name);
+ $this->assertSame($arg, $call->argument);
+ $this->assertSame(0, $call->hits);
+ $this->assertSame(0, $call->misses);
$this->assertNotEmpty($call->start);
$this->assertNotEmpty($call->end);
}
@@ -147,10 +147,10 @@ class TraceableAdapterTest extends AdapterTestCase
$this->assertCount(2, $calls);
$call = $calls[1];
- $this->assertEquals('save', $call->name);
- $this->assertEquals($item, $call->argument);
- $this->assertEquals(0, $call->hits);
- $this->assertEquals(0, $call->misses);
+ $this->assertSame('save', $call->name);
+ $this->assertSame($item, $call->argument);
+ $this->assertSame(0, $call->hits);
+ $this->assertSame(0, $call->misses);
$this->assertNotEmpty($call->start);
$this->assertNotEmpty($call->end);
}
@@ -164,10 +164,10 @@ class TraceableAdapterTest extends AdapterTestCase
$this->assertCount(2, $calls);
$call = $calls[1];
- $this->assertEquals('saveDeferred', $call->name);
- $this->assertEquals($item, $call->argument);
- $this->assertEquals(0, $call->hits);
- $this->assertEquals(0, $call->misses);
+ $this->assertSame('saveDeferred', $call->name);
+ $this->assertSame($item, $call->argument);
+ $this->assertSame(0, $call->hits);
+ $this->assertSame(0, $call->misses);
$this->assertNotEmpty($call->start);
$this->assertNotEmpty($call->end);
}
@@ -180,10 +180,10 @@ class TraceableAdapterTest extends AdapterTestCase
$this->assertCount(1, $calls);
$call = $calls[0];
- $this->assertEquals('commit', $call->name);
+ $this->assertSame('commit', $call->name);
$this->assertNull(null, $call->argument);
- $this->assertEquals(0, $call->hits);
- $this->assertEquals(0, $call->misses);
+ $this->assertSame(0, $call->hits);
+ $this->assertSame(0, $call->misses);
$this->assertNotEmpty($call->start);
$this->assertNotEmpty($call->end);
}
diff --git a/src/Symfony/Component/Cache/Tests/Simple/AbstractRedisCacheTest.php b/src/Symfony/Component/Cache/Tests/Simple/AbstractRedisCacheTest.php
new file mode 100644
index 0000000000..1d097fff85
--- /dev/null
+++ b/src/Symfony/Component/Cache/Tests/Simple/AbstractRedisCacheTest.php
@@ -0,0 +1,47 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Cache\Tests\Simple;
+
+use Symfony\Component\Cache\Simple\RedisCache;
+
+abstract class AbstractRedisCacheTest extends CacheTestCase
+{
+ protected $skippedTests = array(
+ 'testSetTtl' => 'Testing expiration slows down the test suite',
+ 'testSetMultipleTtl' => 'Testing expiration slows down the test suite',
+ 'testDefaultLifeTime' => 'Testing expiration slows down the test suite',
+ );
+
+ protected static $redis;
+
+ public function createSimpleCache($defaultLifetime = 0)
+ {
+ return new RedisCache(self::$redis, str_replace('\\', '.', __CLASS__), $defaultLifetime);
+ }
+
+ public static function setupBeforeClass()
+ {
+ 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()
+ {
+ self::$redis->flushDB();
+ self::$redis = null;
+ }
+}
diff --git a/src/Symfony/Component/Cache/Tests/Simple/ApcuCacheTest.php b/src/Symfony/Component/Cache/Tests/Simple/ApcuCacheTest.php
new file mode 100644
index 0000000000..297a41756f
--- /dev/null
+++ b/src/Symfony/Component/Cache/Tests/Simple/ApcuCacheTest.php
@@ -0,0 +1,35 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Cache\Tests\Simple;
+
+use Symfony\Component\Cache\Simple\ApcuCache;
+
+class ApcuCacheTest extends CacheTestCase
+{
+ protected $skippedTests = array(
+ 'testSetTtl' => 'Testing expiration slows down the test suite',
+ 'testSetMultipleTtl' => 'Testing expiration slows down the test suite',
+ 'testDefaultLifeTime' => 'Testing expiration slows down the test suite',
+ );
+
+ public function createSimpleCache($defaultLifetime = 0)
+ {
+ if (!function_exists('apcu_fetch') || !ini_get('apc.enabled') || ('cli' === PHP_SAPI && !ini_get('apc.enable_cli'))) {
+ $this->markTestSkipped('APCu extension is required.');
+ }
+ if ('\\' === DIRECTORY_SEPARATOR) {
+ $this->markTestSkipped('Fails transiently on Windows.');
+ }
+
+ return new ApcuCache(str_replace('\\', '.', __CLASS__), $defaultLifetime);
+ }
+}
diff --git a/src/Symfony/Component/Cache/Tests/Simple/ArrayCacheTest.php b/src/Symfony/Component/Cache/Tests/Simple/ArrayCacheTest.php
new file mode 100644
index 0000000000..26c3e14d09
--- /dev/null
+++ b/src/Symfony/Component/Cache/Tests/Simple/ArrayCacheTest.php
@@ -0,0 +1,25 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Cache\Tests\Simple;
+
+use Symfony\Component\Cache\Simple\ArrayCache;
+
+/**
+ * @group time-sensitive
+ */
+class ArrayCacheTest extends CacheTestCase
+{
+ public function createSimpleCache($defaultLifetime = 0)
+ {
+ return new ArrayCache($defaultLifetime);
+ }
+}
diff --git a/src/Symfony/Component/Cache/Tests/Simple/CacheTestCase.php b/src/Symfony/Component/Cache/Tests/Simple/CacheTestCase.php
new file mode 100644
index 0000000000..81d412bd66
--- /dev/null
+++ b/src/Symfony/Component/Cache/Tests/Simple/CacheTestCase.php
@@ -0,0 +1,66 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Cache\Tests\Simple;
+
+use Cache\IntegrationTests\SimpleCacheTest;
+
+abstract class CacheTestCase extends SimpleCacheTest
+{
+ public function testDefaultLifeTime()
+ {
+ if (isset($this->skippedTests[__FUNCTION__])) {
+ $this->markTestSkipped($this->skippedTests[__FUNCTION__]);
+ }
+
+ $cache = $this->createSimpleCache(2);
+
+ $cache->set('key.dlt', 'value');
+ sleep(1);
+
+ $this->assertSame('value', $cache->get('key.dlt'));
+
+ sleep(2);
+ $this->assertNull($cache->get('key.dlt'));
+ }
+
+ public function testNotUnserializable()
+ {
+ if (isset($this->skippedTests[__FUNCTION__])) {
+ $this->markTestSkipped($this->skippedTests[__FUNCTION__]);
+ }
+
+ $cache = $this->createSimpleCache();
+
+ $cache->set('foo', new NotUnserializable());
+
+ $this->assertNull($cache->get('foo'));
+
+ $cache->setMultiple(array('foo' => new NotUnserializable()));
+
+ foreach ($cache->getMultiple(array('foo')) as $value) {
+ }
+ $this->assertNull($value);
+ }
+}
+
+class NotUnserializable implements \Serializable
+{
+ public function serialize()
+ {
+ return serialize(123);
+ }
+
+ public function unserialize($ser)
+ {
+ throw new \Exception(__CLASS__);
+ }
+}
diff --git a/src/Symfony/Component/Cache/Tests/Simple/ChainCacheTest.php b/src/Symfony/Component/Cache/Tests/Simple/ChainCacheTest.php
new file mode 100644
index 0000000000..282bb62a65
--- /dev/null
+++ b/src/Symfony/Component/Cache/Tests/Simple/ChainCacheTest.php
@@ -0,0 +1,45 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Cache\Tests\Simple;
+
+use Symfony\Component\Cache\Simple\ArrayCache;
+use Symfony\Component\Cache\Simple\ChainCache;
+use Symfony\Component\Cache\Simple\FilesystemCache;
+
+/**
+ * @group time-sensitive
+ */
+class ChainCacheTest extends CacheTestCase
+{
+ public function createSimpleCache($defaultLifetime = 0)
+ {
+ return new ChainCache(array(new ArrayCache($defaultLifetime), new FilesystemCache('', $defaultLifetime)), $defaultLifetime);
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Cache\Exception\InvalidArgumentException
+ * @expectedExceptionMessage At least one cache must be specified.
+ */
+ public function testEmptyCachesException()
+ {
+ new ChainCache(array());
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Cache\Exception\InvalidArgumentException
+ * @expectedExceptionMessage The class "stdClass" does not implement
+ */
+ public function testInvalidCacheException()
+ {
+ new Chaincache(array(new \stdClass()));
+ }
+}
diff --git a/src/Symfony/Component/Cache/Tests/Simple/DoctrineCacheTest.php b/src/Symfony/Component/Cache/Tests/Simple/DoctrineCacheTest.php
new file mode 100644
index 0000000000..0a185297ab
--- /dev/null
+++ b/src/Symfony/Component/Cache/Tests/Simple/DoctrineCacheTest.php
@@ -0,0 +1,31 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Cache\Tests\Simple;
+
+use Doctrine\Common\Cache\ArrayCache;
+use Symfony\Component\Cache\Simple\DoctrineCache;
+
+/**
+ * @group time-sensitive
+ */
+class DoctrineCacheTest extends CacheTestCase
+{
+ protected $skippedTests = array(
+ 'testObjectDoesNotChangeInCache' => 'ArrayCache does not use serialize/unserialize',
+ 'testNotUnserializable' => 'ArrayCache does not use serialize/unserialize',
+ );
+
+ public function createSimpleCache($defaultLifetime = 0)
+ {
+ return new DoctrineCache(new ArrayCache($defaultLifetime), '', $defaultLifetime);
+ }
+}
diff --git a/src/Symfony/Component/Cache/Tests/Simple/FilesystemCacheTest.php b/src/Symfony/Component/Cache/Tests/Simple/FilesystemCacheTest.php
new file mode 100644
index 0000000000..0f2d519cad
--- /dev/null
+++ b/src/Symfony/Component/Cache/Tests/Simple/FilesystemCacheTest.php
@@ -0,0 +1,25 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Cache\Tests\Simple;
+
+use Symfony\Component\Cache\Simple\FilesystemCache;
+
+/**
+ * @group time-sensitive
+ */
+class FilesystemCacheTest extends CacheTestCase
+{
+ public function createSimpleCache($defaultLifetime = 0)
+ {
+ return new FilesystemCache('', $defaultLifetime);
+ }
+}
diff --git a/src/Symfony/Component/Cache/Tests/Simple/MemcachedCacheTest.php b/src/Symfony/Component/Cache/Tests/Simple/MemcachedCacheTest.php
new file mode 100644
index 0000000000..c4af891af7
--- /dev/null
+++ b/src/Symfony/Component/Cache/Tests/Simple/MemcachedCacheTest.php
@@ -0,0 +1,165 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Cache\Tests\Simple;
+
+use Symfony\Component\Cache\Adapter\AbstractAdapter;
+use Symfony\Component\Cache\Simple\MemcachedCache;
+
+class MemcachedCacheTest extends CacheTestCase
+{
+ protected $skippedTests = array(
+ 'testSetTtl' => 'Testing expiration slows down the test suite',
+ 'testSetMultipleTtl' => 'Testing expiration slows down the test suite',
+ 'testDefaultLifeTime' => 'Testing expiration slows down the test suite',
+ );
+
+ protected static $client;
+
+ public static function setupBeforeClass()
+ {
+ if (!MemcachedCache::isSupported()) {
+ self::markTestSkipped('Extension memcached >=2.2.0 required.');
+ }
+ self::$client = AbstractAdapter::createConnection('memcached://'.getenv('MEMCACHED_HOST'));
+ 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 createSimpleCache($defaultLifetime = 0)
+ {
+ $client = $defaultLifetime ? AbstractAdapter::createConnection('memcached://'.getenv('MEMCACHED_HOST'), array('binary_protocol' => false)) : self::$client;
+
+ return new MemcachedCache($client, str_replace('\\', '.', __CLASS__), $defaultLifetime);
+ }
+
+ public function testOptions()
+ {
+ $client = MemcachedCache::createConnection(array(), array(
+ '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
+ * @expectedException \ErrorException
+ * @expectedExceptionMessage constant(): Couldn't find constant Memcached::
+ */
+ public function testBadOptions($name, $value)
+ {
+ MemcachedCache::createConnection(array(), array($name => $value));
+ }
+
+ public function provideBadOptions()
+ {
+ return array(
+ array('foo', 'bar'),
+ array('hash', 'zyx'),
+ array('serializer', 'zyx'),
+ array('distribution', 'zyx'),
+ );
+ }
+
+ public function testDefaultOptions()
+ {
+ $this->assertTrue(MemcachedCache::isSupported());
+
+ $client = MemcachedCache::createConnection(array());
+
+ $this->assertTrue($client->getOption(\Memcached::OPT_COMPRESSION));
+ $this->assertSame(1, $client->getOption(\Memcached::OPT_BINARY_PROTOCOL));
+ $this->assertSame(1, $client->getOption(\Memcached::OPT_LIBKETAMA_COMPATIBLE));
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Cache\Exception\CacheException
+ * @expectedExceptionMessage MemcachedAdapter: "serializer" option must be "php" or "igbinary".
+ */
+ public function testOptionSerializer()
+ {
+ if (!\Memcached::HAVE_JSON) {
+ $this->markTestSkipped('Memcached::HAVE_JSON required');
+ }
+
+ new MemcachedCache(MemcachedCache::createConnection(array(), array('serializer' => 'json')));
+ }
+
+ /**
+ * @dataProvider provideServersSetting
+ */
+ public function testServersSetting($dsn, $host, $port)
+ {
+ $client1 = MemcachedCache::createConnection($dsn);
+ $client2 = MemcachedCache::createConnection(array($dsn));
+ $client3 = MemcachedCache::createConnection(array(array($host, $port)));
+ $expect = array(
+ 'host' => $host,
+ 'port' => $port,
+ );
+
+ $f = function ($s) { return array('host' => $s['host'], 'port' => $s['port']); };
+ $this->assertSame(array($expect), array_map($f, $client1->getServerList()));
+ $this->assertSame(array($expect), array_map($f, $client2->getServerList()));
+ $this->assertSame(array($expect), array_map($f, $client3->getServerList()));
+ }
+
+ public function provideServersSetting()
+ {
+ yield array(
+ 'memcached://127.0.0.1/50',
+ '127.0.0.1',
+ 11211,
+ );
+ yield array(
+ 'memcached://localhost:11222?weight=25',
+ 'localhost',
+ 11222,
+ );
+ if (ini_get('memcached.use_sasl')) {
+ yield array(
+ 'memcached://user:password@127.0.0.1?weight=50',
+ '127.0.0.1',
+ 11211,
+ );
+ }
+ yield array(
+ 'memcached:///var/run/memcached.sock?weight=25',
+ '/var/run/memcached.sock',
+ 0,
+ );
+ yield array(
+ 'memcached:///var/local/run/memcached.socket?weight=25',
+ '/var/local/run/memcached.socket',
+ 0,
+ );
+ if (ini_get('memcached.use_sasl')) {
+ yield array(
+ 'memcached://user:password@/var/local/run/memcached.socket?weight=25',
+ '/var/local/run/memcached.socket',
+ 0,
+ );
+ }
+ }
+}
diff --git a/src/Symfony/Component/Cache/Tests/Simple/NullCacheTest.php b/src/Symfony/Component/Cache/Tests/Simple/NullCacheTest.php
new file mode 100644
index 0000000000..e7b9674ff1
--- /dev/null
+++ b/src/Symfony/Component/Cache/Tests/Simple/NullCacheTest.php
@@ -0,0 +1,95 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Cache\Tests\Simple;
+
+use Symfony\Component\Cache\Simple\NullCache;
+
+/**
+ * @group time-sensitive
+ */
+class NullCacheTest extends \PHPUnit_Framework_TestCase
+{
+ public function createCachePool()
+ {
+ return new NullCache();
+ }
+
+ public function testGetItem()
+ {
+ $cache = $this->createCachePool();
+
+ $this->assertNull($cache->get('key'));
+ }
+
+ public function testHas()
+ {
+ $this->assertFalse($this->createCachePool()->has('key'));
+ }
+
+ public function testGetMultiple()
+ {
+ $cache = $this->createCachePool();
+
+ $keys = array('foo', 'bar', 'baz', 'biz');
+
+ $default = new \stdClass();
+ $items = $cache->getMultiple($keys, $default);
+ $count = 0;
+
+ foreach ($items as $key => $item) {
+ $this->assertTrue(in_array($key, $keys), 'Cache key can not change.');
+ $this->assertSame($default, $item);
+
+ // Remove $key for $keys
+ foreach ($keys as $k => $v) {
+ if ($v === $key) {
+ unset($keys[$k]);
+ }
+ }
+
+ ++$count;
+ }
+
+ $this->assertSame(4, $count);
+ }
+
+ public function testClear()
+ {
+ $this->assertTrue($this->createCachePool()->clear());
+ }
+
+ public function testDelete()
+ {
+ $this->assertTrue($this->createCachePool()->delete('key'));
+ }
+
+ public function testDeleteMultiple()
+ {
+ $this->assertTrue($this->createCachePool()->deleteMultiple(array('key', 'foo', 'bar')));
+ }
+
+ public function testSet()
+ {
+ $cache = $this->createCachePool();
+
+ $this->assertFalse($cache->set('key', 'val'));
+ $this->assertNull($cache->get('key'));
+ }
+
+ public function testSetMultiple()
+ {
+ $cache = $this->createCachePool();
+
+ $this->assertFalse($cache->setMultiple(array('key' => 'val')));
+ $this->assertNull($cache->get('key'));
+ }
+}
diff --git a/src/Symfony/Component/Cache/Tests/Simple/PdoCacheTest.php b/src/Symfony/Component/Cache/Tests/Simple/PdoCacheTest.php
new file mode 100644
index 0000000000..2605ba9201
--- /dev/null
+++ b/src/Symfony/Component/Cache/Tests/Simple/PdoCacheTest.php
@@ -0,0 +1,44 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Cache\Tests\Simple;
+
+use Symfony\Component\Cache\Simple\PdoCache;
+
+/**
+ * @group time-sensitive
+ */
+class PdoCacheTest extends CacheTestCase
+{
+ protected static $dbFile;
+
+ public static function setupBeforeClass()
+ {
+ if (!extension_loaded('pdo_sqlite')) {
+ throw new \PHPUnit_Framework_SkippedTestError('Extension pdo_sqlite required.');
+ }
+
+ self::$dbFile = tempnam(sys_get_temp_dir(), 'sf_sqlite_cache');
+
+ $pool = new PdoCache('sqlite:'.self::$dbFile);
+ $pool->createTable();
+ }
+
+ public static function tearDownAfterClass()
+ {
+ @unlink(self::$dbFile);
+ }
+
+ public function createSimpleCache($defaultLifetime = 0)
+ {
+ return new PdoCache('sqlite:'.self::$dbFile, 'ns', $defaultLifetime);
+ }
+}
diff --git a/src/Symfony/Component/Cache/Tests/Simple/PdoDbalCacheTest.php b/src/Symfony/Component/Cache/Tests/Simple/PdoDbalCacheTest.php
new file mode 100644
index 0000000000..18847ad925
--- /dev/null
+++ b/src/Symfony/Component/Cache/Tests/Simple/PdoDbalCacheTest.php
@@ -0,0 +1,45 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Cache\Tests\Simple;
+
+use Doctrine\DBAL\DriverManager;
+use Symfony\Component\Cache\Simple\PdoCache;
+
+/**
+ * @group time-sensitive
+ */
+class PdoDbalCacheTest extends CacheTestCase
+{
+ protected static $dbFile;
+
+ public static function setupBeforeClass()
+ {
+ if (!extension_loaded('pdo_sqlite')) {
+ throw new \PHPUnit_Framework_SkippedTestError('Extension pdo_sqlite required.');
+ }
+
+ self::$dbFile = tempnam(sys_get_temp_dir(), 'sf_sqlite_cache');
+
+ $pool = new PdoCache(DriverManager::getConnection(array('driver' => 'pdo_sqlite', 'path' => self::$dbFile)));
+ $pool->createTable();
+ }
+
+ public static function tearDownAfterClass()
+ {
+ @unlink(self::$dbFile);
+ }
+
+ public function createSimpleCache($defaultLifetime = 0)
+ {
+ return new PdoCache(DriverManager::getConnection(array('driver' => 'pdo_sqlite', 'path' => self::$dbFile)), '', $defaultLifetime);
+ }
+}
diff --git a/src/Symfony/Component/Cache/Tests/Simple/PhpArrayCacheTest.php b/src/Symfony/Component/Cache/Tests/Simple/PhpArrayCacheTest.php
new file mode 100644
index 0000000000..3016ac560e
--- /dev/null
+++ b/src/Symfony/Component/Cache/Tests/Simple/PhpArrayCacheTest.php
@@ -0,0 +1,139 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Cache\Tests\Simple;
+
+use Symfony\Component\Cache\Tests\Adapter\FilesystemAdapterTest;
+use Symfony\Component\Cache\Simple\NullCache;
+use Symfony\Component\Cache\Simple\PhpArrayCache;
+
+/**
+ * @group time-sensitive
+ */
+class PhpArrayCacheTest extends CacheTestCase
+{
+ protected $skippedTests = array(
+ 'testDelete' => 'PhpArrayCache does no writes',
+ 'testDeleteMultiple' => 'PhpArrayCache does no writes',
+ 'testDeleteMultipleGenerator' => 'PhpArrayCache does no writes',
+
+ 'testSetTtl' => 'PhpArrayCache does no expiration',
+ 'testSetMultipleTtl' => 'PhpArrayCache does no expiration',
+ 'testSetExpiredTtl' => 'PhpArrayCache does no expiration',
+ 'testSetMultipleExpiredTtl' => 'PhpArrayCache does no expiration',
+
+ 'testGetInvalidKeys' => 'PhpArrayCache does no validation',
+ 'testGetMultipleInvalidKeys' => 'PhpArrayCache does no validation',
+ 'testSetInvalidKeys' => 'PhpArrayCache does no validation',
+ 'testDeleteInvalidKeys' => 'PhpArrayCache does no validation',
+ 'testDeleteMultipleInvalidKeys' => 'PhpArrayCache does no validation',
+ 'testSetInvalidTtl' => 'PhpArrayCache does no validation',
+ 'testSetMultipleInvalidKeys' => 'PhpArrayCache does no validation',
+ 'testSetMultipleInvalidTtl' => 'PhpArrayCache does no validation',
+ 'testHasInvalidKeys' => 'PhpArrayCache does no validation',
+ 'testSetValidData' => 'PhpArrayCache does no validation',
+
+ 'testDefaultLifeTime' => 'PhpArrayCache does not allow configuring a default lifetime.',
+ );
+
+ protected static $file;
+
+ public static function setupBeforeClass()
+ {
+ self::$file = sys_get_temp_dir().'/symfony-cache/php-array-adapter-test.php';
+ }
+
+ protected function tearDown()
+ {
+ if (file_exists(sys_get_temp_dir().'/symfony-cache')) {
+ FilesystemAdapterTest::rmdir(sys_get_temp_dir().'/symfony-cache');
+ }
+ }
+ public function createSimpleCache()
+ {
+ return new PhpArrayCacheWrapper(self::$file, new NullCache());
+ }
+
+ public function testStore()
+ {
+ $arrayWithRefs = array();
+ $arrayWithRefs[0] = 123;
+ $arrayWithRefs[1] = &$arrayWithRefs[0];
+
+ $object = (object) array(
+ 'foo' => 'bar',
+ 'foo2' => 'bar2',
+ );
+
+ $expected = array(
+ 'null' => null,
+ 'serializedString' => serialize($object),
+ 'arrayWithRefs' => $arrayWithRefs,
+ 'object' => $object,
+ 'arrayWithObject' => array('bar' => $object),
+ );
+
+ $cache = new PhpArrayCache(self::$file, new NullCache());
+ $cache->warmUp($expected);
+
+ foreach ($expected as $key => $value) {
+ $this->assertSame(serialize($value), serialize($cache->get($key)), 'Warm up should create a PHP file that OPCache can load in memory');
+ }
+ }
+
+ public function testStoredFile()
+ {
+ $expected = array(
+ 'integer' => 42,
+ 'float' => 42.42,
+ 'boolean' => true,
+ 'array_simple' => array('foo', 'bar'),
+ 'array_associative' => array('foo' => 'bar', 'foo2' => 'bar2'),
+ );
+
+ $cache = new PhpArrayCache(self::$file, new NullCache());
+ $cache->warmUp($expected);
+
+ $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 PhpArrayCacheWrapper extends PhpArrayCache
+{
+ public function set($key, $value, $ttl = null)
+ {
+ call_user_func(\Closure::bind(function () use ($key, $value) {
+ $this->values[$key] = $value;
+ $this->warmUp($this->values);
+ $this->values = eval(substr(file_get_contents($this->file), 6));
+ }, $this, PhpArrayCache::class));
+
+ return true;
+ }
+
+ public function setMultiple($values, $ttl = null)
+ {
+ if (!is_array($values) && !$values instanceof \Traversable) {
+ return parent::setMultiple($values, $ttl);
+ }
+ call_user_func(\Closure::bind(function () use ($values) {
+ foreach ($values as $key => $value) {
+ $this->values[$key] = $value;
+ }
+ $this->warmUp($this->values);
+ $this->values = eval(substr(file_get_contents($this->file), 6));
+ }, $this, PhpArrayCache::class));
+
+ return true;
+ }
+}
diff --git a/src/Symfony/Component/Cache/Tests/Simple/PhpArrayCacheWithFallbackTest.php b/src/Symfony/Component/Cache/Tests/Simple/PhpArrayCacheWithFallbackTest.php
new file mode 100644
index 0000000000..a624fa73e7
--- /dev/null
+++ b/src/Symfony/Component/Cache/Tests/Simple/PhpArrayCacheWithFallbackTest.php
@@ -0,0 +1,54 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Cache\Tests\Simple;
+
+use Symfony\Component\Cache\Simple\FilesystemCache;
+use Symfony\Component\Cache\Simple\PhpArrayCache;
+use Symfony\Component\Cache\Tests\Adapter\FilesystemAdapterTest;
+
+/**
+ * @group time-sensitive
+ */
+class PhpArrayCacheWithFallbackTest extends CacheTestCase
+{
+ protected $skippedTests = array(
+ 'testGetInvalidKeys' => 'PhpArrayCache does no validation',
+ 'testGetMultipleInvalidKeys' => 'PhpArrayCache does no validation',
+ 'testDeleteInvalidKeys' => 'PhpArrayCache does no validation',
+ 'testDeleteMultipleInvalidKeys' => 'PhpArrayCache does no validation',
+ //'testSetValidData' => 'PhpArrayCache does no validation',
+ 'testSetInvalidKeys' => 'PhpArrayCache does no validation',
+ 'testSetInvalidTtl' => 'PhpArrayCache does no validation',
+ 'testSetMultipleInvalidKeys' => 'PhpArrayCache does no validation',
+ 'testSetMultipleInvalidTtl' => 'PhpArrayCache does no validation',
+ 'testHasInvalidKeys' => 'PhpArrayCache does no validation',
+ );
+
+ protected static $file;
+
+ public static function setupBeforeClass()
+ {
+ self::$file = sys_get_temp_dir().'/symfony-cache/php-array-adapter-test.php';
+ }
+
+ protected function tearDown()
+ {
+ if (file_exists(sys_get_temp_dir().'/symfony-cache')) {
+ FilesystemAdapterTest::rmdir(sys_get_temp_dir().'/symfony-cache');
+ }
+ }
+
+ public function createSimpleCache($defaultLifetime = 0)
+ {
+ return new PhpArrayCache(self::$file, new FilesystemCache('php-array-fallback', $defaultLifetime));
+ }
+}
diff --git a/src/Symfony/Component/Cache/Tests/Simple/PhpFilesCacheTest.php b/src/Symfony/Component/Cache/Tests/Simple/PhpFilesCacheTest.php
new file mode 100644
index 0000000000..3118fcf94e
--- /dev/null
+++ b/src/Symfony/Component/Cache/Tests/Simple/PhpFilesCacheTest.php
@@ -0,0 +1,33 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Cache\Tests\Simple;
+
+use Symfony\Component\Cache\Simple\PhpFilesCache;
+
+/**
+ * @group time-sensitive
+ */
+class PhpFilesCacheTest extends CacheTestCase
+{
+ protected $skippedTests = array(
+ 'testDefaultLifeTime' => 'PhpFilesCache does not allow configuring a default lifetime.',
+ );
+
+ public function createSimpleCache()
+ {
+ if (!PhpFilesCache::isSupported()) {
+ $this->markTestSkipped('OPcache extension is not enabled.');
+ }
+
+ return new PhpFilesCache('sf-cache');
+ }
+}
diff --git a/src/Symfony/Component/Cache/Tests/Simple/Psr6CacheTest.php b/src/Symfony/Component/Cache/Tests/Simple/Psr6CacheTest.php
new file mode 100644
index 0000000000..16e21d0c0b
--- /dev/null
+++ b/src/Symfony/Component/Cache/Tests/Simple/Psr6CacheTest.php
@@ -0,0 +1,26 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Cache\Tests\Simple;
+
+use Symfony\Component\Cache\Adapter\FilesystemAdapter;
+use Symfony\Component\Cache\Simple\Psr6Cache;
+
+/**
+ * @group time-sensitive
+ */
+class Psr6CacheTest extends CacheTestCase
+{
+ public function createSimpleCache($defaultLifetime = 0)
+ {
+ return new Psr6Cache(new FilesystemAdapter('', $defaultLifetime));
+ }
+}
diff --git a/src/Symfony/Component/Cache/Tests/Simple/RedisArrayCacheTest.php b/src/Symfony/Component/Cache/Tests/Simple/RedisArrayCacheTest.php
new file mode 100644
index 0000000000..3c903c8a9b
--- /dev/null
+++ b/src/Symfony/Component/Cache/Tests/Simple/RedisArrayCacheTest.php
@@ -0,0 +1,24 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Cache\Tests\Simple;
+
+class RedisArrayCacheTest extends AbstractRedisCacheTest
+{
+ public static function setupBeforeClass()
+ {
+ parent::setupBeforeClass();
+ if (!class_exists('RedisArray')) {
+ self::markTestSkipped('The RedisArray class is required.');
+ }
+ self::$redis = new \RedisArray(array(getenv('REDIS_HOST')), array('lazy_connect' => true));
+ }
+}
diff --git a/src/Symfony/Component/Cache/Tests/Simple/RedisCacheTest.php b/src/Symfony/Component/Cache/Tests/Simple/RedisCacheTest.php
new file mode 100644
index 0000000000..d33421f9aa
--- /dev/null
+++ b/src/Symfony/Component/Cache/Tests/Simple/RedisCacheTest.php
@@ -0,0 +1,82 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Cache\Tests\Simple;
+
+use Symfony\Component\Cache\Simple\RedisCache;
+
+class RedisCacheTest extends AbstractRedisCacheTest
+{
+ public static function setupBeforeClass()
+ {
+ parent::setupBeforeClass();
+ self::$redis = RedisCache::createConnection('redis://'.getenv('REDIS_HOST'));
+ }
+
+ public function testCreateConnection()
+ {
+ $redisHost = getenv('REDIS_HOST');
+
+ $redis = RedisCache::createConnection('redis://'.$redisHost);
+ $this->assertInstanceOf(\Redis::class, $redis);
+ $this->assertTrue($redis->isConnected());
+ $this->assertSame(0, $redis->getDbNum());
+
+ $redis = RedisCache::createConnection('redis://'.$redisHost.'/2');
+ $this->assertSame(2, $redis->getDbNum());
+
+ $redis = RedisCache::createConnection('redis://'.$redisHost, array('timeout' => 3));
+ $this->assertEquals(3, $redis->getTimeout());
+
+ $redis = RedisCache::createConnection('redis://'.$redisHost.'?timeout=4');
+ $this->assertEquals(4, $redis->getTimeout());
+
+ $redis = RedisCache::createConnection('redis://'.$redisHost, array('read_timeout' => 5));
+ $this->assertEquals(5, $redis->getReadTimeout());
+ }
+
+ /**
+ * @dataProvider provideFailedCreateConnection
+ * @expectedException \Symfony\Component\Cache\Exception\InvalidArgumentException
+ * @expectedExceptionMessage Redis connection failed
+ */
+ public function testFailedCreateConnection($dsn)
+ {
+ RedisCache::createConnection($dsn);
+ }
+
+ public function provideFailedCreateConnection()
+ {
+ return array(
+ array('redis://localhost:1234'),
+ array('redis://foo@localhost'),
+ array('redis://localhost/123'),
+ );
+ }
+
+ /**
+ * @dataProvider provideInvalidCreateConnection
+ * @expectedException \Symfony\Component\Cache\Exception\InvalidArgumentException
+ * @expectedExceptionMessage Invalid Redis DSN
+ */
+ public function testInvalidCreateConnection($dsn)
+ {
+ RedisCache::createConnection($dsn);
+ }
+
+ public function provideInvalidCreateConnection()
+ {
+ return array(
+ array('foo://localhost'),
+ array('redis://'),
+ );
+ }
+}
diff --git a/src/Symfony/Component/Cache/Tests/Simple/TraceableCacheTest.php b/src/Symfony/Component/Cache/Tests/Simple/TraceableCacheTest.php
new file mode 100644
index 0000000000..ebdc770add
--- /dev/null
+++ b/src/Symfony/Component/Cache/Tests/Simple/TraceableCacheTest.php
@@ -0,0 +1,170 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Cache\Tests\Simple;
+
+use Symfony\Component\Cache\Simple\FilesystemCache;
+use Symfony\Component\Cache\Simple\TraceableCache;
+
+/**
+ * @group time-sensitive
+ */
+class TraceableCacheTest extends CacheTestCase
+{
+ public function createSimpleCache($defaultLifetime = 0)
+ {
+ return new TraceableCache(new FilesystemCache('', $defaultLifetime));
+ }
+
+ public function testGetMiss()
+ {
+ $pool = $this->createSimpleCache();
+ $pool->get('k');
+ $calls = $pool->getCalls();
+ $this->assertCount(1, $calls);
+
+ $call = $calls[0];
+ $this->assertSame('get', $call->name);
+ $this->assertSame(array('key' => 'k', 'default' => null), $call->arguments);
+ $this->assertSame(0, $call->hits);
+ $this->assertSame(1, $call->misses);
+ $this->assertNull($call->result);
+ $this->assertNotEmpty($call->start);
+ $this->assertNotEmpty($call->end);
+ }
+
+ public function testGetHit()
+ {
+ $pool = $this->createSimpleCache();
+ $pool->set('k', 'foo');
+ $pool->get('k');
+ $calls = $pool->getCalls();
+ $this->assertCount(2, $calls);
+
+ $call = $calls[1];
+ $this->assertSame(1, $call->hits);
+ $this->assertSame(0, $call->misses);
+ }
+
+ public function testGetMultipleMiss()
+ {
+ $pool = $this->createSimpleCache();
+ $arg = array('k0', 'k1');
+ $values = $pool->getMultiple($arg);
+ foreach ($values as $value) {
+ }
+ $calls = $pool->getCalls();
+ $this->assertCount(1, $calls);
+
+ $call = $calls[0];
+ $this->assertSame('getMultiple', $call->name);
+ $this->assertSame(array('keys' => $arg, 'default' => null), $call->arguments);
+ $this->assertSame(2, $call->misses);
+ $this->assertNotEmpty($call->start);
+ $this->assertNotEmpty($call->end);
+ }
+
+ public function testHasMiss()
+ {
+ $pool = $this->createSimpleCache();
+ $pool->has('k');
+ $calls = $pool->getCalls();
+ $this->assertCount(1, $calls);
+
+ $call = $calls[0];
+ $this->assertSame('has', $call->name);
+ $this->assertSame(array('key' => 'k'), $call->arguments);
+ $this->assertFalse($call->result);
+ $this->assertNotEmpty($call->start);
+ $this->assertNotEmpty($call->end);
+ }
+
+ public function testHasHit()
+ {
+ $pool = $this->createSimpleCache();
+ $pool->set('k', 'foo');
+ $pool->has('k');
+ $calls = $pool->getCalls();
+ $this->assertCount(2, $calls);
+
+ $call = $calls[1];
+ $this->assertSame('has', $call->name);
+ $this->assertSame(array('key' => 'k'), $call->arguments);
+ $this->assertTrue($call->result);
+ $this->assertNotEmpty($call->start);
+ $this->assertNotEmpty($call->end);
+ }
+
+ public function testDelete()
+ {
+ $pool = $this->createSimpleCache();
+ $pool->delete('k');
+ $calls = $pool->getCalls();
+ $this->assertCount(1, $calls);
+
+ $call = $calls[0];
+ $this->assertSame('delete', $call->name);
+ $this->assertSame(array('key' => 'k'), $call->arguments);
+ $this->assertSame(0, $call->hits);
+ $this->assertSame(0, $call->misses);
+ $this->assertNotEmpty($call->start);
+ $this->assertNotEmpty($call->end);
+ }
+
+ public function testDeleteMultiple()
+ {
+ $pool = $this->createSimpleCache();
+ $arg = array('k0', 'k1');
+ $pool->deleteMultiple($arg);
+ $calls = $pool->getCalls();
+ $this->assertCount(1, $calls);
+
+ $call = $calls[0];
+ $this->assertSame('deleteMultiple', $call->name);
+ $this->assertSame(array('keys' => $arg), $call->arguments);
+ $this->assertSame(0, $call->hits);
+ $this->assertSame(0, $call->misses);
+ $this->assertNotEmpty($call->start);
+ $this->assertNotEmpty($call->end);
+ }
+
+ public function testSet()
+ {
+ $pool = $this->createSimpleCache();
+ $pool->set('k', 'foo');
+ $calls = $pool->getCalls();
+ $this->assertCount(1, $calls);
+
+ $call = $calls[0];
+ $this->assertSame('set', $call->name);
+ $this->assertSame(array('key' => 'k', 'value' => 'foo', 'ttl' => null), $call->arguments);
+ $this->assertSame(0, $call->hits);
+ $this->assertSame(0, $call->misses);
+ $this->assertNotEmpty($call->start);
+ $this->assertNotEmpty($call->end);
+ }
+
+ public function testSetMultiple()
+ {
+ $pool = $this->createSimpleCache();
+ $pool->setMultiple(array('k' => 'foo'));
+ $calls = $pool->getCalls();
+ $this->assertCount(1, $calls);
+
+ $call = $calls[0];
+ $this->assertSame('setMultiple', $call->name);
+ $this->assertSame(array('values' => array('k' => 'foo'), 'ttl' => null), $call->arguments);
+ $this->assertSame(0, $call->hits);
+ $this->assertSame(0, $call->misses);
+ $this->assertNotEmpty($call->start);
+ $this->assertNotEmpty($call->end);
+ }
+}
diff --git a/src/Symfony/Component/Cache/Traits/AbstractTrait.php b/src/Symfony/Component/Cache/Traits/AbstractTrait.php
new file mode 100644
index 0000000000..375ccf7620
--- /dev/null
+++ b/src/Symfony/Component/Cache/Traits/AbstractTrait.php
@@ -0,0 +1,209 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Cache\Traits;
+
+use Psr\Log\LoggerAwareTrait;
+use Symfony\Component\Cache\CacheItem;
+
+/**
+ * @author Nicolas Grekas
+ *
+ * @internal
+ */
+trait AbstractTrait
+{
+ use LoggerAwareTrait;
+
+ private $namespace;
+ private $deferred = array();
+
+ /**
+ * @var int|null The maximum length to enforce for identifiers or null when no limit applies
+ */
+ protected $maxIdLength;
+
+ /**
+ * Fetches several cache items.
+ *
+ * @param array $ids The cache identifiers to fetch
+ *
+ * @return array|\Traversable The corresponding values found in the cache
+ */
+ abstract protected function doFetch(array $ids);
+
+ /**
+ * Confirms if the cache contains specified cache item.
+ *
+ * @param string $id The identifier for which to check existence
+ *
+ * @return bool True if item exists in the cache, false otherwise
+ */
+ abstract protected function doHave($id);
+
+ /**
+ * Deletes all items in the pool.
+ *
+ * @param string The prefix used for all identifiers managed by this pool
+ *
+ * @return bool True if the pool was successfully cleared, false otherwise
+ */
+ abstract protected function doClear($namespace);
+
+ /**
+ * Removes multiple items from the pool.
+ *
+ * @param array $ids An array of 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);
+
+ /**
+ * Persists several cache items immediately.
+ *
+ * @param array $values The values to cache, indexed by their cache identifier
+ * @param int $lifetime The lifetime of the cached values, 0 for persisting until manual cleaning
+ *
+ * @return array|bool The identifiers that failed to be cached or a boolean stating if caching succeeded or not
+ */
+ abstract protected function doSave(array $values, $lifetime);
+
+ /**
+ * {@inheritdoc}
+ */
+ public function hasItem($key)
+ {
+ $id = $this->getId($key);
+
+ if (isset($this->deferred[$key])) {
+ $this->commit();
+ }
+
+ try {
+ return $this->doHave($id);
+ } catch (\Exception $e) {
+ CacheItem::log($this->logger, 'Failed to check if key "{key}" is cached', array('key' => $key, 'exception' => $e));
+
+ return false;
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function clear()
+ {
+ $this->deferred = array();
+
+ try {
+ return $this->doClear($this->namespace);
+ } catch (\Exception $e) {
+ CacheItem::log($this->logger, 'Failed to clear the cache', array('exception' => $e));
+
+ return false;
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function deleteItem($key)
+ {
+ return $this->deleteItems(array($key));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function deleteItems(array $keys)
+ {
+ $ids = array();
+
+ foreach ($keys as $key) {
+ $ids[$key] = $this->getId($key);
+ unset($this->deferred[$key]);
+ }
+
+ try {
+ if ($this->doDelete($ids)) {
+ 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(array($id))) {
+ continue;
+ }
+ } catch (\Exception $e) {
+ }
+ CacheItem::log($this->logger, 'Failed to delete key "{key}"', array('key' => $key, 'exception' => $e));
+ $ok = false;
+ }
+
+ return $ok;
+ }
+
+ /**
+ * Like the native unserialize() function but throws an exception if anything goes wrong.
+ *
+ * @param string $value
+ *
+ * @return mixed
+ *
+ * @throws \Exception
+ */
+ protected static function unserialize($value)
+ {
+ if ('b:0;' === $value) {
+ return false;
+ }
+ $unserializeCallbackHandler = ini_set('unserialize_callback_func', __CLASS__.'::handleUnserializeCallback');
+ try {
+ if (false !== $value = unserialize($value)) {
+ return $value;
+ }
+ throw new \DomainException('Failed to unserialize cached value');
+ } catch (\Error $e) {
+ throw new \ErrorException($e->getMessage(), $e->getCode(), E_ERROR, $e->getFile(), $e->getLine());
+ } finally {
+ ini_set('unserialize_callback_func', $unserializeCallbackHandler);
+ }
+ }
+
+ private function getId($key)
+ {
+ CacheItem::validateKey($key);
+
+ if (null === $this->maxIdLength) {
+ return $this->namespace.$key;
+ }
+ if (strlen($id = $this->namespace.$key) > $this->maxIdLength) {
+ $id = $this->namespace.substr_replace(base64_encode(hash('sha256', $key, true)), ':', -22);
+ }
+
+ return $id;
+ }
+
+ /**
+ * @internal
+ */
+ public static function handleUnserializeCallback($class)
+ {
+ throw new \DomainException('Class not found: '.$class);
+ }
+}
diff --git a/src/Symfony/Component/Cache/Traits/ApcuTrait.php b/src/Symfony/Component/Cache/Traits/ApcuTrait.php
new file mode 100644
index 0000000000..578aee881e
--- /dev/null
+++ b/src/Symfony/Component/Cache/Traits/ApcuTrait.php
@@ -0,0 +1,109 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Cache\Traits;
+
+use Symfony\Component\Cache\CacheItem;
+use Symfony\Component\Cache\Exception\CacheException;
+
+/**
+ * @author Nicolas Grekas
+ *
+ * @internal
+ */
+trait ApcuTrait
+{
+ public static function isSupported()
+ {
+ return function_exists('apcu_fetch') && ini_get('apc.enabled') && !('cli' === PHP_SAPI && !ini_get('apc.enable_cli'));
+ }
+
+ private function init($namespace, $defaultLifetime, $version)
+ {
+ if (!static::isSupported()) {
+ throw new CacheException('APCu is not enabled');
+ }
+ if ('cli' === PHP_SAPI) {
+ ini_set('apc.use_request_time', 0);
+ }
+ parent::__construct($namespace, $defaultLifetime);
+
+ if (null !== $version) {
+ CacheItem::validateKey($version);
+
+ if (!apcu_exists($version.'@'.$namespace)) {
+ $this->clear($namespace);
+ apcu_add($version.'@'.$namespace, null);
+ }
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function doFetch(array $ids)
+ {
+ try {
+ return apcu_fetch($ids);
+ } catch (\Error $e) {
+ throw new \ErrorException($e->getMessage(), $e->getCode(), E_ERROR, $e->getFile(), $e->getLine());
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function doHave($id)
+ {
+ return apcu_exists($id);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function doClear($namespace)
+ {
+ return isset($namespace[0]) && class_exists('APCuIterator', false)
+ ? apcu_delete(new \APCuIterator(sprintf('/^%s/', preg_quote($namespace, '/')), APC_ITER_KEY))
+ : apcu_clear_cache();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function doDelete(array $ids)
+ {
+ foreach ($ids as $id) {
+ apcu_delete($id);
+ }
+
+ return true;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function doSave(array $values, $lifetime)
+ {
+ try {
+ return array_keys(apcu_store($values, null, $lifetime));
+ } catch (\Error $e) {
+ } catch (\Exception $e) {
+ }
+
+ if (1 === count($values)) {
+ // Workaround https://github.com/krakjoe/apcu/issues/170
+ apcu_delete(key($values));
+ }
+
+ throw $e;
+ }
+}
diff --git a/src/Symfony/Component/Cache/Traits/ArrayTrait.php b/src/Symfony/Component/Cache/Traits/ArrayTrait.php
new file mode 100644
index 0000000000..3fb5fa36be
--- /dev/null
+++ b/src/Symfony/Component/Cache/Traits/ArrayTrait.php
@@ -0,0 +1,100 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Cache\Traits;
+
+use Psr\Log\LoggerAwareTrait;
+use Symfony\Component\Cache\CacheItem;
+
+/**
+ * @author Nicolas Grekas
+ *
+ * @internal
+ */
+trait ArrayTrait
+{
+ use LoggerAwareTrait;
+
+ private $storeSerialized;
+ private $values = array();
+ private $expiries = array();
+
+ /**
+ * Returns all cached values, with cache miss as null.
+ *
+ * @return array
+ */
+ public function getValues()
+ {
+ return $this->values;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function hasItem($key)
+ {
+ CacheItem::validateKey($key);
+
+ return isset($this->expiries[$key]) && ($this->expiries[$key] >= time() || !$this->deleteItem($key));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function clear()
+ {
+ $this->values = $this->expiries = array();
+
+ return true;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function deleteItem($key)
+ {
+ CacheItem::validateKey($key);
+
+ unset($this->values[$key], $this->expiries[$key]);
+
+ return true;
+ }
+
+ private function generateItems(array $keys, $now, $f)
+ {
+ foreach ($keys as $i => $key) {
+ try {
+ if (!$isHit = isset($this->expiries[$key]) && ($this->expiries[$key] >= $now || !$this->deleteItem($key))) {
+ $this->values[$key] = $value = null;
+ } elseif (!$this->storeSerialized) {
+ $value = $this->values[$key];
+ } elseif ('b:0;' === $value = $this->values[$key]) {
+ $value = false;
+ } elseif (false === $value = unserialize($value)) {
+ $this->values[$key] = $value = null;
+ $isHit = false;
+ }
+ } catch (\Exception $e) {
+ CacheItem::log($this->logger, 'Failed to unserialize key "{key}"', array('key' => $key, 'exception' => $e));
+ $this->values[$key] = $value = null;
+ $isHit = false;
+ }
+ unset($keys[$i]);
+
+ yield $key => $f($key, $value, $isHit);
+ }
+
+ foreach ($keys as $key) {
+ yield $key => $f($key, null, false);
+ }
+ }
+}
diff --git a/src/Symfony/Component/Cache/Traits/DoctrineTrait.php b/src/Symfony/Component/Cache/Traits/DoctrineTrait.php
new file mode 100644
index 0000000000..be351cf53a
--- /dev/null
+++ b/src/Symfony/Component/Cache/Traits/DoctrineTrait.php
@@ -0,0 +1,89 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Cache\Traits;
+
+/**
+ * @author Nicolas Grekas
+ *
+ * @internal
+ */
+trait DoctrineTrait
+{
+ private $provider;
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function doFetch(array $ids)
+ {
+ $unserializeCallbackHandler = ini_set('unserialize_callback_func', parent::class.'::handleUnserializeCallback');
+ try {
+ return $this->provider->fetchMultiple($ids);
+ } catch (\Error $e) {
+ $trace = $e->getTrace();
+
+ if (isset($trace[0]['function']) && !isset($trace[0]['class'])) {
+ switch ($trace[0]['function']) {
+ case 'unserialize':
+ case 'apcu_fetch':
+ case 'apc_fetch':
+ throw new \ErrorException($e->getMessage(), $e->getCode(), E_ERROR, $e->getFile(), $e->getLine());
+ }
+ }
+
+ throw $e;
+ } finally {
+ ini_set('unserialize_callback_func', $unserializeCallbackHandler);
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function doHave($id)
+ {
+ return $this->provider->contains($id);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function doClear($namespace)
+ {
+ $namespace = $this->provider->getNamespace();
+
+ return isset($namespace[0])
+ ? $this->provider->deleteAll()
+ : $this->provider->flushAll();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function doDelete(array $ids)
+ {
+ $ok = true;
+ foreach ($ids as $id) {
+ $ok = $this->provider->delete($id) && $ok;
+ }
+
+ return $ok;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function doSave(array $values, $lifetime)
+ {
+ return $this->provider->saveMultiple($values, $lifetime);
+ }
+}
diff --git a/src/Symfony/Component/Cache/Adapter/FilesystemAdapterTrait.php b/src/Symfony/Component/Cache/Traits/FilesystemCommonTrait.php
similarity index 97%
rename from src/Symfony/Component/Cache/Adapter/FilesystemAdapterTrait.php
rename to src/Symfony/Component/Cache/Traits/FilesystemCommonTrait.php
index 156fc5c1fb..f9c9b396fc 100644
--- a/src/Symfony/Component/Cache/Adapter/FilesystemAdapterTrait.php
+++ b/src/Symfony/Component/Cache/Traits/FilesystemCommonTrait.php
@@ -9,7 +9,7 @@
* file that was distributed with this source code.
*/
-namespace Symfony\Component\Cache\Adapter;
+namespace Symfony\Component\Cache\Traits;
use Symfony\Component\Cache\Exception\InvalidArgumentException;
@@ -18,7 +18,7 @@ use Symfony\Component\Cache\Exception\InvalidArgumentException;
*
* @internal
*/
-trait FilesystemAdapterTrait
+trait FilesystemCommonTrait
{
private $directory;
private $tmp;
diff --git a/src/Symfony/Component/Cache/Traits/FilesystemTrait.php b/src/Symfony/Component/Cache/Traits/FilesystemTrait.php
new file mode 100644
index 0000000000..1db720452f
--- /dev/null
+++ b/src/Symfony/Component/Cache/Traits/FilesystemTrait.php
@@ -0,0 +1,84 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Cache\Traits;
+
+use Symfony\Component\Cache\Exception\CacheException;
+
+/**
+ * @author Nicolas Grekas
+ *
+ * @internal
+ */
+trait FilesystemTrait
+{
+ use FilesystemCommonTrait;
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function doFetch(array $ids)
+ {
+ $values = array();
+ $now = time();
+
+ foreach ($ids as $id) {
+ $file = $this->getFile($id);
+ if (!file_exists($file) || !$h = @fopen($file, 'rb')) {
+ continue;
+ }
+ if ($now >= (int) $expiresAt = fgets($h)) {
+ fclose($h);
+ if (isset($expiresAt[0])) {
+ @unlink($file);
+ }
+ } else {
+ $i = rawurldecode(rtrim(fgets($h)));
+ $value = stream_get_contents($h);
+ fclose($h);
+ if ($i === $id) {
+ $values[$id] = parent::unserialize($value);
+ }
+ }
+ }
+
+ return $values;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function doHave($id)
+ {
+ $file = $this->getFile($id);
+
+ return file_exists($file) && (@filemtime($file) > time() || $this->doFetch(array($id)));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function doSave(array $values, $lifetime)
+ {
+ $ok = true;
+ $expiresAt = time() + ($lifetime ?: 31557600); // 31557600s = 1 year
+
+ foreach ($values as $id => $value) {
+ $ok = $this->write($this->getFile($id, true), $expiresAt."\n".rawurlencode($id)."\n".serialize($value), $expiresAt) && $ok;
+ }
+
+ if (!$ok && !is_writable($this->directory)) {
+ throw new CacheException(sprintf('Cache directory is not writable (%s)', $this->directory));
+ }
+
+ return $ok;
+ }
+}
diff --git a/src/Symfony/Component/Cache/Traits/MemcachedTrait.php b/src/Symfony/Component/Cache/Traits/MemcachedTrait.php
new file mode 100644
index 0000000000..ac5c385f9f
--- /dev/null
+++ b/src/Symfony/Component/Cache/Traits/MemcachedTrait.php
@@ -0,0 +1,241 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Cache\Traits;
+
+use Symfony\Component\Cache\Exception\CacheException;
+use Symfony\Component\Cache\Exception\InvalidArgumentException;
+
+/**
+ * @author Rob Frawley 2nd
+ *
+ * @internal
+ */
+trait MemcachedTrait
+{
+ private static $defaultClientOptions = array(
+ 'persistent_id' => null,
+ 'username' => null,
+ 'password' => null,
+ );
+
+ private $client;
+
+ public static function isSupported()
+ {
+ return extension_loaded('memcached') && version_compare(phpversion('memcached'), '2.2.0', '>=');
+ }
+
+ private function init(\Memcached $client, $namespace, $defaultLifetime)
+ {
+ if (!static::isSupported()) {
+ throw new CacheException('Memcached >= 2.2.0 is required');
+ }
+ $opt = $client->getOption(\Memcached::OPT_SERIALIZER);
+ if (\Memcached::SERIALIZER_PHP !== $opt && \Memcached::SERIALIZER_IGBINARY !== $opt) {
+ throw new CacheException('MemcachedAdapter: "serializer" option must be "php" or "igbinary".');
+ }
+ $this->maxIdLength -= strlen($client->getOption(\Memcached::OPT_PREFIX_KEY));
+
+ parent::__construct($namespace, $defaultLifetime);
+ $this->client = $client;
+ }
+
+ /**
+ * Creates a Memcached instance.
+ *
+ * By default, the binary protocol, no block, and libketama compatible options are enabled.
+ *
+ * Examples for servers:
+ * - 'memcached://user:pass@localhost?weight=33'
+ * - array(array('localhost', 11211, 33))
+ *
+ * @param array[]|string|string[] An array of servers, a DSN, or an array of DSNs
+ * @param array An array of options
+ *
+ * @return \Memcached
+ *
+ * @throws \ErrorEception When invalid options or servers are provided
+ */
+ public static function createConnection($servers, array $options = array())
+ {
+ if (is_string($servers)) {
+ $servers = array($servers);
+ } elseif (!is_array($servers)) {
+ throw new InvalidArgumentException(sprintf('MemcachedAdapter::createClient() expects array or string as first argument, %s given.', gettype($servers)));
+ }
+ set_error_handler(function ($type, $msg, $file, $line) { throw new \ErrorException($msg, 0, $type, $file, $line); });
+ try {
+ if (!static::isSupported()) {
+ throw new trigger_error('Memcached >= 2.2.0 is required');
+ }
+ $options += static::$defaultClientOptions;
+ $client = new \Memcached($options['persistent_id']);
+ $username = $options['username'];
+ $password = $options['password'];
+ unset($options['persistent_id'], $options['username'], $options['password']);
+ $options = array_change_key_case($options, CASE_UPPER);
+
+ // set client's options
+ $client->setOption(\Memcached::OPT_BINARY_PROTOCOL, true);
+ $client->setOption(\Memcached::OPT_NO_BLOCK, true);
+ if (!array_key_exists('LIBKETAMA_COMPATIBLE', $options) && !array_key_exists(\Memcached::OPT_LIBKETAMA_COMPATIBLE, $options)) {
+ $client->setOption(\Memcached::OPT_LIBKETAMA_COMPATIBLE, true);
+ }
+ foreach ($options as $name => $value) {
+ if (is_int($name)) {
+ continue;
+ }
+ if ('HASH' === $name || 'SERIALIZER' === $name || 'DISTRIBUTION' === $name) {
+ $value = constant('Memcached::'.$name.'_'.strtoupper($value));
+ }
+ $opt = constant('Memcached::OPT_'.$name);
+
+ unset($options[$name]);
+ $options[$opt] = $value;
+ }
+ $client->setOptions($options);
+
+ // parse any DSN in $servers
+ foreach ($servers as $i => $dsn) {
+ if (is_array($dsn)) {
+ continue;
+ }
+ if (0 !== strpos($dsn, 'memcached://')) {
+ throw new InvalidArgumentException(sprintf('Invalid Memcached DSN: %s does not start with "memcached://"', $dsn));
+ }
+ $params = preg_replace_callback('#^memcached://(?:([^@]*+)@)?#', function ($m) use (&$username, &$password) {
+ if (!empty($m[1])) {
+ list($username, $password) = explode(':', $m[1], 2) + array(1 => null);
+ }
+
+ return 'file://';
+ }, $dsn);
+ if (false === $params = parse_url($params)) {
+ throw new InvalidArgumentException(sprintf('Invalid Memcached DSN: %s', $dsn));
+ }
+ if (!isset($params['host']) && !isset($params['path'])) {
+ throw new InvalidArgumentException(sprintf('Invalid Memcached DSN: %s', $dsn));
+ }
+ if (isset($params['path']) && preg_match('#/(\d+)$#', $params['path'], $m)) {
+ $params['weight'] = $m[1];
+ $params['path'] = substr($params['path'], 0, -strlen($m[0]));
+ }
+ $params += array(
+ 'host' => isset($params['host']) ? $params['host'] : $params['path'],
+ 'port' => isset($params['host']) ? 11211 : null,
+ 'weight' => 0,
+ );
+ if (isset($params['query'])) {
+ parse_str($params['query'], $query);
+ $params += $query;
+ }
+
+ $servers[$i] = array($params['host'], $params['port'], $params['weight']);
+ }
+
+ // set client's servers, taking care of persistent connections
+ if (!$client->isPristine()) {
+ $oldServers = array();
+ foreach ($client->getServerList() as $server) {
+ $oldServers[] = array($server['host'], $server['port']);
+ }
+
+ $newServers = array();
+ foreach ($servers as $server) {
+ if (1 < count($server)) {
+ $server = array_values($server);
+ unset($server[2]);
+ $server[1] = (int) $server[1];
+ }
+ $newServers[] = $server;
+ }
+
+ if ($oldServers !== $newServers) {
+ // before resetting, ensure $servers is valid
+ $client->addServers($servers);
+ $client->resetServerList();
+ }
+ }
+ $client->addServers($servers);
+
+ if (null !== $username || null !== $password) {
+ if (!method_exists($client, 'setSaslAuthData')) {
+ trigger_error('Missing SASL support: the memcached extension must be compiled with --enable-memcached-sasl.');
+ }
+ $client->setSaslAuthData($username, $password);
+ }
+
+ return $client;
+ } finally {
+ restore_error_handler();
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function doSave(array $values, $lifetime)
+ {
+ return $this->checkResultCode($this->client->setMulti($values, $lifetime));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function doFetch(array $ids)
+ {
+ return $this->checkResultCode($this->client->getMulti($ids));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function doHave($id)
+ {
+ return false !== $this->client->get($id) || $this->checkResultCode(\Memcached::RES_SUCCESS === $this->client->getResultCode());
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function doDelete(array $ids)
+ {
+ $ok = true;
+ foreach ($this->checkResultCode($this->client->deleteMulti($ids)) as $result) {
+ if (\Memcached::RES_SUCCESS !== $result && \Memcached::RES_NOTFOUND !== $result) {
+ $ok = false;
+ }
+ }
+
+ return $ok;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function doClear($namespace)
+ {
+ return $this->checkResultCode($this->client->flush());
+ }
+
+ private function checkResultCode($result)
+ {
+ $code = $this->client->getResultCode();
+
+ if (\Memcached::RES_SUCCESS === $code || \Memcached::RES_NOTFOUND === $code) {
+ return $result;
+ }
+
+ throw new CacheException(sprintf('MemcachedAdapter client error: %s.', strtolower($this->client->getResultMessage())));
+ }
+}
diff --git a/src/Symfony/Component/Cache/Traits/PdoTrait.php b/src/Symfony/Component/Cache/Traits/PdoTrait.php
new file mode 100644
index 0000000000..08b55ab72c
--- /dev/null
+++ b/src/Symfony/Component/Cache/Traits/PdoTrait.php
@@ -0,0 +1,381 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Cache\Traits;
+
+use Doctrine\DBAL\Connection;
+use Doctrine\DBAL\Driver\ServerInfoAwareConnection;
+use Doctrine\DBAL\DBALException;
+use Doctrine\DBAL\Schema\Schema;
+use Symfony\Component\Cache\Exception\InvalidArgumentException;
+
+/**
+ * @internal
+ */
+trait PdoTrait
+{
+ private $conn;
+ private $dsn;
+ private $driver;
+ private $serverVersion;
+ private $table = 'cache_items';
+ private $idCol = 'item_id';
+ private $dataCol = 'item_data';
+ private $lifetimeCol = 'item_lifetime';
+ private $timeCol = 'item_time';
+ private $username = '';
+ private $password = '';
+ private $connectionOptions = array();
+
+ private function init($connOrDsn, $namespace, $defaultLifetime, array $options)
+ {
+ if (isset($namespace[0]) && preg_match('#[^-+.A-Za-z0-9]#', $namespace, $match)) {
+ throw new InvalidArgumentException(sprintf('Namespace contains "%s" but only characters in [-+.A-Za-z0-9] are allowed.', $match[0]));
+ }
+
+ if ($connOrDsn instanceof \PDO) {
+ if (\PDO::ERRMODE_EXCEPTION !== $connOrDsn->getAttribute(\PDO::ATTR_ERRMODE)) {
+ throw new InvalidArgumentException(sprintf('"%s" requires PDO error mode attribute be set to throw Exceptions (i.e. $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION))', __CLASS__));
+ }
+
+ $this->conn = $connOrDsn;
+ } elseif ($connOrDsn instanceof Connection) {
+ $this->conn = $connOrDsn;
+ } elseif (is_string($connOrDsn)) {
+ $this->dsn = $connOrDsn;
+ } else {
+ throw new InvalidArgumentException(sprintf('"%s" requires PDO or Doctrine\DBAL\Connection instance or DSN string as first argument, "%s" given.', __CLASS__, is_object($connOrDsn) ? get_class($connOrDsn) : gettype($connOrDsn)));
+ }
+
+ $this->table = isset($options['db_table']) ? $options['db_table'] : $this->table;
+ $this->idCol = isset($options['db_id_col']) ? $options['db_id_col'] : $this->idCol;
+ $this->dataCol = isset($options['db_data_col']) ? $options['db_data_col'] : $this->dataCol;
+ $this->lifetimeCol = isset($options['db_lifetime_col']) ? $options['db_lifetime_col'] : $this->lifetimeCol;
+ $this->timeCol = isset($options['db_time_col']) ? $options['db_time_col'] : $this->timeCol;
+ $this->username = isset($options['db_username']) ? $options['db_username'] : $this->username;
+ $this->password = isset($options['db_password']) ? $options['db_password'] : $this->password;
+ $this->connectionOptions = isset($options['db_connection_options']) ? $options['db_connection_options'] : $this->connectionOptions;
+
+ parent::__construct($namespace, $defaultLifetime);
+ }
+
+ /**
+ * Creates the table to store cache items which can be called once for setup.
+ *
+ * Cache ID are saved in a column of maximum length 255. Cache data is
+ * saved in a BLOB.
+ *
+ * @throws \PDOException When the table already exists
+ * @throws DBALException When the table already exists
+ * @throws \DomainException When an unsupported PDO driver is used
+ */
+ public function createTable()
+ {
+ // connect if we are not yet
+ $conn = $this->getConnection();
+
+ if ($conn instanceof Connection) {
+ $types = array(
+ 'mysql' => 'binary',
+ 'sqlite' => 'text',
+ 'pgsql' => 'string',
+ 'oci' => 'string',
+ 'sqlsrv' => 'string',
+ );
+ if (!isset($types[$this->driver])) {
+ throw new \DomainException(sprintf('Creating the cache table is currently not implemented for PDO driver "%s".', $this->driver));
+ }
+
+ $schema = new Schema();
+ $table = $schema->createTable($this->table);
+ $table->addColumn($this->idCol, $types[$this->driver], array('length' => 255));
+ $table->addColumn($this->dataCol, 'blob', array('length' => 16777215));
+ $table->addColumn($this->lifetimeCol, 'integer', array('unsigned' => true, 'notnull' => false));
+ $table->addColumn($this->timeCol, 'integer', array('unsigned' => true, 'foo' => 'bar'));
+ $table->setPrimaryKey(array($this->idCol));
+
+ foreach ($schema->toSql($conn->getDatabasePlatform()) as $sql) {
+ $conn->exec($sql);
+ }
+
+ return;
+ }
+
+ switch ($this->driver) {
+ case 'mysql':
+ // We use varbinary for the ID column because it prevents unwanted conversions:
+ // - character set conversions between server and client
+ // - trailing space removal
+ // - case-insensitivity
+ // - language processing like é == e
+ $sql = "CREATE TABLE $this->table ($this->idCol VARBINARY(255) NOT NULL PRIMARY KEY, $this->dataCol MEDIUMBLOB NOT NULL, $this->lifetimeCol INTEGER UNSIGNED, $this->timeCol INTEGER UNSIGNED NOT NULL) COLLATE utf8_bin, ENGINE = InnoDB";
+ break;
+ case 'sqlite':
+ $sql = "CREATE TABLE $this->table ($this->idCol TEXT NOT NULL PRIMARY KEY, $this->dataCol BLOB NOT NULL, $this->lifetimeCol INTEGER, $this->timeCol INTEGER NOT NULL)";
+ break;
+ case 'pgsql':
+ $sql = "CREATE TABLE $this->table ($this->idCol VARCHAR(255) NOT NULL PRIMARY KEY, $this->dataCol BYTEA NOT NULL, $this->lifetimeCol INTEGER, $this->timeCol INTEGER NOT NULL)";
+ break;
+ case 'oci':
+ $sql = "CREATE TABLE $this->table ($this->idCol VARCHAR2(255) NOT NULL PRIMARY KEY, $this->dataCol BLOB NOT NULL, $this->lifetimeCol INTEGER, $this->timeCol INTEGER NOT NULL)";
+ break;
+ case 'sqlsrv':
+ $sql = "CREATE TABLE $this->table ($this->idCol VARCHAR(255) NOT NULL PRIMARY KEY, $this->dataCol VARBINARY(MAX) NOT NULL, $this->lifetimeCol INTEGER, $this->timeCol INTEGER NOT NULL)";
+ break;
+ default:
+ throw new \DomainException(sprintf('Creating the cache table is currently not implemented for PDO driver "%s".', $this->driver));
+ }
+
+ $conn->exec($sql);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function doFetch(array $ids)
+ {
+ $now = time();
+ $expired = array();
+
+ $sql = str_pad('', (count($ids) << 1) - 1, '?,');
+ $sql = "SELECT $this->idCol, CASE WHEN $this->lifetimeCol IS NULL OR $this->lifetimeCol + $this->timeCol > ? THEN $this->dataCol ELSE NULL END FROM $this->table WHERE $this->idCol IN ($sql)";
+ $stmt = $this->getConnection()->prepare($sql);
+ $stmt->bindValue($i = 1, $now, \PDO::PARAM_INT);
+ foreach ($ids as $id) {
+ $stmt->bindValue(++$i, $id);
+ }
+ $stmt->execute();
+
+ while ($row = $stmt->fetch(\PDO::FETCH_NUM)) {
+ if (null === $row[1]) {
+ $expired[] = $row[0];
+ } else {
+ yield $row[0] => parent::unserialize(is_resource($row[1]) ? stream_get_contents($row[1]) : $row[1]);
+ }
+ }
+
+ if ($expired) {
+ $sql = str_pad('', (count($expired) << 1) - 1, '?,');
+ $sql = "DELETE FROM $this->table WHERE $this->lifetimeCol + $this->timeCol <= ? AND $this->idCol IN ($sql)";
+ $stmt = $this->getConnection()->prepare($sql);
+ $stmt->bindValue($i = 1, $now, \PDO::PARAM_INT);
+ foreach ($expired as $id) {
+ $stmt->bindValue(++$i, $id);
+ }
+ $stmt->execute($expired);
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function doHave($id)
+ {
+ $sql = "SELECT 1 FROM $this->table WHERE $this->idCol = :id AND ($this->lifetimeCol IS NULL OR $this->lifetimeCol + $this->timeCol > :time)";
+ $stmt = $this->getConnection()->prepare($sql);
+
+ $stmt->bindValue(':id', $id);
+ $stmt->bindValue(':time', time(), \PDO::PARAM_INT);
+ $stmt->execute();
+
+ return (bool) $stmt->fetchColumn();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function doClear($namespace)
+ {
+ $conn = $this->getConnection();
+
+ if ('' === $namespace) {
+ if ('sqlite' === $this->driver) {
+ $sql = "DELETE FROM $this->table";
+ } else {
+ $sql = "TRUNCATE TABLE $this->table";
+ }
+ } else {
+ $sql = "DELETE FROM $this->table WHERE $this->idCol LIKE '$namespace%'";
+ }
+
+ $conn->exec($sql);
+
+ return true;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function doDelete(array $ids)
+ {
+ $sql = str_pad('', (count($ids) << 1) - 1, '?,');
+ $sql = "DELETE FROM $this->table WHERE $this->idCol IN ($sql)";
+ $stmt = $this->getConnection()->prepare($sql);
+ $stmt->execute(array_values($ids));
+
+ return true;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function doSave(array $values, $lifetime)
+ {
+ $serialized = array();
+ $failed = array();
+
+ foreach ($values as $id => $value) {
+ try {
+ $serialized[$id] = serialize($value);
+ } catch (\Exception $e) {
+ $failed[] = $id;
+ }
+ }
+
+ if (!$serialized) {
+ return $failed;
+ }
+
+ $conn = $this->getConnection();
+ $driver = $this->driver;
+ $insertSql = "INSERT INTO $this->table ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (:id, :data, :lifetime, :time)";
+
+ switch (true) {
+ case 'mysql' === $driver:
+ $sql = $insertSql." ON DUPLICATE KEY UPDATE $this->dataCol = VALUES($this->dataCol), $this->lifetimeCol = VALUES($this->lifetimeCol), $this->timeCol = VALUES($this->timeCol)";
+ break;
+ case 'oci' === $driver:
+ // DUAL is Oracle specific dummy table
+ $sql = "MERGE INTO $this->table USING DUAL ON ($this->idCol = ?) ".
+ "WHEN NOT MATCHED THEN INSERT ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (?, ?, ?, ?) ".
+ "WHEN MATCHED THEN UPDATE SET $this->dataCol = ?, $this->lifetimeCol = ?, $this->timeCol = ?";
+ break;
+ case 'sqlsrv' === $driver && version_compare($this->getServerVersion(), '10', '>='):
+ // MERGE is only available since SQL Server 2008 and must be terminated by semicolon
+ // It also requires HOLDLOCK according to http://weblogs.sqlteam.com/dang/archive/2009/01/31/UPSERT-Race-Condition-With-MERGE.aspx
+ $sql = "MERGE INTO $this->table WITH (HOLDLOCK) USING (SELECT 1 AS dummy) AS src ON ($this->idCol = ?) ".
+ "WHEN NOT MATCHED THEN INSERT ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (?, ?, ?, ?) ".
+ "WHEN MATCHED THEN UPDATE SET $this->dataCol = ?, $this->lifetimeCol = ?, $this->timeCol = ?;";
+ break;
+ case 'sqlite' === $driver:
+ $sql = 'INSERT OR REPLACE'.substr($insertSql, 6);
+ break;
+ case 'pgsql' === $driver && version_compare($this->getServerVersion(), '9.5', '>='):
+ $sql = $insertSql." ON CONFLICT ($this->idCol) DO UPDATE SET ($this->dataCol, $this->lifetimeCol, $this->timeCol) = (EXCLUDED.$this->dataCol, EXCLUDED.$this->lifetimeCol, EXCLUDED.$this->timeCol)";
+ break;
+ default:
+ $driver = null;
+ $sql = "UPDATE $this->table SET $this->dataCol = :data, $this->lifetimeCol = :lifetime, $this->timeCol = :time WHERE $this->idCol = :id";
+ break;
+ }
+
+ $now = time();
+ $lifetime = $lifetime ?: null;
+ $stmt = $conn->prepare($sql);
+
+ if ('sqlsrv' === $driver || 'oci' === $driver) {
+ $stmt->bindParam(1, $id);
+ $stmt->bindParam(2, $id);
+ $stmt->bindParam(3, $data, \PDO::PARAM_LOB);
+ $stmt->bindValue(4, $lifetime, \PDO::PARAM_INT);
+ $stmt->bindValue(5, $now, \PDO::PARAM_INT);
+ $stmt->bindParam(6, $data, \PDO::PARAM_LOB);
+ $stmt->bindValue(7, $lifetime, \PDO::PARAM_INT);
+ $stmt->bindValue(8, $now, \PDO::PARAM_INT);
+ } else {
+ $stmt->bindParam(':id', $id);
+ $stmt->bindParam(':data', $data, \PDO::PARAM_LOB);
+ $stmt->bindValue(':lifetime', $lifetime, \PDO::PARAM_INT);
+ $stmt->bindValue(':time', $now, \PDO::PARAM_INT);
+ }
+ if (null === $driver) {
+ $insertStmt = $conn->prepare($insertSql);
+
+ $insertStmt->bindParam(':id', $id);
+ $insertStmt->bindParam(':data', $data, \PDO::PARAM_LOB);
+ $insertStmt->bindValue(':lifetime', $lifetime, \PDO::PARAM_INT);
+ $insertStmt->bindValue(':time', $now, \PDO::PARAM_INT);
+ }
+
+ foreach ($serialized as $id => $data) {
+ $stmt->execute();
+
+ if (null === $driver && !$stmt->rowCount()) {
+ try {
+ $insertStmt->execute();
+ } catch (DBALException $e) {
+ } catch (\PDOException $e) {
+ // A concurrent write won, let it be
+ }
+ }
+ }
+
+ return $failed;
+ }
+
+ /**
+ * @return \PDO|Connection
+ */
+ private function getConnection()
+ {
+ if (null === $this->conn) {
+ $this->conn = new \PDO($this->dsn, $this->username, $this->password, $this->connectionOptions);
+ $this->conn->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
+ }
+ if (null === $this->driver) {
+ if ($this->conn instanceof \PDO) {
+ $this->driver = $this->conn->getAttribute(\PDO::ATTR_DRIVER_NAME);
+ } else {
+ switch ($this->driver = $this->conn->getDriver()->getName()) {
+ case 'mysqli':
+ case 'pdo_mysql':
+ case 'drizzle_pdo_mysql':
+ $this->driver = 'mysql';
+ break;
+ case 'pdo_sqlite':
+ $this->driver = 'sqlite';
+ break;
+ case 'pdo_pgsql':
+ $this->driver = 'pgsql';
+ break;
+ case 'oci8':
+ case 'pdo_oracle':
+ $this->driver = 'oci';
+ break;
+ case 'pdo_sqlsrv':
+ $this->driver = 'sqlsrv';
+ break;
+ }
+ }
+ }
+
+ return $this->conn;
+ }
+
+ /**
+ * @return string
+ */
+ private function getServerVersion()
+ {
+ if (null === $this->serverVersion) {
+ $conn = $this->conn instanceof \PDO ? $this->conn : $this->conn->getWrappedConnection();
+ if ($conn instanceof \PDO) {
+ $this->serverVersion = $conn->getAttribute(\PDO::ATTR_SERVER_VERSION);
+ } elseif ($conn instanceof ServerInfoAwareConnection) {
+ $this->serverVersion = $conn->getServerVersion();
+ } else {
+ $this->serverVersion = '0';
+ }
+ }
+
+ return $this->serverVersion;
+ }
+}
diff --git a/src/Symfony/Component/Cache/Traits/PhpArrayTrait.php b/src/Symfony/Component/Cache/Traits/PhpArrayTrait.php
new file mode 100644
index 0000000000..97a923bfe1
--- /dev/null
+++ b/src/Symfony/Component/Cache/Traits/PhpArrayTrait.php
@@ -0,0 +1,131 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Cache\Traits;
+
+use Symfony\Component\Cache\CacheItem;
+use Symfony\Component\Cache\Exception\InvalidArgumentException;
+
+/**
+ * @author Titouan Galopin
+ *
+ * @internal
+ */
+trait PhpArrayTrait
+{
+ private $file;
+ private $values;
+ private $fallbackPool;
+
+ /**
+ * Store an array of cached values.
+ *
+ * @param array $values The cached values
+ */
+ public function warmUp(array $values)
+ {
+ if (file_exists($this->file)) {
+ if (!is_file($this->file)) {
+ throw new InvalidArgumentException(sprintf('Cache path exists and is not a file: %s.', $this->file));
+ }
+
+ if (!is_writable($this->file)) {
+ throw new InvalidArgumentException(sprintf('Cache file is not writable: %s.', $this->file));
+ }
+ } else {
+ $directory = dirname($this->file);
+
+ if (!is_dir($directory) && !@mkdir($directory, 0777, true)) {
+ throw new InvalidArgumentException(sprintf('Cache directory does not exist and cannot be created: %s.', $directory));
+ }
+
+ if (!is_writable($directory)) {
+ throw new InvalidArgumentException(sprintf('Cache directory is not writable: %s.', $directory));
+ }
+ }
+
+ $dump = <<<'EOF'
+ $value) {
+ CacheItem::validateKey(is_int($key) ? (string) $key : $key);
+
+ if (null === $value || is_object($value)) {
+ try {
+ $value = serialize($value);
+ } catch (\Exception $e) {
+ throw new InvalidArgumentException(sprintf('Cache key "%s" has non-serializable %s value.', $key, get_class($value)), 0, $e);
+ }
+ } elseif (is_array($value)) {
+ try {
+ $serialized = serialize($value);
+ $unserialized = unserialize($serialized);
+ } catch (\Exception $e) {
+ throw new InvalidArgumentException(sprintf('Cache key "%s" has non-serializable array value.', $key), 0, $e);
+ }
+ // Store arrays serialized if they contain any objects or references
+ if ($unserialized !== $value || (false !== strpos($serialized, ';R:') && preg_match('/;R:[1-9]/', $serialized))) {
+ $value = $serialized;
+ }
+ } elseif (is_string($value)) {
+ // Serialize strings if they could be confused with serialized objects or arrays
+ if ('N;' === $value || (isset($value[2]) && ':' === $value[1])) {
+ $value = serialize($value);
+ }
+ } elseif (!is_scalar($value)) {
+ throw new InvalidArgumentException(sprintf('Cache key "%s" has non-serializable %s value.', $key, gettype($value)));
+ }
+
+ $dump .= var_export($key, true).' => '.var_export($value, true).",\n";
+ }
+
+ $dump .= "\n);\n";
+ $dump = str_replace("' . \"\\0\" . '", "\0", $dump);
+
+ $tmpFile = uniqid($this->file, true);
+
+ file_put_contents($tmpFile, $dump);
+ @chmod($tmpFile, 0666);
+ unset($serialized, $unserialized, $value, $dump);
+
+ @rename($tmpFile, $this->file);
+
+ $this->values = (include $this->file) ?: array();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function clear()
+ {
+ $this->values = array();
+
+ $cleared = @unlink($this->file) || !file_exists($this->file);
+
+ return $this->fallbackPool->clear() && $cleared;
+ }
+
+ /**
+ * Load the cache file.
+ */
+ private function initialize()
+ {
+ $this->values = @(include $this->file) ?: array();
+ }
+}
diff --git a/src/Symfony/Component/Cache/Traits/PhpFilesTrait.php b/src/Symfony/Component/Cache/Traits/PhpFilesTrait.php
new file mode 100644
index 0000000000..f915cd46c8
--- /dev/null
+++ b/src/Symfony/Component/Cache/Traits/PhpFilesTrait.php
@@ -0,0 +1,121 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Cache\Traits;
+
+use Symfony\Component\Cache\Exception\CacheException;
+use Symfony\Component\Cache\Exception\InvalidArgumentException;
+
+/**
+ * @author Piotr Stankowski
+ *
+ * @internal
+ */
+trait PhpFilesTrait
+{
+ use FilesystemCommonTrait;
+
+ private $includeHandler;
+
+ public static function isSupported()
+ {
+ return function_exists('opcache_compile_file') && ini_get('opcache.enable');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function doFetch(array $ids)
+ {
+ $values = array();
+ $now = time();
+
+ set_error_handler($this->includeHandler);
+ try {
+ foreach ($ids as $id) {
+ try {
+ $file = $this->getFile($id);
+ list($expiresAt, $values[$id]) = include $file;
+ if ($now >= $expiresAt) {
+ unset($values[$id]);
+ }
+ } catch (\Exception $e) {
+ continue;
+ }
+ }
+ } finally {
+ restore_error_handler();
+ }
+
+ foreach ($values as $id => $value) {
+ if ('N;' === $value) {
+ $values[$id] = null;
+ } elseif (is_string($value) && isset($value[2]) && ':' === $value[1]) {
+ $values[$id] = parent::unserialize($value);
+ }
+ }
+
+ return $values;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function doHave($id)
+ {
+ return (bool) $this->doFetch(array($id));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function doSave(array $values, $lifetime)
+ {
+ $ok = true;
+ $data = array($lifetime ? time() + $lifetime : PHP_INT_MAX, '');
+ $allowCompile = 'cli' !== PHP_SAPI || ini_get('opcache.enable_cli');
+
+ foreach ($values as $key => $value) {
+ if (null === $value || is_object($value)) {
+ $value = serialize($value);
+ } elseif (is_array($value)) {
+ $serialized = serialize($value);
+ $unserialized = parent::unserialize($serialized);
+ // Store arrays serialized if they contain any objects or references
+ if ($unserialized !== $value || (false !== strpos($serialized, ';R:') && preg_match('/;R:[1-9]/', $serialized))) {
+ $value = $serialized;
+ }
+ } elseif (is_string($value)) {
+ // Serialize strings if they could be confused with serialized objects or arrays
+ if ('N;' === $value || (isset($value[2]) && ':' === $value[1])) {
+ $value = serialize($value);
+ }
+ } elseif (!is_scalar($value)) {
+ throw new InvalidArgumentException(sprintf('Cache key "%s" has non-serializable %s value.', $key, gettype($value)));
+ }
+
+ $data[1] = $value;
+ $file = $this->getFile($key, true);
+ $ok = $this->write($file, 'directory)) {
+ throw new CacheException(sprintf('Cache directory is not writable (%s)', $this->directory));
+ }
+
+ return $ok;
+ }
+}
diff --git a/src/Symfony/Component/Cache/Traits/RedisTrait.php b/src/Symfony/Component/Cache/Traits/RedisTrait.php
new file mode 100644
index 0000000000..803e3aa93e
--- /dev/null
+++ b/src/Symfony/Component/Cache/Traits/RedisTrait.php
@@ -0,0 +1,313 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Cache\Traits;
+
+use Predis\Connection\Factory;
+use Predis\Connection\Aggregate\PredisCluster;
+use Predis\Connection\Aggregate\RedisCluster;
+use Symfony\Component\Cache\Exception\InvalidArgumentException;
+
+/**
+ * @author Aurimas Niekis
+ *
+ * @internal
+ */
+trait RedisTrait
+{
+ private static $defaultConnectionOptions = array(
+ 'class' => null,
+ 'persistent' => 0,
+ 'persistent_id' => null,
+ 'timeout' => 30,
+ 'read_timeout' => 0,
+ 'retry_interval' => 0,
+ );
+ private $redis;
+
+ /**
+ * @param \Redis|\RedisArray|\RedisCluster|\Predis\Client $redisClient
+ */
+ public function init($redisClient, $namespace = '', $defaultLifetime = 0)
+ {
+ parent::__construct($namespace, $defaultLifetime);
+
+ if (preg_match('#[^-+_.A-Za-z0-9]#', $namespace, $match)) {
+ throw new InvalidArgumentException(sprintf('RedisAdapter namespace contains "%s" but only characters in [-+_.A-Za-z0-9] are allowed.', $match[0]));
+ }
+ if (!$redisClient instanceof \Redis && !$redisClient instanceof \RedisArray && !$redisClient instanceof \RedisCluster && !$redisClient instanceof \Predis\Client) {
+ throw new InvalidArgumentException(sprintf('%s() expects parameter 1 to be Redis, RedisArray, RedisCluster or Predis\Client, %s given', __METHOD__, is_object($redisClient) ? get_class($redisClient) : gettype($redisClient)));
+ }
+ $this->redis = $redisClient;
+ }
+
+ /**
+ * Creates a Redis connection using a DSN configuration.
+ *
+ * Example DSN:
+ * - redis://localhost
+ * - redis://example.com:1234
+ * - redis://secret@example.com/13
+ * - redis:///var/run/redis.sock
+ * - redis://secret@/var/run/redis.sock/13
+ *
+ * @param string $dsn
+ * @param array $options See self::$defaultConnectionOptions
+ *
+ * @throws InvalidArgumentException When the DSN is invalid.
+ *
+ * @return \Redis|\Predis\Client According to the "class" option
+ */
+ public static function createConnection($dsn, array $options = array())
+ {
+ if (0 !== strpos($dsn, 'redis://')) {
+ throw new InvalidArgumentException(sprintf('Invalid Redis DSN: %s does not start with "redis://"', $dsn));
+ }
+ $params = preg_replace_callback('#^redis://(?:(?:[^:@]*+:)?([^@]*+)@)?#', function ($m) use (&$auth) {
+ if (isset($m[1])) {
+ $auth = $m[1];
+ }
+
+ return 'file://';
+ }, $dsn);
+ if (false === $params = parse_url($params)) {
+ throw new InvalidArgumentException(sprintf('Invalid Redis DSN: %s', $dsn));
+ }
+ if (!isset($params['host']) && !isset($params['path'])) {
+ throw new InvalidArgumentException(sprintf('Invalid Redis DSN: %s', $dsn));
+ }
+ if (isset($params['path']) && preg_match('#/(\d+)$#', $params['path'], $m)) {
+ $params['dbindex'] = $m[1];
+ $params['path'] = substr($params['path'], 0, -strlen($m[0]));
+ }
+ $params += array(
+ 'host' => isset($params['host']) ? $params['host'] : $params['path'],
+ 'port' => isset($params['host']) ? 6379 : null,
+ 'dbindex' => 0,
+ );
+ if (isset($params['query'])) {
+ parse_str($params['query'], $query);
+ $params += $query;
+ }
+ $params += $options + self::$defaultConnectionOptions;
+ $class = null === $params['class'] ? (extension_loaded('redis') ? \Redis::class : \Predis\Client::class) : $params['class'];
+
+ if (is_a($class, \Redis::class, true)) {
+ $connect = $params['persistent'] || $params['persistent_id'] ? 'pconnect' : 'connect';
+ $redis = new $class();
+ @$redis->{$connect}($params['host'], $params['port'], $params['timeout'], $params['persistent_id'], $params['retry_interval']);
+
+ if (@!$redis->isConnected()) {
+ $e = ($e = error_get_last()) && preg_match('/^Redis::p?connect\(\): (.*)/', $e['message'], $e) ? sprintf(' (%s)', $e[1]) : '';
+ throw new InvalidArgumentException(sprintf('Redis connection failed%s: %s', $e, $dsn));
+ }
+
+ if ((null !== $auth && !$redis->auth($auth))
+ || ($params['dbindex'] && !$redis->select($params['dbindex']))
+ || ($params['read_timeout'] && !$redis->setOption(\Redis::OPT_READ_TIMEOUT, $params['read_timeout']))
+ ) {
+ $e = preg_replace('/^ERR /', '', $redis->getLastError());
+ throw new InvalidArgumentException(sprintf('Redis connection failed (%s): %s', $e, $dsn));
+ }
+ } elseif (is_a($class, \Predis\Client::class, true)) {
+ $params['scheme'] = isset($params['host']) ? 'tcp' : 'unix';
+ $params['database'] = $params['dbindex'] ?: null;
+ $params['password'] = $auth;
+ $redis = new $class((new Factory())->create($params));
+ } elseif (class_exists($class, false)) {
+ throw new InvalidArgumentException(sprintf('"%s" is not a subclass of "Redis" or "Predis\Client"', $class));
+ } else {
+ throw new InvalidArgumentException(sprintf('Class "%s" does not exist', $class));
+ }
+
+ return $redis;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function doFetch(array $ids)
+ {
+ if ($ids) {
+ $values = $this->redis->mGet($ids);
+ $index = 0;
+ foreach ($ids as $id) {
+ if ($value = $values[$index++]) {
+ yield $id => parent::unserialize($value);
+ }
+ }
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function doHave($id)
+ {
+ return (bool) $this->redis->exists($id);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function doClear($namespace)
+ {
+ // When using a native Redis cluster, clearing the cache cannot work and always returns false.
+ // Clearing the cache should then be done by any other means (e.g. by restarting the cluster).
+
+ $cleared = true;
+ $hosts = array($this->redis);
+ $evalArgs = array(array($namespace), 0);
+
+ if ($this->redis instanceof \Predis\Client) {
+ $evalArgs = array(0, $namespace);
+
+ $connection = $this->redis->getConnection();
+ if ($connection instanceof PredisCluster) {
+ $hosts = array();
+ foreach ($connection as $c) {
+ $hosts[] = new \Predis\Client($c);
+ }
+ } elseif ($connection instanceof RedisCluster) {
+ return false;
+ }
+ } elseif ($this->redis instanceof \RedisArray) {
+ $hosts = array();
+ foreach ($this->redis->_hosts() as $host) {
+ $hosts[] = $this->redis->_instance($host);
+ }
+ } elseif ($this->redis instanceof \RedisCluster) {
+ return false;
+ }
+ foreach ($hosts as $host) {
+ if (!isset($namespace[0])) {
+ $cleared = $host->flushDb() && $cleared;
+ continue;
+ }
+
+ $info = $host->info('Server');
+ $info = isset($info['Server']) ? $info['Server'] : $info;
+
+ if (!version_compare($info['redis_version'], '2.8', '>=')) {
+ // As documented in Redis documentation (http://redis.io/commands/keys) using KEYS
+ // can hang your server when it is executed against large databases (millions of items).
+ // Whenever you hit this scale, you should really consider upgrading to Redis 2.8 or above.
+ $cleared = $host->eval("local keys=redis.call('KEYS',ARGV[1]..'*') for i=1,#keys,5000 do redis.call('DEL',unpack(keys,i,math.min(i+4999,#keys))) end return 1", $evalArgs[0], $evalArgs[1]) && $cleared;
+ continue;
+ }
+
+ $cursor = null;
+ do {
+ $keys = $host instanceof \Predis\Client ? $host->scan($cursor, 'MATCH', $namespace.'*', 'COUNT', 1000) : $host->scan($cursor, $namespace.'*', 1000);
+ if (isset($keys[1]) && is_array($keys[1])) {
+ $cursor = $keys[0];
+ $keys = $keys[1];
+ }
+ if ($keys) {
+ $host->del($keys);
+ }
+ } while ($cursor = (int) $cursor);
+ }
+
+ return $cleared;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function doDelete(array $ids)
+ {
+ if ($ids) {
+ $this->redis->del($ids);
+ }
+
+ return true;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function doSave(array $values, $lifetime)
+ {
+ $serialized = array();
+ $failed = array();
+
+ foreach ($values as $id => $value) {
+ try {
+ $serialized[$id] = serialize($value);
+ } catch (\Exception $e) {
+ $failed[] = $id;
+ }
+ }
+
+ if (!$serialized) {
+ return $failed;
+ }
+
+ if (0 >= $lifetime) {
+ $this->redis->mSet($serialized);
+
+ return $failed;
+ }
+
+ $this->pipeline(function ($pipe) use (&$serialized, $lifetime) {
+ foreach ($serialized as $id => $value) {
+ $pipe('setEx', $id, array($lifetime, $value));
+ }
+ });
+
+ return $failed;
+ }
+
+ private function execute($command, $id, array $args, $redis = null)
+ {
+ array_unshift($args, $id);
+ call_user_func_array(array($redis ?: $this->redis, $command), $args);
+ }
+
+ private function pipeline(\Closure $callback)
+ {
+ $redis = $this->redis;
+
+ try {
+ if ($redis instanceof \Predis\Client) {
+ $redis->pipeline(function ($pipe) use ($callback) {
+ $this->redis = $pipe;
+ $callback(array($this, 'execute'));
+ });
+ } elseif ($redis instanceof \RedisArray) {
+ $connections = array();
+ $callback(function ($command, $id, $args) use (&$connections) {
+ if (!isset($connections[$h = $this->redis->_target($id)])) {
+ $connections[$h] = $this->redis->_instance($h);
+ $connections[$h]->multi(\Redis::PIPELINE);
+ }
+ $this->execute($command, $id, $args, $connections[$h]);
+ });
+ foreach ($connections as $c) {
+ $c->exec();
+ }
+ } else {
+ $pipe = $redis->multi(\Redis::PIPELINE);
+ try {
+ $callback(array($this, 'execute'));
+ } finally {
+ if ($pipe) {
+ $redis->exec();
+ }
+ }
+ }
+ } finally {
+ $this->redis = $redis;
+ }
+ }
+}
diff --git a/src/Symfony/Component/Cache/composer.json b/src/Symfony/Component/Cache/composer.json
index 6cb772a80f..f3bc3988fa 100644
--- a/src/Symfony/Component/Cache/composer.json
+++ b/src/Symfony/Component/Cache/composer.json
@@ -1,7 +1,7 @@
{
"name": "symfony/cache",
"type": "library",
- "description": "Symfony implementation of PSR-6",
+ "description": "Symfony Cache component with PSR-6, PSR-16, and tags",
"keywords": ["caching", "psr6"],
"homepage": "https://symfony.com",
"license": "MIT",
@@ -16,15 +16,17 @@
}
],
"provide": {
- "psr/cache-implementation": "1.0"
+ "psr/cache-implementation": "1.0",
+ "psr/simple-cache-implementation": "1.0"
},
"require": {
"php": ">=5.5.9",
"psr/cache": "~1.0",
- "psr/log": "~1.0"
+ "psr/log": "~1.0",
+ "psr/simple-cache": "^1.0"
},
"require-dev": {
- "cache/integration-tests": "dev-master",
+ "cache/integration-tests": "^0.15.0",
"doctrine/cache": "~1.6",
"doctrine/dbal": "~2.4",
"predis/predis": "~1.0"
diff --git a/src/Symfony/Component/Cache/phpunit.xml.dist b/src/Symfony/Component/Cache/phpunit.xml.dist
index 19b5496277..c5884dd625 100644
--- a/src/Symfony/Component/Cache/phpunit.xml.dist
+++ b/src/Symfony/Component/Cache/phpunit.xml.dist
@@ -34,6 +34,8 @@