diff --git a/src/Symfony/Component/Cache/Adapter/AdapterInterface.php b/src/Symfony/Component/Cache/Adapter/AdapterInterface.php index 1179d16348..85a0da80db 100644 --- a/src/Symfony/Component/Cache/Adapter/AdapterInterface.php +++ b/src/Symfony/Component/Cache/Adapter/AdapterInterface.php @@ -12,9 +12,10 @@ namespace Symfony\Component\Cache\Adapter; use Psr\Cache\CacheItemPoolInterface; +use Symfony\Component\Cache\CacheItem; /** - * Marker interface for adapters managing {@see \Symfony\Component\Cache\CacheItem} instances. + * Interface for adapters managing instances of Symfony's {@see CacheItem}. * * @author Kévin Dunglas */ diff --git a/src/Symfony/Component/Cache/Adapter/ChainAdapter.php b/src/Symfony/Component/Cache/Adapter/ChainAdapter.php index 8ebc02a100..013ce97839 100644 --- a/src/Symfony/Component/Cache/Adapter/ChainAdapter.php +++ b/src/Symfony/Component/Cache/Adapter/ChainAdapter.php @@ -13,27 +13,30 @@ 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 adapters together. + * Chains several adapters together. * - * Saves, deletes and clears all registered adapter. - * Gets data from the first chained adapter having it in cache. + * 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 AdapterInterface[] $adapters + * @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) + public function __construct(array $adapters, $maxLifetime = 0) { - if (2 > count($adapters)) { - throw new InvalidArgumentException('At least two adapters must be chained.'); + if (!$adapters) { + throw new InvalidArgumentException('At least one adapter must be specified.'); } foreach ($adapters as $adapter) { @@ -47,6 +50,21 @@ class ChainAdapter implements AdapterInterface $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 + ); } /** @@ -54,10 +72,16 @@ class ChainAdapter implements AdapterInterface */ public function getItem($key) { - foreach ($this->adapters as $adapter) { + $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; } } @@ -70,12 +94,36 @@ class ChainAdapter implements AdapterInterface */ public function getItems(array $keys = array()) { - $items = array(); - foreach ($keys as $key) { - $items[$key] = $this->getItem($key); + 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; + } } - return $items; + 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; + } + } } /** diff --git a/src/Symfony/Component/Cache/Tests/Adapter/ChainAdapterTest.php b/src/Symfony/Component/Cache/Tests/Adapter/ChainAdapterTest.php index 97cb6bd24b..e9dce9ee8d 100644 --- a/src/Symfony/Component/Cache/Tests/Adapter/ChainAdapterTest.php +++ b/src/Symfony/Component/Cache/Tests/Adapter/ChainAdapterTest.php @@ -22,12 +22,6 @@ use Symfony\Component\Cache\Tests\Fixtures\ExternalAdapter; */ class ChainAdapterTest extends CachePoolTest { - protected $skippedTests = array( - 'testDeferredSaveWithoutCommit' => 'Assumes a shared cache which ArrayAdapter is not.', - 'testSaveWithoutExpire' => 'Assumes a shared cache which ArrayAdapter is not.', - 'testDeferredExpired' => 'Failing for now, needs to be fixed.', - ); - public function createCachePool() { if (defined('HHVM_VERSION')) { @@ -37,22 +31,24 @@ class ChainAdapterTest extends CachePoolTest $this->markTestSkipped('APCu extension is required.'); } - return new ChainAdapter(array(new ArrayAdapter(), new ExternalAdapter(), new ApcuAdapter(__CLASS__))); + 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 testLessThanTwoAdapterException() + 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(), new \stdClass())); + new ChainAdapter(array(new \stdClass())); } }