From 5fe5737f1f1e36f009128fc7f869fa8fbe4a9eee Mon Sep 17 00:00:00 2001 From: Guilhem Niot Date: Fri, 17 Mar 2017 23:05:35 +0100 Subject: [PATCH 1/3] [Serializer] Add a NormalizerGenerator --- .../Serializer/Dumper/NormalizerDumper.php | 202 ++++++++++++++++++ .../Tests/Dumper/NormalizerDumperTest.php | 39 ++++ .../Tests/Normalizer/ObjectNormalizerTest.php | 55 +++-- 3 files changed, 273 insertions(+), 23 deletions(-) create mode 100644 src/Symfony/Component/Serializer/Dumper/NormalizerDumper.php create mode 100644 src/Symfony/Component/Serializer/Tests/Dumper/NormalizerDumperTest.php diff --git a/src/Symfony/Component/Serializer/Dumper/NormalizerDumper.php b/src/Symfony/Component/Serializer/Dumper/NormalizerDumper.php new file mode 100644 index 0000000000000..3d889e33b30b1 --- /dev/null +++ b/src/Symfony/Component/Serializer/Dumper/NormalizerDumper.php @@ -0,0 +1,202 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Serializer\Dumper; + +use Symfony\Component\Serializer\NormalizerInterface; +use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; +use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface; + +/** + * @author Guilhem Niot + * @author Amrouche Hamza + * @experimental + */ +final class NormalizerDumper +{ + private $classMetadataFactory; + + public function __construct(ClassMetadataFactoryInterface $classMetadataFactory) + { + $this->classMetadataFactory = $classMetadataFactory; + } + + public function dump(string $class, array $context = array()) + { + $reflectionClass = new \ReflectionClass($class); + if (!isset($context['class'])) { + $context['class'] = $reflectionClass->getShortName().'Normalizer'; + } + + $namespaceLine = isset($context['namespace']) ? "\nnamespace {$context['namespace']};\n" : ''; + + return <<generateNormalizeMethod($reflectionClass)} + +{$this->generateSupportsNormalizationMethod($reflectionClass)} +} +EOL; + } + + /** + * Generates the {@see NormalizerInterface::normalize} method. + */ + private function generateNormalizeMethod(\ReflectionClass $reflectionClass): string + { + return <<generateNormalizeMethodInner($reflectionClass)} + } +EOL; + } + + private function generateNormalizeMethodInner(\ReflectionClass $reflectionClass): string + { + $code = <<classMetadataFactory->getMetadataFor($reflectionClass->name)->getAttributesMetadata(); + $maxDepthCode = ''; + foreach ($attributesMetadata as $attributeMetadata) { + if (null === $maxDepth = $attributeMetadata->getMaxDepth()) { + continue; + } + + $key = sprintf(ObjectNormalizer::DEPTH_KEY_PATTERN, $reflectionClass->name, $attributeMetadata->name); + $maxDepthCode .= <<groups) { + $code .= sprintf(" || array_intersect(\$groups, array('%s'))", implode("', '", $attributeMetadata->groups)); + } + $code .= ')'; + + $code .= " && (!isset(\$context['attributes']) || isset(\$context['attributes']['{$attributeMetadata->name}']) || (is_array(\$context['attributes']) && in_array('{$attributeMetadata->name}', \$context['attributes'], true)))"; + + if (null !== $maxDepth = $attributeMetadata->getMaxDepth()) { + $key = sprintf(ObjectNormalizer::DEPTH_KEY_PATTERN, $reflectionClass->name, $attributeMetadata->name); + $code .= " && (!isset(\$context['{$key}']) || {$maxDepth} >= \$context['{$key}'])"; + } + + $code .= ') {'; + + $value = $this->generateGetAttributeValueExpression($attributeMetadata->name, $reflectionClass); + $code .= <<name}'] = \$value; + } else { + \$subContext = \$context; + if (isset(\$context['attributes']['{$attributeMetadata->name}'])) { + \$subContext['attributes'] = \$context['attributes']['{$attributeMetadata->name}']; + } else { + unset(\$subContext['attributes']); + } + + \$output['{$attributeMetadata->name}'] = \$this->normalizer->normalize(\$value, \$format, \$subContext); + } + } +EOL; + } + + $code .= <<camelize($property); + + foreach ($methods = array("get$camelProp", lcfirst($camelProp), "is$camelProp", "has$camelProp)") as $method) { + if ($reflectionClass->hasMethod($method) && $reflectionClass->getMethod($method)) { + return sprintf('$object->%s()', $method); + } + } + + if ($reflectionClass->hasProperty($property) && $reflectionClass->getProperty($property)->isPublic()) { + return sprintf('$object->%s', $property); + } + + if ($reflectionClass->hasMethod('__get') && $reflectionClass->getMethod('__get')) { + return sprintf('$object->__get(\'%s\')', $property); + } + + throw new \DomainException(sprintf('Neither the property "%s" nor one of the methods "%s()", "__get()" exist and have public access in class "%s".', $property, implode('()", "', $methods), $reflectionClass->name)); + } + + private function generateSupportsNormalizationMethod(\ReflectionClass $reflectionClass): string + { + $instanceof = '\\'.$reflectionClass->name; + + return << + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Serializer\Tests\Dumper; + +use Doctrine\Common\Annotations\AnnotationReader; +use Symfony\Component\Serializer\Mapping\Loader\AnnotationLoader; +use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory; +use Symfony\Component\Serializer\Normalizer\NormalizerInterface; +use Symfony\Component\Serializer\Tests\Normalizer\ObjectNormalizerTest; +use Symfony\Component\Serializer\Dumper\NormalizerDumper; + +class NormalizerDumperTest extends ObjectNormalizerTest +{ + protected function getNormalizerFor(string $class): NormalizerInterface + { + $normalizerName = 'Test'.md5($class).'Normalizer'; + + if (!class_exists($normalizerName)) { + $classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader())); + $dumper = new NormalizerDumper($classMetadataFactory); + + eval('?>'.$dumper->dump($class, array('class' => $normalizerName))); + } + + $normalizer = new $normalizerName(); + $normalizer->setNormalizer($this->serializer); + + return $normalizer; + } +} diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/ObjectNormalizerTest.php b/src/Symfony/Component/Serializer/Tests/Normalizer/ObjectNormalizerTest.php index d5d9885f7f679..89c3e094f83e1 100644 --- a/src/Symfony/Component/Serializer/Tests/Normalizer/ObjectNormalizerTest.php +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/ObjectNormalizerTest.php @@ -25,6 +25,7 @@ use Symfony\Component\Serializer\Normalizer\NormalizerInterface; use Symfony\Component\Serializer\Tests\Fixtures\CircularReferenceDummy; use Symfony\Component\Serializer\Tests\Fixtures\MaxDepthDummy; +use Symfony\Component\Serializer\Tests\Fixtures\Sibling; use Symfony\Component\Serializer\Tests\Fixtures\SiblingHolder; use Symfony\Component\Serializer\Mapping\Loader\AnnotationLoader; use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory; @@ -42,7 +43,7 @@ class ObjectNormalizerTest extends TestCase /** * @var SerializerInterface */ - private $serializer; + protected $serializer; protected function setUp() { @@ -51,6 +52,16 @@ protected function setUp() $this->normalizer->setSerializer($this->serializer); } + protected function getNormalizerFor(string $class): NormalizerInterface + { + $classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader())); + + $normalizer = new ObjectNormalizer($classMetadataFactory); + $normalizer->setSerializer($this->serializer); + + return $normalizer; + } + public function testNormalize() { $obj = new ObjectDummy(); @@ -68,6 +79,7 @@ public function testNormalize() ->will($this->returnValue('string_object')) ; + $normalizer = $this->getNormalizerFor(ObjectDummy::class); $this->assertEquals( array( 'foo' => 'foo', @@ -77,7 +89,7 @@ public function testNormalize() 'camelCase' => 'camelcase', 'object' => 'string_object', ), - $this->normalizer->normalize($obj, 'any') + $normalizer->normalize($obj, 'any') ); } @@ -254,9 +266,7 @@ public function testFillWithEmptyDataWhenMissingData() public function testGroupsNormalize() { - $classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader())); - $this->normalizer = new ObjectNormalizer($classMetadataFactory); - $this->normalizer->setSerializer($this->serializer); + $normalizer = $this->getNormalizerFor(GroupDummy::class); $obj = new GroupDummy(); $obj->setFoo('foo'); @@ -268,7 +278,7 @@ public function testGroupsNormalize() $this->assertEquals(array( 'bar' => 'bar', - ), $this->normalizer->normalize($obj, null, array(ObjectNormalizer::GROUPS => array('c')))); + ), $normalizer->normalize($obj, null, array(ObjectNormalizer::GROUPS => array('c')))); $this->assertEquals(array( 'symfony' => 'symfony', @@ -277,7 +287,7 @@ public function testGroupsNormalize() 'bar' => 'bar', 'kevin' => 'kevin', 'coopTilleuls' => 'coopTilleuls', - ), $this->normalizer->normalize($obj, null, array(ObjectNormalizer::GROUPS => array('a', 'c')))); + ), $normalizer->normalize($obj, null, array(ObjectNormalizer::GROUPS => array('a', 'c')))); } public function testGroupsDenormalize() @@ -312,14 +322,12 @@ public function testGroupsDenormalize() public function testNormalizeNoPropertyInGroup() { - $classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader())); - $this->normalizer = new ObjectNormalizer($classMetadataFactory); - $this->normalizer->setSerializer($this->serializer); + $normalizer = $this->getNormalizerFor(GroupDummy::class); $obj = new GroupDummy(); $obj->setFoo('foo'); - $this->assertEquals(array(), $this->normalizer->normalize($obj, null, array('groups' => array('notExist')))); + $this->assertEquals(array(), $normalizer->normalize($obj, null, array('groups' => array('notExist')))); } public function testGroupsNormalizeWithNameConverter() @@ -513,8 +521,10 @@ public function testUnableToNormalizeCircularReference() public function testSiblingReference() { - $serializer = new Serializer(array($this->normalizer)); - $this->normalizer->setSerializer($serializer); + $siblingHolderNormalizer = $this->getNormalizerFor(SiblingHolder::class); + $siblingNormalizer = $this->getNormalizerFor(Sibling::class); + + $serializer = new Serializer(array($siblingHolderNormalizer, $siblingNormalizer)); $siblingHolder = new SiblingHolder(); @@ -523,7 +533,7 @@ public function testSiblingReference() 'sibling1' => array('coopTilleuls' => 'Les-Tilleuls.coop'), 'sibling2' => array('coopTilleuls' => 'Les-Tilleuls.coop'), ); - $this->assertEquals($expected, $this->normalizer->normalize($siblingHolder)); + $this->assertEquals($expected, $serializer->normalize($siblingHolder)); } public function testCircularReferenceHandler() @@ -575,16 +585,15 @@ public function testNormalizeNotSerializableContext() 'bar' => null, ); - $this->assertEquals($expected, $this->normalizer->normalize($objectDummy, null, array('not_serializable' => function () { + $normalizer = $this->getNormalizerFor(ObjectDummy::class); + $this->assertEquals($expected, $normalizer->normalize($objectDummy, null, array('not_serializable' => function () { }))); } public function testMaxDepth() { - $classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader())); - $this->normalizer = new ObjectNormalizer($classMetadataFactory); - $serializer = new Serializer(array($this->normalizer)); - $this->normalizer->setSerializer($serializer); + $normalizer = $this->getNormalizerFor(MaxDepthDummy::class); + $serializer = new Serializer(array($normalizer)); $level1 = new MaxDepthDummy(); $level1->foo = 'level1'; @@ -732,8 +741,9 @@ public function testExtractAttributesRespectsContext() public function testAttributesContextNormalize() { - $normalizer = new ObjectNormalizer(); - $serializer = new Serializer(array($normalizer)); + $innerNormalizer = $this->getNormalizerFor(ObjectInner::class); + $dummyNormalizer = $this->getNormalizerFor(ObjectDummy::class); + $serializer = new Serializer(array($innerNormalizer, $dummyNormalizer)); $objectInner = new ObjectInner(); $objectInner->foo = 'innerFoo'; @@ -744,10 +754,9 @@ public function testAttributesContextNormalize() $objectDummy->setBaz(true); $objectDummy->setObject($objectInner); - $context = array('attributes' => array('foo', 'baz', 'object' => array('foo'))); + $context = array('attributes' => array('baz', 'object' => array('foo'))); $this->assertEquals( array( - 'foo' => 'foo', 'baz' => true, 'object' => array('foo' => 'innerFoo'), ), From 32abf6c45b9200d5b6348e705705cb15d1e619ea Mon Sep 17 00:00:00 2001 From: Guilhem Niot Date: Mon, 19 Feb 2018 13:53:04 +0100 Subject: [PATCH 2/3] Add a config option to enable the feature --- .../DependencyInjection/Configuration.php | 1 + .../FrameworkExtension.php | 6 + .../Resources/config/serializer.xml | 13 ++ .../DependencyInjection/ConfigurationTest.php | 1 + .../Tests/Functional/SerializerTest.php | 26 ++++ .../app/GeneratedSerializer/bundles.php | 16 ++ .../app/GeneratedSerializer/config.yml | 7 + .../Bundle/FrameworkBundle/composer.json | 4 +- .../Serializer/Dumper/NormalizerDumper.php | 11 +- .../Normalizer/AbstractNormalizer.php | 95 +----------- .../Normalizer/CircularReferenceTrait.php | 118 ++++++++++++++ .../Normalizer/GeneratedObjectNormalizer.php | 144 ++++++++++++++++++ .../Tests/Normalizer/ObjectNormalizerTest.php | 32 +++- 13 files changed, 369 insertions(+), 105 deletions(-) create mode 100644 src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/GeneratedSerializer/bundles.php create mode 100644 src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/GeneratedSerializer/config.yml create mode 100644 src/Symfony/Component/Serializer/Normalizer/CircularReferenceTrait.php create mode 100644 src/Symfony/Component/Serializer/Normalizer/GeneratedObjectNormalizer.php diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php index 4eea195b99d2e..1f94d20df0495 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php @@ -728,6 +728,7 @@ private function addSerializerSection(ArrayNodeDefinition $rootNode) ->info('serializer configuration') ->{!class_exists(FullStack::class) && class_exists(Serializer::class) ? 'canBeDisabled' : 'canBeEnabled'}() ->children() + ->booleanNode('enable_normalizer_generation')->defaultFalse()->end() ->booleanNode('enable_annotations')->{!class_exists(FullStack::class) && class_exists(Annotation::class) ? 'defaultTrue' : 'defaultFalse'}()->end() ->scalarNode('name_converter')->end() ->scalarNode('circular_reference_handler')->end() diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index e2c31d5b15b81..2a856314a4399 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -1274,6 +1274,12 @@ private function registerSerializerConfiguration(array $config, ContainerBuilder if (isset($config['circular_reference_handler']) && $config['circular_reference_handler']) { $container->getDefinition('serializer.normalizer.object')->addMethodCall('setCircularReferenceHandler', array(new Reference($config['circular_reference_handler']))); + $container->getDefinition('serializer.normalizer.object.generated')->addMethodCall('setCircularReferenceHandler', array(new Reference($config['circular_reference_handler']))); + } + + if (!$config['enable_normalizer_generation']) { + $container->removeDefinition('serializer.normalizer.object.generated'); + $container->removeDefinition('serializer.normalizer.object.dumper'); } if ($config['max_depth_handler'] ?? false) { diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/serializer.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/serializer.xml index 90c4d1c5b5050..fd77729a09e9e 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/serializer.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/serializer.xml @@ -63,6 +63,19 @@ + + + %kernel.cache_dir% + %kernel.debug% + + + + + + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php index 84921d9737d60..8a4721214c54e 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php @@ -174,6 +174,7 @@ protected static function getBundleDefaultConfig() ), 'serializer' => array( 'enabled' => !class_exists(FullStack::class), + 'enable_normalizer_generation' => false, 'enable_annotations' => !class_exists(FullStack::class), 'mapping' => array('paths' => array()), ), diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/SerializerTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/SerializerTest.php index bc7dc12ebfbca..01301fa08666c 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/SerializerTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/SerializerTest.php @@ -33,6 +33,32 @@ public function testDeserializeArrayOfObject() $this->assertEquals($expected, $result); } + + /** + * @dataProvider caseProvider + */ + public function testSerializeArrayOfObject($testCase) + { + static::bootKernel(array('test_case' => $testCase)); + $container = static::$kernel->getContainer(); + + $bar1 = new Bar(); + $bar1->id = 1; + $bar2 = new Bar(); + $bar2->id = 2; + + $foo = new Foo(); + $foo->bars = array($bar1, $bar2); + + $result = $container->get('serializer')->normalize($foo); + + $this->assertEquals(array('bars' => array(array('id' => 1), array('id' => 2))), $result); + } + + public function caseProvider() + { + return array(array('Serializer'), array('GeneratedSerializer')); + } } class Foo diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/GeneratedSerializer/bundles.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/GeneratedSerializer/bundles.php new file mode 100644 index 0000000000000..144db90236034 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/GeneratedSerializer/bundles.php @@ -0,0 +1,16 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Symfony\Bundle\FrameworkBundle\FrameworkBundle; + +return array( + new FrameworkBundle(), +); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/GeneratedSerializer/config.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/GeneratedSerializer/config.yml new file mode 100644 index 0000000000000..9272078c574ef --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/GeneratedSerializer/config.yml @@ -0,0 +1,7 @@ +imports: + - { resource: ../config/default.yml } + +framework: + serializer: + enable_normalizer_generation: true + enable_annotations: true # required to detect properties diff --git a/src/Symfony/Bundle/FrameworkBundle/composer.json b/src/Symfony/Bundle/FrameworkBundle/composer.json index a2114707c29fb..d4ca7fcd6e617 100644 --- a/src/Symfony/Bundle/FrameworkBundle/composer.json +++ b/src/Symfony/Bundle/FrameworkBundle/composer.json @@ -44,7 +44,7 @@ "symfony/process": "~3.4|~4.0", "symfony/security-core": "~3.4|~4.0", "symfony/security-csrf": "~3.4|~4.0", - "symfony/serializer": "~3.4|~4.0", + "symfony/serializer": "~4.1", "symfony/stopwatch": "~3.4|~4.0", "symfony/translation": "~3.4|~4.0", "symfony/templating": "~3.4|~4.0", @@ -66,7 +66,7 @@ "symfony/asset": "<3.4", "symfony/console": "<3.4", "symfony/form": "<3.4", - "symfony/property-info": "<3.4", + "symfony/property-info": "<4.1", "symfony/serializer": "<3.4", "symfony/stopwatch": "<3.4", "symfony/translation": "<3.4", diff --git a/src/Symfony/Component/Serializer/Dumper/NormalizerDumper.php b/src/Symfony/Component/Serializer/Dumper/NormalizerDumper.php index 3d889e33b30b1..97b9e39174278 100644 --- a/src/Symfony/Component/Serializer/Dumper/NormalizerDumper.php +++ b/src/Symfony/Component/Serializer/Dumper/NormalizerDumper.php @@ -18,6 +18,7 @@ /** * @author Guilhem Niot * @author Amrouche Hamza + * * @experimental */ final class NormalizerDumper @@ -42,6 +43,7 @@ public function dump(string $class, array $context = array()) generateNormalizeMethod($reflectionClass)} @@ -79,11 +81,8 @@ private function generateNormalizeMethodInner(\ReflectionClass $reflectionClass) { $code = <<isCircularReference(\$object, \$context)) { + return \$this->handleCircularReference(\$object); } \$groups = isset(\$context[ObjectNormalizer::GROUPS]) && is_array(\$context[ObjectNormalizer::GROUPS]) ? \$context[ObjectNormalizer::GROUPS] : null; diff --git a/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php index b7a6b4b14b17c..c0be5ebff33e3 100644 --- a/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php @@ -11,7 +11,6 @@ namespace Symfony\Component\Serializer\Normalizer; -use Symfony\Component\Serializer\Exception\CircularReferenceException; use Symfony\Component\Serializer\Exception\MissingConstructorArgumentsException; use Symfony\Component\Serializer\Exception\InvalidArgumentException; use Symfony\Component\Serializer\Exception\LogicException; @@ -29,6 +28,7 @@ */ abstract class AbstractNormalizer implements NormalizerInterface, DenormalizerInterface, SerializerAwareInterface { + use CircularReferenceTrait; use ObjectToPopulateTrait; use SerializerAwareTrait; @@ -39,16 +39,6 @@ abstract class AbstractNormalizer implements NormalizerInterface, DenormalizerIn const ALLOW_EXTRA_ATTRIBUTES = 'allow_extra_attributes'; const DEFAULT_CONSTRUCTOR_ARGUMENTS = 'default_constructor_arguments'; - /** - * @var int - */ - protected $circularReferenceLimit = 1; - - /** - * @var callable - */ - protected $circularReferenceHandler; - /** * @var ClassMetadataFactoryInterface|null */ @@ -83,34 +73,6 @@ public function __construct(ClassMetadataFactoryInterface $classMetadataFactory $this->nameConverter = $nameConverter; } - /** - * Set circular reference limit. - * - * @param int $circularReferenceLimit Limit of iterations for the same object - * - * @return self - */ - public function setCircularReferenceLimit($circularReferenceLimit) - { - $this->circularReferenceLimit = $circularReferenceLimit; - - return $this; - } - - /** - * Set circular reference handler. - * - * @param callable $circularReferenceHandler - * - * @return self - */ - public function setCircularReferenceHandler(callable $circularReferenceHandler) - { - $this->circularReferenceHandler = $circularReferenceHandler; - - return $this; - } - /** * Set normalization callbacks. * @@ -147,56 +109,6 @@ public function setIgnoredAttributes(array $ignoredAttributes) return $this; } - /** - * Detects if the configured circular reference limit is reached. - * - * @param object $object - * @param array $context - * - * @return bool - * - * @throws CircularReferenceException - */ - protected function isCircularReference($object, &$context) - { - $objectHash = spl_object_hash($object); - - if (isset($context[static::CIRCULAR_REFERENCE_LIMIT][$objectHash])) { - if ($context[static::CIRCULAR_REFERENCE_LIMIT][$objectHash] >= $this->circularReferenceLimit) { - unset($context[static::CIRCULAR_REFERENCE_LIMIT][$objectHash]); - - return true; - } - - ++$context[static::CIRCULAR_REFERENCE_LIMIT][$objectHash]; - } else { - $context[static::CIRCULAR_REFERENCE_LIMIT][$objectHash] = 1; - } - - return false; - } - - /** - * Handles a circular reference. - * - * If a circular reference handler is set, it will be called. Otherwise, a - * {@class CircularReferenceException} will be thrown. - * - * @param object $object - * - * @return mixed - * - * @throws CircularReferenceException - */ - protected function handleCircularReference($object) - { - if ($this->circularReferenceHandler) { - return \call_user_func($this->circularReferenceHandler, $object); - } - - throw new CircularReferenceException(sprintf('A circular reference has been detected when serializing the object of class "%s" (configured limit: %d)', \get_class($object), $this->circularReferenceLimit)); - } - /** * Gets attributes to normalize using groups. * @@ -404,4 +316,9 @@ protected function createChildContext(array $parentContext, $attribute) return $parentContext; } + + private function getCircularReferenceLimitField() + { + return static::CIRCULAR_REFERENCE_LIMIT; + } } diff --git a/src/Symfony/Component/Serializer/Normalizer/CircularReferenceTrait.php b/src/Symfony/Component/Serializer/Normalizer/CircularReferenceTrait.php new file mode 100644 index 0000000000000..8ee3776b4acf3 --- /dev/null +++ b/src/Symfony/Component/Serializer/Normalizer/CircularReferenceTrait.php @@ -0,0 +1,118 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Serializer\Normalizer; + +use Symfony\Component\Serializer\Exception\CircularReferenceException; + +/** + * Handle circular references. + * + * @author Kévin Dunglas + * + * @internal + */ +trait CircularReferenceTrait +{ + /** + * @var int + */ + protected $circularReferenceLimit = 1; + + /** + * @var callable + */ + protected $circularReferenceHandler; + + /** + * Set circular reference limit. + * + * @param int $circularReferenceLimit Limit of iterations for the same object + * + * @return self + */ + public function setCircularReferenceLimit($circularReferenceLimit) + { + $this->circularReferenceLimit = $circularReferenceLimit; + + return $this; + } + + /** + * Set circular reference handler. + * + * @param callable $circularReferenceHandler + * + * @return self + */ + public function setCircularReferenceHandler(callable $circularReferenceHandler) + { + $this->circularReferenceHandler = $circularReferenceHandler; + + return $this; + } + + /** + * Detects if the configured circular reference limit is reached. + * + * @param object $object + * @param array $context + * + * @return bool + * + * @throws CircularReferenceException + */ + protected function isCircularReference($object, &$context) + { + $objectHash = spl_object_hash($object); + + $circularReferenceLimitField = $this->getCircularReferenceLimitField(); + if (isset($context[$circularReferenceLimitField][$objectHash])) { + if ($context[$circularReferenceLimitField][$objectHash] >= $this->circularReferenceLimit) { + unset($context[$circularReferenceLimitField][$objectHash]); + + return true; + } + + ++$context[$circularReferenceLimitField][$objectHash]; + } else { + $context[$circularReferenceLimitField][$objectHash] = 1; + } + + return false; + } + + /** + * Handles a circular reference. + * + * If a circular reference handler is set, it will be called. Otherwise, a + * {@class CircularReferenceException} will be thrown. + * + * @param object $object + * + * @return mixed + * + * @throws CircularReferenceException + */ + protected function handleCircularReference($object) + { + if ($this->circularReferenceHandler) { + return \call_user_func($this->circularReferenceHandler, $object); + } + + throw new CircularReferenceException(sprintf('A circular reference has been detected when serializing the object of class "%s" (configured limit: %d)', \get_class($object), $this->circularReferenceLimit)); + } + + private function getCircularReferenceLimitField() + { + return ObjectNormalizer::CIRCULAR_REFERENCE_LIMIT; + } +} diff --git a/src/Symfony/Component/Serializer/Normalizer/GeneratedObjectNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/GeneratedObjectNormalizer.php new file mode 100644 index 0000000000000..58a779cf5f580 --- /dev/null +++ b/src/Symfony/Component/Serializer/Normalizer/GeneratedObjectNormalizer.php @@ -0,0 +1,144 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Serializer\Normalizer; + +use Symfony\Component\Config\ConfigCacheFactoryInterface; +use Symfony\Component\Config\ConfigCacheFactory; +use Symfony\Component\Config\ConfigCacheInterface; +use Symfony\Component\Config\Resource\ReflectionClassResource; +use Symfony\Component\Serializer\Dumper\NormalizerDumper; + +/** + * Normalize objects using generated normalizers. + * + * @author Guilhem Niot + */ +class GeneratedObjectNormalizer implements NormalizerInterface, NormalizerAwareInterface +{ + use NormalizerAwareTrait; + + private $normalizerDumper; + private $cacheDir; + private $debug; + + /** + * @var NormalizerInterface[] + */ + private $normalizers = array(); + + /** + * @var ConfigCacheFactoryInterface|null + */ + private $configCacheFactory; + + /** + * @var int + */ + protected $circularReferenceLimit = 1; + + /** + * @var callable|null + */ + protected $circularReferenceHandler; + + public function __construct(NormalizerDumper $normalizerDumper, string $cacheDir, bool $debug) + { + $this->normalizerDumper = $normalizerDumper; + $this->cacheDir = $cacheDir; + $this->debug = $debug; + } + + /** + * {@inheritdoc} + */ + public function normalize($object, $format = null, array $context = array()) + { + $class = get_class($object); + if (isset($this->normalizers[$class])) { + return $this->normalizers[$class]->normalize($object, $format, $context); + } + + $normalizerClass = 'Symfony\Component\Serializer\Normalizer\Generated\\'.$class.'Normalizer'; + $cache = $this->getConfigCacheFactory()->cache($this->cacheDir.'/normalizers-'.str_replace('\\', '-', $class).'.php', + function (ConfigCacheInterface $cache) use ($class, $normalizerClass) { + $pos = strrpos($normalizerClass, '\\'); + $code = $this->normalizerDumper->dump($class, array( + 'class' => substr($normalizerClass, $pos + 1), + 'namespace' => substr($normalizerClass, 0, $pos), + )); + + $cache->write($code, array(new ReflectionClassResource(new \ReflectionClass($class)))); + } + ); + + require_once $cache->getPath(); + + $this->normalizers[$class] = $normalizer = new $normalizerClass(); + $normalizer->setNormalizer($this->normalizer); + $normalizer->setCircularReferenceLimit($this->circularReferenceLimit); + if (null !== $this->circularReferenceHandler) { + $normalizer->setCircularReferenceHandler($this->circularReferenceHandler); + } + + return $normalizer->normalize($object, $format, $context); + } + + /** + * {@inheritdoc} + */ + public function supportsNormalization($data, $format = null) + { + return \is_object($data) && !$data instanceof \Traversable; + } + + /** + * Set circular reference limit. + * + * @param int $circularReferenceLimit Limit of iterations for the same object + * + * @return self + */ + public function setCircularReferenceLimit($circularReferenceLimit) + { + $this->circularReferenceLimit = $circularReferenceLimit; + + return $this; + } + + /** + * Set circular reference handler. + * + * @param callable $circularReferenceHandler + * + * @return self + */ + public function setCircularReferenceHandler(callable $circularReferenceHandler) + { + $this->circularReferenceHandler = $circularReferenceHandler; + + return $this; + } + + private function generateUniqueName($class) + { + return str_replace('\\', '-', $class); + } + + private function getConfigCacheFactory(): ConfigCacheFactoryInterface + { + if (null === $this->configCacheFactory) { + $this->configCacheFactory = new ConfigCacheFactory($this->debug); + } + + return $this->configCacheFactory; + } +} diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/ObjectNormalizerTest.php b/src/Symfony/Component/Serializer/Tests/Normalizer/ObjectNormalizerTest.php index 89c3e094f83e1..950d2653b6749 100644 --- a/src/Symfony/Component/Serializer/Tests/Normalizer/ObjectNormalizerTest.php +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/ObjectNormalizerTest.php @@ -510,13 +510,19 @@ public function testUnableToNormalizeObjectAttribute() */ public function testUnableToNormalizeCircularReference() { - $serializer = new Serializer(array($this->normalizer)); - $this->normalizer->setSerializer($serializer); - $this->normalizer->setCircularReferenceLimit(2); + $normalizer = $this->getNormalizerFor(CircularReferenceDummy::class); + + $serializer = new Serializer(array($normalizer)); + if ($normalizer instanceof ObjectNormalizer) { + $normalizer->setSerializer($serializer); + } else { + $normalizer->setNormalizer($serializer); + } + $normalizer->setCircularReferenceLimit(2); $obj = new CircularReferenceDummy(); - $this->normalizer->normalize($obj); + $normalizer->normalize($obj); } public function testSiblingReference() @@ -538,16 +544,22 @@ public function testSiblingReference() public function testCircularReferenceHandler() { - $serializer = new Serializer(array($this->normalizer)); - $this->normalizer->setSerializer($serializer); - $this->normalizer->setCircularReferenceHandler(function ($obj) { + $normalizer = $this->getNormalizerFor(CircularReferenceDummy::class); + $serializer = new Serializer(array($normalizer)); + + if ($normalizer instanceof ObjectNormalizer) { + $normalizer->setSerializer($serializer); + } else { + $normalizer->setNormalizer($serializer); + } + $normalizer->setCircularReferenceHandler(function ($obj) { return get_class($obj); }); $obj = new CircularReferenceDummy(); $expected = array('me' => 'Symfony\Component\Serializer\Tests\Fixtures\CircularReferenceDummy'); - $this->assertEquals($expected, $this->normalizer->normalize($obj)); + $this->assertEquals($expected, $normalizer->normalize($obj)); } public function testDenormalizeNonExistingAttribute() @@ -637,6 +649,10 @@ public function testMaxDepth() ), ); + $classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader())); + $this->normalizer = new ObjectNormalizer($classMetadataFactory); + $serializer = new Serializer(array($this->normalizer)); + $this->normalizer->setSerializer($serializer); $this->normalizer->setMaxDepthHandler(function ($obj) { return 'handler'; }); From 133766f87b3fcb0c3ea4d76e06bac050e67276c9 Mon Sep 17 00:00:00 2001 From: Guilhem Niot Date: Sat, 3 Nov 2018 17:43:40 +0100 Subject: [PATCH 3/3] Update the tests --- .../Serializer/Dumper/NormalizerDumper.php | 21 ++++++++-------- .../Normalizer/AbstractNormalizer.php | 1 - .../Normalizer/GeneratedObjectNormalizer.php | 24 +++++-------------- .../Tests/Dumper/NormalizerDumperTest.php | 8 +++---- .../Tests/Normalizer/ObjectNormalizerTest.php | 12 ++++------ 5 files changed, 25 insertions(+), 41 deletions(-) diff --git a/src/Symfony/Component/Serializer/Dumper/NormalizerDumper.php b/src/Symfony/Component/Serializer/Dumper/NormalizerDumper.php index 89f6359f6d474..04e27df8890d1 100644 --- a/src/Symfony/Component/Serializer/Dumper/NormalizerDumper.php +++ b/src/Symfony/Component/Serializer/Dumper/NormalizerDumper.php @@ -11,9 +11,9 @@ namespace Symfony\Component\Serializer\Dumper; -use Symfony\Component\Serializer\NormalizerInterface; -use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface; +use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; +use Symfony\Component\Serializer\NormalizerInterface; /** * @author Guilhem Niot @@ -61,7 +61,7 @@ class {$context['class']} implements NormalizerInterface, NormalizerAwareInterfa use CircularReferenceTrait, NormalizerAwareTrait; - public function __construct(array \$defaultContext) + public function __construct(array \$defaultContext = array()) { \$this->defaultContext = array_merge(\$this->defaultContext, \$defaultContext); } @@ -91,7 +91,7 @@ private function generateNormalizeMethodInner(\ReflectionClass $reflectionClass) $code = <<isCircularReference(\$object, \$context)) { - return \$this->handleCircularReference(\$object); + return \$this->handleCircularReference(\$object, \$format, \$context); } \$groups = isset(\$context[ObjectNormalizer::GROUPS]) && is_array(\$context[ObjectNormalizer::GROUPS]) ? \$context[ObjectNormalizer::GROUPS] : null; @@ -113,10 +113,9 @@ private function generateNormalizeMethodInner(\ReflectionClass $reflectionClass) } if ($maxDepthCode) { - $maxDepthKey = ObjectNormalizer::ENABLE_MAX_DEPTH; $code .= <<defaultContext[$maxDepthKey]) {{$maxDepthCode} + if (\$context[ObjectNormalizer::ENABLE_MAX_DEPTH] ?? \$this->defaultContext[ObjectNormalizer::ENABLE_MAX_DEPTH]) {{$maxDepthCode} } EOL; @@ -125,7 +124,7 @@ private function generateNormalizeMethodInner(\ReflectionClass $reflectionClass) foreach ($attributesMetadata as $attributeMetadata) { $code .= <<defaultContext['attributes'] ?? null + \$attributes = \$context[ObjectNormalizer::ATTRIBUTES] ?? \$this->defaultContext[ObjectNormalizer::ATTRIBUTES] ?? null; if ((null === \$groups EOL; @@ -134,7 +133,7 @@ private function generateNormalizeMethodInner(\ReflectionClass $reflectionClass) } $code .= ')'; - $code .= " && (isset(\$attributes['{$attributeMetadata->name}']) || (is_array(\$attributes) && in_array('{$attributeMetadata->name}', \$attributes, true)))"; + $code .= " && (null === \$attributes || isset(\$attributes['{$attributeMetadata->name}']) || (is_array(\$attributes) && in_array('{$attributeMetadata->name}', \$attributes, true)))"; if (null !== $maxDepth = $attributeMetadata->getMaxDepth()) { $key = sprintf(ObjectNormalizer::DEPTH_KEY_PATTERN, $reflectionClass->name, $attributeMetadata->name); @@ -151,10 +150,10 @@ private function generateNormalizeMethodInner(\ReflectionClass $reflectionClass) \$output['{$attributeMetadata->name}'] = \$value; } else { \$subContext = \$context; - if (isset(\$context['attributes']['{$attributeMetadata->name}'])) { - \$subContext['attributes'] = \$context['attributes']['{$attributeMetadata->name}']; + if (isset(\$attributes['{$attributeMetadata->name}'])) { + \$subContext[ObjectNormalizer::ATTRIBUTES] = \$attributes['{$attributeMetadata->name}']; } else { - unset(\$subContext['attributes']); + unset(\$subContext[ObjectNormalizer::ATTRIBUTES]); } \$output['{$attributeMetadata->name}'] = \$this->normalizer->normalize(\$value, \$format, \$subContext); diff --git a/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php index ba4f156ca698f..51a309e82a06f 100644 --- a/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php @@ -11,7 +11,6 @@ namespace Symfony\Component\Serializer\Normalizer; -use Symfony\Component\Serializer\Exception\CircularReferenceException; use Symfony\Component\Serializer\Exception\InvalidArgumentException; use Symfony\Component\Serializer\Exception\LogicException; use Symfony\Component\Serializer\Exception\MissingConstructorArgumentsException; diff --git a/src/Symfony/Component/Serializer/Normalizer/GeneratedObjectNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/GeneratedObjectNormalizer.php index 58a779cf5f580..986dbed755b9d 100644 --- a/src/Symfony/Component/Serializer/Normalizer/GeneratedObjectNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/GeneratedObjectNormalizer.php @@ -11,8 +11,8 @@ namespace Symfony\Component\Serializer\Normalizer; -use Symfony\Component\Config\ConfigCacheFactoryInterface; use Symfony\Component\Config\ConfigCacheFactory; +use Symfony\Component\Config\ConfigCacheFactoryInterface; use Symfony\Component\Config\ConfigCacheInterface; use Symfony\Component\Config\Resource\ReflectionClassResource; use Symfony\Component\Serializer\Dumper\NormalizerDumper; @@ -29,6 +29,7 @@ class GeneratedObjectNormalizer implements NormalizerInterface, NormalizerAwareI private $normalizerDumper; private $cacheDir; private $debug; + protected $defaultContext; /** * @var NormalizerInterface[] @@ -40,21 +41,12 @@ class GeneratedObjectNormalizer implements NormalizerInterface, NormalizerAwareI */ private $configCacheFactory; - /** - * @var int - */ - protected $circularReferenceLimit = 1; - - /** - * @var callable|null - */ - protected $circularReferenceHandler; - - public function __construct(NormalizerDumper $normalizerDumper, string $cacheDir, bool $debug) + public function __construct(NormalizerDumper $normalizerDumper, string $cacheDir, bool $debug, array $defaultContext = array()) { $this->normalizerDumper = $normalizerDumper; $this->cacheDir = $cacheDir; $this->debug = $debug; + $this->defaultContext = $defaultContext; } /** @@ -62,7 +54,7 @@ public function __construct(NormalizerDumper $normalizerDumper, string $cacheDir */ public function normalize($object, $format = null, array $context = array()) { - $class = get_class($object); + $class = \get_class($object); if (isset($this->normalizers[$class])) { return $this->normalizers[$class]->normalize($object, $format, $context); } @@ -82,12 +74,8 @@ function (ConfigCacheInterface $cache) use ($class, $normalizerClass) { require_once $cache->getPath(); - $this->normalizers[$class] = $normalizer = new $normalizerClass(); + $this->normalizers[$class] = $normalizer = new $normalizerClass($this->defaultContext); $normalizer->setNormalizer($this->normalizer); - $normalizer->setCircularReferenceLimit($this->circularReferenceLimit); - if (null !== $this->circularReferenceHandler) { - $normalizer->setCircularReferenceHandler($this->circularReferenceHandler); - } return $normalizer->normalize($object, $format, $context); } diff --git a/src/Symfony/Component/Serializer/Tests/Dumper/NormalizerDumperTest.php b/src/Symfony/Component/Serializer/Tests/Dumper/NormalizerDumperTest.php index 0b30f0c28baf5..fb42a3483001e 100644 --- a/src/Symfony/Component/Serializer/Tests/Dumper/NormalizerDumperTest.php +++ b/src/Symfony/Component/Serializer/Tests/Dumper/NormalizerDumperTest.php @@ -12,15 +12,15 @@ namespace Symfony\Component\Serializer\Tests\Dumper; use Doctrine\Common\Annotations\AnnotationReader; -use Symfony\Component\Serializer\Mapping\Loader\AnnotationLoader; +use Symfony\Component\Serializer\Dumper\NormalizerDumper; use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory; +use Symfony\Component\Serializer\Mapping\Loader\AnnotationLoader; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; use Symfony\Component\Serializer\Tests\Normalizer\ObjectNormalizerTest; -use Symfony\Component\Serializer\Dumper\NormalizerDumper; class NormalizerDumperTest extends ObjectNormalizerTest { - protected function getNormalizerFor(string $class): NormalizerInterface + protected function getNormalizerFor(string $class, array $defaultContext = array()): NormalizerInterface { $normalizerName = 'Test'.md5($class).'Normalizer'; @@ -31,7 +31,7 @@ protected function getNormalizerFor(string $class): NormalizerInterface eval('?>'.$dumper->dump($class, array('class' => $normalizerName))); } - $normalizer = new $normalizerName(); + $normalizer = new $normalizerName($defaultContext); $normalizer->setNormalizer($this->serializer); return $normalizer; diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/ObjectNormalizerTest.php b/src/Symfony/Component/Serializer/Tests/Normalizer/ObjectNormalizerTest.php index 70d5b393e340d..017e8a305b592 100644 --- a/src/Symfony/Component/Serializer/Tests/Normalizer/ObjectNormalizerTest.php +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/ObjectNormalizerTest.php @@ -61,11 +61,11 @@ private function createNormalizer(array $defaultContext = array(), ClassMetadata $this->normalizer->setSerializer($this->serializer); } - protected function getNormalizerFor(string $class): NormalizerInterface + protected function getNormalizerFor(string $class, array $defaultContext = array()): NormalizerInterface { $classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader())); - $normalizer = new ObjectNormalizer($classMetadataFactory); + $normalizer = new ObjectNormalizer($classMetadataFactory, null, null, null, null, null, $defaultContext); $normalizer->setSerializer($this->serializer); return $normalizer; @@ -607,13 +607,12 @@ public function testLegacyUnableToNormalizeCircularReference() private function doTestUnableToNormalizeCircularReference(bool $legacy = false) { - $legacy ? $this->normalizer->setCircularReferenceLimit(2) : $this->createNormalizer(array(ObjectNormalizer::CIRCULAR_REFERENCE_LIMIT => 2)); + $legacy ? $this->normalizer->setCircularReferenceLimit(2) : $this->normalizer = $this->getNormalizerFor(CircularReferenceDummy::class, array(ObjectNormalizer::CIRCULAR_REFERENCE_LIMIT => 2)); $serializer = new Serializer(array($this->normalizer)); - $this->normalizer->setSerializer($serializer); $obj = new CircularReferenceDummy(); - $normalizer->normalize($obj); + $this->normalizer->normalize($obj); } public function testSiblingReference() @@ -665,9 +664,8 @@ private function doTestCircularReferenceHandler(bool $legacy = false) private function createNormalizerWithCircularReferenceHandler(callable $handler, bool $legacy) { - $legacy ? $this->normalizer->setCircularReferenceHandler($handler) : $this->createNormalizer(array(ObjectNormalizer::CIRCULAR_REFERENCE_HANDLER => $handler)); + $legacy ? $this->normalizer->setCircularReferenceHandler($handler) : $this->normalizer = $this->getNormalizerFor(CircularReferenceDummy::class, array(ObjectNormalizer::CIRCULAR_REFERENCE_HANDLER => $handler)); $this->serializer = new Serializer(array($this->normalizer)); - $this->normalizer->setSerializer($this->serializer); } public function testDenormalizeNonExistingAttribute()