From 9c328c489499c4c42b648dc8ecf61976935fcb88 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Tue, 19 Jun 2018 21:18:37 +0200 Subject: [PATCH] [Cache] Add `MarshallerInterface` allowing to change the serializer, providing a default one that automatically uses igbinary when available --- .travis.yml | 1 + .../FrameworkExtension.php | 5 + .../Resources/config/cache.xml | 7 ++ .../Cache/Adapter/AbstractAdapter.php | 2 +- .../Cache/Adapter/FilesystemAdapter.php | 5 +- .../Cache/Adapter/MemcachedAdapter.php | 5 +- .../Component/Cache/Adapter/PdoAdapter.php | 5 +- .../Cache/Adapter/PhpArrayAdapter.php | 20 +++- .../Component/Cache/Adapter/ProxyAdapter.php | 4 +- .../Component/Cache/Adapter/RedisAdapter.php | 5 +- src/Symfony/Component/Cache/CHANGELOG.md | 2 + .../Cache/Marshaller/DefaultMarshaller.php | 99 +++++++++++++++++ .../Cache/Marshaller/MarshallerInterface.php | 40 +++++++ .../Cache/Simple/FilesystemCache.php | 5 +- .../Component/Cache/Simple/MemcachedCache.php | 5 +- .../Component/Cache/Simple/PdoCache.php | 5 +- .../Component/Cache/Simple/PhpArrayCache.php | 5 +- .../Component/Cache/Simple/RedisCache.php | 5 +- .../Tests/Adapter/MaxIdLengthAdapterTest.php | 2 +- .../Marshaller/DefaultMarshallerTest.php | 104 ++++++++++++++++++ .../Component/Cache/Traits/AbstractTrait.php | 14 ++- .../Component/Cache/Traits/ApcuTrait.php | 3 + .../Cache/Traits/FilesystemTrait.php | 14 ++- .../Component/Cache/Traits/MemcachedTrait.php | 17 ++- .../Component/Cache/Traits/PdoTrait.php | 23 ++-- .../Component/Cache/Traits/PhpFilesTrait.php | 4 +- .../Component/Cache/Traits/RedisTrait.php | 25 ++--- 27 files changed, 358 insertions(+), 73 deletions(-) create mode 100644 src/Symfony/Component/Cache/Marshaller/DefaultMarshaller.php create mode 100644 src/Symfony/Component/Cache/Marshaller/MarshallerInterface.php create mode 100644 src/Symfony/Component/Cache/Tests/Marshaller/DefaultMarshallerTest.php diff --git a/.travis.yml b/.travis.yml index d4c5156a6d..b76383fb1d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -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 - | diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index b3908ace88..de7b13e9b0 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -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; @@ -1539,6 +1540,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']); diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/cache.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/cache.xml index d31be8db3f..3d31361cf1 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/cache.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/cache.xml @@ -75,6 +75,7 @@ 0 %kernel.cache_dir%/pools + @@ -93,6 +94,7 @@ 0 + @@ -104,6 +106,7 @@ 0 + @@ -118,6 +121,10 @@ + + null + + diff --git a/src/Symfony/Component/Cache/Adapter/AbstractAdapter.php b/src/Symfony/Component/Cache/Adapter/AbstractAdapter.php index b6e493916b..2f148e63d2 100644 --- a/src/Symfony/Component/Cache/Adapter/AbstractAdapter.php +++ b/src/Symfony/Component/Cache/Adapter/AbstractAdapter.php @@ -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; } diff --git a/src/Symfony/Component/Cache/Adapter/FilesystemAdapter.php b/src/Symfony/Component/Cache/Adapter/FilesystemAdapter.php index a088883683..7185dd4877 100644 --- a/src/Symfony/Component/Cache/Adapter/FilesystemAdapter.php +++ b/src/Symfony/Component/Cache/Adapter/FilesystemAdapter.php @@ -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); } diff --git a/src/Symfony/Component/Cache/Adapter/MemcachedAdapter.php b/src/Symfony/Component/Cache/Adapter/MemcachedAdapter.php index 65ab9eda86..b678bb5d88 100644 --- a/src/Symfony/Component/Cache/Adapter/MemcachedAdapter.php +++ b/src/Symfony/Component/Cache/Adapter/MemcachedAdapter.php @@ -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); } } diff --git a/src/Symfony/Component/Cache/Adapter/PdoAdapter.php b/src/Symfony/Component/Cache/Adapter/PdoAdapter.php index 32f7be8987..c6ef66e477 100644 --- a/src/Symfony/Component/Cache/Adapter/PdoAdapter.php +++ b/src/Symfony/Component/Cache/Adapter/PdoAdapter.php @@ -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; @@ -43,8 +44,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); } } diff --git a/src/Symfony/Component/Cache/Adapter/PhpArrayAdapter.php b/src/Symfony/Component/Cache/Adapter/PhpArrayAdapter.php index dabc80b097..3a698f0a78 100644 --- a/src/Symfony/Component/Cache/Adapter/PhpArrayAdapter.php +++ b/src/Symfony/Component/Cache/Adapter/PhpArrayAdapter.php @@ -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); } diff --git a/src/Symfony/Component/Cache/Adapter/ProxyAdapter.php b/src/Symfony/Component/Cache/Adapter/ProxyAdapter.php index ddb533e0a9..007f1fe2c0 100644 --- a/src/Symfony/Component/Cache/Adapter/ProxyAdapter.php +++ b/src/Symfony/Component/Cache/Adapter/ProxyAdapter.php @@ -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"]); diff --git a/src/Symfony/Component/Cache/Adapter/RedisAdapter.php b/src/Symfony/Component/Cache/Adapter/RedisAdapter.php index 0bb76fcdd4..9d3931d1f4 100644 --- a/src/Symfony/Component/Cache/Adapter/RedisAdapter.php +++ b/src/Symfony/Component/Cache/Adapter/RedisAdapter.php @@ -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); } } diff --git a/src/Symfony/Component/Cache/CHANGELOG.md b/src/Symfony/Component/Cache/CHANGELOG.md index 98cf028026..ea0cdfef5e 100644 --- a/src/Symfony/Component/Cache/CHANGELOG.md +++ b/src/Symfony/Component/Cache/CHANGELOG.md @@ -4,12 +4,14 @@ 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 * 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 ----- diff --git a/src/Symfony/Component/Cache/Marshaller/DefaultMarshaller.php b/src/Symfony/Component/Cache/Marshaller/DefaultMarshaller.php new file mode 100644 index 0000000000..734f32fcd8 --- /dev/null +++ b/src/Symfony/Component/Cache/Marshaller/DefaultMarshaller.php @@ -0,0 +1,99 @@ + + * + * 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 + */ +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); + } +} diff --git a/src/Symfony/Component/Cache/Marshaller/MarshallerInterface.php b/src/Symfony/Component/Cache/Marshaller/MarshallerInterface.php new file mode 100644 index 0000000000..4d757e38c4 --- /dev/null +++ b/src/Symfony/Component/Cache/Marshaller/MarshallerInterface.php @@ -0,0 +1,40 @@ + + * + * 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 + */ +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); +} diff --git a/src/Symfony/Component/Cache/Simple/FilesystemCache.php b/src/Symfony/Component/Cache/Simple/FilesystemCache.php index 37b3d3fa61..8e04d53355 100644 --- a/src/Symfony/Component/Cache/Simple/FilesystemCache.php +++ b/src/Symfony/Component/Cache/Simple/FilesystemCache.php @@ -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); } diff --git a/src/Symfony/Component/Cache/Simple/MemcachedCache.php b/src/Symfony/Component/Cache/Simple/MemcachedCache.php index 0ff521b9c3..8e418b071e 100644 --- a/src/Symfony/Component/Cache/Simple/MemcachedCache.php +++ b/src/Symfony/Component/Cache/Simple/MemcachedCache.php @@ -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); } } diff --git a/src/Symfony/Component/Cache/Simple/PdoCache.php b/src/Symfony/Component/Cache/Simple/PdoCache.php index 65b9879cd3..083198cb6e 100644 --- a/src/Symfony/Component/Cache/Simple/PdoCache.php +++ b/src/Symfony/Component/Cache/Simple/PdoCache.php @@ -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; @@ -41,8 +42,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); } } diff --git a/src/Symfony/Component/Cache/Simple/PhpArrayCache.php b/src/Symfony/Component/Cache/Simple/PhpArrayCache.php index bb3321b27c..aeef4668ae 100644 --- a/src/Symfony/Component/Cache/Simple/PhpArrayCache.php +++ b/src/Symfony/Component/Cache/Simple/PhpArrayCache.php @@ -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; } diff --git a/src/Symfony/Component/Cache/Simple/RedisCache.php b/src/Symfony/Component/Cache/Simple/RedisCache.php index 45bb5ff799..df2a96e86b 100644 --- a/src/Symfony/Component/Cache/Simple/RedisCache.php +++ b/src/Symfony/Component/Cache/Simple/RedisCache.php @@ -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); } } diff --git a/src/Symfony/Component/Cache/Tests/Adapter/MaxIdLengthAdapterTest.php b/src/Symfony/Component/Cache/Tests/Adapter/MaxIdLengthAdapterTest.php index 04781b8fbd..f4822e9dfc 100644 --- a/src/Symfony/Component/Cache/Tests/Adapter/MaxIdLengthAdapterTest.php +++ b/src/Symfony/Component/Cache/Tests/Adapter/MaxIdLengthAdapterTest.php @@ -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))))); diff --git a/src/Symfony/Component/Cache/Tests/Marshaller/DefaultMarshallerTest.php b/src/Symfony/Component/Cache/Tests/Marshaller/DefaultMarshallerTest.php new file mode 100644 index 0000000000..fc625d12fc --- /dev/null +++ b/src/Symfony/Component/Cache/Tests/Marshaller/DefaultMarshallerTest.php @@ -0,0 +1,104 @@ + + * + * 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(); + } + } +} diff --git a/src/Symfony/Component/Cache/Traits/AbstractTrait.php b/src/Symfony/Component/Cache/Traits/AbstractTrait.php index 361ec5782a..68634b0947 100644 --- a/src/Symfony/Component/Cache/Traits/AbstractTrait.php +++ b/src/Symfony/Component/Cache/Traits/AbstractTrait.php @@ -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) { diff --git a/src/Symfony/Component/Cache/Traits/ApcuTrait.php b/src/Symfony/Component/Cache/Traits/ApcuTrait.php index 4bbb48bc4e..02ca023b08 100644 --- a/src/Symfony/Component/Cache/Traits/ApcuTrait.php +++ b/src/Symfony/Component/Cache/Traits/ApcuTrait.php @@ -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); } } diff --git a/src/Symfony/Component/Cache/Traits/FilesystemTrait.php b/src/Symfony/Component/Cache/Traits/FilesystemTrait.php index 23974b3bc5..52dcf985dd 100644 --- a/src/Symfony/Component/Cache/Traits/FilesystemTrait.php +++ b/src/Symfony/Component/Cache/Traits/FilesystemTrait.php @@ -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; } } diff --git a/src/Symfony/Component/Cache/Traits/MemcachedTrait.php b/src/Symfony/Component/Cache/Traits/MemcachedTrait.php index c0d43c2e61..f312898355 100644 --- a/src/Symfony/Component/Cache/Traits/MemcachedTrait.php +++ b/src/Symfony/Component/Cache/Traits/MemcachedTrait.php @@ -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 @@ -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); } } diff --git a/src/Symfony/Component/Cache/Traits/PdoTrait.php b/src/Symfony/Component/Cache/Traits/PdoTrait.php index a88099ecb1..5bad534154 100644 --- a/src/Symfony/Component/Cache/Traits/PdoTrait.php +++ b/src/Symfony/Component/Cache/Traits/PdoTrait.php @@ -16,12 +16,15 @@ use Doctrine\DBAL\Driver\ServerInfoAwareConnection; use Doctrine\DBAL\DBALException; 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; @@ -36,7 +39,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])); @@ -65,6 +68,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); } @@ -181,7 +185,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]); } } @@ -252,18 +256,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; } @@ -328,7 +321,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()) { diff --git a/src/Symfony/Component/Cache/Traits/PhpFilesTrait.php b/src/Symfony/Component/Cache/Traits/PhpFilesTrait.php index 6d6f88d796..bb3c334c28 100644 --- a/src/Symfony/Component/Cache/Traits/PhpFilesTrait.php +++ b/src/Symfony/Component/Cache/Traits/PhpFilesTrait.php @@ -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; } diff --git a/src/Symfony/Component/Cache/Traits/RedisTrait.php b/src/Symfony/Component/Cache/Traits/RedisTrait.php index 0034c4ce34..7a6f978937 100644 --- a/src/Symfony/Component/Cache/Traits/RedisTrait.php +++ b/src/Symfony/Component/Cache/Traits/RedisTrait.php @@ -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 @@ -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 {