From d42bd602d855a6d211fe1cb7249791506c0dabef Mon Sep 17 00:00:00 2001 From: Titouan Galopin Date: Tue, 28 Jun 2016 14:49:00 +0200 Subject: [PATCH 1/2] [HttpKernel] Allow usage of patterns in classes and annotations to cache --- .../FrameworkExtension.php | 8 ++ .../Bundle/FrameworkBundle/composer.json | 2 +- .../AddClassesToCachePass.php | 105 +++++++++++++++++- .../DependencyInjection/Extension.php | 23 +++- src/Symfony/Component/HttpKernel/Kernel.php | 10 +- .../AddClassesToCachePassTest.php | 97 ++++++++++++++++ 6 files changed, 241 insertions(+), 4 deletions(-) create mode 100644 src/Symfony/Component/HttpKernel/Tests/DependencyInjection/AddClassesToCachePassTest.php diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index 17041fde19c81..154526c8168c2 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -169,6 +169,14 @@ public function load(array $configs, ContainerBuilder $container) $definition->replaceArgument(1, null); } + $this->addAnnotatedClassesToCompile(array( + '**Bundle\\Controller\\', + '**Bundle\\Entity\\', + + // Added explicitly so that we don't rely on the class map being dumped to make it work + 'Symfony\\Bundle\\FrameworkBundle\\Controller\\Controller', + )); + $this->addClassesToCompile(array( 'Symfony\\Component\\Config\\ConfigCache', 'Symfony\\Component\\Config\\FileLocator', diff --git a/src/Symfony/Bundle/FrameworkBundle/composer.json b/src/Symfony/Bundle/FrameworkBundle/composer.json index 910bbc51d6bdb..2ec9ee97195ab 100644 --- a/src/Symfony/Bundle/FrameworkBundle/composer.json +++ b/src/Symfony/Bundle/FrameworkBundle/composer.json @@ -24,7 +24,7 @@ "symfony/config": "~2.8|~3.0", "symfony/event-dispatcher": "~2.8|~3.0", "symfony/http-foundation": "~3.1", - "symfony/http-kernel": "~3.1.2|~3.2", + "symfony/http-kernel": "~3.2", "symfony/polyfill-mbstring": "~1.0", "symfony/filesystem": "~2.8|~3.0", "symfony/finder": "~2.8|~3.0", diff --git a/src/Symfony/Component/HttpKernel/DependencyInjection/AddClassesToCachePass.php b/src/Symfony/Component/HttpKernel/DependencyInjection/AddClassesToCachePass.php index 09af6bd25d938..0cc8d99864391 100644 --- a/src/Symfony/Component/HttpKernel/DependencyInjection/AddClassesToCachePass.php +++ b/src/Symfony/Component/HttpKernel/DependencyInjection/AddClassesToCachePass.php @@ -11,6 +11,8 @@ namespace Symfony\Component\HttpKernel\DependencyInjection; +use Composer\Autoload\ClassLoader; +use Symfony\Component\Debug\DebugClassLoader; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\HttpKernel\Kernel; @@ -35,12 +37,113 @@ public function __construct(Kernel $kernel) public function process(ContainerBuilder $container) { $classes = array(); + $annotatedClasses = array(); foreach ($container->getExtensions() as $extension) { if ($extension instanceof Extension) { $classes = array_merge($classes, $extension->getClassesToCompile()); + $annotatedClasses = array_merge($annotatedClasses, $extension->getAnnotatedClassesToCompile()); } } - $this->kernel->setClassCache(array_unique($container->getParameterBag()->resolveValue($classes))); + $classes = $container->getParameterBag()->resolveValue($classes); + $annotatedClasses = $container->getParameterBag()->resolveValue($annotatedClasses); + $existingClasses = $this->getClassesInComposerClassMaps(); + + $this->kernel->setClassCache($this->expandClasses($classes, $existingClasses)); + $this->kernel->setAnnotatedClassCache($this->expandClasses($annotatedClasses, $existingClasses)); + } + + /** + * Expands the given class patterns using a list of existing classes. + * + * @param array $patterns The class patterns to expand + * @param array $classes The existing classes to match against the patterns + * + * @return array A list of classes derivated from the patterns + */ + private function expandClasses(array $patterns, array $classes) + { + $expanded = array(); + + // Explicit classes declared in the patterns are returned directly + foreach ($patterns as $key => $pattern) { + if (substr($pattern, -1) !== '\\' && false === strpos($pattern, '*')) { + unset($patterns[$key]); + $expanded[] = ltrim($pattern, '\\'); + } + } + + // Match patterns with the classes list + $regexps = $this->patternsToRegexps($patterns); + + foreach ($classes as $class) { + $class = ltrim($class, '\\'); + + if ($this->matchAnyRegexps($class, $regexps)) { + $expanded[] = $class; + } + } + + return array_unique($expanded); + } + + private function getClassesInComposerClassMaps() + { + $classes = array(); + + foreach (spl_autoload_functions() as $function) { + if (!is_array($function)) { + continue; + } + + if ($function[0] instanceof DebugClassLoader) { + $function = $function[0]->getClassLoader(); + } + + if (is_array($function) && $function[0] instanceof ClassLoader) { + $classes += $function[0]->getClassMap(); + } + } + + return array_keys($classes); + } + + private function patternsToRegexps($patterns) + { + $regexps = array(); + + foreach ($patterns as $pattern) { + // Escape user input + $regex = preg_quote(ltrim($pattern, '\\')); + + // Wildcards * and ** + $regex = strtr($regex, array('\\*\\*' => '.*?', '\\*' => '[^\\\\]*?')); + + // If this class does not end by a slash, anchor the end + if (substr($regex, -1) !== '\\') { + $regex .= '$'; + } + + $regexps[] = '{^\\\\'.$regex.'}'; + } + + return $regexps; + } + + private function matchAnyRegexps($class, $regexps) + { + $blacklisted = false !== strpos($class, 'Test'); + + foreach ($regexps as $regex) { + if ($blacklisted && false === strpos($regex, 'Test')) { + continue; + } + + if (preg_match($regex, '\\'.$class)) { + return true; + } + } + + return false; } } diff --git a/src/Symfony/Component/HttpKernel/DependencyInjection/Extension.php b/src/Symfony/Component/HttpKernel/DependencyInjection/Extension.php index 2ca0f132840d6..a71bed04d330e 100644 --- a/src/Symfony/Component/HttpKernel/DependencyInjection/Extension.php +++ b/src/Symfony/Component/HttpKernel/DependencyInjection/Extension.php @@ -21,6 +21,7 @@ abstract class Extension extends BaseExtension { private $classes = array(); + private $annotatedClasses = array(); /** * Gets the classes to cache. @@ -32,13 +33,33 @@ public function getClassesToCompile() return $this->classes; } + /** + * Gets the annotated classes to cache. + * + * @return array An array of classes + */ + public function getAnnotatedClassesToCompile() + { + return $this->annotatedClasses; + } + /** * Adds classes to the class cache. * - * @param array $classes An array of classes + * @param array $classes An array of class patterns */ public function addClassesToCompile(array $classes) { $this->classes = array_merge($this->classes, $classes); } + + /** + * Adds annotated classes to the class cache. + * + * @param array $annotatedClasses An array of class patterns + */ + public function addAnnotatedClassesToCompile(array $annotatedClasses) + { + $this->annotatedClasses = array_merge($this->annotatedClasses, $annotatedClasses); + } } diff --git a/src/Symfony/Component/HttpKernel/Kernel.php b/src/Symfony/Component/HttpKernel/Kernel.php index 993a9a3d095cd..57a43674da1b3 100644 --- a/src/Symfony/Component/HttpKernel/Kernel.php +++ b/src/Symfony/Component/HttpKernel/Kernel.php @@ -329,13 +329,21 @@ public function loadClassCache($name = 'classes', $extension = '.php') } /** - * Used internally. + * @internal */ public function setClassCache(array $classes) { file_put_contents($this->getCacheDir().'/classes.map', sprintf('getCacheDir().'/annotations.map', sprintf(' + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpKernel\Tests\DependencyInjection; + +use Symfony\Component\HttpKernel\DependencyInjection\AddClassesToCachePass; + +class AddClassesToCachePassTest extends \PHPUnit_Framework_TestCase +{ + public function testExpandClasses() + { + $r = new \ReflectionClass(AddClassesToCachePass::class); + $pass = $r->newInstanceWithoutConstructor(); + $r = new \ReflectionMethod(AddClassesToCachePass::class, 'expandClasses'); + $expand = $r->getClosure($pass); + + $this->assertSame('Foo', $expand(array('Foo'), array())[0]); + $this->assertSame('Foo', $expand(array('\\Foo'), array())[0]); + $this->assertSame('Foo', $expand(array('Foo'), array('\\Foo'))[0]); + $this->assertSame('Foo', $expand(array('Foo'), array('Foo'))[0]); + $this->assertSame('Foo', $expand(array('\\Foo'), array('\\Foo\\Bar'))[0]); + $this->assertSame('Foo', $expand(array('Foo'), array('\\Foo\\Bar'))[0]); + $this->assertSame('Foo', $expand(array('\\Foo'), array('\\Foo\\Bar\\Acme'))[0]); + + $this->assertSame('Foo\\Bar', $expand(array('Foo\\'), array('\\Foo\\Bar'))[0]); + $this->assertSame('Foo\\Bar\\Acme', $expand(array('Foo\\'), array('\\Foo\\Bar\\Acme'))[0]); + $this->assertEmpty($expand(array('Foo\\'), array('\\Foo'))); + + $this->assertSame('Acme\\Foo\\Bar', $expand(array('**\\Foo\\'), array('\\Acme\\Foo\\Bar'))[0]); + $this->assertEmpty($expand(array('**\\Foo\\'), array('\\Foo\\Bar'))); + $this->assertEmpty($expand(array('**\\Foo\\'), array('\\Acme\\Foo'))); + $this->assertEmpty($expand(array('**\\Foo\\'), array('\\Foo'))); + + $this->assertSame('Acme\\Foo', $expand(array('**\\Foo'), array('\\Acme\\Foo'))[0]); + $this->assertEmpty($expand(array('**\\Foo'), array('\\Acme\\Foo\\AcmeBundle'))); + $this->assertEmpty($expand(array('**\\Foo'), array('\\Acme\\FooBar\\AcmeBundle'))); + + $this->assertSame('Foo\\Acme\\Bar', $expand(array('Foo\\*\\Bar'), array('\\Foo\\Acme\\Bar'))[0]); + $this->assertEmpty($expand(array('Foo\\*\\Bar'), array('\\Foo\\Acme\\Bundle\\Bar'))); + + $this->assertSame('Foo\\Acme\\Bar', $expand(array('Foo\\**\\Bar'), array('\\Foo\\Acme\\Bar'))[0]); + $this->assertSame('Foo\\Acme\\Bundle\\Bar', $expand(array('Foo\\**\\Bar'), array('\\Foo\\Acme\\Bundle\\Bar'))[0]); + + $this->assertSame('Acme\\Bar', $expand(array('*\\Bar'), array('\\Acme\\Bar'))[0]); + $this->assertEmpty($expand(array('*\\Bar'), array('\\Bar'))); + $this->assertEmpty($expand(array('*\\Bar'), array('\\Foo\\Acme\\Bar'))); + + $this->assertSame('Foo\\Acme\\Bar', $expand(array('**\\Bar'), array('\\Foo\\Acme\\Bar'))[0]); + $this->assertSame('Foo\\Acme\\Bundle\\Bar', $expand(array('**\\Bar'), array('\\Foo\\Acme\\Bundle\\Bar'))[0]); + $this->assertEmpty($expand(array('**\\Bar'), array('\\Bar'))); + + $this->assertSame('Foo\\Bar', $expand(array('Foo\\*'), array('\\Foo\\Bar'))[0]); + $this->assertEmpty($expand(array('Foo\\*'), array('\\Foo\\Acme\\Bar'))); + + $this->assertSame('Foo\\Bar', $expand(array('Foo\\**'), array('\\Foo\\Bar'))[0]); + $this->assertSame('Foo\\Acme\\Bar', $expand(array('Foo\\**'), array('\\Foo\\Acme\\Bar'))[0]); + + $this->assertSame(array('Foo\\Bar'), $expand(array('Foo\\*'), array('Foo\\Bar', 'Foo\\BarTest'))); + $this->assertSame(array('Foo\\Bar', 'Foo\\BarTest'), $expand(array('Foo\\*', 'Foo\\*Test'), array('Foo\\Bar', 'Foo\\BarTest'))); + + $this->assertSame( + 'Acme\\FooBundle\\Controller\\DefaultController', + $expand(array('**Bundle\\Controller\\'), array('\\Acme\\FooBundle\\Controller\\DefaultController'))[0] + ); + + $this->assertSame( + 'FooBundle\\Controller\\DefaultController', + $expand(array('**Bundle\\Controller\\'), array('\\FooBundle\\Controller\\DefaultController'))[0] + ); + + $this->assertSame( + 'Acme\\FooBundle\\Controller\\Bar\\DefaultController', + $expand(array('**Bundle\\Controller\\'), array('\\Acme\\FooBundle\\Controller\\Bar\\DefaultController'))[0] + ); + + $this->assertSame( + 'Bundle\\Controller\\Bar\\DefaultController', + $expand(array('**Bundle\\Controller\\'), array('\\Bundle\\Controller\\Bar\\DefaultController'))[0] + ); + + $this->assertSame( + 'Acme\\Bundle\\Controller\\Bar\\DefaultController', + $expand(array('**Bundle\\Controller\\'), array('\\Acme\\Bundle\\Controller\\Bar\\DefaultController'))[0] + ); + + $this->assertSame('Foo\\Bar', $expand(array('Foo\\Bar'), array())[0]); + $this->assertSame('Foo\\Acme\\Bar', $expand(array('Foo\\**'), array('\\Foo\\Acme\\Bar'))[0]); + } +} From 86a9938c0bfe1cd2b77661c0490f1a264e337b9a Mon Sep 17 00:00:00 2001 From: Titouan Galopin Date: Wed, 29 Jun 2016 18:22:27 +0200 Subject: [PATCH 2/2] [FrameworkBundle] Wire PhpArrayAdapter with a new cache warmer for annotations --- .../CacheWarmer/AnnotationsCacheWarmer.php | 105 ++++++++++++++++++ .../DependencyInjection/Configuration.php | 2 +- .../FrameworkExtension.php | 26 ++++- .../Resources/config/annotations.xml | 16 +++ .../Resources/config/cache.xml | 4 + .../DependencyInjection/ConfigurationTest.php | 2 +- .../Bundle/FrameworkBundle/composer.json | 2 +- .../Component/Cache/Adapter/ArrayAdapter.php | 14 ++- .../Cache/Tests/Adapter/ArrayAdapterTest.php | 26 +++++ 9 files changed, 186 insertions(+), 11 deletions(-) create mode 100644 src/Symfony/Bundle/FrameworkBundle/CacheWarmer/AnnotationsCacheWarmer.php diff --git a/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/AnnotationsCacheWarmer.php b/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/AnnotationsCacheWarmer.php new file mode 100644 index 0000000000000..6d044676eb012 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/AnnotationsCacheWarmer.php @@ -0,0 +1,105 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\CacheWarmer; + +use Doctrine\Common\Annotations\CachedReader; +use Doctrine\Common\Annotations\Reader; +use Psr\Cache\CacheItemPoolInterface; +use Symfony\Component\Cache\Adapter\AdapterInterface; +use Symfony\Component\Cache\Adapter\ArrayAdapter; +use Symfony\Component\Cache\Adapter\PhpArrayAdapter; +use Symfony\Component\Cache\Adapter\ProxyAdapter; +use Symfony\Component\Cache\DoctrineProvider; +use Symfony\Component\HttpKernel\CacheWarmer\CacheWarmerInterface; + +/** + * Warms up annotation caches for classes found in composer's autoload class map + * and declared in DI bundle extensions using the addAnnotatedClassesToCache method. + * + * @author Titouan Galopin + */ +class AnnotationsCacheWarmer implements CacheWarmerInterface +{ + private $annotationReader; + private $phpArrayFile; + private $fallbackPool; + + /** + * @param Reader $annotationReader + * @param string $phpArrayFile The PHP file where annotations are cached. + * @param CacheItemPoolInterface $fallbackPool The pool where runtime-discovered annotations are cached. + */ + public function __construct(Reader $annotationReader, $phpArrayFile, CacheItemPoolInterface $fallbackPool) + { + $this->annotationReader = $annotationReader; + $this->phpArrayFile = $phpArrayFile; + if (!$fallbackPool instanceof AdapterInterface) { + $fallbackPool = new ProxyAdapter($fallbackPool); + } + $this->fallbackPool = $fallbackPool; + } + + /** + * {@inheritdoc} + */ + public function warmUp($cacheDir) + { + $adapter = new PhpArrayAdapter($this->phpArrayFile, $this->fallbackPool); + $annotatedClassPatterns = $cacheDir.'/annotations.map'; + + if (!is_file($annotatedClassPatterns)) { + $adapter->warmUp(array()); + + return; + } + + $annotatedClasses = include $annotatedClassPatterns; + + $arrayPool = new ArrayAdapter(0, false); + $reader = new CachedReader($this->annotationReader, new DoctrineProvider($arrayPool)); + + foreach ($annotatedClasses as $class) { + $this->readAllComponents($reader, $class); + } + + $values = $arrayPool->getValues(); + $adapter->warmUp($values); + + foreach ($values as $k => $v) { + $item = $this->fallbackPool->getItem($k); + $this->fallbackPool->saveDeferred($item->set($v)); + } + $this->fallbackPool->commit(); + } + + /** + * {@inheritdoc} + */ + public function isOptional() + { + return true; + } + + private function readAllComponents(Reader $reader, $class) + { + $reflectionClass = new \ReflectionClass($class); + $reader->getClassAnnotations($reflectionClass); + + foreach ($reflectionClass->getMethods() as $reflectionMethod) { + $reader->getMethodAnnotations($reflectionMethod); + } + + foreach ($reflectionClass->getProperties() as $reflectionProperty) { + $reader->getPropertyAnnotations($reflectionProperty); + } + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php index a8ac7051086dd..c73c9e26e83cf 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php @@ -594,7 +594,7 @@ private function addAnnotationsSection(ArrayNodeDefinition $rootNode) ->info('annotation configuration') ->addDefaultsIfNotSet() ->children() - ->scalarNode('cache')->defaultValue('file')->end() + ->scalarNode('cache')->defaultValue('php_array')->end() ->scalarNode('file_cache_dir')->defaultValue('%kernel.cache_dir%/annotations')->end() ->booleanNode('debug')->defaultValue($this->debug)->end() ->end() diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index 154526c8168c2..d994ae99fc14d 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -914,8 +914,22 @@ private function registerAnnotationsConfiguration(array $config, ContainerBuilde $loader->load('annotations.xml'); if ('none' !== $config['cache']) { - if ('file' === $config['cache']) { + $cacheService = $config['cache']; + + if ('php_array' === $config['cache']) { + $cacheService = 'annotations.cache'; + + // Enable warmer only if PHP array is used for cache + $definition = $container->findDefinition('annotations.cache_warmer'); + $definition->addTag('kernel.cache_warmer'); + + $this->addClassesToCompile(array( + 'Symfony\Component\Cache\Adapter\PhpArrayAdapter', + 'Symfony\Component\Cache\DoctrineProvider', + )); + } elseif ('file' === $config['cache']) { $cacheDir = $container->getParameterBag()->resolveValue($config['file_cache_dir']); + if (!is_dir($cacheDir) && false === @mkdir($cacheDir, 0777, true) && !is_dir($cacheDir)) { throw new \RuntimeException(sprintf('Could not create cache directory "%s".', $cacheDir)); } @@ -924,11 +938,13 @@ private function registerAnnotationsConfiguration(array $config, ContainerBuilde ->getDefinition('annotations.filesystem_cache') ->replaceArgument(0, $cacheDir) ; + + $cacheService = 'annotations.filesystem_cache'; } $container ->getDefinition('annotations.cached_reader') - ->replaceArgument(1, new Reference('file' !== $config['cache'] ? $config['cache'] : 'annotations.filesystem_cache')) + ->replaceArgument(1, new Reference($cacheService)) ->replaceArgument(2, $config['debug']) ->addAutowiringType(Reader::class) ; @@ -1138,10 +1154,8 @@ private function registerCacheConfiguration(array $config, ContainerBuilder $con } $this->addClassesToCompile(array( - 'Psr\Cache\CacheItemInterface', - 'Psr\Cache\CacheItemPoolInterface', - 'Symfony\Component\Cache\Adapter\AdapterInterface', - 'Symfony\Component\Cache\Adapter\AbstractAdapter', + 'Symfony\Component\Cache\Adapter\ApcuAdapter', + 'Symfony\Component\Cache\Adapter\FilesystemAdapter', 'Symfony\Component\Cache\CacheItem', )); } diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/annotations.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/annotations.xml index 7ccf0da2dcb01..a2a0fb4065329 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/annotations.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/annotations.xml @@ -19,6 +19,22 @@ + + + %kernel.cache_dir%/annotations.php + + + + + + + + %kernel.cache_dir%/annotations.php + + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/cache.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/cache.xml index c438a1a66ff14..59a948be502f4 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/cache.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/cache.xml @@ -22,6 +22,10 @@ + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php index 94b6e315b8c20..bcea36768284b 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php @@ -214,7 +214,7 @@ protected static function getBundleDefaultConfig() 'cache' => 'validator.mapping.cache.symfony', ), 'annotations' => array( - 'cache' => 'file', + 'cache' => 'php_array', 'file_cache_dir' => '%kernel.cache_dir%/annotations', 'debug' => true, ), diff --git a/src/Symfony/Bundle/FrameworkBundle/composer.json b/src/Symfony/Bundle/FrameworkBundle/composer.json index 2ec9ee97195ab..91a4f10a732eb 100644 --- a/src/Symfony/Bundle/FrameworkBundle/composer.json +++ b/src/Symfony/Bundle/FrameworkBundle/composer.json @@ -18,7 +18,7 @@ "require": { "php": ">=5.5.9", "symfony/asset": "~2.8|~3.0", - "symfony/cache": "~3.1", + "symfony/cache": "~3.2", "symfony/class-loader": "~3.2", "symfony/dependency-injection": "~3.2", "symfony/config": "~2.8|~3.0", diff --git a/src/Symfony/Component/Cache/Adapter/ArrayAdapter.php b/src/Symfony/Component/Cache/Adapter/ArrayAdapter.php index b9c1686b9b44f..f51b48289bd7a 100644 --- a/src/Symfony/Component/Cache/Adapter/ArrayAdapter.php +++ b/src/Symfony/Component/Cache/Adapter/ArrayAdapter.php @@ -56,7 +56,7 @@ function ($key, $value, $isHit) use ($defaultLifetime) { public function getItem($key) { if (!$isHit = $this->hasItem($key)) { - $value = null; + $this->values[$key] = $value = null; } elseif ($this->storeSerialized) { $value = unserialize($this->values[$key]); } else { @@ -79,6 +79,16 @@ public function getItems(array $keys = array()) return $this->generateItems($keys, time()); } + /** + * Returns all cached values, with cache miss as null. + * + * @return array + */ + public function getValues() + { + return $this->values; + } + /** * {@inheritdoc} */ @@ -183,7 +193,7 @@ private function generateItems(array $keys, $now) foreach ($keys as $key) { if (!$isHit = isset($this->expiries[$key]) && ($this->expiries[$key] >= $now || !$this->deleteItem($key))) { - $value = null; + $this->values[$key] = $value = null; } elseif ($this->storeSerialized) { $value = unserialize($this->values[$key]); } else { diff --git a/src/Symfony/Component/Cache/Tests/Adapter/ArrayAdapterTest.php b/src/Symfony/Component/Cache/Tests/Adapter/ArrayAdapterTest.php index 8059e8c961c46..725d79015082e 100644 --- a/src/Symfony/Component/Cache/Tests/Adapter/ArrayAdapterTest.php +++ b/src/Symfony/Component/Cache/Tests/Adapter/ArrayAdapterTest.php @@ -27,4 +27,30 @@ public function createCachePool($defaultLifetime = 0) { return new ArrayAdapter($defaultLifetime); } + + public function testGetValuesHitAndMiss() + { + /** @var ArrayAdapter $cache */ + $cache = $this->createCachePool(); + + // Hit + $item = $cache->getItem('foo'); + $item->set('4711'); + $cache->save($item); + + $fooItem = $cache->getItem('foo'); + $this->assertTrue($fooItem->isHit()); + $this->assertEquals('4711', $fooItem->get()); + + // Miss (should be present as NULL in $values) + $cache->getItem('bar'); + + $values = $cache->getValues(); + + $this->assertCount(2, $values); + $this->assertArrayHasKey('foo', $values); + $this->assertSame(serialize('4711'), $values['foo']); + $this->assertArrayHasKey('bar', $values); + $this->assertNull($values['bar']); + } }