Skip to content

Commit c221908

Browse files
committed
feature symfony#19277 [Serializer] Argument objects (theofidry, dunglas)
This PR was merged into the 3.2-dev branch. Discussion ---------- [Serializer] Argument objects | Q | A | ------------- | --- | Branch? | 3.1 | Bug fix? | no | New feature? | yes | BC breaks? | no | Deprecations? | no | Tests pass? | TODO | Fixed tickets | none | License | MIT | Doc PR | TODO Assuming with have the two following entities: ```php namespace AppBundle\Entity; class Dummy { public function __construct(int $id, string $name, string $email, AnotherDummy $anotherDummy) { $this->id = $id; $this->name = $name; $this->email = $email; $this->anotherDummy = $anotherDummy; } } class AnotherDummy { public function __construct(int $id, string $uuid, bool $isEnabled) { $this->id = $id; $this->uuid = $uuid; $this->isEnabled = $isEnabled; } } ``` Doing the following will fail: ```php $serializer->denormalize( [ 'id' => $i, 'name' => 'dummy', 'email' => '[email protected]', 'another_dummy' => [ 'id' => 1000 + $i, 'uuid' => 'azerty', 'is_enabled' => true, ], ], \AppBundle\Entity\Dummy::class ); ``` with a type error, because the 4th argument passed to `Dummy::__construct()` will be an array. The following patch checks if the type of the argument is an object, and if it is tries to denormalize that object as well. I'm not sure if it's me missing something or this is a use case that has been omitted (willingly or not), but if it's a valuable patch I would be happy to work on finishing it. Commits ------- 988eba1 fix tests 98bcb91 Merge pull request #1 from dunglas/theofidry-feature/param-object 7b5d55d Prevent BC in instantiateObject e437e04 fix reflection type 3fe9802 revert CS 5556fa5 fix d4cdb00 fix CS 93608dc Add deprecation message f46a176 Apply patch f361e52 fix tests 4884a2e f1 e64e999 Address comments e99a90b Add tests 7bd4ac5 Test
2 parents b38d8d9 + 988eba1 commit c221908

File tree

6 files changed

+152
-5
lines changed

6 files changed

+152
-5
lines changed

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

+16-3
Original file line numberDiff line numberDiff line change
@@ -281,13 +281,16 @@ protected function getConstructor(array &$data, $class, array &$context, \Reflec
281281
* @param array $context
282282
* @param \ReflectionClass $reflectionClass
283283
* @param array|bool $allowedAttributes
284+
* @param string|null $format
284285
*
285286
* @return object
286287
*
287288
* @throws RuntimeException
288289
*/
289-
protected function instantiateObject(array &$data, $class, array &$context, \ReflectionClass $reflectionClass, $allowedAttributes)
290+
protected function instantiateObject(array &$data, $class, array &$context, \ReflectionClass $reflectionClass, $allowedAttributes/*, $format = null*/)
290291
{
292+
$format = func_num_args() >= 6 ? func_get_arg(5) : null;
293+
291294
if (
292295
isset($context[static::OBJECT_TO_POPULATE]) &&
293296
is_object($context[static::OBJECT_TO_POPULATE]) &&
@@ -319,8 +322,18 @@ protected function instantiateObject(array &$data, $class, array &$context, \Ref
319322
$params = array_merge($params, $data[$paramName]);
320323
}
321324
} elseif ($allowed && !$ignored && (isset($data[$key]) || array_key_exists($key, $data))) {
322-
$params[] = $data[$key];
323-
// don't run set for a parameter passed to the constructor
325+
$parameterData = $data[$key];
326+
try {
327+
if (null !== $constructorParameter->getClass()) {
328+
$parameterClass = $constructorParameter->getClass()->getName();
329+
$parameterData = $this->serializer->deserialize($parameterData, $parameterClass, $format, $context);
330+
}
331+
} catch (\ReflectionException $e) {
332+
throw new RuntimeException(sprintf('Could not determine the class of the parameter "%s".', $key), 0, $e);
333+
}
334+
335+
// Don't run set for a parameter passed to the constructor
336+
$params[] = $parameterData;
324337
unset($data[$key]);
325338
} elseif ($constructorParameter->isDefaultValueAvailable()) {
326339
$params[] = $constructorParameter->getDefaultValue();

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ public function denormalize($data, $class, $format = null, array $context = arra
175175
$normalizedData = $this->prepareForDenormalization($data);
176176

177177
$reflectionClass = new \ReflectionClass($class);
178-
$object = $this->instantiateObject($normalizedData, $class, $context, $reflectionClass, $allowedAttributes);
178+
$object = $this->instantiateObject($normalizedData, $class, $context, $reflectionClass, $allowedAttributes, $format);
179179

180180
foreach ($normalizedData as $attribute => $value) {
181181
if ($this->nameConverter) {

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ public function denormalize($data, $class, $format = null, array $context = arra
4747
$normalizedData = $this->prepareForDenormalization($data);
4848

4949
$reflectionClass = new \ReflectionClass($class);
50-
$object = $this->instantiateObject($normalizedData, $class, $context, $reflectionClass, $allowedAttributes);
50+
$object = $this->instantiateObject($normalizedData, $class, $context, $reflectionClass, $allowedAttributes, $format);
5151

5252
$classMethods = get_class_methods($object);
5353
foreach ($normalizedData as $attribute => $value) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<?php
2+
3+
namespace Symfony\Component\Serializer\Tests\Fixtures;
4+
5+
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
6+
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
7+
use Symfony\Component\Serializer\SerializerInterface;
8+
9+
/**
10+
* @author Théo FIDRY <[email protected]>
11+
*/
12+
class DenormalizerDecoratorSerializer implements SerializerInterface
13+
{
14+
private $normalizer;
15+
16+
/**
17+
* @param NormalizerInterface|DenormalizerInterface $normalizer
18+
*/
19+
public function __construct($normalizer)
20+
{
21+
if (false === $normalizer instanceof NormalizerInterface && false === $normalizer instanceof DenormalizerInterface) {
22+
throw new \InvalidArgumentException();
23+
}
24+
25+
$this->normalizer = $normalizer;
26+
}
27+
28+
/**
29+
* {@inheritdoc}
30+
*/
31+
public function serialize($data, $format, array $context = array())
32+
{
33+
return $this->normalizer->normalize($data, $format, $context);
34+
}
35+
36+
/**
37+
* {@inheritdoc}
38+
*/
39+
public function deserialize($data, $type, $format, array $context = array())
40+
{
41+
return $this->normalizer->denormalize($data, $type, $format, $context);
42+
}
43+
}

src/Symfony/Component/Serializer/Tests/Normalizer/AbstractObjectNormalizerTest.php

+18
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,19 @@ public function testDenormalize()
2424
$this->assertNull($normalizedData->bar);
2525
$this->assertSame('baz', $normalizedData->baz);
2626
}
27+
28+
/**
29+
* @group legacy
30+
*/
31+
public function testInstantiateObjectDenormalizer()
32+
{
33+
$data = array('foo' => 'foo', 'bar' => 'bar', 'baz' => 'baz');
34+
$class = __NAMESPACE__.'\Dummy';
35+
$context = array();
36+
37+
$normalizer = new AbstractObjectNormalizerDummy();
38+
$normalizer->instantiateObject($data, $class, $context, new \ReflectionClass($class), array());
39+
}
2740
}
2841

2942
class AbstractObjectNormalizerDummy extends AbstractObjectNormalizer
@@ -45,6 +58,11 @@ protected function isAllowedAttribute($classOrObject, $attribute, $format = null
4558
{
4659
return in_array($attribute, array('foo', 'baz'));
4760
}
61+
62+
public function instantiateObject(array &$data, $class, array &$context, \ReflectionClass $reflectionClass, $allowedAttributes)
63+
{
64+
return parent::instantiateObject($data, $class, $context, $reflectionClass, $allowedAttributes);
65+
}
4866
}
4967

5068
class Dummy

src/Symfony/Component/Serializer/Tests/Normalizer/ObjectNormalizerTest.php

+73
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
use Symfony\Component\Serializer\SerializerInterface;
2222
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
2323
use Symfony\Component\Serializer\Tests\Fixtures\CircularReferenceDummy;
24+
use Symfony\Component\Serializer\Tests\Fixtures\DenormalizerDecoratorSerializer;
2425
use Symfony\Component\Serializer\Tests\Fixtures\MaxDepthDummy;
2526
use Symfony\Component\Serializer\Tests\Fixtures\SiblingHolder;
2627
use Symfony\Component\Serializer\Mapping\Loader\AnnotationLoader;
@@ -157,6 +158,49 @@ public function testConstructorWithObjectDenormalize()
157158
$this->assertEquals('bar', $obj->bar);
158159
}
159160

161+
public function testConstructorWithObjectTypeHintDenormalize()
162+
{
163+
$data = array(
164+
'id' => 10,
165+
'inner' => array(
166+
'foo' => 'oof',
167+
'bar' => 'rab',
168+
),
169+
);
170+
171+
$normalizer = new ObjectNormalizer();
172+
$serializer = new DenormalizerDecoratorSerializer($normalizer);
173+
$normalizer->setSerializer($serializer);
174+
175+
$obj = $normalizer->denormalize($data, DummyWithConstructorObject::class);
176+
$this->assertInstanceOf(DummyWithConstructorObject::class, $obj);
177+
$this->assertEquals(10, $obj->getId());
178+
$this->assertInstanceOf(ObjectInner::class, $obj->getInner());
179+
$this->assertEquals('oof', $obj->getInner()->foo);
180+
$this->assertEquals('rab', $obj->getInner()->bar);
181+
}
182+
183+
/**
184+
* @expectedException \Symfony\Component\Serializer\Exception\RuntimeException
185+
* @expectedExceptionMessage Could not determine the class of the parameter "unknown".
186+
*/
187+
public function testConstructorWithUnknownObjectTypeHintDenormalize()
188+
{
189+
$data = array(
190+
'id' => 10,
191+
'unknown' => array(
192+
'foo' => 'oof',
193+
'bar' => 'rab',
194+
),
195+
);
196+
197+
$normalizer = new ObjectNormalizer();
198+
$serializer = new DenormalizerDecoratorSerializer($normalizer);
199+
$normalizer->setSerializer($serializer);
200+
201+
$normalizer->denormalize($data, DummyWithConstructorInexistingObject::class);
202+
}
203+
160204
public function testGroupsNormalize()
161205
{
162206
$classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader()));
@@ -782,3 +826,32 @@ protected function isAllowedAttribute($classOrObject, $attribute, $format = null
782826
return false;
783827
}
784828
}
829+
830+
class DummyWithConstructorObject
831+
{
832+
private $id;
833+
private $inner;
834+
835+
public function __construct($id, ObjectInner $inner)
836+
{
837+
$this->id = $id;
838+
$this->inner = $inner;
839+
}
840+
841+
public function getId()
842+
{
843+
return $this->id;
844+
}
845+
846+
public function getInner()
847+
{
848+
return $this->inner;
849+
}
850+
}
851+
852+
class DummyWithConstructorInexistingObject
853+
{
854+
public function __construct($id, Unknown $unknown)
855+
{
856+
}
857+
}

0 commit comments

Comments
 (0)