8000 [Serializer] Fix denormalization of arrays by dunglas · Pull Request #19649 · symfony/symfony · GitHub
[go: up one dir, main page]

Skip to content

[Serializer] Fix denormalization of arrays #19649

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -248,8 +248,18 @@ private function validateAndDenormalize($currentClass, $attribute, $data, $forma
return;
}

$builtinType = $type->getBuiltinType();
$class = $type->getClassName();
if ($type->isCollection() && null !== ($collectionValueType = $type->getCollectionValueType()) && Type::BUILTIN_TYPE_OBJECT === $collectionValueType->getBuiltinType()) {
$builtinType = Type::BUILTIN_TYPE_OBJECT;
$class = $collectionValueType->getClassName().'[]';

if (null !== $collectionKeyType = $type->getCollectionKeyType()) {
$context['key_type'] = $collectionKeyType;
}
} else {
$builtinType = $type->getBuiltinType();
$class = $type->getClassName();
}

$expectedTypes[Type::BUILTIN_TYPE_OBJECT === $builtinType && $class ? $class : $builtinType] = true;

if (Type::BUILTIN_TYPE_OBJECT === $builtinType) {
Expand Down
19 changes: 13 additions & 6 deletions src/Symfony/Component/Serializer/Normalizer/ArrayDenormalizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

use Symfony\Component\Serializer\Exception\BadMethodCallException;
use Symfony\Component\Serializer\Exception\InvalidArgumentException;
use Symfony\Component\Serializer\Exception\UnexpectedValueException;
use Symfony\Component\Serializer\SerializerAwareInterface;
use Symfony\Component\Serializer\SerializerInterface;

Expand All @@ -30,6 +31,8 @@ class ArrayDenormalizer implements DenormalizerInterface, SerializerAwareInterfa

/**
* {@inheritdoc}
*
* @throws UnexpectedValueException
*/
public function denormalize($data, $class, $format = null, array $context = array())
{
Expand All @@ -46,12 +49,16 @@ public function denormalize($data, $class, $format = null, array $context = arra
$serializer = $this->serializer;
$class = substr($class, 0, -2);

return array_map(
function ($data) use ($serializer, $class, $format, $context) {
return $serializer->denormalize($data, $class, $format, $context);
},
$data
);
$builtinType = isset($context['key_type']) ? $context['key_type']->getBuiltinType() : null;
foreach ($data as $key => $value) {
if (null !== $builtinType && !call_user_func('is_'.$builtinType, $key)) {
throw new UnexpectedValueException(sprintf('The type of the key "%s" must be "%s" ("%s" given).', $key, $builtinType, gettype($key)));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need for the brackets, just a comma

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Brackets are used for the similar exception here https://github.com/symfony/symfony/blob/master/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php#L270

WSYT of standardizing messages with the rest of the code base in another PR?

}

$data[$key] = $serializer->denormalize($value, $class, $format, $context);
}

return $data;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,12 @@
namespace Symfony\Component\Serializer\Tests\Normalizer;

use Doctrine\Common\Annotations\AnnotationReader;
use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor;
use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor;
use Symfony\Component\PropertyInfo\PropertyInfoExtractor;
use Symfony\Component\Serializer\Exception\UnexpectedValueException;
use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter;
use Symfony\Component\Serializer\Normalizer\ArrayDenormalizer;
use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
use Symfony\Component\Serializer\Serializer;
Expand Down Expand Up @@ -525,13 +528,21 @@ public function testThrowUnexpectedValueException()

public function testDenomalizeRecursive()
{
$normalizer = new ObjectNormalizer(null, null, null, new ReflectionExtractor());
$serializer = new Serializer(array(new DateTimeNormalizer(), $normalizer));
$extractor = new PropertyInfoExtractor(array(), array(new PhpDocExtractor(), new ReflectionExtractor()));
$normalizer = new ObjectNormalizer(null, null, null, $extractor);
$serializer = new Serializer(array(new ArrayDenormalizer(), new DateTimeNormalizer(), $normalizer));

$obj = $serializer->denormalize(array(
'inner' => array('foo' => 'foo', 'bar' => 'bar'),
'date' => '1988/01/21',
'inners' => array(array('foo' => 1), array('foo' => 2)),
), ObjectOuter::class);

$obj = $serializer->denormalize(array('inner' => array('foo' => 'foo', 'bar' => 'bar'), 'date' => '1988/01/21'), ObjectOuter::class);
$this->assertEquals('foo', $obj->getInner()->foo);
$this->assertEquals('bar', $obj->getInner()->bar);
$this->assertEquals('1988-01-21', $obj->getDate()->format('Y-m-d'));
$this->assertEquals(1, $obj->getInners()[0]->foo);
$this->assertEquals(2, $obj->getInners()[1]->foo);
}

/**
Expand All @@ -546,6 +557,19 @@ public function testRejectInvalidType()
$serializer->denormalize(array('date' => 'foo'), ObjectOuter::class);
}

/**
* @expectedException UnexpectedValueException
* @expectedExceptionMessage The type of the key "a" must be "int" ("string" given).
*/
public function testRejectInvalidKey()
{
$extractor = new PropertyInfoExtractor(array(), array(new PhpDocExtractor(), new ReflectionExtractor()));
$normalizer = new ObjectNormalizer(null, null, null, $extractor);
$serializer = new Serializer(array(new ArrayDenormalizer(), new DateTimeNormalizer(), $normalizer));

$serializer->denormalize(array('inners' => array('a' => array('foo' => 1))), ObjectOuter::class);
}

public function testExtractAttributesRespectsFormat()
{
$normalizer = new FormatAndContextAwareNormalizer();
Expand Down Expand Up @@ -740,6 +764,11 @@ class ObjectOuter
private $inner;
private $date;

/**
* @var ObjectInner[]
*/
private $inners;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if we're missing this phpdoc, the serializer won't try to normalize inner before calling setInners() right?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If there is no type defined (through a PHPDoc or any other way supported by registered extractors), the old behavior (don't check anything) occurs.


public function getInner()
{
return $this->inner;
Expand All @@ -759,6 +788,16 @@ public function getDate()
{
return $this->date;
}

public function setInners(array $inners)
Copy link
Contributor
@theofidry theofidry Aug 18, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would also support the case where we have:

class ObjectOuter
{
  private $inners;

  /**
   * @param ObjectInner[] $inners
   */
  public function setInners(array $inners)
  {
    $this->inners = $inners;
  }
}

which is IMO as valid as what you've done and perhaps more in line with Symfony conventions, as you would do:

class ObjectOuter
{
  private $dummy;
  private $inners;

  /**
   * @param ObjectInner[] $inners
   */
  public function __construct(Dummy $dummy, array $inners)
  {
    $this->dummy = $dummy;
    $this->inners = $inners;
  }
}

instead of:

class ObjectOuter
{
  private $dummy;
  /**
   * @var ObjectInner[] $inners
   */
  private $inners;

  public function __construct(Dummy $dummy, array $inners)
  {
    $this->dummy = $dummy;
    $this->inners = $inners;
  }
}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is out of scope of this PR. Support for such notations should be added in the PropertyInfo component.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fair enough

{
$this->inners = $inners;
}

public function getInners()
{
return $this->inners;
}
}

class ObjectInner
Expand Down
5 changes: 3 additions & 2 deletions src/Symfony/Component/Serializer/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,10 @@
"symfony/property-access": "~2.8|~3.0",
"symfony/http-foundation": "~2.8|~3.0",
"symfony/cache": "~3.1",
"symfony/property-info": "~2.8|~3.0",
"symfony/property-info": "~3.1",
"doctrine/annotations": "~1.0",
"doctrine/cache": "~1.0"
"doctrine/cache": "~1.0",
"phpdocumentor/reflection-docblock": "~3.0"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why this one? Required by property info?

Copy link
Member Author
@dunglas dunglas Aug 17, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because I use the ReflectionExtractor in a test (and it's a soft dependency of PropertyInfo).

Theoretically, it should also work with PropertyInfo 2.8 (it's why I've not enforced <3.1 in a conflict section). But 2.8 requires phpdocumentor/reflection:^1.0.7 and 3.1+phpdocumentor/reflection-docblock:^3.0. And those 2 dependencies are in conflict...

So I just required PropertyInfo ^3.1 for the sake of simplicity.

},
"conflict": {
"symfony/property-access": ">=3.0,<3.0.4|>=2.8,<2.8.4"
Expand Down
0