diff --git a/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/SerializerCacheWarmer.php b/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/SerializerCacheWarmer.php new file mode 100644 index 0000000000000..23e6142be4993 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/SerializerCacheWarmer.php @@ -0,0 +1,110 @@ + + * + * 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 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\HttpKernel\CacheWarmer\CacheWarmerInterface; +use Symfony\Component\Serializer\Mapping\Factory\CacheClassMetadataFactory; +use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory; +use Symfony\Component\Serializer\Mapping\Loader\LoaderChain; +use Symfony\Component\Serializer\Mapping\Loader\LoaderInterface; +use Symfony\Component\Serializer\Mapping\Loader\XmlFileLoader; +use Symfony\Component\Serializer\Mapping\Loader\YamlFileLoader; + +/** + * Warms up XML and YAML serializer metadata. + * + * @author Titouan Galopin + */ +class SerializerCacheWarmer implements CacheWarmerInterface +{ + private $loaders; + private $phpArrayFile; + private $fallbackPool; + + /** + * @param LoaderInterface[] $loaders The serializer metadata loaders. + * @param string $phpArrayFile The PHP file where metadata are cached. + * @param CacheItemPoolInterface $fallbackPool The pool where runtime-discovered metadata are cached. + */ + public function __construct(array $loaders, $phpArrayFile, CacheItemPoolInterface $fallbackPool) + { + $this->loaders = $loaders; + $this->phpArrayFile = $phpArrayFile; + if (!$fallbackPool instanceof AdapterInterface) { + $fallbackPool = new ProxyAdapter($fallbackPool); + } + $this->fallbackPool = $fallbackPool; + } + + /** + * {@inheritdoc} + */ + public function warmUp($cacheDir) + { + if (!class_exists(CacheClassMetadataFactory::class) || !method_exists(XmlFileLoader::class, 'getMappedClasses') || !method_exists(YamlFileLoader::class, 'getMappedClasses')) { + return; + } + + $adapter = new PhpArrayAdapter($this->phpArrayFile, $this->fallbackPool); + $arrayPool = new ArrayAdapter(0, false); + + $metadataFactory = new CacheClassMetadataFactory(new ClassMetadataFactory(new LoaderChain($this->loaders)), $arrayPool); + + foreach ($this->extractSupportedLoaders($this->loaders) as $loader) { + foreach ($loader->getMappedClasses() as $mappedClass) { + $metadataFactory->getMetadataFor($mappedClass); + } + } + + $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; + } + + /** + * @param LoaderInterface[] $loaders + * + * @return XmlFileLoader[]|YamlFileLoader[] + */ + private function extractSupportedLoaders(array $loaders) + { + $supportedLoaders = array(); + + foreach ($loaders as $loader) { + if ($loader instanceof XmlFileLoader || $loader instanceof YamlFileLoader) { + $supportedLoaders[] = $loader; + } elseif ($loader instanceof LoaderChain) { + $supportedLoaders = array_merge($supportedLoaders, $this->extractSupportedLoaders($loader->getDelegatedLoaders())); + } + } + + return $supportedLoaders; + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index 806a06197cafc..d82484a262211 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -1067,6 +1067,7 @@ private function registerSerializerConfiguration(array $config, ContainerBuilder } $chainLoader->replaceArgument(0, $serializerLoaders); + $container->getDefinition('serializer.mapping.cache_warmer')->replaceArgument(0, $serializerLoaders); if (isset($config['cache']) && $config['cache']) { @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); @@ -1079,12 +1080,12 @@ private function registerSerializerConfiguration(array $config, ContainerBuilder $container->getDefinition('serializer.mapping.class_metadata_factory')->replaceArgument( 1, new Reference($config['cache']) ); - } elseif (!$container->getParameter('kernel.debug')) { + } elseif (!$container->getParameter('kernel.debug') && class_exists(CacheClassMetadataFactory::class)) { $cacheMetadataFactory = new Definition( CacheClassMetadataFactory::class, array( new Reference('serializer.mapping.cache_class_metadata_factory.inner'), - new Reference('cache.serializer'), + new Reference('serializer.mapping.cache.symfony'), ) ); $cacheMetadataFactory->setPublic(false); diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/serializer.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/serializer.xml index 07fe6ba0ad309..d3f1fdd43ca6f 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/serializer.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/serializer.xml @@ -5,6 +5,7 @@ xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd"> + %kernel.cache_dir%/serialization.php @@ -39,6 +40,19 @@ + + + %serializer.mapping.cache.file% + + + + + + + %serializer.mapping.cache.file% + + + %serializer.mapping.cache.prefix% diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/CacheWarmer/SerializerCacheWarmerTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/CacheWarmer/SerializerCacheWarmerTest.php new file mode 100644 index 0000000000000..e5df7b8c8a7cc --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/CacheWarmer/SerializerCacheWarmerTest.php @@ -0,0 +1,85 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\Tests\CacheWarmer; + +use Symfony\Bundle\FrameworkBundle\CacheWarmer\SerializerCacheWarmer; +use Symfony\Bundle\FrameworkBundle\Tests\TestCase; +use Symfony\Component\Cache\Adapter\ArrayAdapter; +use Symfony\Component\Serializer\Mapping\Factory\CacheClassMetadataFactory; +use Symfony\Component\Serializer\Mapping\Loader\XmlFileLoader; +use Symfony\Component\Serializer\Mapping\Loader\YamlFileLoader; + +class SerializerCacheWarmerTest extends TestCase +{ + public function testWarmUp() + { + if (!class_exists(CacheClassMetadataFactory::class) || !method_exists(XmlFileLoader::class, 'getMappedClasses') || !method_exists(YamlFileLoader::class, 'getMappedClasses')) { + $this->markTestSkipped('The Serializer default cache warmer has been introduced in the Serializer Component version 3.2.'); + } + + $loaders = array( + new XmlFileLoader(__DIR__.'/../Fixtures/Serialization/Resources/person.xml'), + new YamlFileLoader(__DIR__.'/../Fixtures/Serialization/Resources/author.yml'), + ); + + $file = sys_get_temp_dir().'/cache-serializer.php'; + @unlink($file); + + $fallbackPool = new ArrayAdapter(); + + $warmer = new SerializerCacheWarmer($loaders, $file, $fallbackPool); + $warmer->warmUp(dirname($file)); + + $this->assertFileExists($file); + + $values = require $file; + + $this->assertInternalType('array', $values); + $this->assertCount(2, $values); + $this->assertArrayHasKey('Symfony_Bundle_FrameworkBundle_Tests_Fixtures_Serialization_Person', $values); + $this->assertArrayHasKey('Symfony_Bundle_FrameworkBundle_Tests_Fixtures_Serialization_Author', $values); + + $values = $fallbackPool->getValues(); + + $this->assertInternalType('array', $values); + $this->assertCount(2, $values); + $this->assertArrayHasKey('Symfony_Bundle_FrameworkBundle_Tests_Fixtures_Serialization_Person', $values); + $this->assertArrayHasKey('Symfony_Bundle_FrameworkBundle_Tests_Fixtures_Serialization_Author', $values); + } + + public function testWarmUpWithoutLoader() + { + if (!class_exists(CacheClassMetadataFactory::class) || !method_exists(XmlFileLoader::class, 'getMappedClasses') || !method_exists(YamlFileLoader::class, 'getMappedClasses')) { + $this->markTestSkipped('The Serializer default cache warmer has been introduced in the Serializer Component version 3.2.'); + } + + $file = sys_get_temp_dir().'/cache-serializer-without-loader.php'; + @unlink($file); + + $fallbackPool = new ArrayAdapter(); + + $warmer = new SerializerCacheWarmer(array(), $file, $fallbackPool); + $warmer->warmUp(dirname($file)); + + $this->assertFileExists($file); + + $values = require $file; + + $this->assertInternalType('array', $values); + $this->assertCount(0, $values); + + $values = $fallbackPool->getValues(); + + $this->assertInternalType('array', $values); + $this->assertCount(0, $values); + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php index cbf60967e0f1d..121b9c1ed31c2 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php @@ -21,10 +21,14 @@ use Symfony\Component\Cache\Adapter\ProxyAdapter; use Symfony\Component\Cache\Adapter\RedisAdapter; use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\DefinitionDecorator; use Symfony\Component\DependencyInjection\Loader\ClosureLoader; use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag; use Symfony\Component\DependencyInjection\Reference; +use Symfony\Component\Serializer\Mapping\Factory\CacheClassMetadataFactory; +use Symfony\Component\Serializer\Mapping\Loader\XmlFileLoader; +use Symfony\Component\Serializer\Mapping\Loader\YamlFileLoader; use Symfony\Component\Serializer\Normalizer\DataUriNormalizer; use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer; use Symfony\Component\Serializer\Normalizer\JsonSerializableNormalizer; @@ -542,8 +546,16 @@ public function testObjectNormalizerRegistered() public function testSerializerCacheActivated() { + if (!class_exists(CacheClassMetadataFactory::class) || !method_exists(XmlFileLoader::class, 'getMappedClasses') || !method_exists(YamlFileLoader::class, 'getMappedClasses')) { + $this->markTestSkipped('The Serializer default cache warmer has been introduced in the Serializer Component version 3.2.'); + } + $container = $this->createContainerFromFile('serializer_enabled'); + $this->assertTrue($container->hasDefinition('serializer.mapping.cache_class_metadata_factory')); + + $cache = $container->getDefinition('serializer.mapping.cache_class_metadata_factory')->getArgument(1); + $this->assertEquals(new Reference('serializer.mapping.cache.symfony'), $cache); } public function testSerializerCacheDisabled() @@ -562,7 +574,10 @@ public function testDeprecatedSerializerCacheOption() $container = $this->createContainerFromFile('serializer_legacy_cache', array('kernel.debug' => true, 'kernel.container_class' => __CLASS__)); $this->assertFalse($container->hasDefinition('serializer.mapping.cache_class_metadata_factory')); - $this->assertEquals(new Reference('foo'), $container->getDefinition('serializer.mapping.class_metadata_factory')->getArgument(1)); + $this->assertTrue($container->hasDefinition('serializer.mapping.class_metadata_factory')); + + $cache = $container->getDefinition('serializer.mapping.class_metadata_factory')->getArgument(1); + $this->assertEquals(new Reference('foo'), $cache); }); } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Serialization/Author.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Serialization/Author.php new file mode 100644 index 0000000000000..efb0b430279f2 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Serialization/Author.php @@ -0,0 +1,8 @@ + + + + + group1 + group2 + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/composer.json b/src/Symfony/Bundle/FrameworkBundle/composer.json index fe0ebf69db708..93206ee4ec595 100644 --- a/src/Symfony/Bundle/FrameworkBundle/composer.json +++ b/src/Symfony/Bundle/FrameworkBundle/composer.json @@ -47,7 +47,7 @@ "symfony/form": "~2.8|~3.0", "symfony/expression-language": "~2.8|~3.0", "symfony/process": "~2.8|~3.0", - "symfony/serializer": "~2.8|^3.0", + "symfony/serializer": "~2.8|~3.0", "symfony/validator": "~3.1", "symfony/yaml": "~3.2", "symfony/property-info": "~2.8|~3.0", diff --git a/src/Symfony/Component/Serializer/Mapping/Loader/XmlFileLoader.php b/src/Symfony/Component/Serializer/Mapping/Loader/XmlFileLoader.php index f20fba37a214a..76d064326f168 100644 --- a/src/Symfony/Component/Serializer/Mapping/Loader/XmlFileLoader.php +++ b/src/Symfony/Component/Serializer/Mapping/Loader/XmlFileLoader.php @@ -36,12 +36,11 @@ class XmlFileLoader extends FileLoader public function loadClassMetadata(ClassMetadataInterface $classMetadata) { if (null === $this->classes) { - $this->classes = array(); - $xml = $this->parseFile($this->file); + $this->classes = $this->getClassesFromXml(); + } - foreach ($xml->class as $class) { - $this->classes[(string) $class['name']] = $class; - } + if (!$this->classes) { + return false; } $attributesMetadata = $classMetadata->getAttributesMetadata(); @@ -74,6 +73,20 @@ public function loadClassMetadata(ClassMetadataInterface $classMetadata) return false; } + /** + * Return the names of the classes mapped in this file. + * + * @return string[] The classes names + */ + public function getMappedClasses() + { + if (null === $this->classes) { + $this->classes = $this->getClassesFromXml(); + } + + return array_keys($this->classes); + } + /** * Parses a XML File. * @@ -93,4 +106,16 @@ private function parseFile($file) return simplexml_import_dom($dom); } + + private function getClassesFromXml() + { + $xml = $this->parseFile($this->file); + $classes = array(); + + foreach ($xml->class as $class) { + $classes[(string) $class['name']] = $class; + } + + return $classes; + } } diff --git a/src/Symfony/Component/Serializer/Mapping/Loader/YamlFileLoader.php b/src/Symfony/Component/Serializer/Mapping/Loader/YamlFileLoader.php index f68807165d794..60af67757a463 100644 --- a/src/Symfony/Component/Serializer/Mapping/Loader/YamlFileLoader.php +++ b/src/Symfony/Component/Serializer/Mapping/Loader/YamlFileLoader.php @@ -38,26 +38,11 @@ class YamlFileLoader extends FileLoader public function loadClassMetadata(ClassMetadataInterface $classMetadata) { if (null === $this->classes) { - if (!stream_is_local($this->file)) { - throw new MappingException(sprintf('This is not a local file "%s".', $this->file)); - } - - if (null === $this->yamlParser) { - $this->yamlParser = new Parser(); - } - - $classes = $this->yamlParser->parse(file_get_contents($this->file)); - - if (empty($classes)) { - return false; - } - - // not an array - if (!is_array($classes)) { - throw new MappingException(sprintf('The file "%s" must contain a YAML array.', $this->file)); - } + $this->classes = $this->getClassesFromYaml(); + } - $this->classes = $classes; + if (!$this->classes) { + return false; } if (isset($this->classes[$classMetadata->getName()])) { @@ -103,4 +88,41 @@ public function loadClassMetadata(ClassMetadataInterface $classMetadata) return false; } + + /** + * Return the names of the classes mapped in this file. + * + * @return string[] The classes names + */ + public function getMappedClasses() + { + if (null === $this->classes) { + $this->classes = $this->getClassesFromYaml(); + } + + return array_keys($this->classes); + } + + private function getClassesFromYaml() + { + if (!stream_is_local($this->file)) { + throw new MappingException(sprintf('This is not a local file "%s".', $this->file)); + } + + if (null === $this->yamlParser) { + $this->yamlParser = new Parser(); + } + + $classes = $this->yamlParser->parse(file_get_contents($this->file)); + + if (empty($classes)) { + return array(); + } + + if (!is_array($classes)) { + throw new MappingException(sprintf('The file "%s" must contain a YAML array.', $this->file)); + } + + return $classes; + } }