From 63cbf0abe865989afdc4df387e5c976641d63886 Mon Sep 17 00:00:00 2001 From: Fabien Bourigault Date: Wed, 7 Nov 2018 08:21:07 +0100 Subject: [PATCH] [Serializer] Add CompiledClassMetadataFactory --- .php_cs.dist | 2 + src/Symfony/Component/Serializer/CHANGELOG.md | 5 + .../CompiledClassMetadataCacheWarmer.php | 63 +++++++++ .../Factory/ClassMetadataFactoryCompiler.php | 67 +++++++++ .../Factory/CompiledClassMetadataFactory.php | 81 +++++++++++ .../CompiledClassMetadataCacheWarmerTest.php | 58 ++++++++ .../Tests/Fixtures/object-metadata.php | 3 + .../Fixtures/serializer.class.metadata.php | 15 ++ .../ClassMetadataFactoryCompilerTest.php | 82 +++++++++++ .../CompiledClassMetadataFactoryTest.php | 129 ++++++++++++++++++ .../Component/Serializer/composer.json | 6 +- 11 files changed, 510 insertions(+), 1 deletion(-) create mode 100644 src/Symfony/Component/Serializer/CacheWarmer/CompiledClassMetadataCacheWarmer.php create mode 100644 src/Symfony/Component/Serializer/Mapping/Factory/ClassMetadataFactoryCompiler.php create mode 100644 src/Symfony/Component/Serializer/Mapping/Factory/CompiledClassMetadataFactory.php create mode 100644 src/Symfony/Component/Serializer/Tests/CacheWarmer/CompiledClassMetadataCacheWarmerTest.php create mode 100644 src/Symfony/Component/Serializer/Tests/Fixtures/object-metadata.php create mode 100644 src/Symfony/Component/Serializer/Tests/Fixtures/serializer.class.metadata.php create mode 100644 src/Symfony/Component/Serializer/Tests/Mapping/Factory/ClassMetadataFactoryCompilerTest.php create mode 100644 src/Symfony/Component/Serializer/Tests/Mapping/Factory/CompiledClassMetadataFactoryTest.php diff --git a/.php_cs.dist b/.php_cs.dist index 3f0e86f4c38d8..5d699d34d986a 100644 --- a/.php_cs.dist +++ b/.php_cs.dist @@ -36,6 +36,8 @@ return PhpCsFixer\Config::create() ->notPath('#Symfony/Bridge/PhpUnit/.*Legacy#') // file content autogenerated by `var_export` ->notPath('Symfony/Component/Translation/Tests/fixtures/resources.php') + // file content autogenerated by `VarExporter::export` + ->notPath('Symfony/Component/Serializer/Tests/Fixtures/serializer.class.metadata.php') // test template ->notPath('Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/Resources/Custom/_name_entry_label.html.php') // explicit trigger_error tests diff --git a/src/Symfony/Component/Serializer/CHANGELOG.md b/src/Symfony/Component/Serializer/CHANGELOG.md index 874b531e7fb98..ea826ba6c7deb 100644 --- a/src/Symfony/Component/Serializer/CHANGELOG.md +++ b/src/Symfony/Component/Serializer/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +5.2.0 +----- + +* added `CompiledClassMetadataFactory` and `ClassMetadataFactoryCompiler` for faster metadata loading. + 5.1.0 ----- diff --git a/src/Symfony/Component/Serializer/CacheWarmer/CompiledClassMetadataCacheWarmer.php b/src/Symfony/Component/Serializer/CacheWarmer/CompiledClassMetadataCacheWarmer.php new file mode 100644 index 0000000000000..b90f641a3de40 --- /dev/null +++ b/src/Symfony/Component/Serializer/CacheWarmer/CompiledClassMetadataCacheWarmer.php @@ -0,0 +1,63 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Serializer\CacheWarmer; + +use Symfony\Component\Filesystem\Filesystem; +use Symfony\Component\HttpKernel\CacheWarmer\CacheWarmerInterface; +use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryCompiler; +use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface; + +/** + * @author Fabien Bourigault + */ +final class CompiledClassMetadataCacheWarmer implements CacheWarmerInterface +{ + private $classesToCompile; + + private $classMetadataFactory; + + private $classMetadataFactoryCompiler; + + private $filesystem; + + public function __construct(array $classesToCompile, ClassMetadataFactoryInterface $classMetadataFactory, ClassMetadataFactoryCompiler $classMetadataFactoryCompiler, Filesystem $filesystem) + { + $this->classesToCompile = $classesToCompile; + $this->classMetadataFactory = $classMetadataFactory; + $this->classMetadataFactoryCompiler = $classMetadataFactoryCompiler; + $this->filesystem = $filesystem; + } + + /** + * {@inheritdoc} + */ + public function warmUp($cacheDir) + { + $metadatas = []; + + foreach ($this->classesToCompile as $classToCompile) { + $metadatas[] = $this->classMetadataFactory->getMetadataFor($classToCompile); + } + + $code = $this->classMetadataFactoryCompiler->compile($metadatas); + + $this->filesystem->dumpFile("{$cacheDir}/serializer.class.metadata.php", $code); + } + + /** + * {@inheritdoc} + */ + public function isOptional() + { + return true; + } +} diff --git a/src/Symfony/Component/Serializer/Mapping/Factory/ClassMetadataFactoryCompiler.php b/src/Symfony/Component/Serializer/Mapping/Factory/ClassMetadataFactoryCompiler.php new file mode 100644 index 0000000000000..81dd4b9323bab --- /dev/null +++ b/src/Symfony/Component/Serializer/Mapping/Factory/ClassMetadataFactoryCompiler.php @@ -0,0 +1,67 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Serializer\Mapping\Factory; + +use Symfony\Component\Serializer\Mapping\ClassMetadataInterface; +use Symfony\Component\VarExporter\VarExporter; + +/** + * @author Fabien Bourigault + */ +final class ClassMetadataFactoryCompiler +{ + /** + * @param ClassMetadataInterface[] $classMetadatas + */ + public function compile(array $classMetadatas): string + { + return <<generateDeclaredClassMetadata($classMetadatas)} +]; +EOF; + } + + /** + * @param ClassMetadataInterface[] $classMetadatas + */ + private function generateDeclaredClassMetadata(array $classMetadatas): string + { + $compiled = ''; + + foreach ($classMetadatas as $classMetadata) { + $attributesMetadata = []; + foreach ($classMetadata->getAttributesMetadata() as $attributeMetadata) { + $attributesMetadata[$attributeMetadata->getName()] = [ + $attributeMetadata->getGroups(), + $attributeMetadata->getMaxDepth(), + $attributeMetadata->getSerializedName(), + ]; + } + + $classDiscriminatorMapping = $classMetadata->getClassDiscriminatorMapping() ? [ + $classMetadata->getClassDiscriminatorMapping()->getTypeProperty(), + $classMetadata->getClassDiscriminatorMapping()->getTypesMapping(), + ] : null; + + $compiled .= sprintf("\n'%s' => %s,", $classMetadata->getName(), VarExporter::export([ + $attributesMetadata, + $classDiscriminatorMapping, + ])); + } + + return $compiled; + } +} diff --git a/src/Symfony/Component/Serializer/Mapping/Factory/CompiledClassMetadataFactory.php b/src/Symfony/Component/Serializer/Mapping/Factory/CompiledClassMetadataFactory.php new file mode 100644 index 0000000000000..17daf9e66d2e8 --- /dev/null +++ b/src/Symfony/Component/Serializer/Mapping/Factory/CompiledClassMetadataFactory.php @@ -0,0 +1,81 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Serializer\Mapping\Factory; + +use Symfony\Component\Serializer\Mapping\AttributeMetadata; +use Symfony\Component\Serializer\Mapping\ClassDiscriminatorMapping; +use Symfony\Component\Serializer\Mapping\ClassMetadata; + +/** + * @author Fabien Bourigault + */ +final class CompiledClassMetadataFactory implements ClassMetadataFactoryInterface +{ + private $compiledClassMetadata = []; + + private $loadedClasses = []; + + private $classMetadataFactory; + + public function __construct(string $compiledClassMetadataFile, ClassMetadataFactoryInterface $classMetadataFactory) + { + if (!file_exists($compiledClassMetadataFile)) { + throw new \RuntimeException("File \"{$compiledClassMetadataFile}\" could not be found."); + } + + $compiledClassMetadata = require $compiledClassMetadataFile; + if (!\is_array($compiledClassMetadata)) { + throw new \RuntimeException(sprintf('Compiled metadata must be of the type array, %s given.', \gettype($compiledClassMetadata))); + } + + $this->compiledClassMetadata = $compiledClassMetadata; + $this->classMetadataFactory = $classMetadataFactory; + } + + /** + * {@inheritdoc} + */ + public function getMetadataFor($value) + { + $className = \is_object($value) ? \get_class($value) : $value; + + if (!isset($this->compiledClassMetadata[$className])) { + return $this->classMetadataFactory->getMetadataFor($value); + } + + if (!isset($this->loadedClasses[$className])) { + $classMetadata = new ClassMetadata($className); + foreach ($this->compiledClassMetadata[$className][0] as $name => $compiledAttributesMetadata) { + $classMetadata->attributesMetadata[$name] = $attributeMetadata = new AttributeMetadata($name); + [$attributeMetadata->groups, $attributeMetadata->maxDepth, $attributeMetadata->serializedName] = $compiledAttributesMetadata; + } + $classMetadata->classDiscriminatorMapping = $this->compiledClassMetadata[$className][1] + ? new ClassDiscriminatorMapping(...$this->compiledClassMetadata[$className][1]) + : null + ; + + $this->loadedClasses[$className] = $classMetadata; + } + + return $this->loadedClasses[$className]; + } + + /** + * {@inheritdoc} + */ + public function hasMetadataFor($value) + { + $className = \is_object($value) ? \get_class($value) : $value; + + return isset($this->compiledClassMetadata[$className]) || $this->classMetadataFactory->hasMetadataFor($value); + } +} diff --git a/src/Symfony/Component/Serializer/Tests/CacheWarmer/CompiledClassMetadataCacheWarmerTest.php b/src/Symfony/Component/Serializer/Tests/CacheWarmer/CompiledClassMetadataCacheWarmerTest.php new file mode 100644 index 0000000000000..e9cb0afe543fc --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/CacheWarmer/CompiledClassMetadataCacheWarmerTest.php @@ -0,0 +1,58 @@ +createMock(ClassMetadataFactoryInterface::class); + $filesystem = $this->createMock(Filesystem::class); + + $compiledClassMetadataCacheWarmer = new CompiledClassMetadataCacheWarmer([], $classMetadataFactory, new ClassMetadataFactoryCompiler(), $filesystem); + + $this->assertInstanceOf(CacheWarmerInterface::class, $compiledClassMetadataCacheWarmer); + } + + public function testItIsAnOptionalCacheWarmer() + { + $classMetadataFactory = $this->createMock(ClassMetadataFactoryInterface::class); + $filesystem = $this->createMock(Filesystem::class); + + $compiledClassMetadataCacheWarmer = new CompiledClassMetadataCacheWarmer([], $classMetadataFactory, new ClassMetadataFactoryCompiler(), $filesystem); + + $this->assertTrue($compiledClassMetadataCacheWarmer->isOptional()); + } + + public function testItDumpCompiledClassMetadatas() + { + $classMetadataFactory = $this->createMock(ClassMetadataFactoryInterface::class); + + $code = <<createMock(Filesystem::class); + $filesystem + ->expects($this->once()) + ->method('dumpFile') + ->with('/var/cache/prod/serializer.class.metadata.php', $code) + ; + + $compiledClassMetadataCacheWarmer = new CompiledClassMetadataCacheWarmer([], $classMetadataFactory, new ClassMetadataFactoryCompiler(), $filesystem); + + $compiledClassMetadataCacheWarmer->warmUp('/var/cache/prod'); + } +} diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/object-metadata.php b/src/Symfony/Component/Serializer/Tests/Fixtures/object-metadata.php new file mode 100644 index 0000000000000..2b47d12ff2d79 --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Fixtures/object-metadata.php @@ -0,0 +1,3 @@ + [ + [ + 'foo' => [[], null, null], + 'bar' => [[], null, null], + 'baz' => [[], null, null], + 'qux' => [[], null, null], + ], + null, + ], +]; diff --git a/src/Symfony/Component/Serializer/Tests/Mapping/Factory/ClassMetadataFactoryCompilerTest.php b/src/Symfony/Component/Serializer/Tests/Mapping/Factory/ClassMetadataFactoryCompilerTest.php new file mode 100644 index 0000000000000..d3a366a530b37 --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Mapping/Factory/ClassMetadataFactoryCompilerTest.php @@ -0,0 +1,82 @@ +dumpPath = sys_get_temp_dir().\DIRECTORY_SEPARATOR.'php_serializer_metadata.'.uniqid('CompiledClassMetadataFactory').'.php'; + } + + protected function tearDown() + { + @unlink($this->dumpPath); + } + + public function testItDumpMetadata() + { + $classMetatadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader())); + + $dummyMetadata = $classMetatadataFactory->getMetadataFor(Dummy::class); + $maxDepthDummyMetadata = $classMetatadataFactory->getMetadataFor(MaxDepthDummy::class); + $serializedNameDummyMetadata = $classMetatadataFactory->getMetadataFor(SerializedNameDummy::class); + + $code = (new ClassMetadataFactoryCompiler())->compile([ + $dummyMetadata, + $maxDepthDummyMetadata, + $serializedNameDummyMetadata, + ]); + + file_put_contents($this->dumpPath, $code); + $compiledMetadata = require $this->dumpPath; + + $this->assertCount(3, $compiledMetadata); + + $this->assertArrayHasKey(Dummy::class, $compiledMetadata); + $this->assertEquals([ + [ + 'foo' => [[], null, null], + 'bar' => [[], null, null], + 'baz' => [[], null, null], + 'qux' => [[], null, null], + ], + null, + ], $compiledMetadata[Dummy::class]); + + $this->assertArrayHasKey(MaxDepthDummy::class, $compiledMetadata); + $this->assertEquals([ + [ + 'foo' => [[], 2, null], + 'bar' => [[], 3, null], + 'child' => [[], null, null], + ], + null, + ], $compiledMetadata[MaxDepthDummy::class]); + + $this->assertArrayHasKey(SerializedNameDummy::class, $compiledMetadata); + $this->assertEquals([ + [ + 'foo' => [[], null, 'baz'], + 'bar' => [[], null, 'qux'], + 'quux' => [[], null, null], + 'child' => [[], null, null], + ], + null, + ], $compiledMetadata[SerializedNameDummy::class]); + } +} diff --git a/src/Symfony/Component/Serializer/Tests/Mapping/Factory/CompiledClassMetadataFactoryTest.php b/src/Symfony/Component/Serializer/Tests/Mapping/Factory/CompiledClassMetadataFactoryTest.php new file mode 100644 index 0000000000000..c2efbee385cda --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Mapping/Factory/CompiledClassMetadataFactoryTest.php @@ -0,0 +1,129 @@ + + */ +final class CompiledClassMetadataFactoryTest extends TestCase +{ + public function testItImplementsClassMetadataFactoryInterface() + { + $classMetadataFactory = $this->createMock(ClassMetadataFactoryInterface::class); + $compiledClassMetadataFactory = new CompiledClassMetadataFactory(__DIR__.'/../../Fixtures/serializer.class.metadata.php', $classMetadataFactory); + + $this->assertInstanceOf(ClassMetadataFactoryInterface::class, $compiledClassMetadataFactory); + } + + public function testItThrowAnExceptionWhenCacheFileIsNotFound() + { + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessageRegExp('#File ".*/Fixtures/not-found-serializer.class.metadata.php" could not be found.#'); + + $classMetadataFactory = $this->createMock(ClassMetadataFactoryInterface::class); + new CompiledClassMetadataFactory(__DIR__.'/../../Fixtures/not-found-serializer.class.metadata.php', $classMetadataFactory); + } + + public function testItThrowAnExceptionWhenMetadataIsNotOfTypeArray() + { + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('Compiled metadata must be of the type array, object given.'); + + $classMetadataFactory = $this->createMock(ClassMetadataFactoryInterface::class); + new CompiledClassMetadataFactory(__DIR__.'/../../Fixtures/object-metadata.php', $classMetadataFactory); + } + + /** + * @dataProvider valueProvider + */ + public function testItReturnsTheCompiledMetadata($value) + { + $classMetadataFactory = $this->createMock(ClassMetadataFactoryInterface::class); + $compiledClassMetadataFactory = new CompiledClassMetadataFactory(__DIR__.'/../../Fixtures/serializer.class.metadata.php', $classMetadataFactory); + + $classMetadataFactory + ->expects($this->never()) + ->method('getMetadataFor') + ; + + $expected = new ClassMetadata(Dummy::class); + $expected->addAttributeMetadata(new AttributeMetadata('foo')); + $expected->addAttributeMetadata(new AttributeMetadata('bar')); + $expected->addAttributeMetadata(new AttributeMetadata('baz')); + $expected->addAttributeMetadata(new AttributeMetadata('qux')); + + $this->assertEquals($expected, $compiledClassMetadataFactory->getMetadataFor($value)); + } + + public function testItDelegatesGetMetadataForCall() + { + $classMetadataFactory = $this->createMock(ClassMetadataFactoryInterface::class); + $compiledClassMetadataFactory = new CompiledClassMetadataFactory(__DIR__.'/../../Fixtures/serializer.class.metadata.php', $classMetadataFactory); + + $classMetadata = new ClassMetadata(SerializedNameDummy::class); + + $classMetadataFactory + ->expects($this->once()) + ->method('getMetadataFor') + ->with(SerializedNameDummy::class) + ->willReturn($classMetadata) + ; + + $this->assertEquals($classMetadata, $compiledClassMetadataFactory->getMetadataFor(SerializedNameDummy::class)); + } + + public function testItReturnsTheSameInstance() + { + $classMetadataFactory = $this->createMock(ClassMetadataFactoryInterface::class); + $compiledClassMetadataFactory = new CompiledClassMetadataFactory(__DIR__.'/../../Fixtures/serializer.class.metadata.php', $classMetadataFactory); + + $this->assertSame($compiledClassMetadataFactory->getMetadataFor(Dummy::class), $compiledClassMetadataFactory->getMetadataFor(Dummy::class)); + } + + /** + * @dataProvider valueProvider + */ + public function testItHasMetadataFor($value) + { + $classMetadataFactory = $this->createMock(ClassMetadataFactoryInterface::class); + $compiledClassMetadataFactory = new CompiledClassMetadataFactory(__DIR__.'/../../Fixtures/serializer.class.metadata.php', $classMetadataFactory); + + $classMetadataFactory + ->expects($this->never()) + ->method('hasMetadataFor') + ; + + $this->assertTrue($compiledClassMetadataFactory->hasMetadataFor($value)); + } + + public function testItDelegatesHasMetadataForCall() + { + $classMetadataFactory = $this->createMock(ClassMetadataFactoryInterface::class); + $compiledClassMetadataFactory = new CompiledClassMetadataFactory(__DIR__.'/../../Fixtures/serializer.class.metadata.php', $classMetadataFactory); + + $classMetadataFactory + ->expects($this->once()) + ->method('hasMetadataFor') + ->with(SerializedNameDummy::class) + ->willReturn(true) + ; + + $this->assertTrue($compiledClassMetadataFactory->hasMetadataFor(SerializedNameDummy::class)); + } + + public function valueProvider() + { + return [ + [Dummy::class], + [new Dummy()], + ]; + } +} diff --git a/src/Symfony/Component/Serializer/composer.json b/src/Symfony/Component/Serializer/composer.json index 568bff5fa1c34..f2de3df93df7f 100644 --- a/src/Symfony/Component/Serializer/composer.json +++ b/src/Symfony/Component/Serializer/composer.json @@ -28,11 +28,14 @@ "symfony/config": "^4.4|^5.0", "symfony/dependency-injection": "^4.4|^5.0", "symfony/error-handler": "^4.4|^5.0", + "symfony/filesystem": "^4.4|^5.0", "symfony/http-foundation": "^4.4|^5.0", + "symfony/http-kernel": "^4.4|^5.0", "symfony/mime": "^4.4|^5.0", "symfony/property-access": "^4.4|^5.0", "symfony/property-info": "^4.4|^5.0", "symfony/validator": "^4.4|^5.0", + "symfony/var-exporter": "^4.4|^5.0", "symfony/yaml": "^4.4|^5.0" }, "conflict": { @@ -50,7 +53,8 @@ "symfony/property-access": "For using the ObjectNormalizer.", "symfony/mime": "For using a MIME type guesser within the DataUriNormalizer.", "doctrine/annotations": "For using the annotation mapping. You will also need doctrine/cache.", - "doctrine/cache": "For using the default cached annotation reader and metadata cache." + "doctrine/cache": "For using the default cached annotation reader and metadata cache.", + "symfony/var-exporter": "For using the metadata compiler." }, "autoload": { "psr-4": { "Symfony\\Component\\Serializer\\": "" },