diff --git a/src/Symfony/Component/Cache/Adapter/AbstractAdapter.php b/src/Symfony/Component/Cache/Adapter/AbstractAdapter.php index a6da1952a57bd..543bd2da1fdfc 100644 --- a/src/Symfony/Component/Cache/Adapter/AbstractAdapter.php +++ b/src/Symfony/Component/Cache/Adapter/AbstractAdapter.php @@ -12,7 +12,6 @@ namespace Symfony\Component\Cache\Adapter; use Psr\Cache\CacheItemInterface; -use Psr\Cache\CacheItemPoolInterface; use Psr\Log\LoggerAwareInterface; use Psr\Log\LoggerAwareTrait; use Symfony\Component\Cache\CacheItem; @@ -21,7 +20,7 @@ /** * @author Nicolas Grekas */ -abstract class AbstractAdapter implements CacheItemPoolInterface, LoggerAwareInterface +abstract class AbstractAdapter implements AdapterInterface, LoggerAwareInterface { use LoggerAwareTrait; diff --git a/src/Symfony/Component/Cache/Adapter/AdapterInterface.php b/src/Symfony/Component/Cache/Adapter/AdapterInterface.php new file mode 100644 index 0000000000000..85a0da80db079 --- /dev/null +++ b/src/Symfony/Component/Cache/Adapter/AdapterInterface.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\Adapter; + +use Psr\Cache\CacheItemPoolInterface; +use Symfony\Component\Cache\CacheItem; + +/** + * Interface for adapters managing instances of Symfony's {@see CacheItem}. + * + * @author Kévin Dunglas + */ +interface AdapterInterface extends CacheItemPoolInterface +{ +} diff --git a/src/Symfony/Component/Cache/Adapter/ArrayAdapter.php b/src/Symfony/Component/Cache/Adapter/ArrayAdapter.php index e7f69807e8f08..d00488911c838 100644 --- a/src/Symfony/Component/Cache/Adapter/ArrayAdapter.php +++ b/src/Symfony/Component/Cache/Adapter/ArrayAdapter.php @@ -12,7 +12,6 @@ namespace Symfony\Component\Cache\Adapter; use Psr\Cache\CacheItemInterface; -use Psr\Cache\CacheItemPoolInterface; use Psr\Log\LoggerAwareInterface; use Psr\Log\LoggerAwareTrait; use Symfony\Component\Cache\CacheItem; @@ -21,7 +20,7 @@ /** * @author Nicolas Grekas */ -class ArrayAdapter implements CacheItemPoolInterface, LoggerAwareInterface +class ArrayAdapter implements AdapterInterface, LoggerAwareInterface { use LoggerAwareTrait; diff --git a/src/Symfony/Component/Cache/Adapter/ChainAdapter.php b/src/Symfony/Component/Cache/Adapter/ChainAdapter.php new file mode 100644 index 0000000000000..013ce97839a68 --- /dev/null +++ b/src/Symfony/Component/Cache/Adapter/ChainAdapter.php @@ -0,0 +1,226 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Adapter; + +use Psr\Cache\CacheItemInterface; +use Psr\Cache\CacheItemPoolInterface; +use Symfony\Component\Cache\CacheItem; +use Symfony\Component\Cache\Exception\InvalidArgumentException; + +/** + * Chains several adapters together. + * + * Cached items are fetched from the first adapter having them in its data store. + * They are saved and deleted in all adapters at once. + * + * @author Kévin Dunglas + */ +class ChainAdapter implements AdapterInterface +{ + private $adapters = array(); + private $saveUp; + + /** + * @param CacheItemPoolInterface[] $adapters The ordered list of adapters used to fetch cached items. + * @param int $maxLifetime The max lifetime of items propagated from lower adapters to upper ones. + */ + public function __construct(array $adapters, $maxLifetime = 0) + { + if (!$adapters) { + throw new InvalidArgumentException('At least one adapter must be specified.'); + } + + foreach ($adapters as $adapter) { + if (!$adapter instanceof CacheItemPoolInterface) { + throw new InvalidArgumentException(sprintf('The class "%s" does not implement the "%s" interface.', get_class($adapter), CacheItemPoolInterface::class)); + } + + if ($adapter instanceof AdapterInterface) { + $this->adapters[] = $adapter; + } else { + $this->adapters[] = new ProxyAdapter($adapter); + } + } + + $this->saveUp = \Closure::bind( + function ($adapter, $item) use ($maxLifetime) { + $origDefaultLifetime = $item->defaultLifetime; + + if (0 < $maxLifetime && ($origDefaultLifetime <= 0 || $maxLifetime < $origDefaultLifetime)) { + $item->defaultLifetime = $maxLifetime; + } + + $adapter->save($item); + $item->defaultLifetime = $origDefaultLifetime; + }, + $this, + CacheItem::class + ); + } + + /** + * {@inheritdoc} + */ + public function getItem($key) + { + $saveUp = $this->saveUp; + + foreach ($this->adapters as $i => $adapter) { + $item = $adapter->getItem($key); + + if ($item->isHit()) { + while (0 <= --$i) { + $saveUp($this->adapters[$i], $item); + } + + return $item; + } + } + + return $item; + } + + /** + * {@inheritdoc} + */ + public function getItems(array $keys = array()) + { + return $this->generateItems($this->adapters[0]->getItems($keys), 0); + } + + private function generateItems($items, $adapterIndex) + { + $missing = array(); + $nextAdapterIndex = $adapterIndex + 1; + $nextAdapter = isset($this->adapters[$nextAdapterIndex]) ? $this->adapters[$nextAdapterIndex] : null; + + foreach ($items as $k => $item) { + if (!$nextAdapter || $item->isHit()) { + yield $k => $item; + } else { + $missing[] = $k; + } + } + + if ($missing) { + $saveUp = $this->saveUp; + $adapter = $this->adapters[$adapterIndex]; + $items = $this->generateItems($nextAdapter->getItems($missing), $nextAdapterIndex); + + foreach ($items as $k => $item) { + if ($item->isHit()) { + $saveUp($adapter, $item); + } + + yield $k => $item; + } + } + } + + /** + * {@inheritdoc} + */ + public function hasItem($key) + { + foreach ($this->adapters as $adapter) { + if ($adapter->hasItem($key)) { + return true; + } + } + + return false; + } + + /** + * {@inheritdoc} + */ + public function clear() + { + $cleared = true; + + foreach ($this->adapters as $adapter) { + $cleared = $adapter->clear() && $cleared; + } + + return $cleared; + } + + /** + * {@inheritdoc} + */ + public function deleteItem($key) + { + $deleted = true; + + foreach ($this->adapters as $adapter) { + $deleted = $adapter->deleteItem($key) && $deleted; + } + + return $deleted; + } + + /** + * {@inheritdoc} + */ + public function deleteItems(array $keys) + { + $deleted = true; + + foreach ($this->adapters as $adapter) { + $deleted = $adapter->deleteItems($keys) && $deleted; + } + + return $deleted; + } + + /** + * {@inheritdoc} + */ + public function save(CacheItemInterface $item) + { + $saved = true; + + foreach ($this->adapters as $adapter) { + $saved = $adapter->save($item) && $saved; + } + + return $saved; + } + + /** + * {@inheritdoc} + */ + public function saveDeferred(CacheItemInterface $item) + { + $saved = true; + + foreach ($this->adapters as $adapter) { + $saved = $adapter->saveDeferred($item) && $saved; + } + + return $saved; + } + + /** + * {@inheritdoc} + */ + public function commit() + { + $committed = true; + + foreach ($this->adapters as $adapter) { + $committed = $adapter->commit() && $committed; + } + + return $committed; + } +} diff --git a/src/Symfony/Component/Cache/Adapter/ProxyAdapter.php b/src/Symfony/Component/Cache/Adapter/ProxyAdapter.php index 3bce3cb5bb612..83c3f1baea055 100644 --- a/src/Symfony/Component/Cache/Adapter/ProxyAdapter.php +++ b/src/Symfony/Component/Cache/Adapter/ProxyAdapter.php @@ -19,7 +19,7 @@ /** * @author Nicolas Grekas */ -class ProxyAdapter implements CacheItemPoolInterface +class ProxyAdapter implements AdapterInterface { private $pool; private $namespace; diff --git a/src/Symfony/Component/Cache/Tests/Adapter/ChainAdapterTest.php b/src/Symfony/Component/Cache/Tests/Adapter/ChainAdapterTest.php new file mode 100644 index 0000000000000..e9dce9ee8d0e7 --- /dev/null +++ b/src/Symfony/Component/Cache/Tests/Adapter/ChainAdapterTest.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\Adapter; + +use Cache\IntegrationTests\CachePoolTest; +use Symfony\Component\Cache\Adapter\ApcuAdapter; +use Symfony\Component\Cache\Adapter\ArrayAdapter; +use Symfony\Component\Cache\Adapter\ChainAdapter; +use Symfony\Component\Cache\Tests\Fixtures\ExternalAdapter; + +/** + * @author Kévin Dunglas + */ +class ChainAdapterTest extends CachePoolTest +{ + public function createCachePool() + { + if (defined('HHVM_VERSION')) { + $this->skippedTests['testDeferredSaveWithoutCommit'] = 'Fails on HHVM'; + } + if (!function_exists('apcu_fetch') || !ini_get('apc.enabled') || ('cli' === PHP_SAPI && !ini_get('apc.enable_cli'))) { + $this->markTestSkipped('APCu extension is required.'); + } + + return new ChainAdapter(array(new ArrayAdapter(), new ExternalAdapter(), new ApcuAdapter())); + } + + /** + * @expectedException \Symfony\Component\Cache\Exception\InvalidArgumentException + * @expectedExceptionMessage At least one adapter must be specified. + */ + public function testEmptyAdaptersException() + { + new ChainAdapter(array()); + } + + /** + * @expectedException \Symfony\Component\Cache\Exception\InvalidArgumentException + * @expectedExceptionMessage The class "stdClass" does not implement + */ + public function testInvalidAdapterException() + { + new ChainAdapter(array(new \stdClass())); + } +} diff --git a/src/Symfony/Component/Cache/Tests/Fixtures/ExternalAdapter.php b/src/Symfony/Component/Cache/Tests/Fixtures/ExternalAdapter.php new file mode 100644 index 0000000000000..493906ea0cccc --- /dev/null +++ b/src/Symfony/Component/Cache/Tests/Fixtures/ExternalAdapter.php @@ -0,0 +1,76 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Tests\Fixtures; + +use Psr\Cache\CacheItemInterface; +use Psr\Cache\CacheItemPoolInterface; +use Symfony\Component\Cache\Adapter\ArrayAdapter; + +/** + * Adapter not implementing the {@see \Symfony\Component\Cache\Adapter\AdapterInterface}. + * + * @author Kévin Dunglas + */ +class ExternalAdapter implements CacheItemPoolInterface +{ + private $cache; + + public function __construct() + { + $this->cache = new ArrayAdapter(); + } + + public function getItem($key) + { + return $this->cache->getItem($key); + } + + public function getItems(array $keys = array()) + { + return $this->cache->getItems($keys); + } + + public function hasItem($key) + { + return $this->cache->hasItem($key); + } + + public function clear() + { + return $this->cache->clear(); + } + + public function deleteItem($key) + { + return $this->cache->deleteItem($key); + } + + public function deleteItems(array $keys) + { + return $this->cache->deleteItems($keys); + } + + public function save(CacheItemInterface $item) + { + return $this->cache->save($item); + } + + public function saveDeferred(CacheItemInterface $item) + { + return $this->cache->saveDeferred($item); + } + + public function commit() + { + return $this->cache->commit(); + } +}