Navigation Menu

Skip to content

Commit

Permalink
feature #19277 [Serializer] Argument objects (theofidry, dunglas)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
dunglas committed Jul 11, 2016
2 parents b38d8d9 + 988eba1 commit c221908
Show file tree
Hide file tree
Showing 6 changed files with 152 additions and 5 deletions.
19 changes: 16 additions & 3 deletions src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php
Expand Up @@ -281,13 +281,16 @@ protected function getConstructor(array &$data, $class, array &$context, \Reflec
* @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]) &&
Expand Down Expand Up @@ -319,8 +322,18 @@ protected function instantiateObject(array &$data, $class, array &$context, \Ref
$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();
Expand Down
Expand Up @@ -175,7 +175,7 @@ public function denormalize($data, $class, $format = null, array $context = arra
$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) {
Expand Down
Expand Up @@ -47,7 +47,7 @@ public function denormalize($data, $class, $format = null, array $context = arra
$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) {
Expand Down
@@ -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);
}
}
Expand Up @@ -24,6 +24,19 @@ public function testDenormalize()
$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
Expand All @@ -45,6 +58,11 @@ protected function isAllowedAttribute($classOrObject, $attribute, $format = null
{
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
Expand Down
Expand Up @@ -21,6 +21,7 @@
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;
Expand Down Expand Up @@ -157,6 +158,49 @@ public function testConstructorWithObjectDenormalize()
$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()));
Expand Down Expand Up @@ -782,3 +826,32 @@ protected function isAllowedAttribute($classOrObject, $attribute, $format = null
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)
{
}
}

0 comments on commit c221908

Please sign in to comment.