Skip to content

Commit 2aa54b8

Browse files
committed
feature #25493 [Serializer] default_constructor_arguments context option for denormalization (Nek-)
This PR was squashed before being merged into the 4.1-dev branch (closes #25493). Discussion ---------- [Serializer] `default_constructor_arguments` context option for denormalization | Q | A | ------------- | --- | Branch? | master | Bug fix? | no | New feature? | yes | BC breaks? | no | Deprecations? | no | Tests pass? | yes | Fixed tickets | (there is no RFC for this) | License | MIT | Doc PR | symfony/symfony-docs#8914 ## Problems In the case you want to deserialize value-objects, if all the data required by its constructor are **not** given as input, the serializer will throw a simple `RuntimeException` exception. This makes impossible to catch it. (as current fix on my projects I use exception message to be sure to catch the good one X.x") The second problem is a missing feature to fill the required object with an empty one. This needs to be defined by the user because the serializer can't guess how to build it. Here is a project that exposes the problem of the current behavior. https://github.com/Nek-/api-platform-value-object ## Solutions suggested I suggest a solution in 2 parts because the second part is more touchy. 1. Replace the current exception by a new specific one 2. Add a new `empty_data` option to the context of deserialization so you can specify data for objects impossible to instantiate, this is great because the serializer no more throw exception and the validator can then work as expected and send violations to the user. This solution is inspired by forms solutions to fix the issue with value objects Here is what you can do with this feature: ```php class DummyValueObject { public function __construct($foo, $bar) { $this->foo = $foo; $this->bar = $bar; } } $empty = new DummyValueObject('', ''); $result = $normalizer->denormalize(['foo' => 'Hello'], DummyValueObject::class, 'json', [ 'empty_data' => [ DummyValueObject::class => $empty, ], ]); // It's impossible to construct a DummyValueObject with only "foo" value. So the serializer // will replace it with the given empty data ``` There are 2 commits so I can quickly provide you only the first point if you want. Hope you'll like this. ## Solution after discussion 1. New exception `MissingConstructorArgumentsException` 2. New context option `default_constructor_arguments` ```php class DummyValueObject { public function __construct($foo, $bar) { $this->foo = $foo; $this->bar = $bar; } } $result = $normalizer->denormalize(['foo' => 'Hello'], DummyValueObject::class, 'json', [ 'default_constructor_arguments' => [ DummyValueObject::class => ['foo' => '', 'bar' => ''], ], ]); // DummyValueObject is contructed with the given `foo` and empty `bar` ``` Commits ------- 1523a85 [Serializer] context option for denormalization
2 parents fe3e2c3 + 1523a85 commit 2aa54b8

File tree

4 files changed

+78
-1
lines changed

4 files changed

+78
-1
lines changed

src/Symfony/Component/Serializer/CHANGELOG.md

+8
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
11
CHANGELOG
22
=========
33

4+
4.1.0
5+
-----
6+
7+
* added `MissingConstructorArgumentsException` new exception for deserialization failure
8+
of objects that needs data insertion in constructor
9+
* added an optional `default_constructor_arguments` option of context to specify a default data in
10+
case the object is not initializable by its constructor because of data missing
11+
412
4.0.0
513
-----
614

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
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+
namespace Symfony\Component\Serializer\Exception;
13+
14+
/**
15+
* IncompleteInputDataException.
16+
*
17+
* @author Maxime VEBER <maxime.veber@nekland.fr>
18+
*/
19+
class MissingConstructorArgumentsException extends RuntimeException
20+
{
21+
}

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

+6-1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
namespace Symfony\Component\Serializer\Normalizer;
1313

1414
use Symfony\Component\Serializer\Exception\CircularReferenceException;
15+
use Symfony\Component\Serializer\Exception\MissingConstructorArgumentsException;
1516
use Symfony\Component\Serializer\Exception\InvalidArgumentException;
1617
use Symfony\Component\Serializer\Exception\LogicException;
1718
use Symfony\Component\Serializer\Exception\RuntimeException;
@@ -36,6 +37,7 @@ abstract class AbstractNormalizer implements NormalizerInterface, DenormalizerIn
3637
const GROUPS = 'groups';
3738
const ATTRIBUTES = 'attributes';
3839
const ALLOW_EXTRA_ATTRIBUTES = 'allow_extra_attributes';
40+
const DEFAULT_CONSTRUCTOR_ARGUMENTS = 'default_constructor_arguments';
3941

4042
/**
4143
* @var int
@@ -308,6 +310,7 @@ protected function getConstructor(array &$data, $class, array &$context, \Reflec
308310
* @return object
309311
*
310312
* @throws RuntimeException
313+
* @throws MissingConstructorArgumentsException
311314
*/
312315
protected function instantiateObject(array &$data, $class, array &$context, \ReflectionClass $reflectionClass, $allowedAttributes, string $format = null)
313316
{
@@ -353,10 +356,12 @@ protected function instantiateObject(array &$data, $class, array &$context, \Ref
353356
// Don't run set for a parameter passed to the constructor
354357
$params[] = $parameterData;
355358
unset($data[$key]);
359+
} elseif (isset($context[static::DEFAULT_CONSTRUCTOR_ARGUMENTS][$class][$key])) {
360+
$params[] = $context[static::DEFAULT_CONSTRUCTOR_ARGUMENTS][$class][$key];
356361
} elseif ($constructorParameter->isDefaultValueAvailable()) {
357362
$params[] = $constructorParameter->getDefaultValue();
358363
} else {
359-
throw new RuntimeException(
364+
throw new MissingConstructorArgumentsException(
360365
sprintf(
361366
'Cannot create an instance of %s from serialized data because its constructor requires parameter "%s" to be present.',
362367
$class,

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

+43
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,38 @@ public function testConstructorWithUnknownObjectTypeHintDenormalize()
203203
$normalizer->denormalize($data, DummyWithConstructorInexistingObject::class);
204204
}
205205

206+
/**
207+
* @expectedException \Symfony\Component\Serializer\Exception\MissingConstructorArgumentsException
208+
* @expectedExceptionMessage Cannot create an instance of Symfony\Component\Serializer\Tests\Normalizer\DummyValueObject from serialized data because its constructor requires parameter "bar" to be present.
209+
*/
210+
public function testConstructorWithMissingData()
211+
{
212+
$data = array(
213+
'foo' => 10,
214+
);
215+
216+
$normalizer = new ObjectNormalizer();
217+
218+
$normalizer->denormalize($data, DummyValueObject::class);
219+
}
220+
221+
public function testFillWithEmptyDataWhenMissingData()
222+
{
223+
$data = array(
224+
'foo' => 10,
225+
);
226+
227+
$normalizer = new ObjectNormalizer();
228+
229+
$result = $normalizer->denormalize($data, DummyValueObject::class, 'json', array(
230+
'default_constructor_arguments' => array(
231+
DummyValueObject::class => array('foo' => '', 'bar' => ''),
232+
),
233+
));
234+
235+
$this->assertEquals(new DummyValueObject(10, ''), $result);
236+
}
237+
206238
public function testGroupsNormalize()
207239
{
208240
$classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader()));
@@ -1025,6 +1057,17 @@ public function __construct($id, Unknown $unknown)
10251057
{
10261058
}
10271059
}
1060+
class DummyValueObject
1061+
{
1062+
private $foo;
1063+
private $bar;
1064+
1065+
public function __construct($foo, $bar)
1066+
{
1067+
$this->foo = $foo;
1068+
$this->bar = $bar;
1069+
}
1070+
}
10281071

10291072
class JsonNumber
10301073
{

0 commit comments

Comments
 (0)