diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php index b98f7914be3aa..b3d67cabbade9 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php @@ -796,6 +796,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 ab0e271a12dce..8ff5d99652de4 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -1374,6 +1374,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 2cd70ff083894..818ea92433bff 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/serializer.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/serializer.xml @@ -68,6 +68,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 5ef3cbf13585b..2075e6a35b024 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php @@ -210,6 +210,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 bdcf462baea22..9be7a0a9e929a 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/SerializerTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/SerializerTest.php @@ -32,6 +32,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/Component/Serializer/Dumper/NormalizerDumper.php b/src/Symfony/Component/Serializer/Dumper/NormalizerDumper.php new file mode 100644 index 0000000000000..04e27df8890d1 --- /dev/null +++ b/src/Symfony/Component/Serializer/Dumper/NormalizerDumper.php @@ -0,0 +1,211 @@ + + * + * 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\Mapping\Factory\ClassMetadataFactoryInterface; +use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; +use Symfony\Component\Serializer\NormalizerInterface; + +/** + * @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 << 1, + ); + + use CircularReferenceTrait, NormalizerAwareTrait; + + public function __construct(array \$defaultContext = array()) + { + \$this->defaultContext = array_merge(\$this->defaultContext, \$defaultContext); + } + +{$this->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 = <<isCircularReference(\$object, \$context)) { + return \$this->handleCircularReference(\$object, \$format, \$context); + } + + \$groups = isset(\$context[ObjectNormalizer::GROUPS]) && is_array(\$context[ObjectNormalizer::GROUPS]) ? \$context[ObjectNormalizer::GROUPS] : null; + + \$output = array(); +EOL; + + $attributesMetadata = $this->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 .= <<defaultContext[ObjectNormalizer::ENABLE_MAX_DEPTH]) {{$maxDepthCode} + } + +EOL; + } + + foreach ($attributesMetadata as $attributeMetadata) { + $code .= <<defaultContext[ObjectNormalizer::ATTRIBUTES] ?? null; + if ((null === \$groups +EOL; + + if ($attributeMetadata->groups) { + $code .= sprintf(" || array_intersect(\$groups, array('%s'))", implode("', '", $attributeMetadata->groups)); + } + $code .= ')'; + + $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); + $code .= " && (!isset(\$context['{$key}']) || {$maxDepth} >= \$context['{$key}'])"; + } + + $code .= ') {'; + + $value = $this->generateGetAttributeValueExpression($attributeMetadata->name, $reflectionClass); + $code .= <<name}'] = \$value; + } else { + \$subContext = \$context; + if (isset(\$attributes['{$attributeMetadata->name}'])) { + \$subContext[ObjectNormalizer::ATTRIBUTES] = \$attributes['{$attributeMetadata->name}']; + } else { + unset(\$subContext[ObjectNormalizer::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 << array(), ); - /** - * @deprecated since Symfony 4.2 - */ - protected $circularReferenceLimit = 1; - - /** - * @deprecated since Symfony 4.2 - * - * @var callable|null - */ - protected $circularReferenceHandler; - /** * @var ClassMetadataFactoryInterface|null */ @@ -193,68 +181,6 @@ public function hasCacheableSupportsMethod(): bool return false; } - /** - * 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); - - $circularReferenceLimit = $context[self::CIRCULAR_REFERENCE_LIMIT] ?? $this->defaultContext[self::CIRCULAR_REFERENCE_LIMIT] ?? $this->circularReferenceLimit; - if (isset($context[self::CIRCULAR_REFERENCE_LIMIT_COUNTERS][$objectHash])) { - if ($context[self::CIRCULAR_REFERENCE_LIMIT_COUNTERS][$objectHash] >= $circularReferenceLimit) { - unset($context[self::CIRCULAR_REFERENCE_LIMIT_COUNTERS][$objectHash]); - - return true; - } - - ++$context[self::CIRCULAR_REFERENCE_LIMIT_COUNTERS][$objectHash]; - } else { - $context[self::CIRCULAR_REFERENCE_LIMIT_COUNTERS][$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. - * - * @final since Symfony 4.2 - * - * @param object $object - * @param string|null $format - * @param array $context - * - * @return mixed - * - * @throws CircularReferenceException - */ - protected function handleCircularReference($object/*, string $format = null, array $context = array()*/) - { - if (\func_num_args() < 2 && __CLASS__ !== \get_class($this) && __CLASS__ !== (new \ReflectionMethod($this, __FUNCTION__))->getDeclaringClass()->getName() && !$this instanceof \PHPUnit\Framework\MockObject\MockObject && !$this instanceof \Prophecy\Prophecy\ProphecySubjectInterface) { - @trigger_error(sprintf('The "%s()" method will have two new "string $format = null" and "array $context = array()" arguments in version 5.0, not defining it is deprecated since Symfony 4.2.', __METHOD__), E_USER_DEPRECATED); - } - $format = \func_num_args() > 1 ? func_get_arg(1) : null; - $context = \func_num_args() > 2 ? func_get_arg(2) : array(); - - $circularReferenceHandler = $context[self::CIRCULAR_REFERENCE_HANDLER] ?? $this->defaultContext[self::CIRCULAR_REFERENCE_HANDLER] ?? $this->circularReferenceHandler; - if ($circularReferenceHandler) { - return $circularReferenceHandler($object, $format, $context); - } - - 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. * diff --git a/src/Symfony/Component/Serializer/Normalizer/CircularReferenceTrait.php b/src/Symfony/Component/Serializer/Normalizer/CircularReferenceTrait.php new file mode 100644 index 0000000000000..8ed8cff6875b7 --- /dev/null +++ b/src/Symfony/Component/Serializer/Normalizer/CircularReferenceTrait.php @@ -0,0 +1,98 @@ + + * + * 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 +{ + /** + * @deprecated since Symfony 4.2 + */ + protected $circularReferenceLimit = 1; + + /** + * @deprecated since Symfony 4.2 + * + * @var callable|null + */ + protected $circularReferenceHandler; + + /** + * 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); + + $circularReferenceLimit = $context[AbstractNormalizer::CIRCULAR_REFERENCE_LIMIT] ?? $this->defaultContext[AbstractNormalizer::CIRCULAR_REFERENCE_LIMIT] ?? $this->circularReferenceLimit; + if (isset($context[AbstractNormalizer::CIRCULAR_REFERENCE_LIMIT_COUNTERS][$objectHash])) { + if ($context[AbstractNormalizer::CIRCULAR_REFERENCE_LIMIT_COUNTERS][$objectHash] >= $circularReferenceLimit) { + unset($context[AbstractNormalizer::CIRCULAR_REFERENCE_LIMIT_COUNTERS][$objectHash]); + + return true; + } + + ++$context[AbstractNormalizer::CIRCULAR_REFERENCE_LIMIT_COUNTERS][$objectHash]; + } else { + $context[AbstractNormalizer::CIRCULAR_REFERENCE_LIMIT_COUNTERS][$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. + * + * @final since Symfony 4.2 + * + * @param object $object + * @param string|null $format + * @param array $context + * + * @return mixed + * + * @throws CircularReferenceException + */ + protected function handleCircularReference($object/*, string $format = null, array $context = array()*/) + { + if (\func_num_args() < 2 && __CLASS__ !== \get_class($this) && __CLASS__ !== (new \ReflectionMethod($this, __FUNCTION__))->getDeclaringClass()->getName() && !$this instanceof \PHPUnit\Framework\MockObject\MockObject && !$this instanceof \Prophecy\Prophecy\ProphecySubjectInterface) { + @trigger_error(sprintf('The "%s()" method will have two new "string $format = null" and "array $context = array()" arguments in version 5.0, not defining it is deprecated since Symfony 4.2.', __METHOD__), E_USER_DEPRECATED); + } + $format = \func_num_args() > 1 ? func_get_arg(1) : null; + $context = \func_num_args() > 2 ? func_get_arg(2) : array(); + + $circularReferenceHandler = $context[AbstractNormalizer::CIRCULAR_REFERENCE_HANDLER] ?? $this->defaultContext[AbstractNormalizer::CIRCULAR_REFERENCE_HANDLER] ?? $this->circularReferenceHandler; + if ($circularReferenceHandler) { + return $circularReferenceHandler($object, $format, $context); + } + + 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)); + } +} diff --git a/src/Symfony/Component/Serializer/Normalizer/GeneratedObjectNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/GeneratedObjectNormalizer.php new file mode 100644 index 0000000000000..986dbed755b9d --- /dev/null +++ b/src/Symfony/Component/Serializer/Normalizer/GeneratedObjectNormalizer.php @@ -0,0 +1,132 @@ + + * + * 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\ConfigCacheFactory; +use Symfony\Component\Config\ConfigCacheFactoryInterface; +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; + protected $defaultContext; + + /** + * @var NormalizerInterface[] + */ + private $normalizers = array(); + + /** + * @var ConfigCacheFactoryInterface|null + */ + private $configCacheFactory; + + public function __construct(NormalizerDumper $normalizerDumper, string $cacheDir, bool $debug, array $defaultContext = array()) + { + $this->normalizerDumper = $normalizerDumper; + $this->cacheDir = $cacheDir; + $this->debug = $debug; + $this->defaultContext = $defaultContext; + } + + /** + * {@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($this->defaultContext); + $normalizer->setNormalizer($this->normalizer); + + 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/Dumper/NormalizerDumperTest.php b/src/Symfony/Component/Serializer/Tests/Dumper/NormalizerDumperTest.php new file mode 100644 index 0000000000000..fb42a3483001e --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Dumper/NormalizerDumperTest.php @@ -0,0 +1,39 @@ + + * + * 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\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; + +class NormalizerDumperTest extends ObjectNormalizerTest +{ + protected function getNormalizerFor(string $class, array $defaultContext = array()): 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($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 9eff7914d1c89..017e8a305b592 100644 --- a/src/Symfony/Component/Serializer/Tests/Normalizer/ObjectNormalizerTest.php +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/ObjectNormalizerTest.php @@ -32,6 +32,7 @@ use Symfony\Component\Serializer\Tests\Fixtures\GroupDummy; use Symfony\Component\Serializer\Tests\Fixtures\MaxDepthDummy; use Symfony\Component\Serializer\Tests\Fixtures\NotSerializedConstructorArgumentDummy; +use Symfony\Component\Serializer\Tests\Fixtures\Sibling; use Symfony\Component\Serializer\Tests\Fixtures\SiblingHolder; /** @@ -46,7 +47,7 @@ class ObjectNormalizerTest extends TestCase /** * @var SerializerInterface */ - private $serializer; + protected $serializer; protected function setUp() { @@ -60,6 +61,16 @@ private function createNormalizer(array $defaultContext = array(), ClassMetadata $this->normalizer->setSerializer($this->serializer); } + protected function getNormalizerFor(string $class, array $defaultContext = array()): NormalizerInterface + { + $classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader())); + + $normalizer = new ObjectNormalizer($classMetadataFactory, null, null, null, null, null, $defaultContext); + $normalizer->setSerializer($this->serializer); + + return $normalizer; + } + public function testNormalize() { $obj = new ObjectDummy(); @@ -77,6 +88,7 @@ public function testNormalize() ->will($this->returnValue('string_object')) ; + $normalizer = $this->getNormalizerFor(ObjectDummy::class); $this->assertEquals( array( 'foo' => 'foo', @@ -86,7 +98,7 @@ public function testNormalize() 'camelCase' => 'camelcase', 'object' => 'string_object', ), - $this->normalizer->normalize($obj, 'any') + $normalizer->normalize($obj, 'any') ); } @@ -263,9 +275,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'); @@ -277,7 +287,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', @@ -286,7 +296,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() @@ -321,14 +331,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() @@ -599,9 +607,8 @@ 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(); @@ -610,8 +617,10 @@ private function doTestUnableToNormalizeCircularReference(bool $legacy = false) 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(); @@ -620,7 +629,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() @@ -655,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() @@ -695,7 +703,8 @@ 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 () { }))); } @@ -883,8 +892,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'; @@ -895,10 +905,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'), ),