Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

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

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
[Cache] Add MarshallerInterface allowing to change the serializer, …
…providing a default one that automatically uses igbinary when available
  • Loading branch information
nicolas-grekas committed Jul 9, 2018
commit ae4319a971049f67a0aa5680d7e8242372e65b10
1 change: 1 addition & 0 deletions 1 .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

- |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
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;
Expand Down Expand Up @@ -1539,6 +1540,10 @@ private function registerMessengerConfiguration(array $config, ContainerBuilder

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']);
Expand Down
7 changes: 7 additions & 0 deletions 7 src/Symfony/Bundle/FrameworkBundle/Resources/config/cache.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand All @@ -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>
Expand All @@ -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>
Expand All @@ -118,6 +121,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>
Expand Down
2 changes: 1 addition & 1 deletion 2 src/Symfony/Component/Cache/Adapter/AbstractAdapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ function ($deferred, $namespace, &$expiredIds) use ($getId) {
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;
}

Expand Down
5 changes: 4 additions & 1 deletion 5 src/Symfony/Component/Cache/Adapter/FilesystemAdapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,18 @@

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;

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);
}
Expand Down
5 changes: 3 additions & 2 deletions 5 src/Symfony/Component/Cache/Adapter/MemcachedAdapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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);
}
}
5 changes: 3 additions & 2 deletions 5 src/Symfony/Component/Cache/Adapter/PdoAdapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

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;

Expand Down Expand Up @@ -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);
}
}
20 changes: 14 additions & 6 deletions 20 src/Symfony/Component/Cache/Adapter/PhpArrayAdapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
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;
Expand All @@ -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
Expand Down Expand Up @@ -88,6 +90,7 @@ public function get(string $key, callable $callback, float $beta = null)
$this->initialize();
}
if (!isset($this->keys[$key])) {
get_from_pool:
if ($this->pool instanceof CacheInterface) {
return $this->pool->get($key, $callback, $beta);
}
Expand All @@ -99,11 +102,16 @@ public function get(string $key, callable $callback, float $beta = null)
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;
Expand Down Expand Up @@ -278,7 +286,7 @@ private function generateItems(array $keys): \Generator
}
} 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);
}
Expand Down
4 changes: 2 additions & 2 deletions 4 src/Symfony/Component/Cache/Adapter/ProxyAdapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -69,15 +69,15 @@ function ($key, $innerItem) use ($defaultLifetime, $poolHash) {
);
$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
if (isset(($metadata = $item["\0*\0newMetadata"])[CacheItem::METADATA_TAGS])) {
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"]);
Expand Down
5 changes: 3 additions & 2 deletions 5 src/Symfony/Component/Cache/Adapter/RedisAdapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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);
}
}
2 changes: 2 additions & 0 deletions 2 src/Symfony/Component/Cache/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
-----
Expand Down
99 changes: 99 additions & 0 deletions 99 src/Symfony/Component/Cache/Marshaller/DefaultMarshaller.php
Original file line number Diff line number Diff line change
@@ -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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe this has been asked, but why do we have a DefaultMarshaller with some internal logic in all methods to differentiate between PHP and IgBinary ... instead of defining a PhpMarshaller and IgBinaryMarshaller (moreover given that we define a MarshallerInterface).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because there is only one implementation of MarshallerInterface: DefaultMarshaller, which auto-adaptatively uses serialize() or igbinary_serialize().

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But that looks to me like creating a DefaultMailer which makes if($use_smtp) { ... } elseif ($use_gmail) { ...} inside its methods. In other parts of the framework we define 1 interface and multiple different implementation classes. Here it looks like we have multiple implementations inside a single class. It looks confusing to me, sorry.

Copy link
Member Author

@nicolas-grekas nicolas-grekas Jun 28, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@javiereguiluz it's not the same. Here, by dealing with both serialization methods (and there are none others for PHP objects), we provide an important feature: auto-adaptative unserialization, providing safe forward and backward compatibility at the data level. This is not possible without having something that knows about both formats.

Copy link
Contributor

@teohhanhui teohhanhui Jun 28, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IgBinaryMarshaller could take PhpMarshaller as a fallback marshaller (which is optional), no?

Copy link
Contributor

@teohhanhui teohhanhui Jun 28, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the purpose of the MarshallerInterface then if there are no other reasonable implementations?

Copy link
Member Author

@nicolas-grekas nicolas-grekas Jun 28, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks to the interface, the serialized string itself can still be processed, e.g compressed and/or encrypted.
One could also build a serializer that would be dedicated to and optimized for very specific data structures (eg only numbers, etc.), etc.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm... But actually it's not true that igbinary is the only serialization format that might benefit from a fallback to PHP serialization. There are / were other contenders like msgpack (MessagePack) and Protobuf, both of which have PHP extensions.

Copy link
Member Author

@nicolas-grekas nicolas-grekas Jun 28, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, another reason to have the interface. Let's keep the topic: should we split the default marshaller in two. I think we would do a misfeature by doing so: things would be more complex to understand, more complex to wire, less capable, and less performant while in a critical code path. Would the individual envisioned marshallers empower our users in comparison to these downsides? Not at all in my opinion.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you're right, and those other serializer formats are not PHP-specific, so it wouldn't make sense for them to fallback to PHP serialization, actually. 😆

{
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 values, 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 values.');
} 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);
}
}
40 changes: 40 additions & 0 deletions 40 src/Symfony/Component/Cache/Marshaller/MarshallerInterface.php
Original file line number Diff line number Diff line change
@@ -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 carefully. 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);
}
5 changes: 4 additions & 1 deletion 5 src/Symfony/Component/Cache/Simple/FilesystemCache.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,18 @@

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;

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);
}
Expand Down
Loading
Morty Proxy This is a proxified and sanitized view of the page, visit original site.