[FrameworkBundle] Wire PhpArrayAdapter with a new cache warmer for annotations

This commit is contained in:
Titouan Galopin 2016-06-28 14:49:00 +02:00 committed by Fabien Potencier
parent ec3a4ede27
commit f950a2bcde
13 changed files with 427 additions and 15 deletions

View File

@ -0,0 +1,105 @@
<?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\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 <galopintitouan@gmail.com>
*/
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);
}
}
}

View File

@ -594,7 +594,7 @@ class Configuration implements ConfigurationInterface
->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()

View File

@ -169,6 +169,14 @@ class FrameworkExtension extends Extension
$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',
@ -906,8 +914,22 @@ class FrameworkExtension extends Extension
$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));
}
@ -916,11 +938,13 @@ class FrameworkExtension extends Extension
->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)
;
@ -1130,10 +1154,8 @@ class FrameworkExtension extends Extension
}
$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',
));
}

View File

@ -19,6 +19,22 @@
<argument /><!-- Cache-Directory -->
</service>
<service id="annotations.cache_warmer" class="Symfony\Bundle\FrameworkBundle\CacheWarmer\AnnotationsCacheWarmer" public="false">
<argument type="service" id="annotations.reader" />
<argument>%kernel.cache_dir%/annotations.php</argument>
<argument type="service" id="cache.annotations" />
</service>
<service id="annotations.cache" class="Symfony\Component\Cache\DoctrineProvider" public="false">
<argument type="service">
<service class="Symfony\Component\Cache\Adapter\PhpArrayAdapter">
<factory class="Symfony\Component\Cache\Adapter\PhpArrayAdapter" method="create" />
<argument>%kernel.cache_dir%/annotations.php</argument>
<argument type="service" id="cache.annotations" />
</service>
</argument>
</service>
<service id="annotation_reader" alias="annotations.reader" />
</services>
</container>

View File

@ -22,6 +22,10 @@
<tag name="cache.pool" />
</service>
<service id="cache.annotations" parent="cache.system" public="false">
<tag name="cache.pool" />
</service>
<service id="cache.adapter.system" class="Symfony\Component\Cache\Adapter\AdapterInterface" abstract="true">
<factory class="Symfony\Component\Cache\Adapter\AbstractAdapter" method="createSystemCache" />
<tag name="cache.pool" clearer="cache.default_clearer" />

View File

@ -214,7 +214,7 @@ class ConfigurationTest extends \PHPUnit_Framework_TestCase
'cache' => 'validator.mapping.cache.symfony',
),
'annotations' => array(
'cache' => 'file',
'cache' => 'php_array',
'file_cache_dir' => '%kernel.cache_dir%/annotations',
'debug' => true,
),

View File

@ -18,13 +18,13 @@
"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",
"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",

View File

@ -56,7 +56,7 @@ class ArrayAdapter implements AdapterInterface, LoggerAwareInterface
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 @@ class ArrayAdapter implements AdapterInterface, LoggerAwareInterface
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 @@ class ArrayAdapter implements AdapterInterface, LoggerAwareInterface
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 {

View File

@ -27,4 +27,30 @@ class ArrayAdapterTest extends AdapterTestCase
{
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']);
}
}

View File

@ -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 @@ class AddClassesToCachePass implements CompilerPassInterface
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;
}
}

View File

@ -21,6 +21,7 @@ use Symfony\Component\DependencyInjection\Extension\Extension as BaseExtension;
abstract class Extension extends BaseExtension
{
private $classes = array();
private $annotatedClasses = array();
/**
* Gets the classes to cache.
@ -32,13 +33,33 @@ abstract class Extension extends BaseExtension
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);
}
}

View File

@ -329,13 +329,21 @@ abstract class Kernel implements KernelInterface, TerminableInterface
}
/**
* Used internally.
* @internal
*/
public function setClassCache(array $classes)
{
file_put_contents($this->getCacheDir().'/classes.map', sprintf('<?php return %s;', var_export($classes, true)));
}
/**
* @internal
*/
public function setAnnotatedClassCache(array $annotatedClasses)
{
file_put_contents($this->getCacheDir().'/annotations.map', sprintf('<?php return %s;', var_export($annotatedClasses, true)));
}
/**
* {@inheritdoc}
*/

View File

@ -0,0 +1,97 @@
<?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\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]);
}
}