Skip to content

Commit f950a2b

Browse files
tgalopinfabpot
authored andcommitted
[FrameworkBundle] Wire PhpArrayAdapter with a new cache warmer for annotations
1 parent ec3a4ed commit f950a2b

File tree

13 files changed

+427
-15
lines changed

13 files changed

+427
-15
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Bundle\FrameworkBundle\CacheWarmer;
13+
14+
use Doctrine\Common\Annotations\CachedReader;
15+
use Doctrine\Common\Annotations\Reader;
16+
use Psr\Cache\CacheItemPoolInterface;
17+
use Symfony\Component\Cache\Adapter\AdapterInterface;
18+
use Symfony\Component\Cache\Adapter\ArrayAdapter;
19+
use Symfony\Component\Cache\Adapter\PhpArrayAdapter;
20+
use Symfony\Component\Cache\Adapter\ProxyAdapter;
21+
use Symfony\Component\Cache\DoctrineProvider;
22+
use Symfony\Component\HttpKernel\CacheWarmer\CacheWarmerInterface;
23+
24+
/**
25+
* Warms up annotation caches for classes found in composer's autoload class map
26+
* and declared in DI bundle extensions using the addAnnotatedClassesToCache method.
27+
*
28+
* @author Titouan Galopin <galopintitouan@gmail.com>
29+
*/
30+
class AnnotationsCacheWarmer implements CacheWarmerInterface
31+
{
32+
private $annotationReader;
33+
private $phpArrayFile;
34+
private $fallbackPool;
35+
36+
/**
37+
* @param Reader $annotationReader
38+
* @param string $phpArrayFile The PHP file where annotations are cached.
39+
* @param CacheItemPoolInterface $fallbackPool The pool where runtime-discovered annotations are cached.
40+
*/
41+
public function __construct(Reader $annotationReader, $phpArrayFile, CacheItemPoolInterface $fallbackPool)
42+
{
43+
$this->annotationReader = $annotationReader;
44+
$this->phpArrayFile = $phpArrayFile;
45+
if (!$fallbackPool instanceof AdapterInterface) {
46+
$fallbackPool = new ProxyAdapter($fallbackPool);
47+
}
48+
$this->fallbackPool = $fallbackPool;
49+
}
50+
51+
/**
52+
* {@inheritdoc}
53+
*/
54+
public function warmUp($cacheDir)
55+
{
56+
$adapter = new PhpArrayAdapter($this->phpArrayFile, $this->fallbackPool);
57+
$annotatedClassPatterns = $cacheDir.'/annotations.map';
58+
59+
if (!is_file($annotatedClassPatterns)) {
60+
$adapter->warmUp(array());
61+
62+
return;
63+
}
64+
65+
$annotatedClasses = include $annotatedClassPatterns;
66+
67+
$arrayPool = new ArrayAdapter(0, false);
68+
$reader = new CachedReader($this->annotationReader, new DoctrineProvider($arrayPool));
69+
70+
foreach ($annotatedClasses as $class) {
71+
$this->readAllComponents($reader, $class);
72+
}
73+
74+
$values = $arrayPool->getValues();
75+
$adapter->warmUp($values);
76+
77+
foreach ($values as $k => $v) {
78+
$item = $this->fallbackPool->getItem($k);
79+
$this->fallbackPool->saveDeferred($item->set($v));
80+
}
81+
$this->fallbackPool->commit();
82+
}
83+
84+
/**
85+
* {@inheritdoc}
86+
*/
87+
public function isOptional()
88+
{
89+
return true;
90+
}
91+
92+
private function readAllComponents(Reader $reader, $class)
93+
{
94+
$reflectionClass = new \ReflectionClass($class);
95+
$reader->getClassAnnotations($reflectionClass);
96+
97+
foreach ($reflectionClass->getMethods() as $reflectionMethod) {
98+
$reader->getMethodAnnotations($reflectionMethod);
99+
}
100+
101+
foreach ($reflectionClass->getProperties() as $reflectionProperty) {
102+
$reader->getPropertyAnnotations($reflectionProperty);
103+
}
104+
}
105+
}

src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -594,7 +594,7 @@ private function addAnnotationsSection(ArrayNodeDefinition $rootNode)
594594
->info('annotation configuration')
595595
->addDefaultsIfNotSet()
596596
->children()
597-
->scalarNode('cache')->defaultValue('file')->end()
597+
->scalarNode('cache')->defaultValue('php_array')->end()
598598
->scalarNode('file_cache_dir')->defaultValue('%kernel.cache_dir%/annotations')->end()
599599
->booleanNode('debug')->defaultValue($this->debug)->end()
600600
->end()

src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php

+28-6
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,14 @@ public function load(array $configs, ContainerBuilder $container)
169169
$definition->replaceArgument(1, null);
170170
}
171171

172+
$this->addAnnotatedClassesToCompile(array(
173+
'**Bundle\\Controller\\',
174+
'**Bundle\\Entity\\',
175+
176+
// Added explicitly so that we don't rely on the class map being dumped to make it work
177+
'Symfony\\Bundle\\FrameworkBundle\\Controller\\Controller',
178+
));
179+
172180
$this->addClassesToCompile(array(
173181
'Symfony\\Component\\Config\\ConfigCache',
174182
'Symfony\\Component\\Config\\FileLocator',
@@ -906,8 +914,22 @@ private function registerAnnotationsConfiguration(array $config, ContainerBuilde
906914
$loader->load('annotations.xml');
907915

908916
if ('none' !== $config['cache']) {
909-
if ('file' === $config['cache']) {
917+
$cacheService = $config['cache'];
918+
919+
if ('php_array' === $config['cache']) {
920+
$cacheService = 'annotations.cache';
921+
922+
// Enable warmer only if PHP array is used for cache
923+
$definition = $container->findDefinition('annotations.cache_warmer');
924+
$definition->addTag('kernel.cache_warmer');
925+
926+
$this->addClassesToCompile(array(
927+
'Symfony\Component\Cache\Adapter\PhpArrayAdapter',
928+
'Symfony\Component\Cache\DoctrineProvider',
929+
));
930+
} elseif ('file' === $config['cache']) {
910931
$cacheDir = $container->getParameterBag()->resolveValue($config['file_cache_dir']);
932+
911933
if (!is_dir($cacheDir) && false === @mkdir($cacheDir, 0777, true) && !is_dir($cacheDir)) {
912934
throw new \RuntimeException(sprintf('Could not create cache directory "%s".', $cacheDir));
913935
}
@@ -916,11 +938,13 @@ private function registerAnnotationsConfiguration(array $config, ContainerBuilde
916938
->getDefinition('annotations.filesystem_cache')
917939
->replaceArgument(0, $cacheDir)
918940
;
941+
942+
$cacheService = 'annotations.filesystem_cache';
919943
}
920944

921945
$container
922946
->getDefinition('annotations.cached_reader')
923-
->replaceArgument(1, new Reference('file' !== $config['cache'] ? $config['cache'] : 'annotations.filesystem_cache'))
947+
->replaceArgument(1, new Reference($cacheService))
924948
->replaceArgument(2, $config['debug'])
925949
->addAutowiringType(Reader::class)
926950
;
@@ -1130,10 +1154,8 @@ private function registerCacheConfiguration(array $config, ContainerBuilder $con
11301154
}
11311155

11321156
$this->addClassesToCompile(array(
1133-
'Psr\Cache\CacheItemInterface',
1134-
'Psr\Cache\CacheItemPoolInterface',
1135-
'Symfony\Component\Cache\Adapter\AdapterInterface',
1136-
'Symfony\Component\Cache\Adapter\AbstractAdapter',
1157+
'Symfony\Component\Cache\Adapter\ApcuAdapter',
1158+
'Symfony\Component\Cache\Adapter\FilesystemAdapter',
11371159
'Symfony\Component\Cache\CacheItem',
11381160
));
11391161
}

src/Symfony/Bundle/FrameworkBundle/Resources/config/annotations.xml

+16
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,22 @@
1919
<argument /><!-- Cache-Directory -->
2020
</service>
2121

22+
<service id="annotations.cache_warmer" class="Symfony\Bundle\FrameworkBundle\CacheWarmer\AnnotationsCacheWarmer" public="false">
23+
<argument type="service" id="annotations.reader" />
24+
<argument>%kernel.cache_dir%/annotations.php</argument>
25+
<argument type="service" id="cache.annotations" />
26+
</service>
27+
28+
<service id="annotations.cache" class="Symfony\Component\Cache\DoctrineProvider" public="false">
29+
<argument type="service">
30+
<service class="Symfony\Component\Cache\Adapter\PhpArrayAdapter">
31+
<factory class="Symfony\Component\Cache\Adapter\PhpArrayAdapter" method="create" />
32+
<argument>%kernel.cache_dir%/annotations.php</argument>
33+
<argument type="service" id="cache.annotations" />
34+
</service>
35+
</argument>
36+
</service>
37+
2238
<service id="annotation_reader" alias="annotations.reader" />
2339
</services>
2440
</container>

src/Symfony/Bundle/FrameworkBundle/Resources/config/cache.xml

+4
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@
2222
<tag name="cache.pool" />
2323
</service>
2424

25+
<service id="cache.annotations" parent="cache.system" public="false">
26+
<tag name="cache.pool" />
27+
</service>
28+
2529
<service id="cache.adapter.system" class="Symfony\Component\Cache\Adapter\AdapterInterface" abstract="true">
2630
<factory class="Symfony\Component\Cache\Adapter\AbstractAdapter" method="createSystemCache" />
2731
<tag name="cache.pool" clearer="cache.default_clearer" />

src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,7 @@ protected static function getBundleDefaultConfig()
214214
'cache' => 'validator.mapping.cache.symfony',
215215
),
216216
'annotations' => array(
217-
'cache' => 'file',
217+
'cache' => 'php_array',
218218
'file_cache_dir' => '%kernel.cache_dir%/annotations',
219219
'debug' => true,
220220
),

src/Symfony/Bundle/FrameworkBundle/composer.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,13 @@
1818
"require": {
1919
"php": ">=5.5.9",
2020
"symfony/asset": "~2.8|~3.0",
21-
"symfony/cache": "~3.1",
21+
"symfony/cache": "~3.2",
2222
"symfony/class-loader": "~3.2",
2323
"symfony/dependency-injection": "~3.2",
2424
"symfony/config": "~2.8|~3.0",
2525
"symfony/event-dispatcher": "~2.8|~3.0",
2626
"symfony/http-foundation": "~3.1",
27-
"symfony/http-kernel": "~3.1.2|~3.2",
27+
"symfony/http-kernel": "~3.2",
2828
"symfony/polyfill-mbstring": "~1.0",
2929
"symfony/filesystem": "~2.8|~3.0",
3030
"symfony/finder": "~2.8|~3.0",

src/Symfony/Component/Cache/Adapter/ArrayAdapter.php

+12-2
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ function ($key, $value, $isHit) use ($defaultLifetime) {
5656
public function getItem($key)
5757
{
5858
if (!$isHit = $this->hasItem($key)) {
59-
$value = null;
59+
$this->values[$key] = $value = null;
6060
} elseif ($this->storeSerialized) {
6161
$value = unserialize($this->values[$key]);
6262
} else {
@@ -79,6 +79,16 @@ public function getItems(array $keys = array())
7979
return $this->generateItems($keys, time());
8080
}
8181

82+
/**
83+
* Returns all cached values, with cache miss as null.
84+
*
85+
* @return array
86+
*/
87+
public function getValues()
88+
{
89+
return $this->values;
90+
}
91+
8292
/**
8393
* {@inheritdoc}
8494
*/
@@ -183,7 +193,7 @@ private function generateItems(array $keys, $now)
183193

184194
foreach ($keys as $key) {
185195
if (!$isHit = isset($this->expiries[$key]) && ($this->expiries[$key] >= $now || !$this->deleteItem($key))) {
186-
$value = null;
196+
$this->values[$key] = $value = null;
187197
} elseif ($this->storeSerialized) {
188198
$value = unserialize($this->values[$key]);
189199
} else {

src/Symfony/Component/Cache/Tests/Adapter/ArrayAdapterTest.php

+26
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,30 @@ public function createCachePool($defaultLifetime = 0)
2727
{
2828
return new ArrayAdapter($defaultLifetime);
2929
}
30+
31+
public function testGetValuesHitAndMiss()
32+
{
33+
/** @var ArrayAdapter $cache */
34+
$cache = $this->createCachePool();
35+
36+
// Hit
37+
$item = $cache->getItem('foo');
38+
$item->set('4711');
39+
$cache->save($item);
40+
41+
$fooItem = $cache->getItem('foo');
42+
$this->assertTrue($fooItem->isHit());
43+
$this->assertEquals('4711', $fooItem->get());
44+
45+
// Miss (should be present as NULL in $values)
46+
$cache->getItem('bar');
47+
48+
$values = $cache->getValues();
49+
50+
$this->assertCount(2, $values);
51+
$this->assertArrayHasKey('foo', $values);
52+
$this->assertSame(serialize('4711'), $values['foo']);
53+
$this->assertArrayHasKey('bar', $values);
54+
$this->assertNull($values['bar']);
55+
}
3056
}

0 commit comments

Comments
 (0)