From ebdcd16bdd775eb012c9ec5912edb78f9d9ead04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Wed, 27 Jan 2016 09:06:20 +0100 Subject: [PATCH 1/2] [Cache] Add a Chain adapter --- .../Cache/Adapter/AbstractAdapter.php | 3 +- .../Cache/Adapter/AdapterInterface.php | 23 +++ .../Component/Cache/Adapter/ArrayAdapter.php | 3 +- .../Component/Cache/Adapter/ChainAdapter.php | 178 ++++++++++++++++++ .../Component/Cache/Adapter/ProxyAdapter.php | 2 +- .../Cache/Tests/Adapter/ChainAdapterTest.php | 58 ++++++ .../Cache/Tests/Fixtures/ExternalAdapter.php | 76 ++++++++ 7 files changed, 338 insertions(+), 5 deletions(-) create mode 100644 src/Symfony/Component/Cache/Adapter/AdapterInterface.php create mode 100644 src/Symfony/Component/Cache/Adapter/ChainAdapter.php create mode 100644 src/Symfony/Component/Cache/Tests/Adapter/ChainAdapterTest.php create mode 100644 src/Symfony/Component/Cache/Tests/Fixtures/ExternalAdapter.php 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..1179d16348330 --- /dev/null +++ b/src/Symfony/Component/Cache/Adapter/AdapterInterface.php @@ -0,0 +1,23 @@ + + * + * 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; + +/** + * Marker interface for adapters managing {@see \Symfony\Component\Cache\CacheItem} instances. + * + * @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..8ebc02a100a2d --- /dev/null +++ b/src/Symfony/Component/Cache/Adapter/ChainAdapter.php @@ -0,0 +1,178 @@ + + * + * 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\Exception\InvalidArgumentException; + +/** + * Chains adapters together. + * + * Saves, deletes and clears all registered adapter. + * Gets data from the first chained adapter having it in cache. + * + * @author Kévin Dunglas + */ +class ChainAdapter implements AdapterInterface +{ + private $adapters = array(); + + /** + * @param AdapterInterface[] $adapters + */ + public function __construct(array $adapters) + { + if (2 > count($adapters)) { + throw new InvalidArgumentException('At least two adapters must be chained.'); + } + + 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); + } + } + } + + /** + * {@inheritdoc} + */ + public function getItem($key) + { + foreach ($this->adapters as $adapter) { + $item = $adapter->getItem($key); + + if ($item->isHit()) { + return $item; + } + } + + return $item; + } + + /** + * {@inheritdoc} + */ + public function getItems(array $keys = array()) + { + $items = array(); + foreach ($keys as $key) { + $items[$key] = $this->getItem($key); + } + + return $items; + } + + /** + * {@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..97cb6bd24b80f --- /dev/null +++ b/src/Symfony/Component/Cache/Tests/Adapter/ChainAdapterTest.php @@ -0,0 +1,58 @@ + + * + * 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 +{ + 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')) { + $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(__CLASS__))); + } + + /** + * @expectedException \Symfony\Component\Cache\Exception\InvalidArgumentException + */ + public function testLessThanTwoAdapterException() + { + new ChainAdapter(array()); + } + + /** + * @expectedException \Symfony\Component\Cache\Exception\InvalidArgumentException + */ + public function testInvalidAdapterException() + { + new ChainAdapter(array(new \stdClass(), 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(); + } +} From 68d9ceabb26f90d27832a75b17e95894205ace5a Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Thu, 17 Mar 2016 11:12:18 +0100 Subject: [PATCH 2/2] [Cache] Optimize Chain adapter --- .../Cache/Adapter/AdapterInterface.php | 3 +- .../Component/Cache/Adapter/ChainAdapter.php | 72 +++++++++++++++---- .../Cache/Tests/Adapter/ChainAdapterTest.php | 14 ++-- 3 files changed, 67 insertions(+), 22 deletions(-) diff --git a/src/Symfony/Component/Cache/Adapter/AdapterInterface.php b/src/Symfony/Component/Cache/Adapter/AdapterInterface.php index 1179d16348330..85a0da80db079 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 8ebc02a100a2d..013ce97839a68 100644 --- a/src/Symfony/Component/Cache/Adapter/ChainAdapter.php +++ b/src/Symfony/Component/Cache/Adapter/ChainAdapter.php @@ -13,27 +13,30 @@ 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 @@ public function __construct(array $adapters) $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 @@ public function __construct(array $adapters) */ 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 @@ public function getItem($key) */ 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 97cb6bd24b80f..e9dce9ee8d0e7 100644 --- a/src/Symfony/Component/Cache/Tests/Adapter/ChainAdapterTest.php +++ b/src/Symfony/Component/Cache/Tests/Adapter/ChainAdapterTest.php @@ -22,12 +22,6 @@ */ 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 @@ public function createCachePool() $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())); } }