Skip to content

Commit 9f42021

Browse files
committed
[FrameworkBundle] Introduce a cache warmer for Serializer based on PhpArrayAdapter
1 parent 983b560 commit 9f42021

File tree

12 files changed

+342
-46
lines changed

12 files changed

+342
-46
lines changed
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
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 Psr\Cache\CacheItemPoolInterface;
15+
use Symfony\Component\Cache\Adapter\AdapterInterface;
16+
use Symfony\Component\Cache\Adapter\ArrayAdapter;
17+
use Symfony\Component\Cache\Adapter\PhpArrayAdapter;
18+
use Symfony\Component\Cache\Adapter\ProxyAdapter;
19+
use Symfony\Component\Cache\DoctrineProvider;
20+
use Symfony\Component\HttpKernel\CacheWarmer\CacheWarmerInterface;
21+
use Symfony\Component\Serializer\Mapping\Factory\CacheClassMetadataFactory;
22+
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory;
23+
use Symfony\Component\Serializer\Mapping\Loader\LoaderChain;
24+
use Symfony\Component\Serializer\Mapping\Loader\LoaderInterface;
25+
use Symfony\Component\Serializer\Mapping\Loader\XmlFileLoader;
26+
use Symfony\Component\Serializer\Mapping\Loader\YamlFileLoader;
27+
28+
/**
29+
* Warms up XML and YAML serializer metadata.
30+
*
31+
* @author Titouan Galopin <galopintitouan@gmail.com>
32+
*/
33+
class SerializerCacheWarmer implements CacheWarmerInterface
34+
{
35+
private $loader;
36+
private $phpArrayFile;
37+
private $fallbackPool;
38+
39+
/**
40+
* @param LoaderChain $loader
41+
* @param string $phpArrayFile The PHP file where metadata are cached.
42+
* @param CacheItemPoolInterface $fallbackPool The pool where runtime-discovered metadata are cached.
43+
*/
44+
public function __construct(LoaderChain $loader, $phpArrayFile, CacheItemPoolInterface $fallbackPool)
45+
{
46+
$this->loader = $loader;
47+
$this->phpArrayFile = $phpArrayFile;
48+
if (!$fallbackPool instanceof AdapterInterface) {
49+
$fallbackPool = new ProxyAdapter($fallbackPool);
50+
}
51+
$this->fallbackPool = $fallbackPool;
52+
}
53+
54+
/**
55+
* {@inheritdoc}
56+
*/
57+
public function warmUp($cacheDir)
58+
{
59+
$adapter = new PhpArrayAdapter($this->phpArrayFile, $this->fallbackPool);
60+
$arrayPool = new ArrayAdapter(0, false);
61+
62+
$metadataFactory = new CacheClassMetadataFactory(new ClassMetadataFactory($this->loader), $arrayPool);
63+
64+
foreach ($this->extractSupportedLoaders(array($this->loader)) as $loader) {
65+
foreach ($loader->getMappedClasses() as $mappedClass) {
66+
$metadataFactory->getMetadataFor($mappedClass);
67+
}
68+
}
69+
70+
$values = $arrayPool->getValues();
71+
$adapter->warmUp($values);
72+
73+
foreach ($values as $k => $v) {
74+
$item = $this->fallbackPool->getItem($k);
75+
$this->fallbackPool->saveDeferred($item->set($v));
76+
}
77+
$this->fallbackPool->commit();
78+
}
79+
80+
/**
81+
* {@inheritdoc}
82+
*/
83+
public function isOptional()
84+
{
85+
return true;
86+
}
87+
88+
/**
89+
* @param LoaderInterface[] $loaders
90+
*
91+
* @return XmlFileLoader[]|YamlFileLoader[]
92+
*/
93+
private function extractSupportedLoaders(array $loaders)
94+
{
95+
$supportedLoaders = array();
96+
97+
foreach ($loaders as $loader) {
98+
if ($loader instanceof XmlFileLoader || $loader instanceof YamlFileLoader) {
99+
$supportedLoaders[] = $loader;
100+
} elseif ($loader instanceof LoaderChain) {
101+
$supportedLoaders = array_merge($supportedLoaders, $this->extractSupportedLoaders($loader->getDelegatedLoaders()));
102+
}
103+
}
104+
105+
return $supportedLoaders;
106+
}
107+
}

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

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
use Doctrine\Common\Annotations\Reader;
1515
use Symfony\Component\Cache\Adapter\AdapterInterface;
16+
use Symfony\Component\Cache\DoctrineProvider;
1617
use Symfony\Component\DependencyInjection\ContainerBuilder;
1718
use Symfony\Component\DependencyInjection\ContainerInterface;
1819
use Symfony\Component\DependencyInjection\Definition;
@@ -30,6 +31,7 @@
3031
use Symfony\Component\Serializer\Normalizer\DataUriNormalizer;
3132
use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;
3233
use Symfony\Component\Serializer\Normalizer\JsonSerializableNormalizer;
34+
use Symfony\Component\VarDumper\VarDumper;
3335
use Symfony\Component\Workflow;
3436

3537
/**
@@ -1068,29 +1070,26 @@ private function registerSerializerConfiguration(array $config, ContainerBuilder
10681070

10691071
$chainLoader->replaceArgument(0, $serializerLoaders);
10701072

1073+
$cache = null;
1074+
10711075
if (isset($config['cache']) && $config['cache']) {
10721076
@trigger_error('The "framework.serializer.cache" option is deprecated since Symfony 3.1 and will be removed in 4.0. Configure the "cache.serializer" service under "framework.cache.pools" instead.', E_USER_DEPRECATED);
10731077

1078+
$cache = new Definition(DoctrineProvider::class);
1079+
$cache->setDecoratedService($config['cache']);
1080+
$cache->setPublic(false);
1081+
} elseif (!$container->getParameter('kernel.debug')) {
1082+
$cache = new Reference('serializer.mapping.cache.symfony');
1083+
}
1084+
1085+
if ($cache) {
10741086
$container->setParameter(
10751087
'serializer.mapping.cache.prefix',
10761088
'serializer_'.$this->getKernelRootHash($container)
10771089
);
10781090

1079-
$container->getDefinition('serializer.mapping.class_metadata_factory')->replaceArgument(
1080-
1, new Reference($config['cache'])
1081-
);
1082-
} elseif (!$container->getParameter('kernel.debug')) {
1083-
$cacheMetadataFactory = new Definition(
1084-
CacheClassMetadataFactory::class,
1085-
array(
1086-
new Reference('serializer.mapping.cache_class_metadata_factory.inner'),
1087-
new Reference('cache.serializer'),
1088-
)
1089-
);
1090-
$cacheMetadataFactory->setPublic(false);
1091-
$cacheMetadataFactory->setDecoratedService('serializer.mapping.class_metadata_factory');
1092-
1093-
$container->setDefinition('serializer.mapping.cache_class_metadata_factory', $cacheMetadataFactory);
1091+
$container->getDefinition('serializer.mapping.cache_class_metadata_factory')->replaceArgument(1, $cache);
1092+
$container->setAlias('serializer.mapping.class_metadata_factory', 'serializer.mapping.cache_class_metadata_factory');
10941093
}
10951094

10961095
if (isset($config['name_converter']) && $config['name_converter']) {

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

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
<parameters>
88
<parameter key="serializer.mapping.cache.prefix" />
9+
<parameter key="serializer.mapping.cache.file">%kernel.cache_dir%/serialization.php</parameter>
910
</parameters>
1011

1112
<services>
@@ -33,12 +34,32 @@
3334
</service>
3435

3536
<!-- Class Metadata Factory -->
36-
<service id="serializer.mapping.class_metadata_factory" class="Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory" public="false">
37+
<service id="serializer.mapping.raw_class_metadata_factory" class="Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory" public="false">
3738
<argument type="service" id="serializer.mapping.chain_loader" />
3839
<argument>null</argument>
3940
</service>
4041

42+
<service id="serializer.mapping.cache_class_metadata_factory" class="Symfony\Component\Serializer\Mapping\Factory\CacheClassMetadataFactory" public="false">
43+
<argument type="service" id="serializer.mapping.raw_class_metadata_factory" />
44+
<argument /><!-- Cache pool populated by the extension -->
45+
</service>
46+
47+
<service id="serializer.mapping.class_metadata_factory" alias="serializer.mapping.raw_class_metadata_factory" />
48+
4149
<!-- Cache -->
50+
<service id="serializer.mapping.cache_warmer" class="Symfony\Bundle\FrameworkBundle\CacheWarmer\SerializerCacheWarmer" public="false">
51+
<argument type="service" id="serializer.mapping.chain_loader" />
52+
<argument>%serializer.mapping.cache.file%</argument>
53+
<argument type="service" id="cache.serializer" />
54+
<tag name="kernel.cache_warmer" />
55+
</service>
56+
57+
<service id="serializer.mapping.cache.symfony" class="Symfony\Component\Cache\Adapter\PhpArrayAdapter">
58+
<factory class="Symfony\Component\Cache\Adapter\PhpArrayAdapter" method="create" />
59+
<argument>%serializer.mapping.cache.file%</argument>
60+
<argument type="service" id="cache.serializer" />
61+
</service>
62+
4263
<service id="serializer.mapping.cache.doctrine.apc" class="Doctrine\Common\Cache\ApcCache" public="false">
4364
<call method="setNamespace">
4465
<argument>%serializer.mapping.cache.prefix%</argument>
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
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\Tests\CacheWarmer;
13+
14+
use Symfony\Bundle\FrameworkBundle\CacheWarmer\SerializerCacheWarmer;
15+
use Symfony\Bundle\FrameworkBundle\Tests\TestCase;
16+
use Symfony\Component\Cache\Adapter\ArrayAdapter;
17+
use Symfony\Component\Serializer\Mapping\Loader\LoaderChain;
18+
use Symfony\Component\Serializer\Mapping\Loader\XmlFileLoader;
19+
use Symfony\Component\Serializer\Mapping\Loader\YamlFileLoader;
20+
21+
class SerializationCacheWarmerTest extends TestCase
22+
{
23+
public function testWarmUp()
24+
{
25+
$loader = new LoaderChain(array(
26+
new XmlFileLoader(__DIR__.'/../Fixtures/Serialization/Resources/person.xml'),
27+
new YamlFileLoader(__DIR__.'/../Fixtures/Serialization/Resources/author.yml'),
28+
));
29+
30+
$file = sys_get_temp_dir().'/cache-serializer.php';
31+
@unlink($file);
32+
33+
$fallbackPool = new ArrayAdapter();
34+
35+
$warmer = new SerializerCacheWarmer($loader, $file, $fallbackPool);
36+
$warmer->warmUp(dirname($file));
37+
38+
$this->assertFileExists($file);
39+
40+
$values = require $file;
41+
42+
$this->assertInternalType('array', $values);
43+
$this->assertCount(3, $values); // 2 classes + Doctrine namespace
44+
$this->assertArrayHasKey('%5BSymfony%5CBundle%5CFrameworkBundle%5CTests%5CFixtures%5CSerialization%5CPerson%5D%5B1%5D', $values);
45+
$this->assertArrayHasKey('%5BSymfony%5CBundle%5CFrameworkBundle%5CTests%5CFixtures%5CSerialization%5CAuthor%5D%5B1%5D', $values);
46+
47+
$values = $fallbackPool->getValues();
48+
49+
$this->assertInternalType('array', $values);
50+
$this->assertCount(3, $values); // 2 classes + Doctrine namespace
51+
$this->assertArrayHasKey('%5BSymfony%5CBundle%5CFrameworkBundle%5CTests%5CFixtures%5CSerialization%5CPerson%5D%5B1%5D', $values);
52+
$this->assertArrayHasKey('%5BSymfony%5CBundle%5CFrameworkBundle%5CTests%5CFixtures%5CSerialization%5CAuthor%5D%5B1%5D', $values);
53+
}
54+
55+
public function testWarmUpWithoutLoader()
56+
{
57+
$file = sys_get_temp_dir().'/cache-serializer-without-loader.php';
58+
@unlink($file);
59+
60+
$fallbackPool = new ArrayAdapter();
61+
62+
$warmer = new SerializerCacheWarmer(new LoaderChain(array()), $file, $fallbackPool);
63+
$warmer->warmUp(dirname($file));
64+
65+
$this->assertFileExists($file);
66+
67+
$values = require $file;
68+
69+
$this->assertInternalType('array', $values);
70+
$this->assertCount(0, $values);
71+
72+
$values = $fallbackPool->getValues();
73+
74+
$this->assertInternalType('array', $values);
75+
$this->assertCount(0, $values);
76+
}
77+
}

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

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
use Symfony\Component\Cache\Adapter\ProxyAdapter;
2222
use Symfony\Component\Cache\Adapter\RedisAdapter;
2323
use Symfony\Component\DependencyInjection\ContainerBuilder;
24+
use Symfony\Component\DependencyInjection\Definition;
2425
use Symfony\Component\DependencyInjection\DefinitionDecorator;
2526
use Symfony\Component\DependencyInjection\Loader\ClosureLoader;
2627
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag;
@@ -467,7 +468,7 @@ public function testSerializerEnabled()
467468

468469
$this->assertCount(1, $argument);
469470
$this->assertEquals('Symfony\Component\Serializer\Mapping\Loader\AnnotationLoader', $argument[0]->getClass());
470-
$this->assertNull($container->getDefinition('serializer.mapping.class_metadata_factory')->getArgument(1));
471+
$this->assertNull($container->getDefinition('serializer.mapping.raw_class_metadata_factory')->getArgument(1));
471472
$this->assertEquals(new Reference('serializer.name_converter.camel_case_to_snake_case'), $container->getDefinition('serializer.normalizer.object')->getArgument(1));
472473
$this->assertEquals(new Reference('property_info', ContainerBuilder::IGNORE_ON_INVALID_REFERENCE), $container->getDefinition('serializer.normalizer.object')->getArgument(3));
473474
}
@@ -543,13 +544,23 @@ public function testObjectNormalizerRegistered()
543544
public function testSerializerCacheActivated()
544545
{
545546
$container = $this->createContainerFromFile('serializer_enabled');
547+
546548
$this->assertTrue($container->hasDefinition('serializer.mapping.cache_class_metadata_factory'));
549+
$this->assertSame(
550+
'serializer.mapping.cache_class_metadata_factory',
551+
(string) $container->getAlias('serializer.mapping.class_metadata_factory')
552+
);
547553
}
548554

549555
public function testSerializerCacheDisabled()
550556
{
551557
$container = $this->createContainerFromFile('serializer_enabled', array('kernel.debug' => true, 'kernel.container_class' => __CLASS__));
552-
$this->assertFalse($container->hasDefinition('serializer.mapping.cache_class_metadata_factory'));
558+
559+
$this->assertTrue($container->hasDefinition('serializer.mapping.cache_class_metadata_factory'));
560+
$this->assertSame(
561+
'serializer.mapping.raw_class_metadata_factory',
562+
(string) $container->getAlias('serializer.mapping.class_metadata_factory')
563+
);
553564
}
554565

555566
/**
@@ -561,8 +572,16 @@ public function testDeprecatedSerializerCacheOption()
561572
ErrorAssert::assertDeprecationsAreTriggered('The "framework.serializer.cache" option is deprecated', function () {
562573
$container = $this->createContainerFromFile('serializer_legacy_cache', array('kernel.debug' => true, 'kernel.container_class' => __CLASS__));
563574

564-
$this->assertFalse($container->hasDefinition('serializer.mapping.cache_class_metadata_factory'));
565-
$this->assertEquals(new Reference('foo'), $container->getDefinition('serializer.mapping.class_metadata_factory')->getArgument(1));
575+
$this->assertTrue($container->hasDefinition('serializer.mapping.cache_class_metadata_factory'));
576+
$this->assertSame(
577+
'serializer.mapping.cache_class_metadata_factory',
578+
(string) $container->getAlias('serializer.mapping.class_metadata_factory')
579+
);
580+
581+
$cachedDefinition = $container->getDefinition('serializer.mapping.cache_class_metadata_factory');
582+
583+
$this->assertInstanceOf(Definition::class, $cachedDefinition->getArgument(1));
584+
$this->assertSame('foo', $cachedDefinition->getArgument(1)->getDecoratedService()[0]);
566585
});
567586
}
568587

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?php
2+
3+
namespace Symfony\Bundle\FrameworkBundle\Tests\Fixtures\Serialization;
4+
5+
class Author
6+
{
7+
public $gender;
8+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?php
2+
3+
namespace Symfony\Bundle\FrameworkBundle\Tests\Fixtures\Serialization;
4+
5+
class Person
6+
{
7+
public $gender;
8+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Symfony\Bundle\FrameworkBundle\Tests\Fixtures\Serialization\Author:
2+
attributes:
3+
gender:
4+
groups: ['group1', 'group2']
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?xml version="1.0" ?>
2+
<serializer xmlns="http://symfony.com/schema/dic/serializer-mapping"
3+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4+
xsi:schemaLocation="http://symfony.com/schema/dic/serializer-mapping
5+
http://symfony.com/schema/dic/serializer-mapping/serializer-mapping-1.0.xsd"
6+
>
7+
<class name="Symfony\Bundle\FrameworkBundle\Tests\Fixtures\Serialization\Person">
8+
<attribute name="gender">
9+
<group>group1</group>
10+
<group>group2</group>
11+
</attribute>
12+
</class>
13+
</serializer>

src/Symfony/Component/Serializer/Mapping/Loader/LoaderChain.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,4 +63,12 @@ public function loadClassMetadata(ClassMetadataInterface $metadata)
6363

6464
return $success;
6565
}
66+
67+
/**
68+
* @return LoaderInterface[]
69+
*/
70+
public function getDelegatedLoaders()
71+
{
72+
return $this->loaders;
73+
}
6674
}

0 commit comments

Comments
 (0)