From b7eca2d93b16975240d9bc16eefc5d38e1cbc986 Mon Sep 17 00:00:00 2001 From: Antoine Bluchet Date: Tue, 24 May 2016 18:14:29 +0200 Subject: [PATCH 1/2] fix iri identifier on doctrine proxy --- features/relation.feature | 57 ++++++++++ src/Bridge/Symfony/Routing/IriConverter.php | 74 ++++++++++--- .../TestBundle/Entity/DummyFriend.php | 89 +++++++++++++++ .../TestBundle/Entity/RelatedDummy.php | 26 +++++ .../Entity/RelatedToDummyFriend.php | 102 ++++++++++++++++++ 5 files changed, 336 insertions(+), 12 deletions(-) create mode 100644 tests/Fixtures/TestBundle/Entity/DummyFriend.php create mode 100644 tests/Fixtures/TestBundle/Entity/RelatedToDummyFriend.php diff --git a/features/relation.feature b/features/relation.feature index 4822aec6c74..bf505635fc5 100644 --- a/features/relation.feature +++ b/features/relation.feature @@ -23,6 +23,24 @@ Feature: Relations support } """ + Scenario: Create a dummy friend + When I send a "POST" request to "/dummy_friends" with body: + """ + {"name": "Zoidberg"} + """ + 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/ld+json" + And the JSON should be equal to: + """ + { + "@context": "/contexts/DummyFriend", + "@id": "/dummy_friends/1", + "@type": "DummyFriend", + "name": "Zoidberg" + } + """ + Scenario: Create a related dummy When I send a "POST" request to "/related_dummies" with body: """ @@ -42,12 +60,51 @@ Feature: Relations support "name": null, "dummyDate": null, "thirdLevel": "/third_levels/1", + "relatedToDummyFriend": null, "dummyBoolean": null, "symfony": "symfony", "age": null } """ + Scenario: Create a friend relationship + When I send a "POST" request to "/related_to_dummy_friends" with body: + """ + { + "name": "Friends relation", + "dummyFriend": "/dummy_friends/1", + "relatedDummy": "/related_dummies/1" + } + """ + 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/ld+json" + And the JSON should be equal to: + """ + { + "@context": "/contexts/RelatedToDummyFriend", + "@id": "/related_to_dummy_friends/dummyFriend=1;relatedDummy=1", + "@type": "RelatedToDummyFriend", + "name": "Friends relation" + } + """ + + Scenario: Get the relationship + When I send a "GET" request to "/related_to_dummy_friends/dummyFriend=1;relatedDummy=1" + And the response status code should be 200 + And the response should be in JSON + And the header "Content-Type" should be equal to "application/ld+json" + And the JSON should be equal to: + """ + { + "@context": "/contexts/RelatedToDummyFriend", + "@id": "/related_to_dummy_friends/dummyFriend=1;relatedDummy=1", + "@type": "RelatedToDummyFriend", + "name": "Friends relation" + } + """ + + Scenario: Create a dummy with relations When I send a "POST" request to "/dummies" with body: """ diff --git a/src/Bridge/Symfony/Routing/IriConverter.php b/src/Bridge/Symfony/Routing/IriConverter.php index d24c41584ca..a3c8fa46539 100644 --- a/src/Bridge/Symfony/Routing/IriConverter.php +++ b/src/Bridge/Symfony/Routing/IriConverter.php @@ -77,25 +77,75 @@ public function getIriFromItem($item, int $referenceType = UrlGeneratorInterface $resourceClass = $this->getObjectClass($item); $routeName = $this->getRouteName($resourceClass, false); + $identifiers = $this->generateIdentifiersUrl($this->getIdentifiersFromItem($item)); + + return $this->router->generate($routeName, ['id' => implode(';', $identifiers)], $referenceType); + } + + /** + * Generate the identifier url. + * + * @param array $identifiers + * + * @return array + */ + public function generateIdentifiersUrl(array $identifiers) : array + { + if (1 === count($identifiers)) { + return [rawurlencode(array_values($identifiers)[0])]; + } + + foreach ($identifiers as $name => $value) { + $identifiers[$name] = sprintf('%s=%s', $name, $value); + } + + return $identifiers; + } + + /** + * Find identifiers from an Item (Object). + * + * @param object $item + * + * @throws RuntimeException + * + * @return array + */ + private function getIdentifiersFromItem($item) : array + { + $identifiers = []; + $resourceClass = $this->getObjectClass($item); + foreach ($this->propertyNameCollectionFactory->create($resourceClass) as $propertyName) { $propertyMetadata = $this->propertyMetadataFactory->create($resourceClass, $propertyName); - if ($propertyMetadata->isIdentifier()) { - $identifiers[$propertyName] = $this->propertyAccessor->getValue($item, $propertyName); + if (!$propertyMetadata->isIdentifier()) { + continue; } - } - if (1 === count($identifiers)) { - $identifiers = array_map(function ($identifierValue) { - return rawurlencode($identifierValue); - }, $identifiers); - } else { - $identifiers = array_map(function ($identifierName, $identifierValue) { - return sprintf('%s=%s', $identifierName, rawurlencode($identifierValue)); - }, array_keys($identifiers), $identifiers); + $identifiers[$propertyName] = $this->propertyAccessor->getValue($item, $propertyName); + + if (!is_object($identifiers[$propertyName])) { + continue; + } + + $relatedResourceClass = $this->getObjectClass($identifiers[$propertyName]); + $relatedItem = $identifiers[$propertyName]; + + foreach ($this->propertyNameCollectionFactory->create($relatedResourceClass) as $relatedPropertyName) { + $propertyMetadata = $this->propertyMetadataFactory->create($relatedResourceClass, $relatedPropertyName); + + if ($propertyMetadata->isIdentifier()) { + $identifiers[$propertyName] = $this->propertyAccessor->getValue($relatedItem, $relatedPropertyName); + } + } + + if (empty($identifiers[$propertyName])) { + throw new \RuntimeException(sprintf('%s identifiers can not be found', $resourceClass)); + } } - return $this->router->generate($routeName, ['id' => implode(';', $identifiers)], $referenceType); + return $identifiers; } /** diff --git a/tests/Fixtures/TestBundle/Entity/DummyFriend.php b/tests/Fixtures/TestBundle/Entity/DummyFriend.php new file mode 100644 index 00000000000..8b7e271eeb1 --- /dev/null +++ b/tests/Fixtures/TestBundle/Entity/DummyFriend.php @@ -0,0 +1,89 @@ + + * + * 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\ORM\Mapping as ORM; +use Symfony\Component\Serializer\Annotation\Groups; +use Symfony\Component\Validator\Constraints as Assert; + +/** + * DummyFriend. + * + * @author Kévin Dunglas + * + * @ApiResource() + * @ORM\Entity + */ +class DummyFriend +{ + /** + * @var int The id. + * + * @ORM\Column(type="integer") + * @ORM\Id + * @ORM\GeneratedValue(strategy="AUTO") + * @Groups({"fakemanytomany"}) + */ + private $id; + + /** + * @var string The dummy name. + * + * @ORM\Column + * @Assert\NotBlank + * @ApiProperty(iri="http://schema.org/name") + * @Groups({"fakemanytomany"}) + */ + private $name; + + /** + * Get id. + * + * @return id. + */ + public function getId() + { + return $this->id; + } + + /** + * Set id. + * + * @param id the value to set. + */ + public function setId($id) + { + $this->id = $id; + } + + /** + * Get name. + * + * @return name. + */ + public function getName() + { + return $this->name; + } + + /** + * Set name. + * + * @param name the value to set. + */ + public function setName($name) + { + $this->name = $name; + } +} diff --git a/tests/Fixtures/TestBundle/Entity/RelatedDummy.php b/tests/Fixtures/TestBundle/Entity/RelatedDummy.php index e9c36118d0c..ff1789dd668 100644 --- a/tests/Fixtures/TestBundle/Entity/RelatedDummy.php +++ b/tests/Fixtures/TestBundle/Entity/RelatedDummy.php @@ -60,6 +60,12 @@ class RelatedDummy extends ParentDummy */ public $thirdLevel; + /** + * @ORM\OneToMany(targetEntity="RelatedToDummyFriend", cascade={"persist"}, fetch="EAGER", mappedBy="relatedDummy") + * @Groups({"fakemanytomany"}) + */ + public $relatedToDummyFriend; + /** * @var bool A dummy bool. * @@ -117,4 +123,24 @@ public function setDummyBoolean($dummyBoolean) { $this->dummyBoolean = $dummyBoolean; } + + /** + * Get relatedToDummyFriend. + * + * @return relatedToDummyFriend. + */ + public function getRelatedToDummyFriend() + { + return $this->relatedToDummyFriend; + } + + /** + * Set relatedToDummyFriend. + * + * @param relatedToDummyFriend the value to set. + */ + public function setRelatedToDummyFriend(RelatedToDummyFriend $relatedToDummyFriend) + { + $this->relatedToDummyFriend = $relatedToDummyFriend; + } } diff --git a/tests/Fixtures/TestBundle/Entity/RelatedToDummyFriend.php b/tests/Fixtures/TestBundle/Entity/RelatedToDummyFriend.php new file mode 100644 index 00000000000..b2b0d1b98af --- /dev/null +++ b/tests/Fixtures/TestBundle/Entity/RelatedToDummyFriend.php @@ -0,0 +1,102 @@ + + * + * 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\ORM\Mapping as ORM; +use Symfony\Component\Serializer\Annotation\Groups; +use Symfony\Component\Validator\Constraints as Assert; + +/** + * Related To Dummy Friend represent an association table for a manytomany relation. + * + * @ApiResource() + * @ORM\Entity + */ +class RelatedToDummyFriend +{ + /** + * @var string The dummy name. + * + * @ORM\Column + * @Assert\NotBlank + * @ApiProperty(iri="http://schema.org/name") + */ + private $name; + + /** + * @ORM\Id + * @ORM\ManyToOne(targetEntity="DummyFriend", fetch="EAGER") + * @ORM\JoinColumn(name="dummyfriend_id", referencedColumnName="id", nullable=false) + * @Groups({"fakemanytomany"}) + */ + private $dummyFriend; + + /** + * @ORM\Id + * @ORM\ManyToOne(targetEntity="RelatedDummy", inversedBy="relatedToDummyFriend") + * @ORM\JoinColumn(name="relateddummy_id", referencedColumnName="id", nullable=false, onDelete="CASCADE") + * @Groups({"fakemanytomany"}) + */ + private $relatedDummy; + + public function setName($name) + { + $this->name = $name; + } + + public function getName() + { + return $this->name; + } + + /** + * Get dummyFriend. + * + * @return dummyFriend. + */ + public function getDummyFriend() + { + return $this->dummyFriend; + } + + /** + * Set dummyFriend. + * + * @param dummyFriend the value to set. + */ + public function setDummyFriend($dummyFriend) + { + $this->dummyFriend = $dummyFriend; + } + + /** + * Get relatedDummy. + * + * @return relatedDummy. + */ + public function getRelatedDummy() + { + return $this->relatedDummy; + } + + /** + * Set relatedDummy. + * + * @param relatedDummy the value to set. + */ + public function setRelatedDummy($relatedDummy) + { + $this->relatedDummy = $relatedDummy; + } +} From 96fd131302ab57ba223c3ffedfe26bfba21f5385 Mon Sep 17 00:00:00 2001 From: Antoine Bluchet Date: Wed, 25 May 2016 17:39:21 +0200 Subject: [PATCH 2/2] fix identifier normalization group override --- features/relation.feature | 14 ++++++++++++-- .../DoctrineOrmPropertyMetadataFactory.php | 1 - .../Bundle/Resources/config/doctrine_orm.xml | 2 +- src/JsonLd/Serializer/ItemNormalizer.php | 2 +- .../Factory/SerializerPropertyMetadataFactory.php | 4 +++- tests/Fixtures/TestBundle/Entity/DummyFriend.php | 1 - .../TestBundle/Entity/RelatedToDummyFriend.php | 4 ++-- 7 files changed, 19 insertions(+), 9 deletions(-) diff --git a/features/relation.feature b/features/relation.feature index bf505635fc5..c172e9b94bc 100644 --- a/features/relation.feature +++ b/features/relation.feature @@ -85,7 +85,12 @@ Feature: Relations support "@context": "/contexts/RelatedToDummyFriend", "@id": "/related_to_dummy_friends/dummyFriend=1;relatedDummy=1", "@type": "RelatedToDummyFriend", - "name": "Friends relation" + "name": "Friends relation", + "dummyFriend": { + "@id": "/dummy_friends/1", + "@type": "DummyFriend", + "name": "Zoidberg" + } } """ @@ -100,7 +105,12 @@ Feature: Relations support "@context": "/contexts/RelatedToDummyFriend", "@id": "/related_to_dummy_friends/dummyFriend=1;relatedDummy=1", "@type": "RelatedToDummyFriend", - "name": "Friends relation" + "name": "Friends relation", + "dummyFriend": { + "@id": "/dummy_friends/1", + "@type": "DummyFriend", + "name": "Zoidberg" + } } """ diff --git a/src/Bridge/Doctrine/Orm/Metadata/Property/DoctrineOrmPropertyMetadataFactory.php b/src/Bridge/Doctrine/Orm/Metadata/Property/DoctrineOrmPropertyMetadataFactory.php index 4443038714a..c9677c2ff0d 100644 --- a/src/Bridge/Doctrine/Orm/Metadata/Property/DoctrineOrmPropertyMetadataFactory.php +++ b/src/Bridge/Doctrine/Orm/Metadata/Property/DoctrineOrmPropertyMetadataFactory.php @@ -56,7 +56,6 @@ public function create(string $resourceClass, string $property, array $options = foreach ($identifiers as $identifier) { if ($identifier === $property) { $propertyMetadata = $propertyMetadata->withIdentifier(true); - $propertyMetadata = $propertyMetadata->withReadable(false); $propertyMetadata = $propertyMetadata->withWritable($doctrineClassMetadata->isIdentifierNatural()); break; diff --git a/src/Bridge/Symfony/Bundle/Resources/config/doctrine_orm.xml b/src/Bridge/Symfony/Bundle/Resources/config/doctrine_orm.xml index 4da44351980..ef0e4dc072f 100644 --- a/src/Bridge/Symfony/Bundle/Resources/config/doctrine_orm.xml +++ b/src/Bridge/Symfony/Bundle/Resources/config/doctrine_orm.xml @@ -64,7 +64,7 @@ - + diff --git a/src/JsonLd/Serializer/ItemNormalizer.php b/src/JsonLd/Serializer/ItemNormalizer.php index 95940dd675d..2e132de4ac3 100644 --- a/src/JsonLd/Serializer/ItemNormalizer.php +++ b/src/JsonLd/Serializer/ItemNormalizer.php @@ -193,7 +193,7 @@ protected function getAllowedAttributes($classOrObject, array $context, $attribu $propertyMetadata = $this->propertyMetadataFactory->create($context['resource_class'], $propertyName, $options); if ( - (isset($context['jsonld_normalize']) && !$propertyMetadata->isIdentifier() && $propertyMetadata->isReadable()) || + (isset($context['jsonld_normalize']) && $propertyMetadata->isReadable()) || (isset($context['jsonld_denormalize']) && $propertyMetadata->isWritable()) ) { $allowedAttributes[] = $propertyName; diff --git a/src/Metadata/Property/Factory/SerializerPropertyMetadataFactory.php b/src/Metadata/Property/Factory/SerializerPropertyMetadataFactory.php index 9e29486a234..45946e83e14 100644 --- a/src/Metadata/Property/Factory/SerializerPropertyMetadataFactory.php +++ b/src/Metadata/Property/Factory/SerializerPropertyMetadataFactory.php @@ -68,7 +68,9 @@ private function transformReadWrite(PropertyMetadata $propertyMetadata, string $ { $groups = $this->getPropertySerializerGroups($resourceClass, $property); - if (false !== $propertyMetadata->isReadable()) { + if ($propertyMetadata->isIdentifier()) { + $propertyMetadata = $propertyMetadata->withReadable(null !== $normalizationGroups && !empty(array_intersect($normalizationGroups, $groups))); + } elseif (false !== $propertyMetadata->isReadable()) { $propertyMetadata = $propertyMetadata->withReadable(null === $normalizationGroups || !empty(array_intersect($normalizationGroups, $groups))); } if (false !== $propertyMetadata->isWritable()) { diff --git a/tests/Fixtures/TestBundle/Entity/DummyFriend.php b/tests/Fixtures/TestBundle/Entity/DummyFriend.php index 8b7e271eeb1..6bfe36e0c5b 100644 --- a/tests/Fixtures/TestBundle/Entity/DummyFriend.php +++ b/tests/Fixtures/TestBundle/Entity/DummyFriend.php @@ -33,7 +33,6 @@ class DummyFriend * @ORM\Column(type="integer") * @ORM\Id * @ORM\GeneratedValue(strategy="AUTO") - * @Groups({"fakemanytomany"}) */ private $id; diff --git a/tests/Fixtures/TestBundle/Entity/RelatedToDummyFriend.php b/tests/Fixtures/TestBundle/Entity/RelatedToDummyFriend.php index b2b0d1b98af..ef432b45bc2 100644 --- a/tests/Fixtures/TestBundle/Entity/RelatedToDummyFriend.php +++ b/tests/Fixtures/TestBundle/Entity/RelatedToDummyFriend.php @@ -20,7 +20,7 @@ /** * Related To Dummy Friend represent an association table for a manytomany relation. * - * @ApiResource() + * @ApiResource(attributes={"normalization_context"={"groups": {"fakemanytomany"}}}) * @ORM\Entity */ class RelatedToDummyFriend @@ -31,6 +31,7 @@ class RelatedToDummyFriend * @ORM\Column * @Assert\NotBlank * @ApiProperty(iri="http://schema.org/name") + * @Groups({"fakemanytomany"}) */ private $name; @@ -46,7 +47,6 @@ class RelatedToDummyFriend * @ORM\Id * @ORM\ManyToOne(targetEntity="RelatedDummy", inversedBy="relatedToDummyFriend") * @ORM\JoinColumn(name="relateddummy_id", referencedColumnName="id", nullable=false, onDelete="CASCADE") - * @Groups({"fakemanytomany"}) */ private $relatedDummy;