From 866420e2eb1f5295760c10adafdc051adb87545a Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Thu, 7 Jun 2018 16:35:59 +0200 Subject: [PATCH] [Cache] serialize objects using native arrays when possible --- .php_cs.dist | 1 + .../Resources/config/cache.xml | 1 + .../Cache/Adapter/PhpArrayAdapter.php | 38 +++- .../Cache/Adapter/PhpFilesAdapter.php | 6 +- .../Cache/Marshaller/PhpMarshaller.php | 184 ++++++++++++++++++ .../Marshaller/PhpMarshaller/Configurator.php | 119 +++++++++++ .../Marshaller/PhpMarshaller/Reference.php | 30 +++ .../Marshaller/PhpMarshaller/Registry.php | 58 ++++++ .../Component/Cache/Simple/PhpArrayCache.php | 32 ++- .../Component/Cache/Simple/PhpFilesCache.php | 6 +- .../Tests/Adapter/PhpArrayAdapterTest.php | 30 ++- .../Fixtures/array-iterator.optimized.php | 28 +++ .../Marshaller/Fixtures/array-iterator.php | 30 +++ .../array-object-custom.optimized.php | 28 +++ .../Fixtures/array-object-custom.php | 30 +++ .../Fixtures/array-object.optimized.php | 36 ++++ .../Marshaller/Fixtures/array-object.php | 42 ++++ .../Cache/Tests/Marshaller/Fixtures/bool.php | 1 + .../Marshaller/Fixtures/clone.optimized.php | 20 ++ .../Cache/Tests/Marshaller/Fixtures/clone.php | 24 +++ .../Fixtures/datetime.optimized.php | 30 +++ .../Tests/Marshaller/Fixtures/datetime.php | 32 +++ .../Marshaller/Fixtures/private.optimized.php | 39 ++++ .../Tests/Marshaller/Fixtures/private.php | 43 ++++ .../Fixtures/serializable.optimized.php | 19 ++ .../Marshaller/Fixtures/serializable.php | 23 +++ .../Marshaller/Fixtures/simple-array.php | 7 + .../Fixtures/spl-object-storage.optimized.php | 27 +++ .../Fixtures/spl-object-storage.php | 31 +++ .../Marshaller/Fixtures/wakeup.optimized.php | 30 +++ .../Tests/Marshaller/Fixtures/wakeup.php | 34 ++++ .../Tests/Marshaller/PhpMarshallerTest.php | 169 ++++++++++++++++ .../Cache/Tests/Simple/PhpArrayCacheTest.php | 34 +++- .../Component/Cache/Traits/ApcuTrait.php | 5 +- .../Component/Cache/Traits/PhpArrayTrait.php | 64 ++++-- .../Component/Cache/Traits/PhpFilesTrait.php | 144 +++++++++++--- src/Symfony/Component/Cache/composer.json | 3 +- 37 files changed, 1390 insertions(+), 88 deletions(-) create mode 100644 src/Symfony/Component/Cache/Marshaller/PhpMarshaller.php create mode 100644 src/Symfony/Component/Cache/Marshaller/PhpMarshaller/Configurator.php create mode 100644 src/Symfony/Component/Cache/Marshaller/PhpMarshaller/Reference.php create mode 100644 src/Symfony/Component/Cache/Marshaller/PhpMarshaller/Registry.php create mode 100644 src/Symfony/Component/Cache/Tests/Marshaller/Fixtures/array-iterator.optimized.php create mode 100644 src/Symfony/Component/Cache/Tests/Marshaller/Fixtures/array-iterator.php create mode 100644 src/Symfony/Component/Cache/Tests/Marshaller/Fixtures/array-object-custom.optimized.php create mode 100644 src/Symfony/Component/Cache/Tests/Marshaller/Fixtures/array-object-custom.php create mode 100644 src/Symfony/Component/Cache/Tests/Marshaller/Fixtures/array-object.optimized.php create mode 100644 src/Symfony/Component/Cache/Tests/Marshaller/Fixtures/array-object.php create mode 100644 src/Symfony/Component/Cache/Tests/Marshaller/Fixtures/bool.php create mode 100644 src/Symfony/Component/Cache/Tests/Marshaller/Fixtures/clone.optimized.php create mode 100644 src/Symfony/Component/Cache/Tests/Marshaller/Fixtures/clone.php create mode 100644 src/Symfony/Component/Cache/Tests/Marshaller/Fixtures/datetime.optimized.php create mode 100644 src/Symfony/Component/Cache/Tests/Marshaller/Fixtures/datetime.php create mode 100644 src/Symfony/Component/Cache/Tests/Marshaller/Fixtures/private.optimized.php create mode 100644 src/Symfony/Component/Cache/Tests/Marshaller/Fixtures/private.php create mode 100644 src/Symfony/Component/Cache/Tests/Marshaller/Fixtures/serializable.optimized.php create mode 100644 src/Symfony/Component/Cache/Tests/Marshaller/Fixtures/serializable.php create mode 100644 src/Symfony/Component/Cache/Tests/Marshaller/Fixtures/simple-array.php create mode 100644 src/Symfony/Component/Cache/Tests/Marshaller/Fixtures/spl-object-storage.optimized.php create mode 100644 src/Symfony/Component/Cache/Tests/Marshaller/Fixtures/spl-object-storage.php create mode 100644 src/Symfony/Component/Cache/Tests/Marshaller/Fixtures/wakeup.optimized.php create mode 100644 src/Symfony/Component/Cache/Tests/Marshaller/Fixtures/wakeup.php create mode 100644 src/Symfony/Component/Cache/Tests/Marshaller/PhpMarshallerTest.php diff --git a/.php_cs.dist b/.php_cs.dist index e6def5bc4438..4720014f40b7 100644 --- a/.php_cs.dist +++ b/.php_cs.dist @@ -20,6 +20,7 @@ return PhpCsFixer\Config::create() ->append(array(__FILE__)) ->exclude(array( // directories containing files with content that is autogenerated by `var_export`, which breaks CS in output code + 'Symfony/Component/Cache/Tests/Marshaller/Fixtures', 'Symfony/Component/DependencyInjection/Tests/Fixtures', 'Symfony/Component/Routing/Tests/Fixtures/dumper', // fixture templates diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/cache.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/cache.xml index 4040709c788f..d31be8db3fc9 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/cache.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/cache.xml @@ -41,6 +41,7 @@ 0 %kernel.cache_dir%/pools + true diff --git a/src/Symfony/Component/Cache/Adapter/PhpArrayAdapter.php b/src/Symfony/Component/Cache/Adapter/PhpArrayAdapter.php index f72fb8a6f8be..dabc80b097bb 100644 --- a/src/Symfony/Component/Cache/Adapter/PhpArrayAdapter.php +++ b/src/Symfony/Component/Cache/Adapter/PhpArrayAdapter.php @@ -87,16 +87,21 @@ public function get(string $key, callable $callback, float $beta = null) if (null === $this->values) { $this->initialize(); } - if (null === $value = $this->values[$key] ?? null) { + if (!isset($this->keys[$key])) { if ($this->pool instanceof CacheInterface) { return $this->pool->get($key, $callback, $beta); } return $this->doGet($this->pool, $key, $callback, $beta ?? 1.0); } + $value = $this->values[$this->keys[$key]]; + if ('N;' === $value) { return null; } + if ($value instanceof \Closure) { + return $value(); + } if (\is_string($value) && isset($value[2]) && ':' === $value[1]) { return unserialize($value); } @@ -115,15 +120,22 @@ public function getItem($key) if (null === $this->values) { $this->initialize(); } - if (!isset($this->values[$key])) { + if (!isset($this->keys[$key])) { return $this->pool->getItem($key); } - $value = $this->values[$key]; + $value = $this->values[$this->keys[$key]]; $isHit = true; if ('N;' === $value) { $value = null; + } elseif ($value instanceof \Closure) { + try { + $value = $value(); + } catch (\Throwable $e) { + $value = null; + $isHit = false; + } } elseif (\is_string($value) && isset($value[2]) && ':' === $value[1]) { try { $value = unserialize($value); @@ -167,7 +179,7 @@ public function hasItem($key) $this->initialize(); } - return isset($this->values[$key]) || $this->pool->hasItem($key); + return isset($this->keys[$key]) || $this->pool->hasItem($key); } /** @@ -182,7 +194,7 @@ public function deleteItem($key) $this->initialize(); } - return !isset($this->values[$key]) && $this->pool->deleteItem($key); + return !isset($this->keys[$key]) && $this->pool->deleteItem($key); } /** @@ -198,7 +210,7 @@ public function deleteItems(array $keys) throw new InvalidArgumentException(sprintf('Cache key must be string, "%s" given.', is_object($key) ? get_class($key) : gettype($key))); } - if (isset($this->values[$key])) { + if (isset($this->keys[$key])) { $deleted = false; } else { $fallbackKeys[] = $key; @@ -224,7 +236,7 @@ public function save(CacheItemInterface $item) $this->initialize(); } - return !isset($this->values[$item->getKey()]) && $this->pool->save($item); + return !isset($this->keys[$item->getKey()]) && $this->pool->save($item); } /** @@ -236,7 +248,7 @@ public function saveDeferred(CacheItemInterface $item) $this->initialize(); } - return !isset($this->values[$item->getKey()]) && $this->pool->saveDeferred($item); + return !isset($this->keys[$item->getKey()]) && $this->pool->saveDeferred($item); } /** @@ -253,11 +265,17 @@ private function generateItems(array $keys): \Generator $fallbackKeys = array(); foreach ($keys as $key) { - if (isset($this->values[$key])) { - $value = $this->values[$key]; + if (isset($this->keys[$key])) { + $value = $this->values[$this->keys[$key]]; if ('N;' === $value) { yield $key => $f($key, null, true); + } elseif ($value instanceof \Closure) { + try { + yield $key => $f($key, $value(), true); + } catch (\Throwable $e) { + yield $key => $f($key, null, false); + } } elseif (\is_string($value) && isset($value[2]) && ':' === $value[1]) { try { yield $key => $f($key, unserialize($value), true); diff --git a/src/Symfony/Component/Cache/Adapter/PhpFilesAdapter.php b/src/Symfony/Component/Cache/Adapter/PhpFilesAdapter.php index 7c1850662d86..1f4e05e176f9 100644 --- a/src/Symfony/Component/Cache/Adapter/PhpFilesAdapter.php +++ b/src/Symfony/Component/Cache/Adapter/PhpFilesAdapter.php @@ -20,10 +20,14 @@ class PhpFilesAdapter extends AbstractAdapter implements PruneableInterface use PhpFilesTrait; /** + * @param $appendOnly Set to `true` to gain extra performance when the items stored in this pool never expire. + * Doing so is encouraged because it fits perfectly OPcache's memory model. + * * @throws CacheException if OPcache is not enabled */ - public function __construct(string $namespace = '', int $defaultLifetime = 0, string $directory = null) + public function __construct(string $namespace = '', int $defaultLifetime = 0, string $directory = null, bool $appendOnly = false) { + $this->appendOnly = $appendOnly; self::$startTime = self::$startTime ?? $_SERVER['REQUEST_TIME'] ?? time(); parent::__construct('', $defaultLifetime); $this->init($namespace, $directory); diff --git a/src/Symfony/Component/Cache/Marshaller/PhpMarshaller.php b/src/Symfony/Component/Cache/Marshaller/PhpMarshaller.php new file mode 100644 index 000000000000..e4dd9a2c66de --- /dev/null +++ b/src/Symfony/Component/Cache/Marshaller/PhpMarshaller.php @@ -0,0 +1,184 @@ + + * + * 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\Marshaller\PhpMarshaller\Configurator; +use Symfony\Component\Cache\Marshaller\PhpMarshaller\Reference; +use Symfony\Component\Cache\Marshaller\PhpMarshaller\Registry; + +/** + * @author Nicolas Grekas + * + * PhpMarshaller allows serializing PHP data structures using var_export() + * while preserving all the semantics associated to serialize(). + * + * By leveraging OPcache, the generated PHP code is faster than doing the same with unserialize(). + * + * @internal + */ +class PhpMarshaller +{ + public static function marshall($value, int &$objectsCount) + { + if (!\is_object($value) && !\is_array($value)) { + return $value; + } + $objectsPool = new \SplObjectStorage(); + $value = array($value); + $objectsCount = self::doMarshall($value, $objectsPool); + + $classes = array(); + $values = array(); + $wakeups = array(); + foreach ($objectsPool as $i => $v) { + list(, $classes[], $values[], $wakeup) = $objectsPool[$v]; + if ($wakeup) { + $wakeups[$wakeup] = $i; + } + } + ksort($wakeups); + $properties = array(); + foreach ($values as $i => $vars) { + foreach ($vars as $class => $values) { + foreach ($values as $name => $v) { + $properties[$class][$name][$i] = $v; + } + } + } + if (!$classes) { + return $value[0]; + } + + return new Configurator(new Registry($classes), $properties, $value[0], $wakeups); + } + + public static function optimize(string $exportedValue) + { + return preg_replace(sprintf("{%s::__set_state\(array\(\s++'0' => (\d+),\s++\)\)}", preg_quote(Reference::class)), Registry::class.'::$objects[$1]', $exportedValue); + } + + private static function doMarshall(array &$array, \SplObjectStorage $objectsPool): int + { + $objectsCount = 0; + + foreach ($array as &$value) { + if (\is_array($value) && $value) { + $objectsCount += self::doMarshall($value, $objectsPool); + } + if (!\is_object($value)) { + continue; + } + if (isset($objectsPool[$value])) { + ++$objectsCount; + $value = new Reference($objectsPool[$value][0]); + continue; + } + $class = \get_class($value); + $properties = array(); + $sleep = null; + $arrayValue = (array) $value; + $proto = (Registry::$reflectors[$class] ?? Registry::getClassReflector($class))->newInstanceWithoutConstructor(); + + if ($value instanceof \ArrayIterator || $value instanceof \ArrayObject) { + // ArrayIterator and ArrayObject need special care because their "flags" + // option changes the behavior of the (array) casting operator. + $reflector = $value instanceof \ArrayIterator ? 'ArrayIterator' : 'ArrayObject'; + $reflector = Registry::$reflectors[$reflector] ?? Registry::getClassReflector($reflector); + + $properties = array( + $arrayValue, + $reflector->getMethod('getFlags')->invoke($value), + $value instanceof \ArrayObject ? $reflector->getMethod('getIteratorClass')->invoke($value) : 'ArrayIterator', + ); + + $reflector = $reflector->getMethod('setFlags'); + $reflector->invoke($proto, \ArrayObject::STD_PROP_LIST); + + if ($properties[1] & \ArrayObject::STD_PROP_LIST) { + $reflector->invoke($value, 0); + $properties[0] = (array) $value; + } else { + $reflector->invoke($value, \ArrayObject::STD_PROP_LIST); + $arrayValue = (array) $value; + } + $reflector->invoke($value, $properties[1]); + + if (array(array(), 0, 'ArrayIterator') === $properties) { + $properties = array(); + } else { + if ('ArrayIterator' === $properties[2]) { + unset($properties[2]); + } + $properties = array($reflector->class => array("\0" => $properties)); + } + } elseif ($value instanceof \SplObjectStorage) { + foreach (clone $value as $v) { + $properties[] = $v; + $properties[] = $value[$v]; + } + $properties = array('SplObjectStorage' => array("\0" => $properties)); + } elseif ($value instanceof \Serializable) { + ++$objectsCount; + $objectsPool[$value] = array($id = \count($objectsPool), serialize($value), array(), 0); + $value = new Reference($id); + continue; + } + + if (\method_exists($class, '__sleep')) { + if (!\is_array($sleep = $value->__sleep())) { + trigger_error('serialize(): __sleep should return an array only containing the names of instance-variables to serialize', E_USER_NOTICE); + $value = null; + continue; + } + $sleep = array_flip($sleep); + } + + $proto = (array) $proto; + + foreach ($arrayValue as $name => $v) { + $k = (string) $name; + if ('' === $k || "\0" !== $k[0]) { + $c = $class; + } elseif ('*' === $k[1]) { + $c = $class; + $k = substr($k, 3); + } else { + $i = strpos($k, "\0", 2); + $c = substr($k, 1, $i - 1); + $k = substr($k, 1 + $i); + } + if (null === $sleep) { + $properties[$c][$k] = $v; + } elseif (isset($sleep[$k]) && $c === $class) { + $properties[$c][$k] = $v; + unset($sleep[$k]); + } + if (\array_key_exists($name, $proto) && $proto[$name] === $v) { + unset($properties[$c][$k]); + } + } + if ($sleep) { + foreach ($sleep as $k => $v) { + trigger_error(sprintf('serialize(): "%s" returned as member variable from __sleep() but does not exist', $k), E_USER_NOTICE); + } + } + + $objectsPool[$value] = array($id = \count($objectsPool)); + $objectsCount += 1 + self::doMarshall($properties, $objectsPool); + $objectsPool[$value] = array($id, $class, $properties, \method_exists($class, '__wakeup') ? $objectsCount : 0); + + $value = new Reference($id); + } + + return $objectsCount; + } +} diff --git a/src/Symfony/Component/Cache/Marshaller/PhpMarshaller/Configurator.php b/src/Symfony/Component/Cache/Marshaller/PhpMarshaller/Configurator.php new file mode 100644 index 000000000000..49658f42b650 --- /dev/null +++ b/src/Symfony/Component/Cache/Marshaller/PhpMarshaller/Configurator.php @@ -0,0 +1,119 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Marshaller\PhpMarshaller; + +/** + * @author Nicolas Grekas + * + * @internal + */ +class Configurator +{ + public static $configurators = array(); + + public function __construct(Registry $registry, array $properties, $value, array $wakeups) + { + $this->{0} = $registry; + $this->{1} = $properties; + $this->{2} = $value; + $this->{3} = $wakeups; + } + + public static function __set_state($state) + { + $objects = Registry::$objects; + Registry::$objects = \array_pop(Registry::$stack); + list(, $properties, $value, $wakeups) = $state; + + foreach ($properties as $class => $vars) { + (self::$configurators[$class] ?? self::getConfigurator($class))($vars, $objects); + } + foreach ($wakeups as $i) { + $objects[$i]->__wakeup(); + } + + return $value; + } + + public static function getConfigurator($class) + { + $classReflector = Registry::$reflectors[$class] ?? Registry::getClassReflector($class); + + if (!$classReflector->isInternal()) { + return self::$configurators[$class] = \Closure::bind(function ($properties, $objects) { + foreach ($properties as $name => $values) { + foreach ($values as $i => $v) { + $objects[$i]->$name = $v; + } + } + }, null, $class); + } + + switch ($class) { + case 'ArrayIterator': + case 'ArrayObject': + $constructor = $classReflector->getConstructor(); + + return self::$configurators[$class] = static function ($properties, $objects) use ($constructor) { + foreach ($properties as $name => $values) { + if ("\0" !== $name) { + foreach ($values as $i => $v) { + $objects[$i]->$name = $v; + } + } + } + foreach ($properties["\0"] as $i => $v) { + $constructor->invokeArgs($objects[$i], $v); + } + }; + + case 'SplObjectStorage': + return self::$configurators[$class] = static function ($properties, $objects) { + foreach ($properties as $name => $values) { + if ("\0" === $name) { + foreach ($values as $i => $v) { + for ($j = 0; $j < \count($v); ++$j) { + $objects[$i]->attach($v[$j], $v[++$j]); + } + } + continue; + } + foreach ($values as $i => $v) { + $objects[$i]->$name = $v; + } + } + }; + } + + $propertyReflectors = array(); + foreach ($classReflector->getProperties(\ReflectionProperty::IS_PROTECTED | \ReflectionProperty::IS_PRIVATE) as $propertyReflector) { + if (!$propertyReflector->isStatic()) { + $propertyReflector->setAccessible(true); + $propertyReflectors[$propertyReflector->name] = $propertyReflector; + } + } + + return self::$configurators[$class] = static function ($properties, $objects) use ($propertyReflectors) { + foreach ($properties as $name => $values) { + if (isset($propertyReflectors[$name])) { + foreach ($values as $i => $v) { + $propertyReflectors[$name]->setValue($objects[$i], $v); + } + } else { + foreach ($values as $i => $v) { + $objects[$i]->$name = $v; + } + } + } + }; + } +} diff --git a/src/Symfony/Component/Cache/Marshaller/PhpMarshaller/Reference.php b/src/Symfony/Component/Cache/Marshaller/PhpMarshaller/Reference.php new file mode 100644 index 000000000000..52c43af63618 --- /dev/null +++ b/src/Symfony/Component/Cache/Marshaller/PhpMarshaller/Reference.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Marshaller\PhpMarshaller; + +/** + * @author Nicolas Grekas + * + * @internal + */ +class Reference +{ + public function __construct(int $id) + { + $this->{0} = $id; + } + + public static function __set_state($state) + { + return Registry::$objects[$state[0]]; + } +} diff --git a/src/Symfony/Component/Cache/Marshaller/PhpMarshaller/Registry.php b/src/Symfony/Component/Cache/Marshaller/PhpMarshaller/Registry.php new file mode 100644 index 000000000000..1eb250c3909c --- /dev/null +++ b/src/Symfony/Component/Cache/Marshaller/PhpMarshaller/Registry.php @@ -0,0 +1,58 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Marshaller\PhpMarshaller; + +/** + * @author Nicolas Grekas + * + * @internal + */ +class Registry +{ + public static $stack = array(); + public static $objects = array(); + public static $reflectors = array(); + public static $prototypes = array(); + + public function __construct(array $classes) + { + foreach ($classes as $i => $class) { + $this->$i = $class; + } + } + + public static function __set_state($classes) + { + self::$stack[] = self::$objects; + self::$objects = $classes; + foreach (self::$objects as &$class) { + if (isset(self::$prototypes[$class])) { + $class = clone self::$prototypes[$class]; + } elseif (':' === ($class[1] ?? null)) { + $class = \unserialize($class); + } else { + $class = (self::$reflectors[$class] ?? self::getClassReflector($class))->newInstanceWithoutConstructor(); + } + } + } + + public static function getClassReflector($class) + { + $reflector = new \ReflectionClass($class); + + if (!$reflector->hasMethod('__clone')) { + self::$prototypes[$class] = $reflector->newInstanceWithoutConstructor(); + } + + return self::$reflectors[$class] = $reflector; + } +} diff --git a/src/Symfony/Component/Cache/Simple/PhpArrayCache.php b/src/Symfony/Component/Cache/Simple/PhpArrayCache.php index 5d401be767d7..bb3321b27cd0 100644 --- a/src/Symfony/Component/Cache/Simple/PhpArrayCache.php +++ b/src/Symfony/Component/Cache/Simple/PhpArrayCache.php @@ -66,15 +66,21 @@ public function get($key, $default = null) if (null === $this->values) { $this->initialize(); } - if (!isset($this->values[$key])) { + if (!isset($this->keys[$key])) { return $this->pool->get($key, $default); } - - $value = $this->values[$key]; + $value = $this->values[$this->keys[$key]]; if ('N;' === $value) { return null; } + if ($value instanceof \Closure) { + try { + return $value(); + } catch (\Throwable $e) { + return $default; + } + } if (\is_string($value) && isset($value[2]) && ':' === $value[1]) { try { return unserialize($value); @@ -120,7 +126,7 @@ public function has($key) $this->initialize(); } - return isset($this->values[$key]) || $this->pool->has($key); + return isset($this->keys[$key]) || $this->pool->has($key); } /** @@ -135,7 +141,7 @@ public function delete($key) $this->initialize(); } - return !isset($this->values[$key]) && $this->pool->delete($key); + return !isset($this->keys[$key]) && $this->pool->delete($key); } /** @@ -155,7 +161,7 @@ public function deleteMultiple($keys) throw new InvalidArgumentException(sprintf('Cache key must be string, "%s" given.', is_object($key) ? get_class($key) : gettype($key))); } - if (isset($this->values[$key])) { + if (isset($this->keys[$key])) { $deleted = false; } else { $fallbackKeys[] = $key; @@ -184,7 +190,7 @@ public function set($key, $value, $ttl = null) $this->initialize(); } - return !isset($this->values[$key]) && $this->pool->set($key, $value, $ttl); + return !isset($this->keys[$key]) && $this->pool->set($key, $value, $ttl); } /** @@ -204,7 +210,7 @@ public function setMultiple($values, $ttl = null) throw new InvalidArgumentException(sprintf('Cache key must be string, "%s" given.', is_object($key) ? get_class($key) : gettype($key))); } - if (isset($this->values[$key])) { + if (isset($this->keys[$key])) { $saved = false; } else { $fallbackValues[$key] = $value; @@ -223,11 +229,17 @@ private function generateItems(array $keys, $default) $fallbackKeys = array(); foreach ($keys as $key) { - if (isset($this->values[$key])) { - $value = $this->values[$key]; + if (isset($this->keys[$key])) { + $value = $this->values[$this->keys[$key]]; if ('N;' === $value) { yield $key => null; + } elseif ($value instanceof \Closure) { + try { + yield $key => $value(); + } catch (\Throwable $e) { + yield $key => $default; + } } elseif (\is_string($value) && isset($value[2]) && ':' === $value[1]) { try { yield $key => unserialize($value); diff --git a/src/Symfony/Component/Cache/Simple/PhpFilesCache.php b/src/Symfony/Component/Cache/Simple/PhpFilesCache.php index 3347038bb33a..19ac8b41525a 100644 --- a/src/Symfony/Component/Cache/Simple/PhpFilesCache.php +++ b/src/Symfony/Component/Cache/Simple/PhpFilesCache.php @@ -20,10 +20,14 @@ class PhpFilesCache extends AbstractCache implements PruneableInterface use PhpFilesTrait; /** + * @param $appendOnly Set to `true` to gain extra performance when the items stored in this pool never expire. + * Doing so is encouraged because it fits perfectly OPcache's memory model. + * * @throws CacheException if OPcache is not enabled */ - public function __construct(string $namespace = '', int $defaultLifetime = 0, string $directory = null) + public function __construct(string $namespace = '', int $defaultLifetime = 0, string $directory = null, bool $appendOnly = false) { + $this->appendOnly = $appendOnly; self::$startTime = self::$startTime ?? $_SERVER['REQUEST_TIME'] ?? time(); parent::__construct('', $defaultLifetime); $this->init($namespace, $directory); diff --git a/src/Symfony/Component/Cache/Tests/Adapter/PhpArrayAdapterTest.php b/src/Symfony/Component/Cache/Tests/Adapter/PhpArrayAdapterTest.php index 19c1285af40d..86085b1650b3 100644 --- a/src/Symfony/Component/Cache/Tests/Adapter/PhpArrayAdapterTest.php +++ b/src/Symfony/Component/Cache/Tests/Adapter/PhpArrayAdapterTest.php @@ -107,16 +107,32 @@ public function testStore() public function testStoredFile() { - $expected = array( + $data = array( 'integer' => 42, 'float' => 42.42, 'boolean' => true, 'array_simple' => array('foo', 'bar'), 'array_associative' => array('foo' => 'bar', 'foo2' => 'bar2'), ); + $expected = array( + array( + 'integer' => 0, + 'float' => 1, + 'boolean' => 2, + 'array_simple' => 3, + 'array_associative' => 4, + ), + array( + 0 => 42, + 1 => 42.42, + 2 => true, + 3 => array('foo', 'bar'), + 4 => array('foo' => 'bar', 'foo2' => 'bar2'), + ), + ); $adapter = $this->createCachePool(); - $adapter->warmUp($expected); + $adapter->warmUp($data); $values = eval(substr(file_get_contents(self::$file), 6)); @@ -126,12 +142,16 @@ public function testStoredFile() class PhpArrayAdapterWrapper extends PhpArrayAdapter { + protected $data = array(); + public function save(CacheItemInterface $item) { call_user_func(\Closure::bind(function () use ($item) { - $this->values[$item->getKey()] = $item->get(); - $this->warmUp($this->values); - $this->values = eval(substr(file_get_contents($this->file), 6)); + $key = $item->getKey(); + $this->keys[$key] = $id = \count($this->values); + $this->data[$key] = $this->values[$id] = $item->get(); + $this->warmUp($this->data); + list($this->keys, $this->values) = eval(substr(file_get_contents($this->file), 6)); }, $this, PhpArrayAdapter::class)); return true; diff --git a/src/Symfony/Component/Cache/Tests/Marshaller/Fixtures/array-iterator.optimized.php b/src/Symfony/Component/Cache/Tests/Marshaller/Fixtures/array-iterator.optimized.php new file mode 100644 index 000000000000..f8964fed9665 --- /dev/null +++ b/src/Symfony/Component/Cache/Tests/Marshaller/Fixtures/array-iterator.optimized.php @@ -0,0 +1,28 @@ + + Symfony\Component\Cache\Marshaller\PhpMarshaller\Registry::__set_state(array( + '0' => 'ArrayIterator', + )), + '1' => + array ( + 'ArrayIterator' => + array ( + '' . "\0" . '' => + array ( + 0 => + array ( + 0 => + array ( + 0 => 123, + ), + 1 => 1, + ), + ), + ), + ), + '2' => + Symfony\Component\Cache\Marshaller\PhpMarshaller\Registry::$objects[0], + '3' => + array ( + ), +)); diff --git a/src/Symfony/Component/Cache/Tests/Marshaller/Fixtures/array-iterator.php b/src/Symfony/Component/Cache/Tests/Marshaller/Fixtures/array-iterator.php new file mode 100644 index 000000000000..cc910b3fa296 --- /dev/null +++ b/src/Symfony/Component/Cache/Tests/Marshaller/Fixtures/array-iterator.php @@ -0,0 +1,30 @@ + + Symfony\Component\Cache\Marshaller\PhpMarshaller\Registry::__set_state(array( + '0' => 'ArrayIterator', + )), + '1' => + array ( + 'ArrayIterator' => + array ( + '' . "\0" . '' => + array ( + 0 => + array ( + 0 => + array ( + 0 => 123, + ), + 1 => 1, + ), + ), + ), + ), + '2' => + Symfony\Component\Cache\Marshaller\PhpMarshaller\Reference::__set_state(array( + '0' => 0, + )), + '3' => + array ( + ), +)); diff --git a/src/Symfony/Component/Cache/Tests/Marshaller/Fixtures/array-object-custom.optimized.php b/src/Symfony/Component/Cache/Tests/Marshaller/Fixtures/array-object-custom.optimized.php new file mode 100644 index 000000000000..06e9cd8cc1e2 --- /dev/null +++ b/src/Symfony/Component/Cache/Tests/Marshaller/Fixtures/array-object-custom.optimized.php @@ -0,0 +1,28 @@ + + Symfony\Component\Cache\Marshaller\PhpMarshaller\Registry::__set_state(array( + '0' => 'Symfony\\Component\\Cache\\Tests\\Marshaller\\MyArrayObject', + )), + '1' => + array ( + 'ArrayObject' => + array ( + '' . "\0" . '' => + array ( + 0 => + array ( + 0 => + array ( + 0 => 234, + ), + 1 => 1, + ), + ), + ), + ), + '2' => + Symfony\Component\Cache\Marshaller\PhpMarshaller\Registry::$objects[0], + '3' => + array ( + ), +)); diff --git a/src/Symfony/Component/Cache/Tests/Marshaller/Fixtures/array-object-custom.php b/src/Symfony/Component/Cache/Tests/Marshaller/Fixtures/array-object-custom.php new file mode 100644 index 000000000000..1474853f0006 --- /dev/null +++ b/src/Symfony/Component/Cache/Tests/Marshaller/Fixtures/array-object-custom.php @@ -0,0 +1,30 @@ + + Symfony\Component\Cache\Marshaller\PhpMarshaller\Registry::__set_state(array( + '0' => 'Symfony\\Component\\Cache\\Tests\\Marshaller\\MyArrayObject', + )), + '1' => + array ( + 'ArrayObject' => + array ( + '' . "\0" . '' => + array ( + 0 => + array ( + 0 => + array ( + 0 => 234, + ), + 1 => 1, + ), + ), + ), + ), + '2' => + Symfony\Component\Cache\Marshaller\PhpMarshaller\Reference::__set_state(array( + '0' => 0, + )), + '3' => + array ( + ), +)); diff --git a/src/Symfony/Component/Cache/Tests/Marshaller/Fixtures/array-object.optimized.php b/src/Symfony/Component/Cache/Tests/Marshaller/Fixtures/array-object.optimized.php new file mode 100644 index 000000000000..511f9f8a6bd0 --- /dev/null +++ b/src/Symfony/Component/Cache/Tests/Marshaller/Fixtures/array-object.optimized.php @@ -0,0 +1,36 @@ + + Symfony\Component\Cache\Marshaller\PhpMarshaller\Registry::__set_state(array( + '0' => 'ArrayObject', + '1' => 'ArrayObject', + )), + '1' => + array ( + 'ArrayObject' => + array ( + '' . "\0" . '' => + array ( + 0 => + array ( + 0 => + array ( + 0 => 1, + 1 => + Symfony\Component\Cache\Marshaller\PhpMarshaller\Registry::$objects[0], + ), + 1 => 0, + ), + ), + 'foo' => + array ( + 0 => + Symfony\Component\Cache\Marshaller\PhpMarshaller\Registry::$objects[1], + ), + ), + ), + '2' => + Symfony\Component\Cache\Marshaller\PhpMarshaller\Registry::$objects[0], + '3' => + array ( + ), +)); diff --git a/src/Symfony/Component/Cache/Tests/Marshaller/Fixtures/array-object.php b/src/Symfony/Component/Cache/Tests/Marshaller/Fixtures/array-object.php new file mode 100644 index 000000000000..30c516a0e6d2 --- /dev/null +++ b/src/Symfony/Component/Cache/Tests/Marshaller/Fixtures/array-object.php @@ -0,0 +1,42 @@ + + Symfony\Component\Cache\Marshaller\PhpMarshaller\Registry::__set_state(array( + '0' => 'ArrayObject', + '1' => 'ArrayObject', + )), + '1' => + array ( + 'ArrayObject' => + array ( + '' . "\0" . '' => + array ( + 0 => + array ( + 0 => + array ( + 0 => 1, + 1 => + Symfony\Component\Cache\Marshaller\PhpMarshaller\Reference::__set_state(array( + '0' => 0, + )), + ), + 1 => 0, + ), + ), + 'foo' => + array ( + 0 => + Symfony\Component\Cache\Marshaller\PhpMarshaller\Reference::__set_state(array( + '0' => 1, + )), + ), + ), + ), + '2' => + Symfony\Component\Cache\Marshaller\PhpMarshaller\Reference::__set_state(array( + '0' => 0, + )), + '3' => + array ( + ), +)); diff --git a/src/Symfony/Component/Cache/Tests/Marshaller/Fixtures/bool.php b/src/Symfony/Component/Cache/Tests/Marshaller/Fixtures/bool.php new file mode 100644 index 000000000000..7c0eaedf00b7 --- /dev/null +++ b/src/Symfony/Component/Cache/Tests/Marshaller/Fixtures/bool.php @@ -0,0 +1 @@ + + Symfony\Component\Cache\Marshaller\PhpMarshaller\Registry::__set_state(array( + '0' => 'Symfony\\Component\\Cache\\Tests\\Marshaller\\MyCloneable', + '1' => 'Symfony\\Component\\Cache\\Tests\\Marshaller\\MyNotCloneable', + )), + '1' => + array ( + ), + '2' => + array ( + 0 => + Symfony\Component\Cache\Marshaller\PhpMarshaller\Registry::$objects[0], + 1 => + Symfony\Component\Cache\Marshaller\PhpMarshaller\Registry::$objects[1], + ), + '3' => + array ( + ), +)); diff --git a/src/Symfony/Component/Cache/Tests/Marshaller/Fixtures/clone.php b/src/Symfony/Component/Cache/Tests/Marshaller/Fixtures/clone.php new file mode 100644 index 000000000000..b82bd43e56bf --- /dev/null +++ b/src/Symfony/Component/Cache/Tests/Marshaller/Fixtures/clone.php @@ -0,0 +1,24 @@ + + Symfony\Component\Cache\Marshaller\PhpMarshaller\Registry::__set_state(array( + '0' => 'Symfony\\Component\\Cache\\Tests\\Marshaller\\MyCloneable', + '1' => 'Symfony\\Component\\Cache\\Tests\\Marshaller\\MyNotCloneable', + )), + '1' => + array ( + ), + '2' => + array ( + 0 => + Symfony\Component\Cache\Marshaller\PhpMarshaller\Reference::__set_state(array( + '0' => 0, + )), + 1 => + Symfony\Component\Cache\Marshaller\PhpMarshaller\Reference::__set_state(array( + '0' => 1, + )), + ), + '3' => + array ( + ), +)); diff --git a/src/Symfony/Component/Cache/Tests/Marshaller/Fixtures/datetime.optimized.php b/src/Symfony/Component/Cache/Tests/Marshaller/Fixtures/datetime.optimized.php new file mode 100644 index 000000000000..0bbd6f0b75e3 --- /dev/null +++ b/src/Symfony/Component/Cache/Tests/Marshaller/Fixtures/datetime.optimized.php @@ -0,0 +1,30 @@ + + Symfony\Component\Cache\Marshaller\PhpMarshaller\Registry::__set_state(array( + '0' => 'DateTime', + )), + '1' => + array ( + 'DateTime' => + array ( + 'date' => + array ( + 0 => '1970-01-01 00:00:00.000000', + ), + 'timezone_type' => + array ( + 0 => 1, + ), + 'timezone' => + array ( + 0 => '+00:00', + ), + ), + ), + '2' => + Symfony\Component\Cache\Marshaller\PhpMarshaller\Registry::$objects[0], + '3' => + array ( + 1 => 0, + ), +)); diff --git a/src/Symfony/Component/Cache/Tests/Marshaller/Fixtures/datetime.php b/src/Symfony/Component/Cache/Tests/Marshaller/Fixtures/datetime.php new file mode 100644 index 000000000000..c89375bf0057 --- /dev/null +++ b/src/Symfony/Component/Cache/Tests/Marshaller/Fixtures/datetime.php @@ -0,0 +1,32 @@ + + Symfony\Component\Cache\Marshaller\PhpMarshaller\Registry::__set_state(array( + '0' => 'DateTime', + )), + '1' => + array ( + 'DateTime' => + array ( + 'date' => + array ( + 0 => '1970-01-01 00:00:00.000000', + ), + 'timezone_type' => + array ( + 0 => 1, + ), + 'timezone' => + array ( + 0 => '+00:00', + ), + ), + ), + '2' => + Symfony\Component\Cache\Marshaller\PhpMarshaller\Reference::__set_state(array( + '0' => 0, + )), + '3' => + array ( + 1 => 0, + ), +)); diff --git a/src/Symfony/Component/Cache/Tests/Marshaller/Fixtures/private.optimized.php b/src/Symfony/Component/Cache/Tests/Marshaller/Fixtures/private.optimized.php new file mode 100644 index 000000000000..c835a199afe6 --- /dev/null +++ b/src/Symfony/Component/Cache/Tests/Marshaller/Fixtures/private.optimized.php @@ -0,0 +1,39 @@ + + Symfony\Component\Cache\Marshaller\PhpMarshaller\Registry::__set_state(array( + '0' => 'Symfony\\Component\\Cache\\Tests\\Marshaller\\MyPrivateValue', + '1' => 'Symfony\\Component\\Cache\\Tests\\Marshaller\\MyPrivateChildValue', + )), + '1' => + array ( + 'Symfony\\Component\\Cache\\Tests\\Marshaller\\MyPrivateValue' => + array ( + 'prot' => + array ( + 0 => 123, + ), + 'priv' => + array ( + 0 => 234, + 1 => 234, + ), + ), + 'Symfony\\Component\\Cache\\Tests\\Marshaller\\MyPrivateChildValue' => + array ( + 'prot' => + array ( + 1 => 123, + ), + ), + ), + '2' => + array ( + 0 => + Symfony\Component\Cache\Marshaller\PhpMarshaller\Registry::$objects[0], + 1 => + Symfony\Component\Cache\Marshaller\PhpMarshaller\Registry::$objects[1], + ), + '3' => + array ( + ), +)); diff --git a/src/Symfony/Component/Cache/Tests/Marshaller/Fixtures/private.php b/src/Symfony/Component/Cache/Tests/Marshaller/Fixtures/private.php new file mode 100644 index 000000000000..ef9ce4f1e674 --- /dev/null +++ b/src/Symfony/Component/Cache/Tests/Marshaller/Fixtures/private.php @@ -0,0 +1,43 @@ + + Symfony\Component\Cache\Marshaller\PhpMarshaller\Registry::__set_state(array( + '0' => 'Symfony\\Component\\Cache\\Tests\\Marshaller\\MyPrivateValue', + '1' => 'Symfony\\Component\\Cache\\Tests\\Marshaller\\MyPrivateChildValue', + )), + '1' => + array ( + 'Symfony\\Component\\Cache\\Tests\\Marshaller\\MyPrivateValue' => + array ( + 'prot' => + array ( + 0 => 123, + ), + 'priv' => + array ( + 0 => 234, + 1 => 234, + ), + ), + 'Symfony\\Component\\Cache\\Tests\\Marshaller\\MyPrivateChildValue' => + array ( + 'prot' => + array ( + 1 => 123, + ), + ), + ), + '2' => + array ( + 0 => + Symfony\Component\Cache\Marshaller\PhpMarshaller\Reference::__set_state(array( + '0' => 0, + )), + 1 => + Symfony\Component\Cache\Marshaller\PhpMarshaller\Reference::__set_state(array( + '0' => 1, + )), + ), + '3' => + array ( + ), +)); diff --git a/src/Symfony/Component/Cache/Tests/Marshaller/Fixtures/serializable.optimized.php b/src/Symfony/Component/Cache/Tests/Marshaller/Fixtures/serializable.optimized.php new file mode 100644 index 000000000000..ce1c6ca5f751 --- /dev/null +++ b/src/Symfony/Component/Cache/Tests/Marshaller/Fixtures/serializable.optimized.php @@ -0,0 +1,19 @@ + + Symfony\Component\Cache\Marshaller\PhpMarshaller\Registry::__set_state(array( + '0' => 'C:55:"Symfony\\Component\\Cache\\Tests\\Marshaller\\MySerializable":3:{123}', + )), + '1' => + array ( + ), + '2' => + array ( + 0 => + Symfony\Component\Cache\Marshaller\PhpMarshaller\Registry::$objects[0], + 1 => + Symfony\Component\Cache\Marshaller\PhpMarshaller\Registry::$objects[0], + ), + '3' => + array ( + ), +)); diff --git a/src/Symfony/Component/Cache/Tests/Marshaller/Fixtures/serializable.php b/src/Symfony/Component/Cache/Tests/Marshaller/Fixtures/serializable.php new file mode 100644 index 000000000000..4030fde63403 --- /dev/null +++ b/src/Symfony/Component/Cache/Tests/Marshaller/Fixtures/serializable.php @@ -0,0 +1,23 @@ + + Symfony\Component\Cache\Marshaller\PhpMarshaller\Registry::__set_state(array( + '0' => 'C:55:"Symfony\\Component\\Cache\\Tests\\Marshaller\\MySerializable":3:{123}', + )), + '1' => + array ( + ), + '2' => + array ( + 0 => + Symfony\Component\Cache\Marshaller\PhpMarshaller\Reference::__set_state(array( + '0' => 0, + )), + 1 => + Symfony\Component\Cache\Marshaller\PhpMarshaller\Reference::__set_state(array( + '0' => 0, + )), + ), + '3' => + array ( + ), +)); diff --git a/src/Symfony/Component/Cache/Tests/Marshaller/Fixtures/simple-array.php b/src/Symfony/Component/Cache/Tests/Marshaller/Fixtures/simple-array.php new file mode 100644 index 000000000000..8dd73eb31d21 --- /dev/null +++ b/src/Symfony/Component/Cache/Tests/Marshaller/Fixtures/simple-array.php @@ -0,0 +1,7 @@ + 123, + 1 => + array ( + 0 => 'abc', + ), +); diff --git a/src/Symfony/Component/Cache/Tests/Marshaller/Fixtures/spl-object-storage.optimized.php b/src/Symfony/Component/Cache/Tests/Marshaller/Fixtures/spl-object-storage.optimized.php new file mode 100644 index 000000000000..e604b13c86c1 --- /dev/null +++ b/src/Symfony/Component/Cache/Tests/Marshaller/Fixtures/spl-object-storage.optimized.php @@ -0,0 +1,27 @@ + + Symfony\Component\Cache\Marshaller\PhpMarshaller\Registry::__set_state(array( + '0' => 'SplObjectStorage', + '1' => 'stdClass', + )), + '1' => + array ( + 'SplObjectStorage' => + array ( + '' . "\0" . '' => + array ( + 0 => + array ( + 0 => + Symfony\Component\Cache\Marshaller\PhpMarshaller\Registry::$objects[1], + 1 => 345, + ), + ), + ), + ), + '2' => + Symfony\Component\Cache\Marshaller\PhpMarshaller\Registry::$objects[0], + '3' => + array ( + ), +)); diff --git a/src/Symfony/Component/Cache/Tests/Marshaller/Fixtures/spl-object-storage.php b/src/Symfony/Component/Cache/Tests/Marshaller/Fixtures/spl-object-storage.php new file mode 100644 index 000000000000..1249f7c45d73 --- /dev/null +++ b/src/Symfony/Component/Cache/Tests/Marshaller/Fixtures/spl-object-storage.php @@ -0,0 +1,31 @@ + + Symfony\Component\Cache\Marshaller\PhpMarshaller\Registry::__set_state(array( + '0' => 'SplObjectStorage', + '1' => 'stdClass', + )), + '1' => + array ( + 'SplObjectStorage' => + array ( + '' . "\0" . '' => + array ( + 0 => + array ( + 0 => + Symfony\Component\Cache\Marshaller\PhpMarshaller\Reference::__set_state(array( + '0' => 1, + )), + 1 => 345, + ), + ), + ), + ), + '2' => + Symfony\Component\Cache\Marshaller\PhpMarshaller\Reference::__set_state(array( + '0' => 0, + )), + '3' => + array ( + ), +)); diff --git a/src/Symfony/Component/Cache/Tests/Marshaller/Fixtures/wakeup.optimized.php b/src/Symfony/Component/Cache/Tests/Marshaller/Fixtures/wakeup.optimized.php new file mode 100644 index 000000000000..b09b5036efdf --- /dev/null +++ b/src/Symfony/Component/Cache/Tests/Marshaller/Fixtures/wakeup.optimized.php @@ -0,0 +1,30 @@ + + Symfony\Component\Cache\Marshaller\PhpMarshaller\Registry::__set_state(array( + '0' => 'Symfony\\Component\\Cache\\Tests\\Marshaller\\MyWakeup', + '1' => 'Symfony\\Component\\Cache\\Tests\\Marshaller\\MyWakeup', + )), + '1' => + array ( + 'Symfony\\Component\\Cache\\Tests\\Marshaller\\MyWakeup' => + array ( + 'sub' => + array ( + 0 => + Symfony\Component\Cache\Marshaller\PhpMarshaller\Registry::$objects[1], + 1 => 123, + ), + 'baz' => + array ( + 1 => 123, + ), + ), + ), + '2' => + Symfony\Component\Cache\Marshaller\PhpMarshaller\Registry::$objects[0], + '3' => + array ( + 1 => 1, + 2 => 0, + ), +)); diff --git a/src/Symfony/Component/Cache/Tests/Marshaller/Fixtures/wakeup.php b/src/Symfony/Component/Cache/Tests/Marshaller/Fixtures/wakeup.php new file mode 100644 index 000000000000..baa3e87ea0c2 --- /dev/null +++ b/src/Symfony/Component/Cache/Tests/Marshaller/Fixtures/wakeup.php @@ -0,0 +1,34 @@ + + Symfony\Component\Cache\Marshaller\PhpMarshaller\Registry::__set_state(array( + '0' => 'Symfony\\Component\\Cache\\Tests\\Marshaller\\MyWakeup', + '1' => 'Symfony\\Component\\Cache\\Tests\\Marshaller\\MyWakeup', + )), + '1' => + array ( + 'Symfony\\Component\\Cache\\Tests\\Marshaller\\MyWakeup' => + array ( + 'sub' => + array ( + 0 => + Symfony\Component\Cache\Marshaller\PhpMarshaller\Reference::__set_state(array( + '0' => 1, + )), + 1 => 123, + ), + 'baz' => + array ( + 1 => 123, + ), + ), + ), + '2' => + Symfony\Component\Cache\Marshaller\PhpMarshaller\Reference::__set_state(array( + '0' => 0, + )), + '3' => + array ( + 1 => 1, + 2 => 0, + ), +)); diff --git a/src/Symfony/Component/Cache/Tests/Marshaller/PhpMarshallerTest.php b/src/Symfony/Component/Cache/Tests/Marshaller/PhpMarshallerTest.php new file mode 100644 index 000000000000..962b5fc1075d --- /dev/null +++ b/src/Symfony/Component/Cache/Tests/Marshaller/PhpMarshallerTest.php @@ -0,0 +1,169 @@ + + * + * 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\PhpMarshaller; +use Symfony\Component\VarDumper\Test\VarDumperTestTrait; + +class DoctrineProviderTest extends TestCase +{ + use VarDumperTestTrait; + + /** + * @dataProvider provideMarshall + */ + public function testMarshall(string $testName, $value, int $expectedObjectsCount) + { + $objectsCount = 0; + $marshalledValue = PhpMarshaller::marshall($value, $objectsCount); + + $this->assertSame($expectedObjectsCount, $objectsCount); + + $dump = 'assertStringEqualsFile($fixtureFile, $dump); + + if ($objectsCount) { + $marshalledValue = include $fixtureFile; + $this->assertDumpEquals($value, $marshalledValue); + + $dump = PhpMarshaller::optimize($dump); + $fixtureFile = __DIR__.'/Fixtures/'.$testName.'.optimized.php'; + $this->assertStringEqualsFile($fixtureFile, $dump); + + $marshalledValue = include $fixtureFile; + $this->assertDumpEquals($value, $marshalledValue); + } else { + $this->assertSame($value, $marshalledValue); + } + } + + public function provideMarshall() + { + yield ['bool', true, 0]; + yield ['simple-array', [123, ['abc']], 0]; + yield ['datetime', \DateTime::createFromFormat('U', 0), 1]; + + $value = new \ArrayObject(); + $value[0] = 1; + $value->foo = new \ArrayObject(); + $value[1] = $value; + + yield ['array-object', $value, 3]; + + yield array('array-iterator', new \ArrayIterator(array(123), 1), 1); + yield array('array-object-custom', new MyArrayObject(array(234)), 1); + + $value = new MySerializable(); + + yield ['serializable', array($value, $value), 2]; + + $value = new MyWakeup(); + $value->sub = new MyWakeup(); + $value->sub->sub = 123; + $value->sub->bis = 123; + $value->sub->baz = 123; + + yield ['wakeup', $value, 2]; + + yield ['clone', array(new MyCloneable(), new MyNotCloneable()), 2]; + + yield ['private', array(new MyPrivateValue(123, 234), new MyPrivateChildValue(123, 234)), 2]; + + $value = new \SplObjectStorage(); + $value[new \stdClass()] = 345; + + yield ['spl-object-storage', $value, 2]; + } +} + +class MySerializable implements \Serializable +{ + public function serialize() + { + return '123'; + } + + public function unserialize($data) + { + // no-op + } +} + +class MyWakeup +{ + public $sub; + public $bis; + public $baz; + public $def = 234; + + public function __sleep() + { + return array('sub', 'baz'); + } + + public function __wakeup() + { + if (123 === $this->sub) { + $this->bis = 123; + $this->baz = 123; + } + } +} + +class MyCloneable +{ + public function __clone() + { + throw new \Exception('__clone should never be called'); + } +} + +class MyNotCloneable +{ + private function __clone() + { + throw new \Exception('__clone should never be called'); + } +} + +class MyPrivateValue +{ + protected $prot; + private $priv; + + public function __construct($prot, $priv) + { + $this->prot = $prot; + $this->priv = $priv; + } +} + +class MyPrivateChildValue extends MyPrivateValue +{ +} + +class MyArrayObject extends \ArrayObject +{ + private $unused = 123; + + public function __construct(array $array) + { + parent::__construct($array, 1); + } + + public function setFlags($flags) + { + throw new \BadMethodCallException('Calling MyArrayObject::setFlags() is forbidden'); + } +} diff --git a/src/Symfony/Component/Cache/Tests/Simple/PhpArrayCacheTest.php b/src/Symfony/Component/Cache/Tests/Simple/PhpArrayCacheTest.php index 1bd0ca2778bb..437a33ad3e56 100644 --- a/src/Symfony/Component/Cache/Tests/Simple/PhpArrayCacheTest.php +++ b/src/Symfony/Component/Cache/Tests/Simple/PhpArrayCacheTest.php @@ -95,16 +95,32 @@ public function testStore() public function testStoredFile() { - $expected = array( + $data = array( 'integer' => 42, 'float' => 42.42, 'boolean' => true, 'array_simple' => array('foo', 'bar'), 'array_associative' => array('foo' => 'bar', 'foo2' => 'bar2'), ); + $expected = array( + array( + 'integer' => 0, + 'float' => 1, + 'boolean' => 2, + 'array_simple' => 3, + 'array_associative' => 4, + ), + array( + 0 => 42, + 1 => 42.42, + 2 => true, + 3 => array('foo', 'bar'), + 4 => array('foo' => 'bar', 'foo2' => 'bar2'), + ), + ); $cache = new PhpArrayCache(self::$file, new NullCache()); - $cache->warmUp($expected); + $cache->warmUp($data); $values = eval(substr(file_get_contents(self::$file), 6)); @@ -114,12 +130,14 @@ public function testStoredFile() class PhpArrayCacheWrapper extends PhpArrayCache { + protected $data = array(); + public function set($key, $value, $ttl = null) { call_user_func(\Closure::bind(function () use ($key, $value) { - $this->values[$key] = $value; - $this->warmUp($this->values); - $this->values = eval(substr(file_get_contents($this->file), 6)); + $this->data[$key] = $value; + $this->warmUp($this->data); + list($this->keys, $this->values) = eval(substr(file_get_contents($this->file), 6)); }, $this, PhpArrayCache::class)); return true; @@ -132,10 +150,10 @@ public function setMultiple($values, $ttl = null) } call_user_func(\Closure::bind(function () use ($values) { foreach ($values as $key => $value) { - $this->values[$key] = $value; + $this->data[$key] = $value; } - $this->warmUp($this->values); - $this->values = eval(substr(file_get_contents($this->file), 6)); + $this->warmUp($this->data); + list($this->keys, $this->values) = eval(substr(file_get_contents($this->file), 6)); }, $this, PhpArrayCache::class)); return true; diff --git a/src/Symfony/Component/Cache/Traits/ApcuTrait.php b/src/Symfony/Component/Cache/Traits/ApcuTrait.php index cec621b868f4..4bbb48bc4eee 100644 --- a/src/Symfony/Component/Cache/Traits/ApcuTrait.php +++ b/src/Symfony/Component/Cache/Traits/ApcuTrait.php @@ -52,11 +52,14 @@ private function init($namespace, $defaultLifetime, $version) protected function doFetch(array $ids) { try { + $values = array(); foreach (apcu_fetch($ids, $ok) ?: array() as $k => $v) { if (null !== $v || $ok) { - yield $k => $v; + $values[$k] = $v; } } + + return $values; } catch (\Error $e) { throw new \ErrorException($e->getMessage(), $e->getCode(), E_ERROR, $e->getFile(), $e->getLine()); } diff --git a/src/Symfony/Component/Cache/Traits/PhpArrayTrait.php b/src/Symfony/Component/Cache/Traits/PhpArrayTrait.php index 837d429854fc..249300684b6d 100644 --- a/src/Symfony/Component/Cache/Traits/PhpArrayTrait.php +++ b/src/Symfony/Component/Cache/Traits/PhpArrayTrait.php @@ -13,6 +13,7 @@ use Symfony\Component\Cache\CacheItem; use Symfony\Component\Cache\Exception\InvalidArgumentException; +use Symfony\Component\Cache\Marshaller\PhpMarshaller; /** * @author Titouan Galopin @@ -25,6 +26,7 @@ trait PhpArrayTrait use ProxyTrait; private $file; + private $keys; private $values; /** @@ -54,55 +56,66 @@ public function warmUp(array $values) } } + $dumpedValues = ''; + $dumpedMap = array(); $dump = <<<'EOF' $value) { CacheItem::validateKey(\is_int($key) ? (string) $key : $key); + $objectsCount = 0; - if (null === $value || \is_object($value)) { - try { - $value = serialize($value); - } catch (\Exception $e) { - throw new InvalidArgumentException(sprintf('Cache key "%s" has non-serializable %s value.', $key, get_class($value)), 0, $e); - } - } elseif (\is_array($value)) { + if (null === $value) { + $value = 'N;'; + } elseif (\is_object($value) || \is_array($value)) { try { + $e = null; $serialized = serialize($value); - $unserialized = unserialize($serialized); } catch (\Exception $e) { - throw new InvalidArgumentException(sprintf('Cache key "%s" has non-serializable array value.', $key), 0, $e); } - // Store arrays serialized if they contain any objects or references - if ($unserialized !== $value || (false !== strpos($serialized, ';R:') && preg_match('/;R:[1-9]/', $serialized))) { - $value = $serialized; + if (null !== $e || false === $serialized) { + throw new InvalidArgumentException(sprintf('Cache key "%s" has non-serializable %s value.', $key, \is_object($value) ? get_class($value) : 'array'), 0, $e); } + // Keep value serialized if it contains any internal references + $value = false !== strpos($serialized, ';R:') ? $serialized : PhpMarshaller::marshall($value, $objectsCount); } elseif (\is_string($value)) { - // Serialize strings if they could be confused with serialized objects or arrays + // Wrap strings if they could be confused with serialized objects or arrays if ('N;' === $value || (isset($value[2]) && ':' === $value[1])) { - $value = serialize($value); + ++$objectsCount; } } elseif (!\is_scalar($value)) { throw new InvalidArgumentException(sprintf('Cache key "%s" has non-serializable %s value.', $key, gettype($value))); } - $dump .= var_export($key, true).' => '.var_export($value, true).",\n"; + $value = var_export($value, true); + if ($objectsCount) { + $value = PhpMarshaller::optimize($value); + $value = "static function () {\nreturn {$value};\n}"; + } + $hash = hash('md5', $value); + + if (null === $id = $dumpedMap[$hash] ?? null) { + $id = $dumpedMap[$hash] = \count($dumpedMap); + $dumpedValues .= "{$id} => {$value},\n"; + } + + $dump .= var_export($key, true)." => {$id},\n"; } - $dump .= "\n);\n"; + $dump .= "\n), array(\n\n{$dumpedValues}\n));\n"; $tmpFile = uniqid($this->file, true); file_put_contents($tmpFile, $dump); @chmod($tmpFile, 0666 & ~umask()); - unset($serialized, $unserialized, $value, $dump); + unset($serialized, $value, $dump); @rename($tmpFile, $this->file); @@ -114,7 +127,7 @@ public function warmUp(array $values) */ public function clear() { - $this->values = array(); + $this->keys = $this->values = array(); $cleared = @unlink($this->file) || !file_exists($this->file); @@ -126,6 +139,17 @@ public function clear() */ private function initialize() { - $this->values = file_exists($this->file) ? (include $this->file ?: array()) : array(); + if (!file_exists($this->file)) { + $this->keys = $this->values = array(); + + return; + } + $values = (include $this->file) ?: array(array(), array()); + + if (2 !== \count($values) || !isset($values[0], $values[1])) { + $this->keys = $this->values = array(); + } else { + list($this->keys, $this->values) = $values; + } } } diff --git a/src/Symfony/Component/Cache/Traits/PhpFilesTrait.php b/src/Symfony/Component/Cache/Traits/PhpFilesTrait.php index 2c0ff3aef157..6d6f88d796b8 100644 --- a/src/Symfony/Component/Cache/Traits/PhpFilesTrait.php +++ b/src/Symfony/Component/Cache/Traits/PhpFilesTrait.php @@ -13,6 +13,7 @@ use Symfony\Component\Cache\Exception\CacheException; use Symfony\Component\Cache\Exception\InvalidArgumentException; +use Symfony\Component\Cache\Marshaller\PhpMarshaller; /** * @author Piotr Stankowski @@ -23,9 +24,15 @@ */ trait PhpFilesTrait { - use FilesystemCommonTrait; + use FilesystemCommonTrait { + doClear as private doCommonClear; + doDelete as private doCommonDelete; + } private $includeHandler; + private $appendOnly; + private $values = array(); + private $files = array(); private static $startTime; @@ -65,35 +72,58 @@ public function prune() */ protected function doFetch(array $ids) { + if ($this->appendOnly) { + $now = 0; + $missingIds = array(); + } else { + $now = time(); + $missingIds = $ids; + $ids = array(); + } $values = array(); - $now = time(); + + begin: + foreach ($ids as $id) { + if (null === $value = $this->values[$id] ?? null) { + $missingIds[] = $id; + } elseif ('N;' === $value) { + $values[$id] = null; + } elseif ($value instanceof \Closure) { + $values[$id] = $value(); + } elseif (\is_string($value) && isset($value[2]) && ':' === $value[1]) { + $values[$id] = parent::unserialize($value); + } else { + $values[$id] = $value; + } + if (!$this->appendOnly) { + unset($this->values[$id]); + } + } + + if (!$missingIds) { + return $values; + } set_error_handler($this->includeHandler); try { - foreach ($ids as $id) { + foreach ($missingIds as $k => $id) { try { - $file = $this->getFile($id); - list($expiresAt, $values[$id]) = include $file; + $file = $this->files[$id] ?? $this->files[$id] = $this->getFile($id); + list($expiresAt, $this->values[$id]) = include $file; if ($now >= $expiresAt) { - unset($values[$id]); + unset($this->values[$id], $missingIds[$k]); } } catch (\Exception $e) { - continue; + unset($missingIds[$k]); } } } finally { restore_error_handler(); } - foreach ($values as $id => $value) { - if ('N;' === $value) { - $values[$id] = null; - } elseif (\is_string($value) && isset($value[2]) && ':' === $value[1]) { - $values[$id] = parent::unserialize($value); - } - } - - return $values; + $ids = $missingIds; + $missingIds = array(); + goto begin; } /** @@ -101,7 +131,25 @@ protected function doFetch(array $ids) */ protected function doHave($id) { - return (bool) $this->doFetch(array($id)); + if ($this->appendOnly && $this->values[$id]) { + return true; + } + + set_error_handler($this->includeHandler); + try { + $file = $this->files[$id] ?? $this->files[$id] = $this->getFile($id); + list($expiresAt, $value) = include $file; + } finally { + restore_error_handler(); + } + if ($this->appendOnly) { + $now = 0; + $this->values[$id] = $value; + } else { + $now = time(); + } + + return $now < $expiresAt; } /** @@ -110,35 +158,47 @@ protected function doHave($id) protected function doSave(array $values, $lifetime) { $ok = true; - $data = array($lifetime ? time() + $lifetime : PHP_INT_MAX, ''); + $expiry = $lifetime ? time() + $lifetime : 'PHP_INT_MAX'; $allowCompile = self::isSupported(); foreach ($values as $key => $value) { - if (null === $value || \is_object($value)) { - $value = serialize($value); - } elseif (\is_array($value)) { - $serialized = serialize($value); - $unserialized = parent::unserialize($serialized); - // Store arrays serialized if they contain any objects or references - if ($unserialized !== $value || (false !== strpos($serialized, ';R:') && preg_match('/;R:[1-9]/', $serialized))) { - $value = $serialized; + unset($this->values[$key]); + $objectsCount = 0; + if (null === $value) { + $value = 'N;'; + } elseif (\is_object($value) || \is_array($value)) { + try { + $e = null; + $serialized = serialize($value); + } catch (\Exception $e) { + } + if (null !== $e || false === $serialized) { + throw new InvalidArgumentException(sprintf('Cache key "%s" has non-serializable %s value.', $key, \is_object($value) ? get_class($value) : 'array'), 0, $e); } + // Keep value serialized if it contains any internal references + $value = false !== strpos($serialized, ';R:') ? $serialized : PhpMarshaller::marshall($value, $objectsCount); } elseif (\is_string($value)) { - // Serialize strings if they could be confused with serialized objects or arrays + // Wrap strings if they could be confused with serialized objects or arrays if ('N;' === $value || (isset($value[2]) && ':' === $value[1])) { - $value = serialize($value); + ++$objectsCount; } } elseif (!\is_scalar($value)) { throw new InvalidArgumentException(sprintf('Cache key "%s" has non-serializable %s value.', $key, gettype($value))); } - $data[1] = $value; - $file = $this->getFile($key, true); + $value = var_export($value, true); + if ($objectsCount) { + $value = PhpMarshaller::optimize($value); + $value = "static function () {\n\nreturn {$value};\n\n}"; + } + + $file = $this->files[$key] = $this->getFile($key, true); // Since OPcache only compiles files older than the script execution start, set the file's mtime in the past - $ok = $this->write($file, 'write($file, "values = array(); + + return $this->doCommonClear($namespace); + } + + /** + * {@inheritdoc} + */ + protected function doDelete(array $ids) + { + foreach ($ids as $id) { + unset($this->values[$id]); + } + + return $this->doCommonDelete($ids); + } + protected function doUnlink($file) { if (self::isSupported()) { diff --git a/src/Symfony/Component/Cache/composer.json b/src/Symfony/Component/Cache/composer.json index 869a9bbd5ab0..e76644186efa 100644 --- a/src/Symfony/Component/Cache/composer.json +++ b/src/Symfony/Component/Cache/composer.json @@ -29,7 +29,8 @@ "cache/integration-tests": "dev-master", "doctrine/cache": "~1.6", "doctrine/dbal": "~2.4", - "predis/predis": "~1.0" + "predis/predis": "~1.0", + "symfony/var-dumper": "^4.1.1" }, "conflict": { "symfony/var-dumper": "<3.4"