diff --git a/features/main/custom_normalized.feature b/features/main/custom_normalized.feature index d4aeb276c11..8c40cd451f0 100644 --- a/features/main/custom_normalized.feature +++ b/features/main/custom_normalized.feature @@ -22,12 +22,67 @@ Feature: Using custom normalized entity "@context": "/contexts/CustomNormalizedDummy", "@id": "/custom_normalized_dummies/1", "@type": "CustomNormalizedDummy", + "id": 1, "name": "My Dummy", "alias": "My alias" } """ - Scenario: Get a resource + Scenario: Create a resource with a custom normalized dummy + When I add "Content-Type" header equal to "application/json" + When I add "Accept" header equal to "application/json" + And I send a "POST" request to "/related_normalized_dummies" with body: + """ + { + "name": "My Dummy" + } + """ + Then the response status code should be 201 + And the response should be in JSON + And the header "Content-Type" should be equal to "application/json; charset=utf-8" + And the JSON should be equal to: + """ + { + "id": 1, + "name": "My Dummy", + "customNormalizedDummy": [] + } + """ + + Scenario: Create a resource with a custom normalized dummy and an id + When I add "Content-Type" header equal to "application/json" + When I add "Accept" header equal to "application/json" + And I send a "PUT" request to "/related_normalized_dummies/1" with body: + """ + { + "name": "My Dummy", + "customNormalizedDummy":[{ + "@context": "/contexts/CustomNormalizedDummy", + "@id": "/custom_normalized_dummies/1", + "@type": "CustomNormalizedDummy", + "id": 1, + "name": "My Dummy" + }] + } + """ + Then the response status code should be 200 + And the response should be in JSON + And the header "Content-Type" should be equal to "application/json; charset=utf-8" + And the JSON should be equal to: + """ + { + "id": 1, + "name": "My Dummy", + "customNormalizedDummy":[{ + "id": 1, + "name": "My Dummy", + "alias": "My alias" + }] + } + """ + + + Scenario: Get a custom normalized dummy resource When I send a "GET" request to "/custom_normalized_dummies/1" Then the response status code should be 200 And the response should be in JSON @@ -38,6 +93,7 @@ Feature: Using custom normalized entity "@context": "/contexts/CustomNormalizedDummy", "@id": "/custom_normalized_dummies/1", "@type": "CustomNormalizedDummy", + "id": 1, "name": "My Dummy", "alias": "My alias" } @@ -58,6 +114,7 @@ Feature: Using custom normalized entity { "@id": "/custom_normalized_dummies/1", "@type": "CustomNormalizedDummy", + "id": 1, "name": "My Dummy", "alias": "My alias" } @@ -83,6 +140,7 @@ Feature: Using custom normalized entity "@context": "/contexts/CustomNormalizedDummy", "@id": "/custom_normalized_dummies/1", "@type": "CustomNormalizedDummy", + "id": 1, "name": "My Dummy modified", "alias": "My alias" } diff --git a/src/Serializer/ItemNormalizer.php b/src/Serializer/ItemNormalizer.php index 1f8249c2662..244371d4ee7 100644 --- a/src/Serializer/ItemNormalizer.php +++ b/src/Serializer/ItemNormalizer.php @@ -33,9 +33,30 @@ public function denormalize($data, $class, $format = null, array $context = []) throw new InvalidArgumentException('Update is not allowed for this operation.'); } - $context['object_to_populate'] = $this->iriConverter->getItemFromIri($data['id'], $context + ['fetch_data' => false]); + $this->updateObjectToPopulate($data, $context); } return parent::denormalize($data, $class, $format, $context); } + + private function updateObjectToPopulate(array $data, array &$context) + { + try { + $context['object_to_populate'] = $this->iriConverter->getItemFromIri($data['id'], $context + ['fetch_data' => false]); + } catch (InvalidArgumentException $e) { + $identifier = null; + foreach ($this->propertyNameCollectionFactory->create($context['resource_class'], $context) as $propertyName) { + if (true === $this->propertyMetadataFactory->create($context['resource_class'], $propertyName)->isIdentifier()) { + $identifier = $propertyName; + break; + } + } + + if (null === $identifier) { + throw $e; + } + + $context['object_to_populate'] = $this->iriConverter->getItemFromIri(sprintf('%s/%s', $this->iriConverter->getIriFromResourceClass($context['resource_class']), $data[$identifier]), $context + ['fetch_data' => false]); + } + } } diff --git a/tests/Fixtures/TestBundle/Entity/CustomNormalizedDummy.php b/tests/Fixtures/TestBundle/Entity/CustomNormalizedDummy.php index 5bf8139ed51..3ca542ca0e3 100644 --- a/tests/Fixtures/TestBundle/Entity/CustomNormalizedDummy.php +++ b/tests/Fixtures/TestBundle/Entity/CustomNormalizedDummy.php @@ -36,6 +36,7 @@ class CustomNormalizedDummy * @ORM\Column(type="integer") * @ORM\Id * @ORM\GeneratedValue(strategy="AUTO") + * @Groups({"input", "output"}) */ private $id; diff --git a/tests/Fixtures/TestBundle/Entity/DummyFriend.php b/tests/Fixtures/TestBundle/Entity/DummyFriend.php index bed9270ff83..9c7e85a751e 100644 --- a/tests/Fixtures/TestBundle/Entity/DummyFriend.php +++ b/tests/Fixtures/TestBundle/Entity/DummyFriend.php @@ -49,7 +49,7 @@ class DummyFriend /** * Get id. * - * @return id + * @return int */ public function getId() { @@ -69,7 +69,7 @@ public function setId($id) /** * Get name. * - * @return name + * @return string */ public function getName() { @@ -79,7 +79,7 @@ public function getName() /** * Set name. * - * @param name the value to set + * @param string the value to set */ public function setName($name) { diff --git a/tests/Fixtures/TestBundle/Entity/RelatedNormalizedDummy.php b/tests/Fixtures/TestBundle/Entity/RelatedNormalizedDummy.php new file mode 100644 index 00000000000..57c8642baec --- /dev/null +++ b/tests/Fixtures/TestBundle/Entity/RelatedNormalizedDummy.php @@ -0,0 +1,100 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity; + +use ApiPlatform\Core\Annotation\ApiProperty; +use ApiPlatform\Core\Annotation\ApiResource; +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\ORM\Mapping as ORM; +use Symfony\Component\Serializer\Annotation\Groups; +use Symfony\Component\Validator\Constraints as Assert; + +/** + * Related to Normalized Dummy. + * + * @author Amrouche Hamza + * + * @ApiResource(attributes={ + * "normalization_context"={"groups"={"related_output", "output"}}, + * "denormalization_context"={"groups"={"related_input", "input"}} + * }) + * @ORM\Entity + */ +class RelatedNormalizedDummy +{ + /** + * @var int The id + * + * @ORM\Column(type="integer") + * @ORM\Id + * @ORM\GeneratedValue(strategy="AUTO") + * @Groups({"related_output", "related_input"}) + */ + private $id; + + /** + * @var string The dummy name + * + * @ORM\Column + * @Assert\NotBlank + * @ApiProperty(iri="http://schema.org/name") + * @Groups({"related_output", "related_input"}) + */ + private $name; + + /** + * @var ArrayCollection Several Normalized dummies + * + * @ORM\ManyToMany(targetEntity="CustomNormalizedDummy") + * @Groups({"related_output", "related_input"}) + */ + public $customNormalizedDummy; + + public function __construct() + { + $this->customNormalizedDummy = new ArrayCollection(); + } + + public function getId(): int + { + return $this->id; + } + + /** + * @param string $name + */ + public function setName($name) + { + $this->name = $name; + } + + public function getName(): string + { + return $this->name; + } + + /** + * @return ArrayCollection + */ + public function getCustomNormalizedDummy() + { + return $this->customNormalizedDummy; + } + + /** + * @param ArrayCollection $customNormalizedDummy + */ + public function setCustomNormalizedDummy($customNormalizedDummy) + { + $this->customNormalizedDummy = $customNormalizedDummy; + } +} diff --git a/tests/Serializer/ItemNormalizerTest.php b/tests/Serializer/ItemNormalizerTest.php index f154b8486ff..347f54c6403 100644 --- a/tests/Serializer/ItemNormalizerTest.php +++ b/tests/Serializer/ItemNormalizerTest.php @@ -125,6 +125,37 @@ public function testDenormalize() $this->assertInstanceOf(Dummy::class, $normalizer->denormalize(['name' => 'hello'], Dummy::class, null, $context)); } + public function testDenormalizeWithIri() + { + $context = ['resource_class' => Dummy::class, 'api_allow_update' => true]; + + $propertyNameCollection = new PropertyNameCollection(['name']); + $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); + $propertyNameCollectionFactoryProphecy->create(Dummy::class, [])->willReturn($propertyNameCollection)->shouldBeCalled(); + + $propertyMetadataFactory = new PropertyMetadata(null, null, true); + $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); + $propertyMetadataFactoryProphecy->create(Dummy::class, 'name', [])->willReturn($propertyMetadataFactory)->shouldBeCalled(); + + $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); + $iriConverterProphecy->getItemFromIri('/dummies/12', ['resource_class' => Dummy::class, 'api_allow_update' => true, 'fetch_data' => false])->shouldBeCalled(); + + $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); + + $serializerProphecy = $this->prophesize(SerializerInterface::class); + $serializerProphecy->willImplement(DenormalizerInterface::class); + + $normalizer = new ItemNormalizer( + $propertyNameCollectionFactoryProphecy->reveal(), + $propertyMetadataFactoryProphecy->reveal(), + $iriConverterProphecy->reveal(), + $resourceClassResolverProphecy->reveal() + ); + $normalizer->setSerializer($serializerProphecy->reveal()); + + $this->assertInstanceOf(Dummy::class, $normalizer->denormalize(['id' => '/dummies/12', 'name' => 'hello'], Dummy::class, null, $context)); + } + /** * @expectedException \ApiPlatform\Core\Exception\InvalidArgumentException * @expectedExceptionMessage Update is not allowed for this operation. @@ -151,6 +182,6 @@ public function testDenormalizeWithIdAndUpdateNotAllowed() $resourceClassResolverProphecy->reveal() ); $normalizer->setSerializer($serializerProphecy->reveal()); - $normalizer->denormalize(['id' => '/dummies/12', 'name' => 'hello'], Dummy::class, null, $context); + $normalizer->denormalize(['id' => '12', 'name' => 'hello'], Dummy::class, null, $context); } }