feature #27645 [Cache] Add `MarshallerInterface` allowing to change the serializer, providing a default one that automatically uses igbinary when available (nicolas-grekas)

This PR was merged into the 4.2-dev branch.

Discussion
----------

[Cache] Add `MarshallerInterface` allowing to change the serializer, providing a default one that automatically uses igbinary when available

| Q             | A
| ------------- | ---
| Branch?       | master
| Bug fix?      | no
| New feature?  | yes
| BC breaks?    | no
| Deprecations? | yes
| Tests pass?   | yes
| Fixed tickets | #19895
| License       | MIT
| Doc PR        | -

With this PR, when igbinary is available, it is automatically used to serialize values.
This provides faster and smaller cache payloads.
The unserializing logic is autoadaptative:
- when an igbinary-serialized value is unserialized but the extension is missing, a cache miss is triggered
- when a natively-serialized value is unserialized and the extension is available, the native `unserialize()` is used

Ping @palex-fpt since you provided very useful comments on the topic and might be interested in reviewing here also.

Commits
-------

9c328c4894 [Cache] Add `MarshallerInterface` allowing to change the serializer, providing a default one that automatically uses igbinary when available
This commit is contained in:
Fabien Potencier 2018-07-09 16:39:33 +02:00
commit f20eaf26c2
27 changed files with 358 additions and 73 deletions

View File

@ -151,6 +151,7 @@ before_install:
tfold ext.libsodium tpecl libsodium sodium.so $INI
tfold ext.mongodb tpecl mongodb-1.5.0 mongodb.so $INI
tfold ext.amqp tpecl amqp-1.9.3 amqp.so $INI
tfold ext.igbinary tpecl igbinary-2.0.6 igbinary.so $INI
fi
- |

View File

@ -24,6 +24,7 @@ use Symfony\Component\Cache\Adapter\AbstractAdapter;
use Symfony\Component\Cache\Adapter\AdapterInterface;
use Symfony\Component\Cache\Adapter\ArrayAdapter;
use Symfony\Component\Cache\Adapter\TagAwareAdapter;
use Symfony\Component\Cache\Marshaller\DefaultMarshaller;
use Symfony\Component\Cache\ResettableInterface;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\Config\Loader\LoaderInterface;
@ -1542,6 +1543,10 @@ class FrameworkExtension extends Extension
private function registerCacheConfiguration(array $config, ContainerBuilder $container)
{
if (!class_exists(DefaultMarshaller::class)) {
$container->removeDefinition('cache.default_marshaller');
}
$version = new Parameter('container.build_id');
$container->getDefinition('cache.adapter.apcu')->replaceArgument(2, $version);
$container->getDefinition('cache.adapter.filesystem')->replaceArgument(2, $config['directory']);

View File

@ -75,6 +75,7 @@
<argument /> <!-- namespace -->
<argument>0</argument> <!-- default lifetime -->
<argument>%kernel.cache_dir%/pools</argument>
<argument type="service" id="cache.default_marshaller" on-invalid="ignore" />
<call method="setLogger">
<argument type="service" id="logger" on-invalid="ignore" />
</call>
@ -93,6 +94,7 @@
<argument /> <!-- Redis connection service -->
<argument /> <!-- namespace -->
<argument>0</argument> <!-- default lifetime -->
<argument type="service" id="cache.default_marshaller" on-invalid="ignore" />
<call method="setLogger">
<argument type="service" id="logger" on-invalid="ignore" />
</call>
@ -104,6 +106,7 @@
<argument /> <!-- Memcached connection service -->
<argument /> <!-- namespace -->
<argument>0</argument> <!-- default lifetime -->
<argument type="service" id="cache.default_marshaller" on-invalid="ignore" />
<call method="setLogger">
<argument type="service" id="logger" on-invalid="ignore" />
</call>
@ -131,6 +134,10 @@
</call>
</service>
<service id="cache.default_marshaller" class="Symfony\Component\Cache\Marshaller\DefaultMarshaller">
<argument>null</argument> <!-- use igbinary_serialize() when available -->
</service>
<service id="cache.default_clearer" class="Symfony\Component\HttpKernel\CacheClearer\Psr6CacheClearer">
<argument type="collection" />
</service>

View File

@ -81,7 +81,7 @@ abstract class AbstractAdapter implements AdapterInterface, CacheInterface, Logg
if (isset(($metadata = $item->newMetadata)[CacheItem::METADATA_TAGS])) {
unset($metadata[CacheItem::METADATA_TAGS]);
}
// For compactness, expiry and creation duration are packed in the key of a array, using magic numbers as separators
// For compactness, expiry and creation duration are packed in the key of an array, using magic numbers as separators
$byLifetime[$ttl][$getId($key)] = $metadata ? array("\x9D".pack('VN', (int) $metadata[CacheItem::METADATA_EXPIRY] - CacheItem::METADATA_EXPIRY_OFFSET, $metadata[CacheItem::METADATA_CTIME])."\x5F" => $item->value) : $item->value;
}

View File

@ -11,6 +11,8 @@
namespace Symfony\Component\Cache\Adapter;
use Symfony\Component\Cache\Marshaller\DefaultMarshaller;
use Symfony\Component\Cache\Marshaller\MarshallerInterface;
use Symfony\Component\Cache\PruneableInterface;
use Symfony\Component\Cache\Traits\FilesystemTrait;
@ -18,8 +20,9 @@ class FilesystemAdapter extends AbstractAdapter implements PruneableInterface
{
use FilesystemTrait;
public function __construct(string $namespace = '', int $defaultLifetime = 0, string $directory = null)
public function __construct(string $namespace = '', int $defaultLifetime = 0, string $directory = null, MarshallerInterface $marshaller = null)
{
$this->marshaller = $marshaller ?? new DefaultMarshaller();
parent::__construct('', $defaultLifetime);
$this->init($namespace, $directory);
}

View File

@ -11,6 +11,7 @@
namespace Symfony\Component\Cache\Adapter;
use Symfony\Component\Cache\Marshaller\MarshallerInterface;
use Symfony\Component\Cache\Traits\MemcachedTrait;
class MemcachedAdapter extends AbstractAdapter
@ -29,8 +30,8 @@ class MemcachedAdapter extends AbstractAdapter
*
* Using a MemcachedAdapter as a pure items store is fine.
*/
public function __construct(\Memcached $client, string $namespace = '', int $defaultLifetime = 0)
public function __construct(\Memcached $client, string $namespace = '', int $defaultLifetime = 0, MarshallerInterface $marshaller = null)
{
$this->init($client, $namespace, $defaultLifetime);
$this->init($client, $namespace, $defaultLifetime, $marshaller);
}
}

View File

@ -13,6 +13,7 @@ namespace Symfony\Component\Cache\Adapter;
use Doctrine\DBAL\Connection;
use Symfony\Component\Cache\Exception\InvalidArgumentException;
use Symfony\Component\Cache\Marshaller\MarshallerInterface;
use Symfony\Component\Cache\PruneableInterface;
use Symfony\Component\Cache\Traits\PdoTrait;
@ -46,8 +47,8 @@ class PdoAdapter extends AbstractAdapter implements PruneableInterface
* @throws InvalidArgumentException When PDO error mode is not PDO::ERRMODE_EXCEPTION
* @throws InvalidArgumentException When namespace contains invalid characters
*/
public function __construct($connOrDsn, string $namespace = '', int $defaultLifetime = 0, array $options = array())
public function __construct($connOrDsn, string $namespace = '', int $defaultLifetime = 0, array $options = array(), MarshallerInterface $marshaller = null)
{
$this->init($connOrDsn, $namespace, $defaultLifetime, $options);
$this->init($connOrDsn, $namespace, $defaultLifetime, $options, $marshaller);
}
}

View File

@ -16,6 +16,7 @@ use Psr\Cache\CacheItemPoolInterface;
use Symfony\Component\Cache\CacheInterface;
use Symfony\Component\Cache\CacheItem;
use Symfony\Component\Cache\Exception\InvalidArgumentException;
use Symfony\Component\Cache\Marshaller\DefaultMarshaller;
use Symfony\Component\Cache\PruneableInterface;
use Symfony\Component\Cache\ResettableInterface;
use Symfony\Component\Cache\Traits\GetTrait;
@ -34,6 +35,7 @@ class PhpArrayAdapter implements AdapterInterface, CacheInterface, PruneableInte
use GetTrait;
private $createCacheItem;
private $marshaller;
/**
* @param string $file The PHP file were values are cached
@ -88,6 +90,7 @@ class PhpArrayAdapter implements AdapterInterface, CacheInterface, PruneableInte
$this->initialize();
}
if (!isset($this->keys[$key])) {
get_from_pool:
if ($this->pool instanceof CacheInterface) {
return $this->pool->get($key, $callback, $beta);
}
@ -99,11 +102,16 @@ class PhpArrayAdapter implements AdapterInterface, CacheInterface, PruneableInte
if ('N;' === $value) {
return null;
}
if ($value instanceof \Closure) {
return $value();
}
if (\is_string($value) && isset($value[2]) && ':' === $value[1]) {
return unserialize($value);
try {
if ($value instanceof \Closure) {
return $value();
}
if (\is_string($value) && isset($value[2]) && ':' === $value[1]) {
return ($this->marshaller ?? $this->marshaller = new DefaultMarshaller())->unmarshall($value);
}
} catch (\Throwable $e) {
unset($this->keys[$key]);
goto get_from_pool;
}
return $value;
@ -278,7 +286,7 @@ class PhpArrayAdapter implements AdapterInterface, CacheInterface, PruneableInte
}
} elseif (\is_string($value) && isset($value[2]) && ':' === $value[1]) {
try {
yield $key => $f($key, unserialize($value), true);
yield $key => $f($key, $this->unserializeValue($value), true);
} catch (\Throwable $e) {
yield $key => $f($key, null, false);
}

View File

@ -69,7 +69,7 @@ class ProxyAdapter implements AdapterInterface, CacheInterface, PruneableInterfa
);
$this->setInnerItem = \Closure::bind(
/**
* @param array $item A CacheItem cast to (array); accessing protected properties requires adding the \0*\0" PHP prefix
* @param array $item A CacheItem cast to (array); accessing protected properties requires adding the "\0*\0" PHP prefix
*/
function (CacheItemInterface $innerItem, array $item) {
// Tags are stored separately, no need to account for them when considering this item's newly set metadata
@ -77,7 +77,7 @@ class ProxyAdapter implements AdapterInterface, CacheInterface, PruneableInterfa
unset($metadata[CacheItem::METADATA_TAGS]);
}
if ($metadata) {
// For compactness, expiry and creation duration are packed in the key of a array, using magic numbers as separators
// For compactness, expiry and creation duration are packed in the key of an array, using magic numbers as separators
$item["\0*\0value"] = array("\x9D".pack('VN', (int) $metadata[CacheItem::METADATA_EXPIRY] - CacheItem::METADATA_EXPIRY_OFFSET, $metadata[CacheItem::METADATA_CTIME])."\x5F" => $item["\0*\0value"]);
}
$innerItem->set($item["\0*\0value"]);

View File

@ -11,6 +11,7 @@
namespace Symfony\Component\Cache\Adapter;
use Symfony\Component\Cache\Marshaller\MarshallerInterface;
use Symfony\Component\Cache\Traits\RedisTrait;
class RedisAdapter extends AbstractAdapter
@ -22,8 +23,8 @@ class RedisAdapter extends AbstractAdapter
* @param string $namespace The default namespace
* @param int $defaultLifetime The default lifetime
*/
public function __construct($redisClient, string $namespace = '', int $defaultLifetime = 0)
public function __construct($redisClient, string $namespace = '', int $defaultLifetime = 0, MarshallerInterface $marshaller = null)
{
$this->init($redisClient, $namespace, $defaultLifetime);
$this->init($redisClient, $namespace, $defaultLifetime, $marshaller);
}
}

View File

@ -4,6 +4,7 @@ CHANGELOG
4.2.0
-----
* added `MarshallerInterface` and `DefaultMarshaller` to allow changing the serializer and provide one that automatically uses igbinary when available
* added `CacheInterface`, which provides stampede protection via probabilistic early expiration and should become the preferred way to use a cache
* added sub-second expiry accuracy for backends that support it
* added support for phpredis 4 `compression` and `tcp_keepalive` options
@ -11,6 +12,7 @@ CHANGELOG
* throw `LogicException` when `CacheItem::tag()` is called on an item coming from a non tag-aware pool
* deprecated `CacheItem::getPreviousTags()`, use `CacheItem::getMetadata()` instead
* deprecated the `AbstractAdapter::createSystemCache()` method
* deprecated the `AbstractAdapter::unserialize()` and `AbstractCache::unserialize()` methods
3.4.0
-----

View File

@ -0,0 +1,99 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Cache\Marshaller;
use Symfony\Component\Cache\Exception\CacheException;
/**
* Serializes/unserializes values using igbinary_serialize() if available, serialize() otherwise.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
class DefaultMarshaller implements MarshallerInterface
{
private $useIgbinarySerialize = true;
public function __construct(bool $useIgbinarySerialize = null)
{
if (null === $useIgbinarySerialize) {
$useIgbinarySerialize = \extension_loaded('igbinary');
} elseif ($useIgbinarySerialize && !\extension_loaded('igbinary')) {
throw new CacheException('The "igbinary" PHP extension is not loaded.');
}
$this->useIgbinarySerialize = $useIgbinarySerialize;
}
/**
* {@inheritdoc}
*/
public function marshall(array $values, ?array &$failed): array
{
$serialized = $failed = array();
foreach ($values as $id => $value) {
try {
if ($this->useIgbinarySerialize) {
$serialized[$id] = igbinary_serialize($value);
} else {
$serialized[$id] = serialize($value);
}
} catch (\Exception $e) {
$failed[] = $id;
}
}
return $serialized;
}
/**
* {@inheritdoc}
*/
public function unmarshall(string $value)
{
if ('b:0;' === $value) {
return false;
}
if ('N;' === $value) {
return null;
}
static $igbinaryNull;
if ($value === ($igbinaryNull ?? $igbinaryNull = \extension_loaded('igbinary') ? igbinary_serialize(null) : false)) {
return null;
}
$unserializeCallbackHandler = ini_set('unserialize_callback_func', __CLASS__.'::handleUnserializeCallback');
try {
if (':' === ($value[1] ?? ':')) {
if (false !== $value = unserialize($value)) {
return $value;
}
} elseif (false === $igbinaryNull) {
throw new \RuntimeException('Failed to unserialize cached value, did you forget to install the "igbinary" extension?');
} elseif (null !== $value = igbinary_unserialize($value)) {
return $value;
}
throw new \DomainException(error_get_last() ? error_get_last()['message'] : '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);
}
}
/**
* @internal
*/
public static function handleUnserializeCallback($class)
{
throw new \DomainException('Class not found: '.$class);
}
}

View File

@ -0,0 +1,40 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Cache\Marshaller;
/**
* Serializes/unserializes PHP values.
*
* Implementations of this interface MUST deal with errors carefuly. They MUST
* also deal with forward and backward compatibility at the storage format level.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
interface MarshallerInterface
{
/**
* Serializes a list of values.
*
* When serialization fails for a specific value, no exception should be
* thrown. Instead, its key should be listed in $failed.
*/
public function marshall(array $values, ?array &$failed): array;
/**
* Unserializes a single value and throws an exception if anything goes wrong.
*
* @return mixed
*
* @throws \Exception Whenever unserialization fails
*/
public function unmarshall(string $value);
}

View File

@ -11,6 +11,8 @@
namespace Symfony\Component\Cache\Simple;
use Symfony\Component\Cache\Marshaller\DefaultMarshaller;
use Symfony\Component\Cache\Marshaller\MarshallerInterface;
use Symfony\Component\Cache\PruneableInterface;
use Symfony\Component\Cache\Traits\FilesystemTrait;
@ -18,8 +20,9 @@ class FilesystemCache extends AbstractCache implements PruneableInterface
{
use FilesystemTrait;
public function __construct(string $namespace = '', int $defaultLifetime = 0, string $directory = null)
public function __construct(string $namespace = '', int $defaultLifetime = 0, string $directory = null, MarshallerInterface $marshaller = null)
{
$this->marshaller = $marshaller ?? new DefaultMarshaller();
parent::__construct('', $defaultLifetime);
$this->init($namespace, $directory);
}

View File

@ -11,6 +11,7 @@
namespace Symfony\Component\Cache\Simple;
use Symfony\Component\Cache\Marshaller\MarshallerInterface;
use Symfony\Component\Cache\Traits\MemcachedTrait;
class MemcachedCache extends AbstractCache
@ -19,8 +20,8 @@ class MemcachedCache extends AbstractCache
protected $maxIdLength = 250;
public function __construct(\Memcached $client, string $namespace = '', int $defaultLifetime = 0)
public function __construct(\Memcached $client, string $namespace = '', int $defaultLifetime = 0, MarshallerInterface $marshaller = null)
{
$this->init($client, $namespace, $defaultLifetime);
$this->init($client, $namespace, $defaultLifetime, $marshaller);
}
}

View File

@ -11,6 +11,7 @@
namespace Symfony\Component\Cache\Simple;
use Symfony\Component\Cache\Marshaller\MarshallerInterface;
use Symfony\Component\Cache\PruneableInterface;
use Symfony\Component\Cache\Traits\PdoTrait;
@ -44,8 +45,8 @@ class PdoCache extends AbstractCache implements PruneableInterface
* @throws InvalidArgumentException When PDO error mode is not PDO::ERRMODE_EXCEPTION
* @throws InvalidArgumentException When namespace contains invalid characters
*/
public function __construct($connOrDsn, string $namespace = '', int $defaultLifetime = 0, array $options = array())
public function __construct($connOrDsn, string $namespace = '', int $defaultLifetime = 0, array $options = array(), MarshallerInterface $marshaller = null)
{
$this->init($connOrDsn, $namespace, $defaultLifetime, $options);
$this->init($connOrDsn, $namespace, $defaultLifetime, $options, $marshaller);
}
}

View File

@ -13,6 +13,7 @@ namespace Symfony\Component\Cache\Simple;
use Psr\SimpleCache\CacheInterface;
use Symfony\Component\Cache\Exception\InvalidArgumentException;
use Symfony\Component\Cache\Marshaller\DefaultMarshaller;
use Symfony\Component\Cache\Traits\PhpArrayTrait;
use Symfony\Component\Cache\PruneableInterface;
use Symfony\Component\Cache\ResettableInterface;
@ -28,6 +29,8 @@ class PhpArrayCache implements CacheInterface, PruneableInterface, ResettableInt
{
use PhpArrayTrait;
private $marshaller;
/**
* @param string $file The PHP file were values are cached
* @param CacheInterface $fallbackPool A pool to fallback on when an item is not hit
@ -83,7 +86,7 @@ class PhpArrayCache implements CacheInterface, PruneableInterface, ResettableInt
}
if (\is_string($value) && isset($value[2]) && ':' === $value[1]) {
try {
return unserialize($value);
return ($this->marshaller ?? $this->marshaller = new DefaultMarshaller())->unmarshall($value);
} catch (\Throwable $e) {
return $default;
}

View File

@ -11,6 +11,7 @@
namespace Symfony\Component\Cache\Simple;
use Symfony\Component\Cache\Marshaller\MarshallerInterface;
use Symfony\Component\Cache\Traits\RedisTrait;
class RedisCache extends AbstractCache
@ -22,8 +23,8 @@ class RedisCache extends AbstractCache
* @param string $namespace
* @param int $defaultLifetime
*/
public function __construct($redisClient, string $namespace = '', int $defaultLifetime = 0)
public function __construct($redisClient, string $namespace = '', int $defaultLifetime = 0, MarshallerInterface $marshaller = null)
{
$this->init($redisClient, $namespace, $defaultLifetime);
$this->init($redisClient, $namespace, $defaultLifetime, $marshaller);
}
}

View File

@ -56,7 +56,7 @@ class MaxIdLengthAdapterTest extends TestCase
$reflectionProperty->setValue($cache, true);
// Versioning enabled
$this->assertEquals('--------------------------:1:------------', $reflectionMethod->invokeArgs($cache, array(str_repeat('-', 12))));
$this->assertEquals('--------------------------:1/------------', $reflectionMethod->invokeArgs($cache, array(str_repeat('-', 12))));
$this->assertLessThanOrEqual(50, strlen($reflectionMethod->invokeArgs($cache, array(str_repeat('-', 12)))));
$this->assertLessThanOrEqual(50, strlen($reflectionMethod->invokeArgs($cache, array(str_repeat('-', 23)))));
$this->assertLessThanOrEqual(50, strlen($reflectionMethod->invokeArgs($cache, array(str_repeat('-', 40)))));

View File

@ -0,0 +1,104 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Cache\Tests\Marshaller;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Cache\Marshaller\DefaultMarshaller;
class DefaultMarshallerTest extends TestCase
{
public function testSerialize()
{
$marshaller = new DefaultMarshaller();
$values = array(
'a' => 123,
'b' => function () {},
);
$expected = array('a' => \extension_loaded('igbinary') ? igbinary_serialize(123) : serialize(123));
$this->assertSame($expected, $marshaller->marshall($values, $failed));
$this->assertSame(array('b'), $failed);
}
public function testNativeUnserialize()
{
$marshaller = new DefaultMarshaller();
$this->assertNull($marshaller->unmarshall(serialize(null)));
$this->assertFalse($marshaller->unmarshall(serialize(false)));
$this->assertSame('', $marshaller->unmarshall(serialize('')));
$this->assertSame(0, $marshaller->unmarshall(serialize(0)));
}
/**
* @requires extension igbinary
*/
public function testIgbinaryUnserialize()
{
$marshaller = new DefaultMarshaller();
$this->assertNull($marshaller->unmarshall(igbinary_serialize(null)));
$this->assertFalse($marshaller->unmarshall(igbinary_serialize(false)));
$this->assertSame('', $marshaller->unmarshall(igbinary_serialize('')));
$this->assertSame(0, $marshaller->unmarshall(igbinary_serialize(0)));
}
/**
* @expectedException \DomainException
* @expectedExceptionMessage Class not found: NotExistingClass
*/
public function testNativeUnserializeNotFoundClass()
{
$marshaller = new DefaultMarshaller();
$marshaller->unmarshall('O:16:"NotExistingClass":0:{}');
}
/**
* @requires extension igbinary
* @expectedException \DomainException
* @expectedExceptionMessage Class not found: NotExistingClass
*/
public function testIgbinaryUnserializeNotFoundClass()
{
$marshaller = new DefaultMarshaller();
$marshaller->unmarshall(rawurldecode('%00%00%00%02%17%10NotExistingClass%14%00'));
}
/**
* @expectedException \DomainException
* @expectedExceptionMessage unserialize(): Error at offset 0 of 3 bytes
*/
public function testNativeUnserializeInvalid()
{
$marshaller = new DefaultMarshaller();
set_error_handler(function () { return false; });
try {
@$marshaller->unmarshall(':::');
} finally {
restore_error_handler();
}
}
/**
* @requires extension igbinary
* @expectedException \DomainException
* @expectedExceptionMessage igbinary_unserialize_zval: unknown type '61', position 5
*/
public function testIgbinaryUnserializeInvalid()
{
$marshaller = new DefaultMarshaller();
set_error_handler(function () { return false; });
try {
@$marshaller->unmarshall(rawurldecode('%00%00%00%02abc'));
} finally {
restore_error_handler();
}
}
}

View File

@ -109,14 +109,14 @@ trait AbstractTrait
if ($cleared = $this->versioningIsEnabled) {
$namespaceVersion = 2;
try {
foreach ($this->doFetch(array('@'.$this->namespace)) as $v) {
foreach ($this->doFetch(array('/'.$this->namespace)) as $v) {
$namespaceVersion = 1 + (int) $v;
}
} catch (\Exception $e) {
}
$namespaceVersion .= ':';
$namespaceVersion .= '/';
try {
$cleared = $this->doSave(array('@'.$this->namespace => $namespaceVersion), 0);
$cleared = $this->doSave(array('/'.$this->namespace => $namespaceVersion), 0);
} catch (\Exception $e) {
$cleared = false;
}
@ -222,9 +222,13 @@ trait AbstractTrait
* @return mixed
*
* @throws \Exception
*
* @deprecated since Symfony 4.2, use DefaultMarshaller instead.
*/
protected static function unserialize($value)
{
@trigger_error(sprintf('The "%s::unserialize()" method is deprecated since Symfony 4.2, use DefaultMarshaller instead.', __CLASS__), E_USER_DEPRECATED);
if ('b:0;' === $value) {
return false;
}
@ -245,9 +249,9 @@ trait AbstractTrait
{
if ($this->versioningIsEnabled && '' === $this->namespaceVersion) {
$this->ids = array();
$this->namespaceVersion = '1:';
$this->namespaceVersion = '1/';
try {
foreach ($this->doFetch(array('@'.$this->namespace)) as $v) {
foreach ($this->doFetch(array('/'.$this->namespace)) as $v) {
$this->namespaceVersion = $v;
}
} catch (\Exception $e) {

View File

@ -51,6 +51,7 @@ trait ApcuTrait
*/
protected function doFetch(array $ids)
{
$unserializeCallbackHandler = ini_set('unserialize_callback_func', __CLASS__.'::handleUnserializeCallback');
try {
$values = array();
foreach (apcu_fetch($ids, $ok) ?: array() as $k => $v) {
@ -62,6 +63,8 @@ trait ApcuTrait
return $values;
} catch (\Error $e) {
throw new \ErrorException($e->getMessage(), $e->getCode(), E_ERROR, $e->getFile(), $e->getLine());
} finally {
ini_set('unserialize_callback_func', $unserializeCallbackHandler);
}
}

View File

@ -23,6 +23,8 @@ trait FilesystemTrait
{
use FilesystemCommonTrait;
private $marshaller;
/**
* @return bool
*/
@ -68,7 +70,7 @@ trait FilesystemTrait
$value = stream_get_contents($h);
fclose($h);
if ($i === $id) {
$values[$id] = parent::unserialize($value);
$values[$id] = $this->marshaller->unmarshall($value);
}
}
}
@ -91,17 +93,19 @@ trait FilesystemTrait
*/
protected function doSave(array $values, $lifetime)
{
$ok = true;
$expiresAt = $lifetime ? (time() + $lifetime) : 0;
$values = $this->marshaller->marshall($values, $failed);
foreach ($values as $id => $value) {
$ok = $this->write($this->getFile($id, true), $expiresAt."\n".rawurlencode($id)."\n".serialize($value), $expiresAt) && $ok;
if (!$this->write($this->getFile($id, true), $expiresAt."\n".rawurlencode($id)."\n".$value, $expiresAt)) {
$failed[] = $id;
}
}
if (!$ok && !is_writable($this->directory)) {
if ($failed && !is_writable($this->directory)) {
throw new CacheException(sprintf('Cache directory is not writable (%s)', $this->directory));
}
return $ok;
return $failed;
}
}

View File

@ -13,6 +13,8 @@ namespace Symfony\Component\Cache\Traits;
use Symfony\Component\Cache\Exception\CacheException;
use Symfony\Component\Cache\Exception\InvalidArgumentException;
use Symfony\Component\Cache\Marshaller\DefaultMarshaller;
use Symfony\Component\Cache\Marshaller\MarshallerInterface;
/**
* @author Rob Frawley 2nd <rmf@src.run>
@ -29,6 +31,7 @@ trait MemcachedTrait
'serializer' => 'php',
);
private $marshaller;
private $client;
private $lazyClient;
@ -37,7 +40,7 @@ trait MemcachedTrait
return extension_loaded('memcached') && version_compare(phpversion('memcached'), '2.2.0', '>=');
}
private function init(\Memcached $client, $namespace, $defaultLifetime)
private function init(\Memcached $client, $namespace, $defaultLifetime, ?MarshallerInterface $marshaller)
{
if (!static::isSupported()) {
throw new CacheException('Memcached >= 2.2.0 is required');
@ -55,6 +58,7 @@ trait MemcachedTrait
parent::__construct($namespace, $defaultLifetime);
$this->enableVersioning();
$this->marshaller = $marshaller ?? new DefaultMarshaller();
}
/**
@ -194,6 +198,10 @@ trait MemcachedTrait
*/
protected function doSave(array $values, $lifetime)
{
if (!$values = $this->marshaller->marshall($values, $failed)) {
return $failed;
}
if ($lifetime && $lifetime > 30 * 86400) {
$lifetime += time();
}
@ -203,7 +211,7 @@ trait MemcachedTrait
$encodedValues[rawurlencode($key)] = $value;
}
return $this->checkResultCode($this->getClient()->setMulti($encodedValues, $lifetime));
return $this->checkResultCode($this->getClient()->setMulti($encodedValues, $lifetime)) ? $failed : false;
}
/**
@ -211,7 +219,6 @@ trait MemcachedTrait
*/
protected function doFetch(array $ids)
{
$unserializeCallbackHandler = ini_set('unserialize_callback_func', __CLASS__.'::handleUnserializeCallback');
try {
$encodedIds = array_map('rawurlencode', $ids);
@ -219,14 +226,12 @@ trait MemcachedTrait
$result = array();
foreach ($encodedResult as $key => $value) {
$result[rawurldecode($key)] = $value;
$result[rawurldecode($key)] = $this->marshaller->unmarshall($value);
}
return $result;
} catch (\Error $e) {
throw new \ErrorException($e->getMessage(), $e->getCode(), E_ERROR, $e->getFile(), $e->getLine());
} finally {
ini_set('unserialize_callback_func', $unserializeCallbackHandler);
}
}

View File

@ -17,12 +17,15 @@ use Doctrine\DBAL\DBALException;
use Doctrine\DBAL\Exception\TableNotFoundException;
use Doctrine\DBAL\Schema\Schema;
use Symfony\Component\Cache\Exception\InvalidArgumentException;
use Symfony\Component\Cache\Marshaller\DefaultMarshaller;
use Symfony\Component\Cache\Marshaller\MarshallerInterface;
/**
* @internal
*/
trait PdoTrait
{
private $marshaller;
private $conn;
private $dsn;
private $driver;
@ -37,7 +40,7 @@ trait PdoTrait
private $connectionOptions = array();
private $namespace;
private function init($connOrDsn, $namespace, $defaultLifetime, array $options)
private function init($connOrDsn, $namespace, $defaultLifetime, array $options, ?MarshallerInterface $marshaller)
{
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]));
@ -66,6 +69,7 @@ trait PdoTrait
$this->password = isset($options['db_password']) ? $options['db_password'] : $this->password;
$this->connectionOptions = isset($options['db_connection_options']) ? $options['db_connection_options'] : $this->connectionOptions;
$this->namespace = $namespace;
$this->marshaller = $marshaller ?? new DefaultMarshaller();
parent::__construct($namespace, $defaultLifetime);
}
@ -186,7 +190,7 @@ trait PdoTrait
if (null === $row[1]) {
$expired[] = $row[0];
} else {
yield $row[0] => parent::unserialize(is_resource($row[1]) ? stream_get_contents($row[1]) : $row[1]);
yield $row[0] => $this->marshaller->unmarshall(is_resource($row[1]) ? stream_get_contents($row[1]) : $row[1]);
}
}
@ -263,18 +267,7 @@ trait PdoTrait
*/
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) {
if (!$values = $this->marshaller->marshall($values, $failed)) {
return $failed;
}
@ -346,7 +339,7 @@ trait PdoTrait
$insertStmt->bindValue(':time', $now, \PDO::PARAM_INT);
}
foreach ($serialized as $id => $data) {
foreach ($values as $id => $data) {
$stmt->execute();
if (null === $driver && !$stmt->rowCount()) {

View File

@ -13,6 +13,7 @@ namespace Symfony\Component\Cache\Traits;
use Symfony\Component\Cache\Exception\CacheException;
use Symfony\Component\Cache\Exception\InvalidArgumentException;
use Symfony\Component\Cache\Marshaller\DefaultMarshaller;
use Symfony\Component\Cache\Marshaller\PhpMarshaller;
/**
@ -29,6 +30,7 @@ trait PhpFilesTrait
doDelete as private doCommonDelete;
}
private $marshaller;
private $includeHandler;
private $appendOnly;
private $values = array();
@ -91,7 +93,7 @@ trait PhpFilesTrait
} elseif ($value instanceof \Closure) {
$values[$id] = $value();
} elseif (\is_string($value) && isset($value[2]) && ':' === $value[1]) {
$values[$id] = parent::unserialize($value);
$values[$id] = ($this->marshaller ?? $this->marshaller = new DefaultMarshaller())->unmarshall($value);
} else {
$values[$id] = $value;
}

View File

@ -18,6 +18,8 @@ use Predis\Connection\Aggregate\RedisCluster;
use Predis\Response\Status;
use Symfony\Component\Cache\Exception\CacheException;
use Symfony\Component\Cache\Exception\InvalidArgumentException;
use Symfony\Component\Cache\Marshaller\DefaultMarshaller;
use Symfony\Component\Cache\Marshaller\MarshallerInterface;
/**
* @author Aurimas Niekis <aurimas@niekis.lt>
@ -39,11 +41,12 @@ trait RedisTrait
'lazy' => false,
);
private $redis;
private $marshaller;
/**
* @param \Redis|\RedisArray|\RedisCluster|\Predis\Client $redisClient
*/
private function init($redisClient, $namespace = '', $defaultLifetime = 0)
private function init($redisClient, $namespace, $defaultLifetime, ?MarshallerInterface $marshaller)
{
parent::__construct($namespace, $defaultLifetime);
@ -56,6 +59,7 @@ trait RedisTrait
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;
$this->marshaller = $marshaller ?? new DefaultMarshaller();
}
/**
@ -186,7 +190,7 @@ trait RedisTrait
});
foreach ($values as $id => $v) {
if ($v) {
yield $id => parent::unserialize($v);
yield $id => $this->marshaller->unmarshall($v);
}
}
}
@ -282,23 +286,12 @@ trait RedisTrait
*/
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) {
if (!$values = $this->marshaller->marshall($values, $failed)) {
return $failed;
}
$results = $this->pipeline(function () use ($serialized, $lifetime) {
foreach ($serialized as $id => $value) {
$results = $this->pipeline(function () use ($values, $lifetime) {
foreach ($values as $id => $value) {
if (0 >= $lifetime) {
yield 'set' => array($id, $value);
} else {