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:
commit
f20eaf26c2
|
@ -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
|
||||
|
||||
- |
|
||||
|
|
|
@ -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']);
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
try {
|
||||
if ($value instanceof \Closure) {
|
||||
return $value();
|
||||
}
|
||||
if (\is_string($value) && isset($value[2]) && ':' === $value[1]) {
|
||||
return unserialize($value);
|
||||
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);
|
||||
}
|
||||
|
|
|
@ -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"]);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
-----
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)))));
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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()) {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
Reference in New Issue