Skip to content

Commit

Permalink
zendframework#156 PSR-6 / PSR-16: use serialization with storage plugin
Browse files Browse the repository at this point in the history
  • Loading branch information
thomasvargiu committed Apr 22, 2018
1 parent 4e84cd1 commit d56e0b7
Show file tree
Hide file tree
Showing 9 changed files with 89 additions and 311 deletions.
39 changes: 7 additions & 32 deletions src/Psr/CacheItemPool/CacheItemPoolDecorator.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@ class CacheItemPoolDecorator implements CacheItemPoolInterface
public function __construct(StorageInterface $storage)
{
$this->validateStorage($storage);
$this->memoizeSerializationCapabilities($storage);
$this->storage = $storage;
}

Expand All @@ -75,11 +74,6 @@ public function getItem($key)
$isHit = false;
try {
$value = $this->storage->getItem($key, $isHit);

if ($this->serializeValues && $isHit) {
// will set $isHit = false if unserialization fails
extract($this->unserialize($value));
}
} catch (Exception\InvalidArgumentException $e) {
throw new InvalidArgumentException($e->getMessage(), $e->getCode(), $e);
} catch (Exception\ExceptionInterface $e) {
Expand Down Expand Up @@ -121,11 +115,6 @@ public function getItems(array $keys = [])

foreach ($cacheItems as $key => $value) {
$isHit = true;
if ($this->serializeValues) {
// will set $isHit = false if unserialization fails
extract($this->unserialize($value));
}

$items[$key] = new CacheItem($key, $value, $isHit);
}

Expand Down Expand Up @@ -241,9 +230,6 @@ public function save(CacheItemInterface $item)
try {
// get item value and serialize, if required
$value = $item->get();
if ($this->serializeValues) {
$value = serialize($value);
}

// reset TTL on adapter, if required
if ($itemTtl > 0) {
Expand Down Expand Up @@ -304,6 +290,13 @@ public function commit()
*/
private function validateStorage(StorageInterface $storage)
{
if ($this->isSerializationRequired($storage)) {
throw new CacheException(sprintf(
'Storage %s requires a serializer plugin',
get_class($storage)
));
}

// all current adapters implement this
if (! $storage instanceof FlushableInterface) {
throw new CacheException(sprintf(
Expand Down Expand Up @@ -337,24 +330,6 @@ private function validateStorage(StorageInterface $storage)
}
}

/**
* Unserializes value, marking isHit false if it fails
* @param $value
* @return array
*/
private function unserialize($value)
{
if ($value == static::$serializedFalse) {
return ['value' => false, 'isHit' => true];
}

if (false === ($value = unserialize($value))) {
return ['value' => null, 'isHit' => false];
}

return ['value' => $value, 'isHit' => true];
}

/**
* Returns true if deferred item exists for given key and has not expired
* @param string $key
Expand Down
36 changes: 4 additions & 32 deletions src/Psr/SerializationTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,55 +15,27 @@
*/
trait SerializationTrait
{
/**
* @var bool
*/
private $serializeValues = false;

/**
* @var string
*/
private static $serializedFalse;

/**
* Determine if the given storage adapter requires serialization.
*
* Determines if the given storage adapter requires serialization. If so,
* set $serializeValues to true, and serialize a boolean false for later
* comparisons.
*
* @param StorageInterface $storage
* @return void
* @return bool
*/
private function memoizeSerializationCapabilities(StorageInterface $storage)
private function isSerializationRequired(StorageInterface $storage)
{
$capabilities = $storage->getCapabilities();
$requiredTypes = ['string', 'integer', 'double', 'boolean', 'NULL', 'array', 'object'];
$types = $capabilities->getSupportedDatatypes();
$shouldSerialize = false;

foreach ($requiredTypes as $type) {
// 'object' => 'object' is OK
// 'integer' => 'string' is not (redis)
// 'integer' => 'integer' is not (memcache)
if (! (isset($types[$type]) && in_array($types[$type], [true, 'array', 'object'], true))) {
$shouldSerialize = true;
break;
return true;
}
}

if ($shouldSerialize) {
static::$serializedFalse = serialize(false);
}

$this->serializeValues = $shouldSerialize;
return false;
}

/**
* Unserialize a value retrieved from the cache.
*
* @param string $value
* @return mixed
*/
abstract public function unserialize($value);
}
76 changes: 7 additions & 69 deletions src/Psr/SimpleCache/SimpleCacheDecorator.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,13 @@ class SimpleCacheDecorator implements SimpleCacheInterface

public function __construct(StorageInterface $storage)
{
$this->memoizeSerializationCapabilities($storage);
if ($this->isSerializationRequired($storage)) {
throw new SimpleCacheException(sprintf(
'Storage %s requires a serializer plugin',
get_class($storage)
));
}

$this->memoizeTtlCapabilities($storage);
$this->storage = $storage;
$this->utc = new DateTimeZone('UTC');
Expand All @@ -78,11 +84,6 @@ public function get($key, $default = null)
throw static::translateException($e);
}

if ($this->serializeValues && $this->success) {
$result = $this->unserialize($result);
return $result === null ? $default : $result;
}

$result = $result === null ? $default : $result;
return $this->success ? $result : $default;
}
Expand Down Expand Up @@ -110,7 +111,6 @@ public function set($key, $value, $ttl = null)
$options = $this->storage->getOptions();
$previousTtl = $options->getTtl();
$options->setTtl($ttl);
$value = $this->serializeValues ? serialize($value) : $value;

try {
$result = $this->storage->setItem($key, $value);
Expand Down Expand Up @@ -177,12 +177,6 @@ public function getMultiple($keys, $default = null)
$results[$key] = $default;
continue;
}

if (isset($results[$key]) && $this->serializeValues) {
$value = $this->unserialize($results[$key]);
$results[$key] = null === $value ? $default : $value;
continue;
}
}

return $results;
Expand Down Expand Up @@ -211,10 +205,6 @@ public function setMultiple($values, $ttl = null)
return false;
}

if ($this->serializeValues) {
return $this->setMultipleForStorageRequiringSerialization($values, $ttl);
}

$options = $this->storage->getOptions();
$previousTtl = $options->getTtl();
$options->setTtl($ttl);
Expand Down Expand Up @@ -352,32 +342,6 @@ private function validateKey($key)
}
}

/**
* Unserializes a value.
*
* If the $value returned matches a serialized false value, returns
* false for the value.
*
* Otherwise, it unserializes the value. If it is a boolean false at
* that point, it returns a null; otherwise it returns the unserialized
* value.
*
* @param string $value
* @return mixed
*/
private function unserialize($value)
{
if ($value == static::$serializedFalse) {
return false;
}

if (false === ($value = unserialize($value))) {
return null;
}

return $value;
}

/**
* Determine if the storage adapter provides per-item TTL capabilities
*
Expand Down Expand Up @@ -470,30 +434,4 @@ private function convertIterableToArray($iterable, $useKeys, $forMethod)
}
return $array;
}

/**
* Workaround for adapters requiring serialization of values.
*
* In performing integration tests, we found that every adapter that
* requires serialization would fail the tests for setMultiple(), in
* exactly the same way: inability to unserialize the data returned.
*
* The problem appears to be how each adapter's setItems() method handles
* the data provided.
*
* Storing each one by one works perfectly, however, so this is the
* approach taken.
*
* @param iterable $values
* @param null|int $ttl
* @return bool
*/
private function setMultipleForStorageRequiringSerialization($values, $ttl)
{
$result = true;
foreach ($values as $key => $value) {
$result = $result && $this->set($key, $value, $ttl);
}
return $result;
}
}
78 changes: 34 additions & 44 deletions test/Psr/CacheItemPool/CacheItemPoolDecoratorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
use Zend\Cache\Exception;
use Zend\Cache\Psr\CacheItemPool\CacheItemPoolDecorator;
use Zend\Cache\Storage\Adapter\AbstractAdapter;
use Zend\Cache\Storage\Capabilities;
use Zend\Cache\Storage\StorageInterface;

class CacheItemPoolDecoratorTest extends TestCase
Expand All @@ -25,6 +26,39 @@ class CacheItemPoolDecoratorTest extends TestCase
public function testStorageNotFlushableThrowsException()
{
$storage = $this->prophesize(StorageInterface::class);

$capabilities = new Capabilities($storage->reveal(), new \stdClass(), $this->defaultCapabilities);

$storage->getCapabilities()->willReturn($capabilities);

$this->getAdapter($storage);
}

/**
* @expectedException \Zend\Cache\Psr\CacheItemPool\CacheException
*/
public function testStorageNeedsSerializerWillThrowException()
{
$storage = $this->prophesize(StorageInterface::class);

$dataTypes = [
'staticTtl' => true,
'minTtl' => 1,
'supportedDatatypes' => [
'NULL' => true,
'boolean' => true,
'integer' => true,
'double' => false,
'string' => true,
'array' => true,
'object' => 'object',
'resource' => false,
],
];
$capabilities = new Capabilities($storage->reveal(), new \stdClass(), $dataTypes);

$storage->getCapabilities()->willReturn($capabilities);

$this->getAdapter($storage);
}

Expand All @@ -46,50 +80,6 @@ public function testStorageZeroMinTtlThrowsException()
$this->getAdapter($storage);
}

public function testUnserialize()
{
// we can't test this without reflection: we can't prophesy args-by-ref (ie $storage->getItem('key', $isHit))
$unserialize = new \ReflectionMethod(CacheItemPoolDecorator::class, 'unserialize');
$unserialize->setAccessible(true);

$capabilities = $this->defaultCapabilities;
$capabilities['supportedDatatypes']['object'] = false;
$storage = $this->getStorageProphesy($capabilities);
$adapter = $this->getAdapter($storage);

$value = false;
$result = $unserialize->invoke($adapter, serialize($value));
$this->assertTrue($result['isHit'], "False value should be a hit");
$this->assertFalse($result['value'], "False value should be unserialized correctly");

$value = ['a' => 'b'];
$result = $unserialize->invoke($adapter, serialize($value));
$this->assertTrue($result['isHit'], "Array should be a hit");
$this->assertEquals($value, $result['value'], "Array should be unserialized correctly");

$result = $unserialize->invoke($adapter, null);
$this->assertFalse($result['isHit'], "Unserializable value should not be a hit");
$this->assertNull($result['value'], "Unserializable value should be null");
}

public function testUnsupportedDatatypeSerializesValues()
{
$test = ['a' => 'b'];
foreach ($this->defaultCapabilities['supportedDatatypes'] as $type => $value) {
if ($value) {
$capabilities = $this->defaultCapabilities;
$capabilities['supportedDatatypes'][$type] = false;
$storage = $this->getStorageProphesy($capabilities, false, AbstractAdapter::class);
$adapter = $this->getAdapter($storage);
$item = $adapter->getItem('foo');
$item->set($test);
$adapter->save($item);
$items = $adapter->getItems(['foo']);
$this->assertEquals($test, $items['foo']->get());
}
}
}

public function testGetDeferredItem()
{
$adapter = $this->getAdapter();
Expand Down
2 changes: 2 additions & 0 deletions test/Psr/CacheItemPool/FilesystemIntegrationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

use PHPUnit\Framework\TestCase;
use Zend\Cache\Psr\CacheItemPool\CacheItemPoolDecorator;
use Zend\Cache\Storage\Plugin\Serializer;
use Zend\Cache\StorageFactory;

class FilesystemIntegrationTest extends TestCase
Expand All @@ -19,6 +20,7 @@ class FilesystemIntegrationTest extends TestCase
public function testAdapterNotSupported()
{
$storage = StorageFactory::adapterFactory('filesystem');
$storage->addPlugin(new Serializer());
new CacheItemPoolDecorator($storage);
}
}
2 changes: 2 additions & 0 deletions test/Psr/CacheItemPool/RedisIntegrationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use Cache\IntegrationTests\CachePoolTest;
use Zend\Cache\Psr\CacheItemPool\CacheItemPoolDecorator;
use Zend\Cache\Storage\Adapter\Redis;
use Zend\Cache\Storage\Plugin\Serializer;
use Zend\Cache\StorageFactory;
use Zend\Cache\Exception;
use Zend\ServiceManager\Exception\ServiceNotCreatedException;
Expand Down Expand Up @@ -71,6 +72,7 @@ public function createCachePool()

try {
$storage = StorageFactory::adapterFactory('redis', $options);
$storage->addPlugin(new Serializer());

$deferredSkippedMessage = sprintf(
'%s storage doesn\'t support driver deferred',
Expand Down
Loading

0 comments on commit d56e0b7

Please sign in to comment.