From 5ab3dab293606d10bc3a533b66a1d43daa2c4d23 Mon Sep 17 00:00:00 2001 From: Amrouche Hamza Date: Sat, 7 May 2016 17:18:33 +0200 Subject: [PATCH 1/2] Add behat test and change behavior of ItemDataProvider.php when using uuid feature. and fix composite --- features/bootstrap/FeatureContext.php | 14 +++ features/composite.feature | 20 ++++- features/uuid.feature | 81 +++++++++++++++++ src/Bridge/Doctrine/Orm/ItemDataProvider.php | 86 ++++++++++++++----- .../TestBundle/Entity/UuidIdentifierDummy.php | 59 +++++++++++++ 5 files changed, 238 insertions(+), 22 deletions(-) create mode 100644 features/uuid.feature create mode 100644 tests/Fixtures/TestBundle/Entity/UuidIdentifierDummy.php diff --git a/features/bootstrap/FeatureContext.php b/features/bootstrap/FeatureContext.php index fc38755c236..2cfdf5abcfa 100644 --- a/features/bootstrap/FeatureContext.php +++ b/features/bootstrap/FeatureContext.php @@ -16,6 +16,7 @@ use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\FileConfigDummy; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\RelatedDummy; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\RelationEmbedder; +use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\UuidIdentifierDummy; use Behat\Behat\Context\Context; use Behat\Behat\Context\SnippetAcceptingContext; use Doctrine\Common\Persistence\ManagerRegistry; @@ -251,6 +252,19 @@ public function thereIsARelationEmbedderObject() $this->manager->flush(); } + /** + * @Given there is a Dummy Object mapped by UUID + */ + public function thereIsADummyObjectMappedByUUID() + { + $dummy = new UuidIdentifierDummy(); + $dummy->setName('My Dummy'); + $dummy->setUuid('41B29566-144B-11E6-A148-3E1D05DEFE78'); + + $this->manager->persist($dummy); + $this->manager->flush(); + } + /** * @Given there are Composite identifier objects */ diff --git a/features/composite.feature b/features/composite.feature index fc2d5632fbe..22a15cb76de 100644 --- a/features/composite.feature +++ b/features/composite.feature @@ -5,7 +5,7 @@ Feature: Retrieve data with Composite identifiers @createSchema @dropSchema - Scenario: Get collection with composite identifiers + Scenario: Get a composite item with composite identifiers Given there are Composite identifier objects When I send a "GET" request to "/composite_items" Then the response status code should be 200 @@ -78,3 +78,21 @@ Feature: Retrieve data with Composite identifiers } } """ + + @createSchema + @dropSchema + Scenario: Get the first composite relation + Given there are Composite identifier objects + When I send a "GET" request to "/composite_relations/1-1" + 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/ld+json" + + @createSchema + @dropSchema + Scenario: Get first composite item + Given there are Composite identifier objects + When I send a "GET" request to "/composite_items/1" + 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/ld+json" diff --git a/features/uuid.feature b/features/uuid.feature new file mode 100644 index 00000000000..f6c1ecc7f2f --- /dev/null +++ b/features/uuid.feature @@ -0,0 +1,81 @@ +Feature: Using uuid identifier on resource + In order to use an hypermedia API + As a client software developer + I need to be able to user other identifier than id in resource and set it via API call on POST / PUT. + + @createSchema + Scenario: Create a resource + When I send a "POST" request to "/uuid_identifier_dummies" with body: + """ + { + "name": "My Dummy", + "uuid": "41B29566-144B-11E6-A148-3E1D05DEFE78" + } + """ + 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" + + Scenario: Get a resource + When I send a "GET" request to "/uuid_identifier_dummies/41B29566-144B-11E6-A148-3E1D05DEFE78" + 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/ld+json" + And the JSON should be equal to: + """ + { + "@context": "/contexts/UuidIdentifierDummy", + "@id": "/uuid_identifier_dummies/41B29566-144B-11E6-A148-3E1D05DEFE78", + "@type": "UuidIdentifierDummy", + "name": "My Dummy" + } + """ + + Scenario: Get a collection + When I send a "GET" request to "/uuid_identifier_dummies" + 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/ld+json" + And the JSON should be equal to: + """ + { + "@context": "/contexts/UuidIdentifierDummy", + "@id": "/uuid_identifier_dummies", + "@type": "hydra:Collection", + "hydra:member": [ + { + "@id": "/uuid_identifier_dummies/41B29566-144B-11E6-A148-3E1D05DEFE78", + "@type": "UuidIdentifierDummy", + "name": "My Dummy" + } + ], + "hydra:totalItems": 1 + } + """ + + Scenario: Update a resource + When I send a "PUT" request to "/uuid_identifier_dummies/41B29566-144B-11E6-A148-3E1D05DEFE78" with body: + """ + { + "name": "My Dummy modified" + } + """ + 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/ld+json" + And the JSON should be equal to: + """ + { + "@context": "/contexts/UuidIdentifierDummy", + "@id": "/uuid_identifier_dummies/41B29566-144B-11E6-A148-3E1D05DEFE78", + "@type": "UuidIdentifierDummy", + "name": "My Dummy modified" + } + """ + + + @dropSchema + Scenario: Delete a resource + When I send a "DELETE" request to "/uuid_identifier_dummies/41B29566-144B-11E6-A148-3E1D05DEFE78" + Then the response status code should be 204 + And the response should be empty diff --git a/src/Bridge/Doctrine/Orm/ItemDataProvider.php b/src/Bridge/Doctrine/Orm/ItemDataProvider.php index 78f2f0ed3a6..ba4a78db7de 100644 --- a/src/Bridge/Doctrine/Orm/ItemDataProvider.php +++ b/src/Bridge/Doctrine/Orm/ItemDataProvider.php @@ -19,6 +19,7 @@ use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; use Doctrine\Common\Persistence\ManagerRegistry; use Doctrine\ORM\EntityManagerInterface; +use Doctrine\ORM\QueryBuilder; /** * Item data provider for the Doctrine ORM. @@ -68,7 +69,69 @@ public function getItem(string $resourceClass, $id, string $operationName = null throw new ResourceClassNotSupportedException(); } - $identifierValues = explode('-', $id); + $identifiers = $this->normalizeIdentifiers($id, $manager, $resourceClass); + + if (!$fetchData && $manager instanceof EntityManagerInterface) { + return $manager->getReference($resourceClass, $identifiers); + } + + $repository = $manager->getRepository($resourceClass); + $queryBuilder = $repository->createQueryBuilder('o'); + + $this->addWhereForIdentifiers($identifiers, $queryBuilder); + + foreach ($this->itemExtensions as $extension) { + $extension->applyToItem($queryBuilder, $resourceClass, $identifiers, $operationName); + } + + return $queryBuilder->getQuery()->getOneOrNullResult(); + } + + /** + * Add where into the query when multiple identifiers are passed (composite). + * + * @param array $identifiers + * @param QueryBuilder $queryBuilder + * + * Populate the query with where when needed. + */ + private function addWhereForIdentifiers(array $identifiers, QueryBuilder $queryBuilder) + { + if (empty($identifiers)) { + return; + } + + foreach ($identifiers as $identifier => $value) { + $placeholder = ':id_'.$identifier; + $expression = $queryBuilder->expr()->eq( + 'o.'.$identifier, + $placeholder + ); + + $queryBuilder->andWhere($expression); + + $queryBuilder->setParameter($placeholder, $value); + } + } + + /** + * Transform and check the identifier, composite or not. + * + * @param $id + * @param $manager + * @param $resourceClass + * + * @return array + */ + private function normalizeIdentifiers($id, $manager, $resourceClass) : array + { + $doctrineMetadataIdentifier = $manager->getClassMetadata($resourceClass)->getIdentifier(); + $identifierValues = [$id]; + + if (count($doctrineMetadataIdentifier) >= 2) { + $identifierValues = explode('-', $id); + } + $identifiers = []; $i = 0; @@ -88,25 +151,6 @@ public function getItem(string $resourceClass, $id, string $operationName = null ++$i; } - if (!$fetchData && $manager instanceof EntityManagerInterface) { - return $manager->getReference($resourceClass, $identifiers); - } - - $repository = $manager->getRepository($resourceClass); - $queryBuilder = $repository->createQueryBuilder('o'); - - foreach ($identifiers as $propertyName => $value) { - $placeholder = 'id_'.$propertyName; - - $queryBuilder - ->where($queryBuilder->expr()->eq('o.'.$propertyName, ':'.$placeholder)) - ->setParameter($placeholder, $value); - } - - foreach ($this->itemExtensions as $extension) { - $extension->applyToItem($queryBuilder, $resourceClass, $identifiers, $operationName); - } - - return $queryBuilder->getQuery()->getOneOrNullResult(); + return $identifiers; } } diff --git a/tests/Fixtures/TestBundle/Entity/UuidIdentifierDummy.php b/tests/Fixtures/TestBundle/Entity/UuidIdentifierDummy.php new file mode 100644 index 00000000000..f0c27f19b16 --- /dev/null +++ b/tests/Fixtures/TestBundle/Entity/UuidIdentifierDummy.php @@ -0,0 +1,59 @@ + + * + * 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\Resource; +use Doctrine\ORM\Mapping as ORM; + +/** + * Custom identifier dummy. + * + * @Resource + * @ORM\Entity + */ +class UuidIdentifierDummy +{ + /** + * @var string The custom identifier. + * + * @ORM\Column(type="guid") + * @ORM\Id + */ + private $uuid; + + /** + * @var string The dummy name. + * + * @ORM\Column(length=30) + */ + private $name; + + public function getUuid(): string + { + return $this->uuid; + } + + public function setUuid(string $uuid) + { + $this->uuid = $uuid; + } + + public function getName(): string + { + return $this->name; + } + + public function setName(string $name) + { + $this->name = $name; + } +} From 1025bbecbd2c01e627d5dfd458d775a8b3aea1fc Mon Sep 17 00:00:00 2001 From: Amrouche Hamza Date: Wed, 11 May 2016 22:05:54 +0200 Subject: [PATCH 2/2] fix #530 - Support id;id for composite --- features/composite.feature | 61 +++++++++---------- src/Bridge/Doctrine/Orm/ItemDataProvider.php | 14 +++-- src/Bridge/Symfony/Routing/IriConverter.php | 15 ++++- .../TestBundle/Entity/CompositeRelation.php | 10 --- 4 files changed, 51 insertions(+), 49 deletions(-) diff --git a/features/composite.feature b/features/composite.feature index 22a15cb76de..c5b0a4f65a8 100644 --- a/features/composite.feature +++ b/features/composite.feature @@ -5,7 +5,7 @@ Feature: Retrieve data with Composite identifiers @createSchema @dropSchema - Scenario: Get a composite item with composite identifiers + Scenario: Get a collection with composite identifiers Given there are Composite identifier objects When I send a "GET" request to "/composite_items" Then the response status code should be 200 @@ -14,23 +14,23 @@ Feature: Retrieve data with Composite identifiers And the JSON should be equal to: """ { - "@context": "/contexts/CompositeItem", - "@id": "/composite_items", - "@type": "hydra:Collection", - "hydra:member": [ - { - "@id": "/composite_items/1", - "@type": "CompositeItem", - "field1": "foobar", - "compositeValues": [ - "/composite_relations/1-1", - "/composite_relations/1-2", - "/composite_relations/1-3", - "/composite_relations/1-4" - ] - } - ], - "hydra:totalItems": 1 + "@context": "/contexts/CompositeItem", + "@id": "/composite_items", + "@type": "hydra:Collection", + "hydra:member": [ + { + "@id": "/composite_items/1", + "@type": "CompositeItem", + "field1": "foobar", + "compositeValues": [ + "/composite_relations/compositeItem=1;compositeLabel=1", + "/composite_relations/compositeItem=1;compositeLabel=2", + "/composite_relations/compositeItem=1;compositeLabel=3", + "/composite_relations/compositeItem=1;compositeLabel=4" + ] + } + ], + "hydra:totalItems": 1 } """ @@ -45,45 +45,42 @@ Feature: Retrieve data with Composite identifiers And the JSON should be equal to: """ { - "@context": "\/contexts\/CompositeRelation", - "@id": "\/composite_relations", + "@context": "/contexts/CompositeRelation", + "@id": "/composite_relations", "@type": "hydra:Collection", "hydra:member": [ { - "@id": "\/composite_relations\/1-1", + "@id": "/composite_relations/compositeItem=1;compositeLabel=1", "@type": "CompositeRelation", - "id": "1-1", "value": "somefoobardummy" }, { - "@id": "\/composite_relations\/1-2", + "@id": "/composite_relations/compositeItem=1;compositeLabel=2", "@type": "CompositeRelation", - "id": "1-2", "value": "somefoobardummy" }, { - "@id": "\/composite_relations\/1-3", + "@id": "/composite_relations/compositeItem=1;compositeLabel=3", "@type": "CompositeRelation", - "id": "1-3", "value": "somefoobardummy" } ], "hydra:totalItems": 4, "hydra:view": { - "@id": "\/composite_relations?page=1", + "@id": "/composite_relations?page=1", "@type": "hydra:PartialCollectionView", - "hydra:first": "\/composite_relations?page=1", - "hydra:last": "\/composite_relations?page=2", - "hydra:next": "\/composite_relations?page=2" + "hydra:first": "/composite_relations?page=1", + "hydra:last": "/composite_relations?page=2", + "hydra:next": "/composite_relations?page=2" } - } + } """ @createSchema @dropSchema Scenario: Get the first composite relation Given there are Composite identifier objects - When I send a "GET" request to "/composite_relations/1-1" + When I send a "GET" request to "/composite_relations/compositeItem=1;compositeLabel=1" 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/ld+json" diff --git a/src/Bridge/Doctrine/Orm/ItemDataProvider.php b/src/Bridge/Doctrine/Orm/ItemDataProvider.php index ba4a78db7de..ae50773fab3 100644 --- a/src/Bridge/Doctrine/Orm/ItemDataProvider.php +++ b/src/Bridge/Doctrine/Orm/ItemDataProvider.php @@ -88,12 +88,10 @@ public function getItem(string $resourceClass, $id, string $operationName = null } /** - * Add where into the query when multiple identifiers are passed (composite). + * Add WHERE conditions to the query for one or more identifiers (simple or composite). * * @param array $identifiers * @param QueryBuilder $queryBuilder - * - * Populate the query with where when needed. */ private function addWhereForIdentifiers(array $identifiers, QueryBuilder $queryBuilder) { @@ -127,9 +125,17 @@ private function normalizeIdentifiers($id, $manager, $resourceClass) : array { $doctrineMetadataIdentifier = $manager->getClassMetadata($resourceClass)->getIdentifier(); $identifierValues = [$id]; + $identifierValuesArray = []; if (count($doctrineMetadataIdentifier) >= 2) { - $identifierValues = explode('-', $id); + $identifierValues = explode(';', $id); + foreach ($identifierValues as $key => $value) { + $identifierValueArray = explode('=', $value); + if ($doctrineMetadataIdentifier[$key] === $identifierValueArray[0]) { + $identifierValuesArray[] = $identifierValueArray[1]; + } + } + $identifierValues = $identifierValuesArray; } $identifiers = []; diff --git a/src/Bridge/Symfony/Routing/IriConverter.php b/src/Bridge/Symfony/Routing/IriConverter.php index ca7b4f8b7f1..d24c41584ca 100644 --- a/src/Bridge/Symfony/Routing/IriConverter.php +++ b/src/Bridge/Symfony/Routing/IriConverter.php @@ -77,16 +77,25 @@ public function getIriFromItem($item, int $referenceType = UrlGeneratorInterface $resourceClass = $this->getObjectClass($item); $routeName = $this->getRouteName($resourceClass, false); - $identifierValues = []; foreach ($this->propertyNameCollectionFactory->create($resourceClass) as $propertyName) { $propertyMetadata = $this->propertyMetadataFactory->create($resourceClass, $propertyName); if ($propertyMetadata->isIdentifier()) { - $identifierValues[] = $this->propertyAccessor->getValue($item, $propertyName); + $identifiers[$propertyName] = $this->propertyAccessor->getValue($item, $propertyName); } } - return $this->router->generate($routeName, ['id' => implode('-', $identifierValues)], $referenceType); + 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); + } + + return $this->router->generate($routeName, ['id' => implode(';', $identifiers)], $referenceType); } /** diff --git a/tests/Fixtures/TestBundle/Entity/CompositeRelation.php b/tests/Fixtures/TestBundle/Entity/CompositeRelation.php index 9be1303b093..abc1cd7201a 100644 --- a/tests/Fixtures/TestBundle/Entity/CompositeRelation.php +++ b/tests/Fixtures/TestBundle/Entity/CompositeRelation.php @@ -43,16 +43,6 @@ class CompositeRelation */ private $compositeLabel; - /** - * Get composite id. - * - * @return string - */ - public function getId() - { - return sprintf('%s-%s', $this->compositeItem->getId(), $this->compositeLabel->getId()); - } - /** * Get value. *