Skip to content

Commit 24ec4e7

Browse files
committed
Merge 2.7 into 3.0
2 parents e7114b7 + 810e445 commit 24ec4e7

File tree

3 files changed

+137
-2
lines changed

3 files changed

+137
-2
lines changed

src/Serializer/AbstractItemNormalizer.php

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
2626
use ApiPlatform\Symfony\Security\ResourceAccessCheckerInterface;
2727
use ApiPlatform\Util\ClassInfoTrait;
28+
use ApiPlatform\Util\CloneTrait;
2829
use Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException;
2930
use Symfony\Component\PropertyAccess\PropertyAccess;
3031
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
@@ -51,6 +52,7 @@
5152
abstract class AbstractItemNormalizer extends AbstractObjectNormalizer
5253
{
5354
use ClassInfoTrait;
55+
use CloneTrait;
5456
use ContextTrait;
5557
use InputOutputMetadataTrait;
5658

@@ -214,14 +216,19 @@ public function denormalize(mixed $data, string $class, string $format = null, a
214216
throw new UnexpectedValueException(sprintf('Expected IRI or document for resource "%s", "%s" given.', $resourceClass, \gettype($data)));
215217
}
216218

217-
$previousObject = isset($objectToPopulate) ? clone $objectToPopulate : null;
218-
219+
$previousObject = $this->clone($objectToPopulate);
219220
$object = parent::denormalize($data, $class, $format, $context);
220221

221222
if (!$this->resourceClassResolver->isResourceClass($class)) {
222223
return $object;
223224
}
224225

226+
// Bypass the post-denormalize attribute revert logic if the object could not be
227+
// cloned since we cannot possibly revert any changes made to it.
228+
if (null !== $objectToPopulate && null === $previousObject) {
229+
return $object;
230+
}
231+
225232
// Revert attributes that aren't allowed to be changed after a post-denormalize check
226233
foreach (array_keys($data) as $attribute) {
227234
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
@@ -26,6 +26,7 @@
2626
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\DummyTableInheritance;
2727
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\DummyTableInheritanceChild;
2828
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\DummyTableInheritanceRelated;
29+
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\NonCloneableDummy;
2930
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\RelatedDummy;
3031
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\SecuredDummy;
3132
use Doctrine\Common\Collections\ArrayCollection;
@@ -1296,6 +1297,56 @@ public function testDenormalizeCollectionDecodedFromXmlWithOneChild(): void
12961297

12971298
$normalizer->denormalize($data, Dummy::class, 'xml');
12981299
}
1300+
1301+
public function testDenormalizePopulatingNonCloneableObject()
1302+
{
1303+
$dummy = new NonCloneableDummy();
1304+
$dummy->setName('foo');
1305+
1306+
$data = [
1307+
'name' => 'bar',
1308+
];
1309+
1310+
$propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class);
1311+
$propertyNameCollectionFactoryProphecy->create(NonCloneableDummy::class, [])->willReturn(new PropertyNameCollection(['name']));
1312+
1313+
$propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class);
1314+
$propertyMetadataFactoryProphecy->create(NonCloneableDummy::class, 'name', [])->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)])->withDescription('')->withReadable(false)->withWritable(true));
1315+
1316+
$iriConverterProphecy = $this->prophesize(IriConverterInterface::class);
1317+
$propertyAccessorProphecy = $this->prophesize(PropertyAccessorInterface::class);
1318+
$resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class);
1319+
$resourceClassResolverProphecy->getResourceClass(null, NonCloneableDummy::class)->willReturn(NonCloneableDummy::class);
1320+
$resourceClassResolverProphecy->getResourceClass($dummy, NonCloneableDummy::class)->willReturn(NonCloneableDummy::class);
1321+
$resourceClassResolverProphecy->isResourceClass(NonCloneableDummy::class)->willReturn(true);
1322+
1323+
$serializerProphecy = $this->prophesize(SerializerInterface::class);
1324+
$serializerProphecy->willImplement(NormalizerInterface::class);
1325+
1326+
$normalizer = $this->getMockForAbstractClass(AbstractItemNormalizer::class, [
1327+
$propertyNameCollectionFactoryProphecy->reveal(),
1328+
$propertyMetadataFactoryProphecy->reveal(),
1329+
$iriConverterProphecy->reveal(),
1330+
$resourceClassResolverProphecy->reveal(),
1331+
$propertyAccessorProphecy->reveal(),
1332+
null,
1333+
null,
1334+
null,
1335+
false,
1336+
[],
1337+
[],
1338+
null,
1339+
null,
1340+
]);
1341+
$normalizer->setSerializer($serializerProphecy->reveal());
1342+
1343+
$context = [AbstractItemNormalizer::OBJECT_TO_POPULATE => $dummy];
1344+
$actual = $normalizer->denormalize($data, NonCloneableDummy::class, null, $context);
1345+
1346+
$this->assertInstanceOf(NonCloneableDummy::class, $actual);
1347+
$this->assertSame($dummy, $actual);
1348+
$propertyAccessorProphecy->setValue($actual, 'name', 'bar')->shouldHaveBeenCalled();
1349+
}
12991350
}
13001351

13011352
class ObjectWithBasicProperties

0 commit comments

Comments
 (0)