Skip to content

Commit 32abf6c

Browse files
committed
Add a config option to enable the feature
1 parent 5fe5737 commit 32abf6c

File tree

13 files changed

+369
-105
lines changed

13 files changed

+369
-105
lines changed

src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php

+1
Original file line numberDiff line numberDiff line change
@@ -728,6 +728,7 @@ private function addSerializerSection(ArrayNodeDefinition $rootNode)
728728
->info('serializer configuration')
729729
->{!class_exists(FullStack::class) && class_exists(Serializer::class) ? 'canBeDisabled' : 'canBeEnabled'}()
730730
->children()
731+
->booleanNode('enable_normalizer_generation')->defaultFalse()->end()
731732
->booleanNode('enable_annotations')->{!class_exists(FullStack::class) && class_exists(Annotation::class) ? 'defaultTrue' : 'defaultFalse'}()->end()
732733
->scalarNode('name_converter')->end()
733734
->scalarNode('circular_reference_handler')->end()

src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php

+6
Original file line numberDiff line numberDiff line change
@@ -1274,6 +1274,12 @@ private function registerSerializerConfiguration(array $config, ContainerBuilder
12741274

12751275
if (isset($config['circular_reference_handler']) && $config['circular_reference_handler']) {
12761276
$container->getDefinition('serializer.normalizer.object')->addMethodCall('setCircularReferenceHandler', array(new Reference($config['circular_reference_handler'])));
1277+
$container->getDefinition('serializer.normalizer.object.generated')->addMethodCall('setCircularReferenceHandler', array(new Reference($config['circular_reference_handler'])));
1278+
}
1279+
1280+
if (!$config['enable_normalizer_generation']) {
1281+
$container->removeDefinition('serializer.normalizer.object.generated');
1282+
$container->removeDefinition('serializer.normalizer.object.dumper');
12771283
}
12781284

12791285
if ($config['max_depth_handler'] ?? false) {

src/Symfony/Bundle/FrameworkBundle/Resources/config/serializer.xml

+13
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,19 @@
6363
</service>
6464
<service id="Symfony\Component\Serializer\Normalizer\ObjectNormalizer" alias="serializer.normalizer.object" />
6565

66+
<service id="serializer.normalizer.object.generated" class="Symfony\Component\Serializer\Normalizer\GeneratedObjectNormalizer">
67+
<argument type="service" id="serializer.normalizer.object.dumper" />
68+
<argument>%kernel.cache_dir%</argument>
69+
<argument>%kernel.debug%</argument>
70+
71+
<!-- Run after all custom normalizers but before the object normalizer -->
72+
<tag name="serializer.normalizer" priority="-995" />
73+
</service>
74+
75+
<service id="serializer.normalizer.object.dumper" class="Symfony\Component\Serializer\Dumper\NormalizerDumper">
76+
<argument type="service" id="serializer.mapping.class_metadata_factory" />
77+
</service>
78+
6679
<service id="serializer.denormalizer.array" class="Symfony\Component\Serializer\Normalizer\ArrayDenormalizer">
6780
<!-- Run before the object normalizer -->
6881
<tag name="serializer.normalizer" priority="-990" />

src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php

+1
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,7 @@ protected static function getBundleDefaultConfig()
174174
),
175175
'serializer' => array(
176176
'enabled' => !class_exists(FullStack::class),
177+
'enable_normalizer_generation' => false,
177178
'enable_annotations' => !class_exists(FullStack::class),
178179
'mapping' => array('paths' => array()),
179180
),

src/Symfony/Bundle/FrameworkBundle/Tests/Functional/SerializerTest.php

+26
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,32 @@ public function testDeserializeArrayOfObject()
3333

3434
$this->assertEquals($expected, $result);
3535
}
36+
37+
/**
38+
* @dataProvider caseProvider
39+
*/
40+
public function testSerializeArrayOfObject($testCase)
41+
{
42+
static::bootKernel(array('test_case' => $testCase));
43+
$container = static::$kernel->getContainer();
44+
45+
$bar1 = new Bar();
46+
$bar1->id = 1;
47+
$bar2 = new Bar();
48+
$bar2->id = 2;
49+
50+
$foo = new Foo();
51+
$foo->bars = array($bar1, $bar2);
52+
53+
$result = $container->get('serializer')->normalize($foo);
54+
55+
$this->assertEquals(array('bars' => array(array('id' => 1), array('id' => 2))), $result);
56+
}
57+
58+
public function caseProvider()
59+
{
60+
return array(array('Serializer'), array('GeneratedSerializer'));
61+
}
3662
}
3763

3864
class Foo
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
use Symfony\Bundle\FrameworkBundle\FrameworkBundle;
13+
14+
return array(
15+
new FrameworkBundle(),
16+
);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
imports:
2+
- { resource: ../config/default.yml }
3+
4+
framework:
5+
serializer:
6+
enable_normalizer_generation: true
7+
enable_annotations: true # required to detect properties

src/Symfony/Bundle/FrameworkBundle/composer.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@
4444
"symfony/process": "~3.4|~4.0",
4545
"symfony/security-core": "~3.4|~4.0",
4646
"symfony/security-csrf": "~3.4|~4.0",
47-
"symfony/serializer": "~3.4|~4.0",
47+
"symfony/serializer": "~4.1",
4848
"symfony/stopwatch": "~3.4|~4.0",
4949
"symfony/translation": "~3.4|~4.0",
5050
"symfony/templating": "~3.4|~4.0",
@@ -66,7 +66,7 @@
6666
"symfony/asset": "<3.4",
6767
"symfony/console": "<3.4",
6868
"symfony/form": "<3.4",
69-
"symfony/property-info": "<3.4",
69+
"symfony/property-info": "<4.1",
7070
"symfony/serializer": "<3.4",
7171
"symfony/stopwatch": "<3.4",
7272
"symfony/translation": "<3.4",

src/Symfony/Component/Serializer/Dumper/NormalizerDumper.php

+5-6
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
/**
1919
* @author Guilhem Niot <guilhem.niot@gmail.com>
2020
* @author Amrouche Hamza <hamza.simperfit@gmail.com>
21+
*
2122
* @experimental
2223
*/
2324
final class NormalizerDumper
@@ -42,6 +43,7 @@ public function dump(string $class, array $context = array())
4243
<?php
4344
$namespaceLine
4445
use Symfony\Component\Serializer\Exception\CircularReferenceException;
46+
use Symfony\Component\Serializer\Normalizer\CircularReferenceTrait;
4547
use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
4648
use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait;
4749
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
@@ -53,7 +55,7 @@ public function dump(string $class, array $context = array())
5355
*/
5456
class {$context['class']} implements NormalizerInterface, NormalizerAwareInterface
5557
{
56-
use NormalizerAwareTrait;
58+
use CircularReferenceTrait, NormalizerAwareTrait;
5759
5860
{$this->generateNormalizeMethod($reflectionClass)}
5961
@@ -79,11 +81,8 @@ private function generateNormalizeMethodInner(\ReflectionClass $reflectionClass)
7981
{
8082
$code = <<<EOL
8183
82-
\$objectHash = spl_object_hash(\$object);
83-
if (isset(\$context[ObjectNormalizer::CIRCULAR_REFERENCE_LIMIT][\$objectHash])) {
84-
return null;
85-
} else {
86-
\$context[ObjectNormalizer::CIRCULAR_REFERENCE_LIMIT][\$objectHash] = 1;
84+
if (\$this->isCircularReference(\$object, \$context)) {
85+
return \$this->handleCircularReference(\$object);
8786
}
8887
8988
\$groups = isset(\$context[ObjectNormalizer::GROUPS]) && is_array(\$context[ObjectNormalizer::GROUPS]) ? \$context[ObjectNormalizer::GROUPS] : null;

src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php

+6-89
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111

1212
namespace Symfony\Component\Serializer\Normalizer;
1313

14-
use Symfony\Component\Serializer\Exception\CircularReferenceException;
1514
use Symfony\Component\Serializer\Exception\MissingConstructorArgumentsException;
1615
use Symfony\Component\Serializer\Exception\InvalidArgumentException;
1716
use Symfony\Component\Serializer\Exception\LogicException;
@@ -29,6 +28,7 @@
2928
*/
3029
abstract class AbstractNormalizer implements NormalizerInterface, DenormalizerInterface, SerializerAwareInterface
3130
{
31+
use CircularReferenceTrait;
3232
use ObjectToPopulateTrait;
3333
use SerializerAwareTrait;
3434

@@ -39,16 +39,6 @@ abstract class AbstractNormalizer implements NormalizerInterface, DenormalizerIn
3939
const ALLOW_EXTRA_ATTRIBUTES = 'allow_extra_attributes';
4040
const DEFAULT_CONSTRUCTOR_ARGUMENTS = 'default_constructor_arguments';
4141

42-
/**
43-
* @var int
44-
*/
45-
protected $circularReferenceLimit = 1;
46-
47-
/**
48-
* @var callable
49-
*/
50-
protected $circularReferenceHandler;
51-
5242
/**
5343
* @var ClassMetadataFactoryInterface|null
5444
*/
@@ -83,34 +73,6 @@ public function __construct(ClassMetadataFactoryInterface $classMetadataFactory
8373
$this->nameConverter = $nameConverter;
8474
}
8575

86-
/**
87-
* Set circular reference limit.
88-
*
89-
* @param int $circularReferenceLimit Limit of iterations for the same object
90-
*
91-
* @return self
92-
*/
93-
public function setCircularReferenceLimit($circularReferenceLimit)
94-
{
95-
$this->circularReferenceLimit = $circularReferenceLimit;
96-
97-
return $this;
98-
}
99-
100-
/**
101-
* Set circular reference handler.
102-
*
103-
* @param callable $circularReferenceHandler
104-
*
105-
* @return self
106-
*/
107-
public function setCircularReferenceHandler(callable $circularReferenceHandler)
108-
{
109-
$this->circularReferenceHandler = $circularReferenceHandler;
110-
111-
return $this;
112-
}
113-
11476
/**
11577
* Set normalization callbacks.
11678
*
@@ -147,56 +109,6 @@ public function setIgnoredAttributes(array $ignoredAttributes)
147109
return $this;
148110
}
149111

150-
/**
151-
* Detects if the configured circular reference limit is reached.
152-
*
153-
* @param object $object
154-
* @param array $context
155-
*
156-
* @return bool
157-
*
158-
* @throws CircularReferenceException
159-
*/
160-
protected function isCircularReference($object, &$context)
161-
{
162-
$objectHash = spl_object_hash($object);
163-
164-
if (isset($context[static::CIRCULAR_REFERENCE_LIMIT][$objectHash])) {
165-
if ($context[static::CIRCULAR_REFERENCE_LIMIT][$objectHash] >= $this->circularReferenceLimit) {
166-
unset($context[static::CIRCULAR_REFERENCE_LIMIT][$objectHash]);
167-
168-
return true;
169-
}
170-
171-
++$context[static::CIRCULAR_REFERENCE_LIMIT][$objectHash];
172-
} else {
173-
$context[static::CIRCULAR_REFERENCE_LIMIT][$objectHash] = 1;
174-
}
175-
176-
return false;
177-
}
178-
179-
/**
180-
* Handles a circular reference.
181-
*
182-
* If a circular reference handler is set, it will be called. Otherwise, a
183-
* {@class CircularReferenceException} will be thrown.
184-
*
185-
* @param object $object
186-
*
187-
* @return mixed
188-
*
189-
* @throws CircularReferenceException
190-
*/
191-
protected function handleCircularReference($object)
192-
{
193-
if ($this->circularReferenceHandler) {
194-
return \call_user_func($this->circularReferenceHandler, $object);
195-
}
196-
197-
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));
198-
}
199-
200112
/**
201113
* Gets attributes to normalize using groups.
202114
*
@@ -404,4 +316,9 @@ protected function createChildContext(array $parentContext, $attribute)
404316

405317
return $parentContext;
406318
}
319+
320+
private function getCircularReferenceLimitField()
321+
{
322+
return static::CIRCULAR_REFERENCE_LIMIT;
323+
}
407324
}

0 commit comments

Comments
 (0)