Skip to content

Commit 810e445

Browse files
authored
fix(serializer): fix denormalizing to non-cloneable objects (#5569)
* test(serializer): regression test for deserializing non-cloneable objects * fix(serializer): fix error denormalizing to non-cloneable objects
1 parent b6dc772 commit 810e445

File tree

3 files changed

+137
-1
lines changed

3 files changed

+137
-1
lines changed

src/Serializer/AbstractItemNormalizer.php

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
3434
use ApiPlatform\Symfony\Security\ResourceAccessCheckerInterface;
3535
use ApiPlatform\Util\ClassInfoTrait;
36+
use ApiPlatform\Util\CloneTrait;
3637
use Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException;
3738
use Symfony\Component\PropertyAccess\PropertyAccess;
3839
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
@@ -59,6 +60,7 @@
5960
abstract class AbstractItemNormalizer extends AbstractObjectNormalizer
6061
{
6162
use ClassInfoTrait;
63+
use CloneTrait;
6264
use ContextTrait;
6365
use InputOutputMetadataTrait;
6466

@@ -360,13 +362,19 @@ public function denormalize($data, $class, $format = null, array $context = [])
360362
return $item;
361363
}
362364

363-
$previousObject = null !== $objectToPopulate ? clone $objectToPopulate : null;
365+
$previousObject = $this->clone($objectToPopulate);
364366
$object = parent::denormalize($data, $resourceClass, $format, $context);
365367

366368
if (!$this->resourceClassResolver->isResourceClass($context['resource_class'])) {
367369
return $object;
368370
}
369371

372+
// Bypass the post-denormalize attribute revert logic if the object could not be
373+
// cloned since we cannot possibly revert any changes made to it.
374+
if (null !== $objectToPopulate && null === $previousObject) {
375+
return $object;
376+
}
377+
370378
// Revert attributes that aren't allowed to be changed after a post-denormalize check
371379
foreach (array_keys($data) as $attribute) {
372380
if (!$this->canAccessAttributePostDenormalize($object, $previousObject, $attribute, $context)) {
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
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+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Tests\Fixtures\TestBundle\Entity;
15+
16+
use ApiPlatform\Core\Annotation\ApiProperty;
17+
use ApiPlatform\Core\Annotation\ApiResource;
18+
use Doctrine\ORM\Mapping as ORM;
19+
use Symfony\Component\Validator\Constraints as Assert;
20+
21+
/**
22+
* Dummy class that cannot be cloned.
23+
*
24+
* @author Colin O'Dell <[email protected]>
25+
*
26+
* @ApiResource
27+
*
28+
* @ORM\Entity
29+
*/
30+
class NonCloneableDummy
31+
{
32+
/**
33+
* @var int|null The id
34+
*
35+
* @ORM\Column(type="integer", nullable=true)
36+
*
37+
* @ORM\Id
38+
*
39+
* @ORM\GeneratedValue(strategy="AUTO")
40+
*/
41+
private $id;
42+
43+
/**
44+
* @var string The dummy name
45+
*
46+
* @ORM\Column
47+
*
48+
* @Assert\NotBlank
49+
*
50+
* @ApiProperty(iri="http://schema.org/name")
51+
*/
52+
private $name;
53+
54+
public function getId()
55+
{
56+
return $this->id;
57+
}
58+
59+
public function setId($id)
60+
{
61+
$this->id = $id;
62+
}
63+
64+
public function setName(string $name)
65+
{
66+
$this->name = $name;
67+
}
68+
69+
public function getName(): string
70+
{
71+
return $this->name;
72+
}
73+
74+
private function __clone()
75+
{
76+
}
77+
}

tests/Serializer/AbstractItemNormalizerTest.php

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\DummyForAdditionalFieldsInput;
4242
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\DummyTableInheritance;
4343
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\DummyTableInheritanceChild;
44+
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\NonCloneableDummy;
4445
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\RelatedDummy;
4546
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\SecuredDummy;
4647
use Doctrine\Common\Collections\ArrayCollection;
@@ -1702,6 +1703,56 @@ public function testDenormalizeCollectionDecodedFromXmlWithOneChild()
17021703

17031704
$normalizer->denormalize($data, Dummy::class, 'xml');
17041705
}
1706+
1707+
public function testDenormalizePopulatingNonCloneableObject()
1708+
{
1709+
$dummy = new NonCloneableDummy();
1710+
$dummy->setName('foo');
1711+
1712+
$data = [
1713+
'name' => 'bar',
1714+
];
1715+
1716+
$propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class);
1717+
$propertyNameCollectionFactoryProphecy->create(NonCloneableDummy::class, [])->willReturn(new PropertyNameCollection(['name']));
1718+
1719+
$propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class);
1720+
$propertyMetadataFactoryProphecy->create(NonCloneableDummy::class, 'name', [])->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)])->withDescription('')->withReadable(false)->withWritable(true));
1721+
1722+
$iriConverterProphecy = $this->prophesize(IriConverterInterface::class);
1723+
$propertyAccessorProphecy = $this->prophesize(PropertyAccessorInterface::class);
1724+
$resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class);
1725+
$resourceClassResolverProphecy->getResourceClass(null, NonCloneableDummy::class)->willReturn(NonCloneableDummy::class);
1726+
$resourceClassResolverProphecy->getResourceClass($dummy, NonCloneableDummy::class)->willReturn(NonCloneableDummy::class);
1727+
$resourceClassResolverProphecy->isResourceClass(NonCloneableDummy::class)->willReturn(true);
1728+
1729+
$serializerProphecy = $this->prophesize(SerializerInterface::class);
1730+
$serializerProphecy->willImplement(NormalizerInterface::class);
1731+
1732+
$normalizer = $this->getMockForAbstractClass(AbstractItemNormalizer::class, [
1733+
$propertyNameCollectionFactoryProphecy->reveal(),
1734+
$propertyMetadataFactoryProphecy->reveal(),
1735+
$iriConverterProphecy->reveal(),
1736+
$resourceClassResolverProphecy->reveal(),
1737+
$propertyAccessorProphecy->reveal(),
1738+
null,
1739+
null,
1740+
null,
1741+
false,
1742+
[],
1743+
[],
1744+
null,
1745+
null,
1746+
]);
1747+
$normalizer->setSerializer($serializerProphecy->reveal());
1748+
1749+
$context = [AbstractItemNormalizer::OBJECT_TO_POPULATE => $dummy];
1750+
$actual = $normalizer->denormalize($data, NonCloneableDummy::class, null, $context);
1751+
1752+
$this->assertInstanceOf(NonCloneableDummy::class, $actual);
1753+
$this->assertSame($dummy, $actual);
1754+
$propertyAccessorProphecy->setValue($actual, 'name', 'bar')->shouldHaveBeenCalled();
1755+
}
17051756
}
17061757

17071758
class ObjectWithBasicProperties

0 commit comments

Comments
 (0)