feature #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' => 'du@ex.com',
        '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
This commit is contained in:
Kévin Dunglas 2016-07-11 09:50:44 +02:00
commit c221908fc4
No known key found for this signature in database
GPG Key ID: 4D04EBEF06AAF3A6
6 changed files with 152 additions and 5 deletions

View File

@ -281,13 +281,16 @@ abstract class AbstractNormalizer extends SerializerAwareNormalizer implements N
* @param array $context
* @param \ReflectionClass $reflectionClass
* @param array|bool $allowedAttributes
* @param string|null $format
*
* @return object
*
* @throws RuntimeException
*/
protected function instantiateObject(array &$data, $class, array &$context, \ReflectionClass $reflectionClass, $allowedAttributes)
protected function instantiateObject(array &$data, $class, array &$context, \ReflectionClass $reflectionClass, $allowedAttributes/*, $format = null*/)
{
$format = func_num_args() >= 6 ? func_get_arg(5) : null;
if (
isset($context[static::OBJECT_TO_POPULATE]) &&
is_object($context[static::OBJECT_TO_POPULATE]) &&
@ -319,8 +322,18 @@ abstract class AbstractNormalizer extends SerializerAwareNormalizer implements N
$params = array_merge($params, $data[$paramName]);
}
} elseif ($allowed && !$ignored && (isset($data[$key]) || array_key_exists($key, $data))) {
$params[] = $data[$key];
// don't run set for a parameter passed to the constructor
$parameterData = $data[$key];
try {
if (null !== $constructorParameter->getClass()) {
$parameterClass = $constructorParameter->getClass()->getName();
$parameterData = $this->serializer->deserialize($parameterData, $parameterClass, $format, $context);
}
} catch (\ReflectionException $e) {
throw new RuntimeException(sprintf('Could not determine the class of the parameter "%s".', $key), 0, $e);
}
// Don't run set for a parameter passed to the constructor
$params[] = $parameterData;
unset($data[$key]);
} elseif ($constructorParameter->isDefaultValueAvailable()) {
$params[] = $constructorParameter->getDefaultValue();

View File

@ -175,7 +175,7 @@ abstract class AbstractObjectNormalizer extends AbstractNormalizer
$normalizedData = $this->prepareForDenormalization($data);
$reflectionClass = new \ReflectionClass($class);
$object = $this->instantiateObject($normalizedData, $class, $context, $reflectionClass, $allowedAttributes);
$object = $this->instantiateObject($normalizedData, $class, $context, $reflectionClass, $allowedAttributes, $format);
foreach ($normalizedData as $attribute => $value) {
if ($this->nameConverter) {

View File

@ -47,7 +47,7 @@ class GetSetMethodNormalizer extends AbstractObjectNormalizer
$normalizedData = $this->prepareForDenormalization($data);
$reflectionClass = new \ReflectionClass($class);
$object = $this->instantiateObject($normalizedData, $class, $context, $reflectionClass, $allowedAttributes);
$object = $this->instantiateObject($normalizedData, $class, $context, $reflectionClass, $allowedAttributes, $format);
$classMethods = get_class_methods($object);
foreach ($normalizedData as $attribute => $value) {

View File

@ -0,0 +1,43 @@
<?php
namespace Symfony\Component\Serializer\Tests\Fixtures;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use Symfony\Component\Serializer\SerializerInterface;
/**
* @author Théo FIDRY <theo.fidry@gmail.com>
*/
class DenormalizerDecoratorSerializer implements SerializerInterface
{
private $normalizer;
/**
* @param NormalizerInterface|DenormalizerInterface $normalizer
*/
public function __construct($normalizer)
{
if (false === $normalizer instanceof NormalizerInterface && false === $normalizer instanceof DenormalizerInterface) {
throw new \InvalidArgumentException();
}
$this->normalizer = $normalizer;
}
/**
* {@inheritdoc}
*/
public function serialize($data, $format, array $context = array())
{
return $this->normalizer->normalize($data, $format, $context);
}
/**
* {@inheritdoc}
*/
public function deserialize($data, $type, $format, array $context = array())
{
return $this->normalizer->denormalize($data, $type, $format, $context);
}
}

View File

@ -24,6 +24,19 @@ class AbstractObjectNormalizerTest extends \PHPUnit_Framework_TestCase
$this->assertNull($normalizedData->bar);
$this->assertSame('baz', $normalizedData->baz);
}
/**
* @group legacy
*/
public function testInstantiateObjectDenormalizer()
{
$data = array('foo' => 'foo', 'bar' => 'bar', 'baz' => 'baz');
$class = __NAMESPACE__.'\Dummy';
$context = array();
$normalizer = new AbstractObjectNormalizerDummy();
$normalizer->instantiateObject($data, $class, $context, new \ReflectionClass($class), array());
}
}
class AbstractObjectNormalizerDummy extends AbstractObjectNormalizer
@ -45,6 +58,11 @@ class AbstractObjectNormalizerDummy extends AbstractObjectNormalizer
{
return in_array($attribute, array('foo', 'baz'));
}
public function instantiateObject(array &$data, $class, array &$context, \ReflectionClass $reflectionClass, $allowedAttributes)
{
return parent::instantiateObject($data, $class, $context, $reflectionClass, $allowedAttributes);
}
}
class Dummy

View File

@ -21,6 +21,7 @@ use Symfony\Component\Serializer\Serializer;
use Symfony\Component\Serializer\SerializerInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use Symfony\Component\Serializer\Tests\Fixtures\CircularReferenceDummy;
use Symfony\Component\Serializer\Tests\Fixtures\DenormalizerDecoratorSerializer;
use Symfony\Component\Serializer\Tests\Fixtures\MaxDepthDummy;
use Symfony\Component\Serializer\Tests\Fixtures\SiblingHolder;
use Symfony\Component\Serializer\Mapping\Loader\AnnotationLoader;
@ -157,6 +158,49 @@ class ObjectNormalizerTest extends \PHPUnit_Framework_TestCase
$this->assertEquals('bar', $obj->bar);
}
public function testConstructorWithObjectTypeHintDenormalize()
{
$data = array(
'id' => 10,
'inner' => array(
'foo' => 'oof',
'bar' => 'rab',
),
);
$normalizer = new ObjectNormalizer();
$serializer = new DenormalizerDecoratorSerializer($normalizer);
$normalizer->setSerializer($serializer);
$obj = $normalizer->denormalize($data, DummyWithConstructorObject::class);
$this->assertInstanceOf(DummyWithConstructorObject::class, $obj);
$this->assertEquals(10, $obj->getId());
$this->assertInstanceOf(ObjectInner::class, $obj->getInner());
$this->assertEquals('oof', $obj->getInner()->foo);
$this->assertEquals('rab', $obj->getInner()->bar);
}
/**
* @expectedException \Symfony\Component\Serializer\Exception\RuntimeException
* @expectedExceptionMessage Could not determine the class of the parameter "unknown".
*/
public function testConstructorWithUnknownObjectTypeHintDenormalize()
{
$data = array(
'id' => 10,
'unknown' => array(
'foo' => 'oof',
'bar' => 'rab',
),
);
$normalizer = new ObjectNormalizer();
$serializer = new DenormalizerDecoratorSerializer($normalizer);
$normalizer->setSerializer($serializer);
$normalizer->denormalize($data, DummyWithConstructorInexistingObject::class);
}
public function testGroupsNormalize()
{
$classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader()));
@ -782,3 +826,32 @@ class FormatAndContextAwareNormalizer extends ObjectNormalizer
return false;
}
}
class DummyWithConstructorObject
{
private $id;
private $inner;
public function __construct($id, ObjectInner $inner)
{
$this->id = $id;
$this->inner = $inner;
}
public function getId()
{
return $this->id;
}
public function getInner()
{
return $this->inner;
}
}
class DummyWithConstructorInexistingObject
{
public function __construct($id, Unknown $unknown)
{
}
}