Skip to content

Commit 6532d61

Browse files
committed
feature #19507 [FrameworkBundle] Introduce a cache warmer for Serializer based on PhpArrayAdapter (tgalopin)
This PR was merged into the 3.2-dev branch. Discussion ---------- [FrameworkBundle] Introduce a cache warmer for Serializer based on PhpArrayAdapter | Q | A | ------------- | --- | Branch? | master | Bug fix? | no | New feature? | yes | BC breaks? | no | Deprecations? | no | Tests pass? | yes | Fixed tickets | - | License | MIT | Doc PR | - Following the cache warmer for annotations (#18533) and for the validator (#19485), this PR introduces a cache warmer for the Serializer YAML and XML metadata configuration (mainly groups). Based on the PhpArrayAdapter, it uses the naming conventions (Resources/config/serialization) to find the files and compile them into a single PHP file stored in the cache directory. This file uses shared memory on PHP 7. The benefit of this PR are the same than the ones of the previous PR: - serialization metadata cache can be warmed up offline - on PHP 7, there is no need for user extension to get maximum performances (ie. if you use this PR and the other one, you probably won't need to enable APCu to have great performances) - on PHP 7 again, we are not sensitive to APCu memory fragmentation last but not least, global performance is slightly better (I get 30us per class gain in Blackfire) As previous work on the Serializer cache system introduced issues (see symfony/symfony@96e418a), it would be interesting to pay careful attention to the backward compatibility during the review (ping @Ener-Getick). Commits ------- 810f469 [FrameworkBundle] Introduce a cache warmer for Serializer based on PhpArrayAdapter
2 parents f0815c8 + ab0f8a5 commit 6532d61

File tree

10 files changed

+262
-4
lines changed

10 files changed

+262
-4
lines changed

CacheWarmer/SerializerCacheWarmer.php

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

DependencyInjection/FrameworkExtension.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1087,6 +1087,7 @@ private function registerSerializerConfiguration(array $config, ContainerBuilder
10871087
}
10881088

10891089
$chainLoader->replaceArgument(0, $serializerLoaders);
1090+
$container->getDefinition('serializer.mapping.cache_warmer')->replaceArgument(0, $serializerLoaders);
10901091

10911092
if (isset($config['cache']) && $config['cache']) {
10921093
@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);
@@ -1099,12 +1100,12 @@ private function registerSerializerConfiguration(array $config, ContainerBuilder
10991100
$container->getDefinition('serializer.mapping.class_metadata_factory')->replaceArgument(
11001101
1, new Reference($config['cache'])
11011102
);
1102-
} elseif (!$container->getParameter('kernel.debug')) {
1103+
} elseif (!$container->getParameter('kernel.debug') && class_exists(CacheClassMetadataFactory::class)) {
11031104
$cacheMetadataFactory = new Definition(
11041105
CacheClassMetadataFactory::class,
11051106
array(
11061107
new Reference('serializer.mapping.cache_class_metadata_factory.inner'),
1107-
new Reference('cache.serializer'),
1108+
new Reference('serializer.mapping.cache.symfony'),
11081109
)
11091110
);
11101111
$cacheMetadataFactory->setPublic(false);

Resources/config/serializer.xml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
66

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

@@ -39,6 +40,19 @@
3940
</service>
4041

4142
<!-- Cache -->
43+
<service id="serializer.mapping.cache_warmer" class="Symfony\Bundle\FrameworkBundle\CacheWarmer\SerializerCacheWarmer" public="false">
44+
<argument type="collection" /><!-- Loaders injected by the extension -->
45+
<argument>%serializer.mapping.cache.file%</argument>
46+
<argument type="service" id="cache.serializer" />
47+
<tag name="kernel.cache_warmer" />
48+
</service>
49+
50+
<service id="serializer.mapping.cache.symfony" class="Symfony\Component\Cache\Adapter\PhpArrayAdapter">
51+
<factory class="Symfony\Component\Cache\Adapter\PhpArrayAdapter" method="create" />
52+
<argument>%serializer.mapping.cache.file%</argument>
53+
<argument type="service" id="cache.serializer" />
54+
</service>
55+
4256
<service id="serializer.mapping.cache.doctrine.apc" class="Doctrine\Common\Cache\ApcCache" public="false">
4357
<call method="setNamespace">
4458
<argument>%serializer.mapping.cache.prefix%</argument>
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
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\Factory\CacheClassMetadataFactory;
18+
use Symfony\Component\Serializer\Mapping\Loader\XmlFileLoader;
19+
use Symfony\Component\Serializer\Mapping\Loader\YamlFileLoader;
20+
21+
class SerializerCacheWarmerTest extends TestCase
22+
{
23+
public function testWarmUp()
24+
{
25+
if (!class_exists(CacheClassMetadataFactory::class) || !method_exists(XmlFileLoader::class, 'getMappedClasses') || !method_exists(YamlFileLoader::class, 'getMappedClasses')) {
26+
$this->markTestSkipped('The Serializer default cache warmer has been introduced in the Serializer Component version 3.2.');
27+
}
28+
29+
$loaders = array(
30+
new XmlFileLoader(__DIR__.'/../Fixtures/Serialization/Resources/person.xml'),
31+
new YamlFileLoader(__DIR__.'/../Fixtures/Serialization/Resources/author.yml'),
32+
);
33+
34+
$file = sys_get_temp_dir().'/cache-serializer.php';
35+
@unlink($file);
36+
37+
$fallbackPool = new ArrayAdapter();
38+
39+
$warmer = new SerializerCacheWarmer($loaders, $file, $fallbackPool);
40+
$warmer->warmUp(dirname($file));
41+
42+
$this->assertFileExists($file);
43+
44+
$values = require $file;
45+
46+
$this->assertInternalType('array', $values);
47+
$this->assertCount(2, $values);
48+
$this->assertArrayHasKey('Symfony_Bundle_FrameworkBundle_Tests_Fixtures_Serialization_Person', $values);
49+
$this->assertArrayHasKey('Symfony_Bundle_FrameworkBundle_Tests_Fixtures_Serialization_Author', $values);
50+
51+
$values = $fallbackPool->getValues();
52+
53+
$this->assertInternalType('array', $values);
54+
$this->assertCount(2, $values);
55+
$this->assertArrayHasKey('Symfony_Bundle_FrameworkBundle_Tests_Fixtures_Serialization_Person', $values);
56+
$this->assertArrayHasKey('Symfony_Bundle_FrameworkBundle_Tests_Fixtures_Serialization_Author', $values);
57+
}
58+
59+
public function testWarmUpWithoutLoader()
60+
{
61+
if (!class_exists(CacheClassMetadataFactory::class) || !method_exists(XmlFileLoader::class, 'getMappedClasses') || !method_exists(YamlFileLoader::class, 'getMappedClasses')) {
62+
$this->markTestSkipped('The Serializer default cache warmer has been introduced in the Serializer Component version 3.2.');
63+
}
64+
65+
$file = sys_get_temp_dir().'/cache-serializer-without-loader.php';
66+
@unlink($file);
67+
68+
$fallbackPool = new ArrayAdapter();
69+
70+
$warmer = new SerializerCacheWarmer(array(), $file, $fallbackPool);
71+
$warmer->warmUp(dirname($file));
72+
73+
$this->assertFileExists($file);
74+
75+
$values = require $file;
76+
77+
$this->assertInternalType('array', $values);
78+
$this->assertCount(0, $values);
79+
80+
$values = $fallbackPool->getValues();
81+
82+
$this->assertInternalType('array', $values);
83+
$this->assertCount(0, $values);
84+
}
85+
}

Tests/DependencyInjection/FrameworkExtensionTest.php

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,14 @@
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;
2728
use Symfony\Component\DependencyInjection\Reference;
29+
use Symfony\Component\Serializer\Mapping\Factory\CacheClassMetadataFactory;
30+
use Symfony\Component\Serializer\Mapping\Loader\XmlFileLoader;
31+
use Symfony\Component\Serializer\Mapping\Loader\YamlFileLoader;
2832
use Symfony\Component\Serializer\Normalizer\DataUriNormalizer;
2933
use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;
3034
use Symfony\Component\Serializer\Normalizer\JsonSerializableNormalizer;
@@ -542,8 +546,16 @@ public function testObjectNormalizerRegistered()
542546

543547
public function testSerializerCacheActivated()
544548
{
549+
if (!class_exists(CacheClassMetadataFactory::class) || !method_exists(XmlFileLoader::class, 'getMappedClasses') || !method_exists(YamlFileLoader::class, 'getMappedClasses')) {
550+
$this->markTestSkipped('The Serializer default cache warmer has been introduced in the Serializer Component version 3.2.');
551+
}
552+
545553
$container = $this->createContainerFromFile('serializer_enabled');
554+
546555
$this->assertTrue($container->hasDefinition('serializer.mapping.cache_class_metadata_factory'));
556+
557+
$cache = $container->getDefinition('serializer.mapping.cache_class_metadata_factory')->getArgument(1);
558+
$this->assertEquals(new Reference('serializer.mapping.cache.symfony'), $cache);
547559
}
548560

549561
public function testSerializerCacheDisabled()
@@ -562,7 +574,10 @@ public function testDeprecatedSerializerCacheOption()
562574
$container = $this->createContainerFromFile('serializer_legacy_cache', array('kernel.debug' => true, 'kernel.container_class' => __CLASS__));
563575

564576
$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));
577+
$this->assertTrue($container->hasDefinition('serializer.mapping.class_metadata_factory'));
578+
579+
$cache = $container->getDefinition('serializer.mapping.class_metadata_factory')->getArgument(1);
580+
$this->assertEquals(new Reference('foo'), $cache);
566581
});
567582
}
568583

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>

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@
4747
"symfony/form": "~2.8|~3.0",
4848
"symfony/expression-language": "~2.8|~3.0",
4949
"symfony/process": "~2.8|~3.0",
50-
"symfony/serializer": "~2.8|^3.0",
50+
"symfony/serializer": "~2.8|~3.0",
5151
"symfony/validator": "~3.1",
5252
"symfony/yaml": "~3.2",
5353
"symfony/property-info": "~2.8|~3.0",

0 commit comments

Comments
 (0)