diff --git a/features/bootstrap/DoctrineContext.php b/features/bootstrap/DoctrineContext.php index c6c4ba87e9e..319c717e9ce 100644 --- a/features/bootstrap/DoctrineContext.php +++ b/features/bootstrap/DoctrineContext.php @@ -15,6 +15,7 @@ use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\AbsoluteUrlRelationDummy as AbsoluteUrlRelationDummyDocument; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\Address as AddressDocument; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\Answer as AnswerDocument; +use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\Book as BookDocument; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\CompositeItem as CompositeItemDocument; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\CompositeLabel as CompositeLabelDocument; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\CompositePrimitiveItem as CompositePrimitiveItemDocument; @@ -26,6 +27,7 @@ use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\ConvertedRelated as ConvertedRelatedDocument; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\ConvertedString as ConvertedStringDocument; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\Customer as CustomerDocument; +use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\CustomMultipleIdentifierDummy as CustomMultipleIdentifierDummyDocument; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\Dummy as DummyDocument; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\DummyAggregateOffer as DummyAggregateOfferDocument; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\DummyCar as DummyCarDocument; @@ -78,6 +80,7 @@ use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\AbsoluteUrlRelationDummy; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Address; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Answer; +use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Book; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\CompositeItem; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\CompositeLabel; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\CompositePrimitiveItem; @@ -89,6 +92,7 @@ use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\ConvertedRelated; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\ConvertedString; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Customer; +use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\CustomMultipleIdentifierDummy; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Dummy; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyAggregateOffer; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyCar; @@ -1605,6 +1609,32 @@ public function thereIsAnInitializeInput(int $id) $this->manager->flush(); } + /** + * @Given there is a book + */ + public function thereIsABook() + { + $book = $this->buildBook(); + $book->name = '1984'; + $book->isbn = '9780451524935'; + $this->manager->persist($book); + $this->manager->flush(); + } + + /** + * @Given there is a custom multiple identifier dummy + */ + public function thereIsACustomMultipleIdentifierDummy() + { + $dummy = $this->buildCustomMultipleIdentifierDummy(); + $dummy->setName('Orwell'); + $dummy->setFirstId(1); + $dummy->setSecondId(2); + + $this->manager->persist($dummy); + $this->manager->flush(); + } + private function isOrm(): bool { return null !== $this->schemaTool; @@ -2030,4 +2060,20 @@ private function buildInitializeInput() { return $this->isOrm() ? new InitializeInput() : new InitializeInputDocument(); } + + /** + * @return BookDocument | Book + */ + private function buildBook() + { + return $this->isOrm() ? new Book() : new BookDocument(); + } + + /** + * @return CustomMultipleIdentifierDummy | CustomMultipleIdentifierDummyDocument + */ + private function buildCustomMultipleIdentifierDummy() + { + return $this->isOrm() ? new CustomMultipleIdentifierDummy() : new CustomMultipleIdentifierDummyDocument(); + } } diff --git a/features/json/relation.feature b/features/json/relation.feature index 7c39dc75f71..57e7e41cbf1 100644 --- a/features/json/relation.feature +++ b/features/json/relation.feature @@ -156,7 +156,7 @@ Feature: JSON relations support And I send a "POST" request to "/related_dummies" with body: """ { - "thirdLevel": "1" + "thirdLevel": "/third_levels/1" } """ Then the response status code should be 201 @@ -184,16 +184,16 @@ Feature: JSON relations support } """ - Scenario: Passing a (valid) plain identifier on a relation + Scenario: Create a Dummy with relations to RelatedDummy When I add "Content-Type" header equal to "application/json" And I send a "POST" request to "/dummies" with body: """ { - "relatedDummy": "1", + "relatedDummy": "/related_dummies/1", "relatedDummies": [ - "1" + "/related_dummies/1" ], - "name": "Dummy with plain relations" + "name": "Dummy with relations" } """ Then the response status code should be 201 @@ -221,7 +221,7 @@ Feature: JSON relations support "relatedOwnedDummy": null, "relatedOwningDummy": null, "id": 1, - "name": "Dummy with plain relations", + "name": "Dummy with relations", "alias": null, "foo": null } diff --git a/features/main/custom_identifier.feature b/features/main/custom_identifier.feature index ba37934d76b..6c078bcc684 100644 --- a/features/main/custom_identifier.feature +++ b/features/main/custom_identifier.feature @@ -101,3 +101,21 @@ Feature: Using custom identifier on resource When I send a "DELETE" request to "/custom_identifier_dummies/1" Then the response status code should be 204 And the response should be empty + + @createSchema + Scenario: Get a resource + Given there is a custom multiple identifier dummy + When I send a "GET" request to "/custom_multiple_identifier_dummies/1/2" + Then the response status code should be 200 + And the response should be in JSON + And the JSON should be equal to: + """ + { + "@context": "/contexts/CustomMultipleIdentifierDummy", + "@id": "/custom_multiple_identifier_dummies/1/2", + "@type": "CustomMultipleIdentifierDummy", + "firstId": 1, + "secondId": 2, + "name": "Orwell" + } + """ diff --git a/features/main/operation.feature b/features/main/operation.feature index 8c70d1c2b99..c5f885a33f3 100644 --- a/features/main/operation.feature +++ b/features/main/operation.feature @@ -63,3 +63,23 @@ Feature: Operation support Scenario: Get a 404 response for the disabled item operation When I send a "GET" request to "/disable_item_operations/1" Then the response status code should be 404 + + @createSchema + Scenario: Get a book by its ISBN + Given there is a book + When I send a "GET" request to "books/by_isbn/9780451524935" + 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; charset=utf-8" + And the JSON should be equal to: + """ + { + "@context": "/contexts/Book", + "@id": "/books/1", + "@type": "Book", + "name": "1984", + "isbn": "9780451524935", + "id": 1 + } + """ + diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 4cd9ea3abb1..b15cbb85bd0 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -138,3 +138,9 @@ parameters: paths: - src/Bridge/Elasticsearch/DataProvider/Filter/OrderFilter.php - src/Bridge/Elasticsearch/DataProvider/Filter/AbstractSearchFilter.php + + - + message: '#Unreachable statement - code above always terminates.#' + paths: + - tests/Serializer/AbstractItemNormalizerTest.php + - tests/Bridge/Doctrine/MongoDbOdm/ItemDataProviderTest.php diff --git a/phpunit_mongodb.xml b/phpunit_mongodb.xml index 051f8f9e583..cb677f9e30a 100644 --- a/phpunit_mongodb.xml +++ b/phpunit_mongodb.xml @@ -10,7 +10,10 @@ - + + + + diff --git a/src/Annotation/ApiResource.php b/src/Annotation/ApiResource.php index 831fd76f0cd..b7102812d7b 100644 --- a/src/Annotation/ApiResource.php +++ b/src/Annotation/ApiResource.php @@ -28,6 +28,7 @@ * @Attribute("attributes", type="array"), * @Attribute("cacheHeaders", type="array"), * @Attribute("collectionOperations", type="array"), + * @Attribute("compositeIdentifier", type="bool"), * @Attribute("denormalizationContext", type="array"), * @Attribute("deprecationReason", type="string"), * @Attribute("description", type="string"), @@ -89,6 +90,7 @@ final class ApiResource 'securityPostDenormalizeMessage', 'cacheHeaders', 'collectionOperations', + 'compositeIdentifier', 'denormalizationContext', 'deprecationReason', 'description', @@ -452,6 +454,13 @@ final class ApiResource */ private $urlGenerationStrategy; + /** + * @see https://github.com/Haehnchen/idea-php-annotation-plugin/issues/112 + * + * @var bool + */ + private $compositeIdentifier; + /** * @throws InvalidArgumentException */ diff --git a/src/Bridge/Doctrine/MongoDbOdm/ItemDataProvider.php b/src/Bridge/Doctrine/MongoDbOdm/ItemDataProvider.php index 12db2dda163..d58c0d919a5 100644 --- a/src/Bridge/Doctrine/MongoDbOdm/ItemDataProvider.php +++ b/src/Bridge/Doctrine/MongoDbOdm/ItemDataProvider.php @@ -16,10 +16,9 @@ use ApiPlatform\Core\Bridge\Doctrine\Common\Util\IdentifierManagerTrait; use ApiPlatform\Core\Bridge\Doctrine\MongoDbOdm\Extension\AggregationItemExtensionInterface; use ApiPlatform\Core\Bridge\Doctrine\MongoDbOdm\Extension\AggregationResultItemExtensionInterface; -use ApiPlatform\Core\DataProvider\DenormalizedIdentifiersAwareItemDataProviderInterface; +use ApiPlatform\Core\DataProvider\ItemDataProviderInterface; use ApiPlatform\Core\DataProvider\RestrictedDataProviderInterface; use ApiPlatform\Core\Exception\RuntimeException; -use ApiPlatform\Core\Identifier\IdentifierConverterInterface; use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface; use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface; @@ -34,7 +33,7 @@ * * @author Alan Poulain */ -final class ItemDataProvider implements DenormalizedIdentifiersAwareItemDataProviderInterface, RestrictedDataProviderInterface +final class ItemDataProvider implements ItemDataProviderInterface, RestrictedDataProviderInterface { use IdentifierManagerTrait; @@ -64,19 +63,13 @@ public function supports(string $resourceClass, string $operationName = null, ar * * @throws RuntimeException */ - public function getItem(string $resourceClass, $id, string $operationName = null, array $context = []) + public function getItem(string $resourceClass, array $identifiers, ?string $operationName = null, array $context = []) { /** @var DocumentManager $manager */ $manager = $this->managerRegistry->getManagerForClass($resourceClass); - if (!\is_array($id) && !($context[IdentifierConverterInterface::HAS_IDENTIFIER_CONVERTER] ?? false)) { - $id = $this->normalizeIdentifiers($id, $manager, $resourceClass); - } - - $id = (array) $id; - if (!($context['fetch_data'] ?? true)) { - return $manager->getReference($resourceClass, reset($id)); + return $manager->getReference($resourceClass, reset($identifiers)); } $repository = $manager->getRepository($resourceClass); @@ -86,12 +79,12 @@ public function getItem(string $resourceClass, $id, string $operationName = null $aggregationBuilder = $repository->createAggregationBuilder(); - foreach ($id as $propertyName => $value) { + foreach ($identifiers as $propertyName => $value) { $aggregationBuilder->match()->field($propertyName)->equals($value); } foreach ($this->itemExtensions as $extension) { - $extension->applyToItem($aggregationBuilder, $resourceClass, $id, $operationName, $context); + $extension->applyToItem($aggregationBuilder, $resourceClass, $identifiers, $operationName, $context); if ($extension instanceof AggregationResultItemExtensionInterface && $extension->supportsResult($resourceClass, $operationName, $context)) { return $extension->getResult($aggregationBuilder, $resourceClass, $operationName, $context); diff --git a/src/Bridge/Doctrine/MongoDbOdm/SubresourceDataProvider.php b/src/Bridge/Doctrine/MongoDbOdm/SubresourceDataProvider.php index beceaf0a22d..16a2e05b06c 100644 --- a/src/Bridge/Doctrine/MongoDbOdm/SubresourceDataProvider.php +++ b/src/Bridge/Doctrine/MongoDbOdm/SubresourceDataProvider.php @@ -21,7 +21,6 @@ use ApiPlatform\Core\DataProvider\SubresourceDataProviderInterface; use ApiPlatform\Core\Exception\ResourceClassNotSupportedException; use ApiPlatform\Core\Exception\RuntimeException; -use ApiPlatform\Core\Identifier\IdentifierConverterInterface; use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface; use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface; @@ -136,24 +135,14 @@ private function buildAggregation(array $identifiers, array $context, array $exe } $aggregation = $manager->createAggregationBuilder($identifierResourceClass); - $normalizedIdentifiers = []; - - if (isset($identifiers[$identifier])) { - // if it's an array it's already normalized, the IdentifierManagerTrait is deprecated - if ($context[IdentifierConverterInterface::HAS_IDENTIFIER_CONVERTER] ?? false) { - $normalizedIdentifiers = $identifiers[$identifier]; - } else { - $normalizedIdentifiers = $this->normalizeIdentifiers($identifiers[$identifier], $manager, $identifierResourceClass); - } - } if ($classMetadata->hasAssociation($previousAssociationProperty)) { $aggregation->lookup($previousAssociationProperty)->alias($previousAssociationProperty); - foreach ($normalizedIdentifiers as $key => $value) { + foreach (\is_array($identifiers[$identifier]) ? $identifiers[$identifier] : $identifiers as $key => $value) { $aggregation->match()->field($key)->equals($value); } } elseif ($classMetadata->isIdentifier($previousAssociationProperty)) { - foreach ($normalizedIdentifiers as $key => $value) { + foreach (\is_array($identifiers[$identifier]) ? $identifiers[$identifier] : $identifiers as $key => $value) { $aggregation->match()->field($key)->equals($value); } diff --git a/src/Bridge/Doctrine/Orm/ItemDataProvider.php b/src/Bridge/Doctrine/Orm/ItemDataProvider.php index 9535b162427..3c826a10ed3 100644 --- a/src/Bridge/Doctrine/Orm/ItemDataProvider.php +++ b/src/Bridge/Doctrine/Orm/ItemDataProvider.php @@ -13,20 +13,16 @@ namespace ApiPlatform\Core\Bridge\Doctrine\Orm; -use ApiPlatform\Core\Bridge\Doctrine\Common\Util\IdentifierManagerTrait; use ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\QueryItemExtensionInterface; use ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\QueryResultItemExtensionInterface; use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGenerator; -use ApiPlatform\Core\DataProvider\DenormalizedIdentifiersAwareItemDataProviderInterface; +use ApiPlatform\Core\DataProvider\ItemDataProviderInterface; use ApiPlatform\Core\DataProvider\RestrictedDataProviderInterface; use ApiPlatform\Core\Exception\RuntimeException; -use ApiPlatform\Core\Identifier\IdentifierConverterInterface; -use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface; -use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; +use Doctrine\Common\Persistence\ManagerRegistry; +use Doctrine\Common\Persistence\Mapping\ClassMetadata; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\QueryBuilder; -use Doctrine\Persistence\ManagerRegistry; -use Doctrine\Persistence\Mapping\ClassMetadata; /** * Item data provider for the Doctrine ORM. @@ -35,21 +31,17 @@ * @author Samuel ROZE * @final */ -class ItemDataProvider implements DenormalizedIdentifiersAwareItemDataProviderInterface, RestrictedDataProviderInterface +class ItemDataProvider implements ItemDataProviderInterface, RestrictedDataProviderInterface { - use IdentifierManagerTrait; - private $managerRegistry; private $itemExtensions; /** * @param QueryItemExtensionInterface[] $itemExtensions */ - public function __construct(ManagerRegistry $managerRegistry, PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, iterable $itemExtensions = []) + public function __construct(ManagerRegistry $managerRegistry, iterable $itemExtensions = []) { $this->managerRegistry = $managerRegistry; - $this->propertyNameCollectionFactory = $propertyNameCollectionFactory; - $this->propertyMetadataFactory = $propertyMetadataFactory; $this->itemExtensions = $itemExtensions; } @@ -65,19 +57,11 @@ public function supports(string $resourceClass, string $operationName = null, ar * * @throws RuntimeException */ - public function getItem(string $resourceClass, $id, string $operationName = null, array $context = []) + public function getItem(string $resourceClass, array $identifiers, string $operationName = null, array $context = []) { /** @var EntityManagerInterface $manager */ $manager = $this->managerRegistry->getManagerForClass($resourceClass); - if ((\is_int($id) || \is_string($id)) && !($context[IdentifierConverterInterface::HAS_IDENTIFIER_CONVERTER] ?? false)) { - $id = $this->normalizeIdentifiers($id, $manager, $resourceClass); - } - if (!\is_array($id)) { - throw new \InvalidArgumentException(sprintf('$id must be array when "%s" key is set to true in the $context', IdentifierConverterInterface::HAS_IDENTIFIER_CONVERTER)); - } - $identifiers = $id; - $fetchData = $context['fetch_data'] ?? true; if (!$fetchData) { return $manager->getReference($resourceClass, $identifiers); @@ -119,7 +103,6 @@ private function addWhereForIdentifiers(array $identifiers, QueryBuilder $queryB ); $queryBuilder->andWhere($expression); - $queryBuilder->setParameter($placeholder, $value, $classMetadata->getTypeOfField($identifier)); } } diff --git a/src/Bridge/Doctrine/Orm/SubresourceDataProvider.php b/src/Bridge/Doctrine/Orm/SubresourceDataProvider.php index 9438a708fde..3238ee11db2 100644 --- a/src/Bridge/Doctrine/Orm/SubresourceDataProvider.php +++ b/src/Bridge/Doctrine/Orm/SubresourceDataProvider.php @@ -13,7 +13,6 @@ namespace ApiPlatform\Core\Bridge\Doctrine\Orm; -use ApiPlatform\Core\Bridge\Doctrine\Common\Util\IdentifierManagerTrait; use ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\FilterEagerLoadingExtension; use ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\QueryCollectionExtensionInterface; use ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\QueryItemExtensionInterface; @@ -23,13 +22,10 @@ use ApiPlatform\Core\DataProvider\SubresourceDataProviderInterface; use ApiPlatform\Core\Exception\ResourceClassNotSupportedException; use ApiPlatform\Core\Exception\RuntimeException; -use ApiPlatform\Core\Identifier\IdentifierConverterInterface; -use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface; -use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; +use Doctrine\Common\Persistence\ManagerRegistry; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\Mapping\ClassMetadataInfo; use Doctrine\ORM\QueryBuilder; -use Doctrine\Persistence\ManagerRegistry; /** * Subresource data provider for the Doctrine ORM. @@ -38,8 +34,6 @@ */ final class SubresourceDataProvider implements SubresourceDataProviderInterface { - use IdentifierManagerTrait; - private $managerRegistry; private $collectionExtensions; private $itemExtensions; @@ -48,11 +42,9 @@ final class SubresourceDataProvider implements SubresourceDataProviderInterface * @param QueryCollectionExtensionInterface[] $collectionExtensions * @param QueryItemExtensionInterface[] $itemExtensions */ - public function __construct(ManagerRegistry $managerRegistry, PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, iterable $collectionExtensions = [], iterable $itemExtensions = []) + public function __construct(ManagerRegistry $managerRegistry, iterable $collectionExtensions = [], iterable $itemExtensions = []) { $this->managerRegistry = $managerRegistry; - $this->propertyNameCollectionFactory = $propertyNameCollectionFactory; - $this->propertyMetadataFactory = $propertyMetadataFactory; $this->collectionExtensions = $collectionExtensions; $this->itemExtensions = $itemExtensions; } @@ -147,16 +139,6 @@ private function buildQuery(array $identifiers, array $context, QueryNameGenerat $qb = $manager->createQueryBuilder(); $alias = $queryNameGenerator->generateJoinAlias($identifier); - $normalizedIdentifiers = []; - - if (isset($identifiers[$identifier])) { - // if it's an array it's already normalized, the IdentifierManagerTrait is deprecated - if ($context[IdentifierConverterInterface::HAS_IDENTIFIER_CONVERTER] ?? false) { - $normalizedIdentifiers = $identifiers[$identifier]; - } else { - $normalizedIdentifiers = $this->normalizeIdentifiers($identifiers[$identifier], $manager, $identifierResourceClass); - } - } if ($classMetadata->hasAssociation($previousAssociationProperty)) { $relationType = $classMetadata->getAssociationMapping($previousAssociationProperty)['type']; @@ -201,7 +183,7 @@ private function buildQuery(array $identifiers, array $context, QueryNameGenerat $isLeaf = 1 === $remainingIdentifiers; // Add where clause for identifiers - foreach ($normalizedIdentifiers as $key => $value) { + foreach (\is_array($identifiers[$identifier]) ? $identifiers[$identifier] : $identifiers as $key => $value) { $placeholder = $queryNameGenerator->generateParameterName($key); $topQueryBuilder->setParameter($placeholder, $value, (string) $classMetadata->getTypeOfField($key)); diff --git a/src/Bridge/Elasticsearch/DataProvider/ItemDataProvider.php b/src/Bridge/Elasticsearch/DataProvider/ItemDataProvider.php index 9d3fcb1c75e..6bc92299294 100644 --- a/src/Bridge/Elasticsearch/DataProvider/ItemDataProvider.php +++ b/src/Bridge/Elasticsearch/DataProvider/ItemDataProvider.php @@ -83,19 +83,15 @@ public function supports(string $resourceClass, ?string $operationName = null, a /** * {@inheritdoc} */ - public function getItem(string $resourceClass, $id, ?string $operationName = null, array $context = []) + public function getItem(string $resourceClass, array $identifiers, ?string $operationName = null, array $context = []) { - if (\is_array($id)) { - $id = $id[$this->identifierExtractor->getIdentifierFromResourceClass($resourceClass)]; - } - $documentMetadata = $this->documentMetadataFactory->create($resourceClass); try { $document = $this->client->get([ 'index' => $documentMetadata->getIndex(), 'type' => $documentMetadata->getType(), - 'id' => (string) $id, + 'id' => $identifiers[$this->identifierExtractor->getIdentifierFromResourceClass($resourceClass)], ]); } catch (Missing404Exception $e) { return null; diff --git a/src/Bridge/Symfony/Bundle/DataProvider/TraceableChainItemDataProvider.php b/src/Bridge/Symfony/Bundle/DataProvider/TraceableChainItemDataProvider.php index 0b95ece153a..6dcfd03ea7e 100644 --- a/src/Bridge/Symfony/Bundle/DataProvider/TraceableChainItemDataProvider.php +++ b/src/Bridge/Symfony/Bundle/DataProvider/TraceableChainItemDataProvider.php @@ -14,7 +14,6 @@ namespace ApiPlatform\Core\Bridge\Symfony\Bundle\DataProvider; use ApiPlatform\Core\DataProvider\ChainItemDataProvider; -use ApiPlatform\Core\DataProvider\DenormalizedIdentifiersAwareItemDataProviderInterface; use ApiPlatform\Core\DataProvider\ItemDataProviderInterface; use ApiPlatform\Core\DataProvider\RestrictedDataProviderInterface; use ApiPlatform\Core\Exception\ResourceClassNotSupportedException; @@ -45,7 +44,7 @@ public function getContext(): array return $this->context; } - public function getItem(string $resourceClass, $id, string $operationName = null, array $context = []) + public function getItem(string $resourceClass, array $identifiers, string $operationName = null, array $context = []) { $this->context = $context; $match = false; @@ -62,17 +61,7 @@ public function getItem(string $resourceClass, $id, string $operationName = null continue; } - $identifier = $id; - if (!$dataProvider instanceof DenormalizedIdentifiersAwareItemDataProviderInterface && $identifier && \is_array($identifier)) { - if (\count($identifier) > 1) { - @trigger_error(sprintf('Receiving "$id" as non-array in an item data provider is deprecated in 2.3 in favor of implementing "%s".', DenormalizedIdentifiersAwareItemDataProviderInterface::class), E_USER_DEPRECATED); - $identifier = http_build_query($identifier, '', ';'); - } else { - $identifier = current($identifier); - } - } - - $result = $dataProvider->getItem($resourceClass, $identifier, $operationName, $context); + $result = $dataProvider->getItem($resourceClass, $identifiers, $operationName, $context); $this->providersResponse[\get_class($dataProvider)] = $match = true; } catch (ResourceClassNotSupportedException $e) { @trigger_error(sprintf('Throwing a "%s" is deprecated in favor of implementing "%s"', \get_class($e), RestrictedDataProviderInterface::class), E_USER_DEPRECATED); diff --git a/src/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php b/src/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php index b7bf18a94d1..c1b77f220e4 100644 --- a/src/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php +++ b/src/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php @@ -184,7 +184,6 @@ private function registerCommonConfiguration(ContainerBuilder $container, array $container->setParameter('api_platform.formats', $formats); $container->setParameter('api_platform.patch_formats', $patchFormats); $container->setParameter('api_platform.error_formats', $errorFormats); - $container->setParameter('api_platform.allow_plain_identifiers', $config['allow_plain_identifiers']); $container->setParameter('api_platform.eager_loading.enabled', $this->isConfigEnabled($container, $config['eager_loading'])); $container->setParameter('api_platform.eager_loading.max_joins', $config['eager_loading']['max_joins']); $container->setParameter('api_platform.eager_loading.fetch_partial', $config['eager_loading']['fetch_partial']); diff --git a/src/Bridge/Symfony/Bundle/DependencyInjection/Configuration.php b/src/Bridge/Symfony/Bundle/DependencyInjection/Configuration.php index 51ca4471e2b..738f36b5164 100644 --- a/src/Bridge/Symfony/Bundle/DependencyInjection/Configuration.php +++ b/src/Bridge/Symfony/Bundle/DependencyInjection/Configuration.php @@ -93,7 +93,6 @@ public function getConfigTreeBuilder() ->end() ->scalarNode('name_converter')->defaultNull()->info('Specify a name converter to use.')->end() ->scalarNode('path_segment_name_generator')->defaultValue('api_platform.path_segment_name_generator.underscore')->info('Specify a path name generator to use.')->end() - ->booleanNode('allow_plain_identifiers')->defaultFalse()->info('Allow plain identifiers, for example "id" instead of "@id" when denormalizing a relation.')->end() ->arrayNode('validator') ->addDefaultsIfNotSet() ->children() diff --git a/src/Bridge/Symfony/Bundle/Resources/config/api.xml b/src/Bridge/Symfony/Bundle/Resources/config/api.xml index ffa00199f6d..7530e3dc267 100644 --- a/src/Bridge/Symfony/Bundle/Resources/config/api.xml +++ b/src/Bridge/Symfony/Bundle/Resources/config/api.xml @@ -43,6 +43,7 @@ %api_platform.enable_docs% %api_platform.graphql.graphiql.enabled% %api_platform.graphql.graphql_playground.enabled% + @@ -55,15 +56,12 @@ - - - - + @@ -110,7 +108,6 @@ - %api_platform.allow_plain_identifiers% null @@ -163,7 +160,7 @@ - + @@ -265,8 +262,7 @@ - - + @@ -286,6 +282,7 @@ + diff --git a/src/Bridge/Symfony/Bundle/Resources/config/doctrine_orm.xml b/src/Bridge/Symfony/Bundle/Resources/config/doctrine_orm.xml index f60b7d564af..25390c8bc76 100644 --- a/src/Bridge/Symfony/Bundle/Resources/config/doctrine_orm.xml +++ b/src/Bridge/Symfony/Bundle/Resources/config/doctrine_orm.xml @@ -22,15 +22,11 @@ - - - - diff --git a/src/Bridge/Symfony/Bundle/Resources/config/graphql.xml b/src/Bridge/Symfony/Bundle/Resources/config/graphql.xml index 48638b0e99a..f14b7b328a8 100644 --- a/src/Bridge/Symfony/Bundle/Resources/config/graphql.xml +++ b/src/Bridge/Symfony/Bundle/Resources/config/graphql.xml @@ -216,7 +216,6 @@ - %api_platform.allow_plain_identifiers% null diff --git a/src/Bridge/Symfony/Bundle/Resources/config/hal.xml b/src/Bridge/Symfony/Bundle/Resources/config/hal.xml index 9ba48f25be3..74ccc77d146 100644 --- a/src/Bridge/Symfony/Bundle/Resources/config/hal.xml +++ b/src/Bridge/Symfony/Bundle/Resources/config/hal.xml @@ -37,7 +37,6 @@ null - false diff --git a/src/Bridge/Symfony/Bundle/Resources/config/openapi.xml b/src/Bridge/Symfony/Bundle/Resources/config/openapi.xml index 03bda2f24da..604b37ec366 100644 --- a/src/Bridge/Symfony/Bundle/Resources/config/openapi.xml +++ b/src/Bridge/Symfony/Bundle/Resources/config/openapi.xml @@ -39,6 +39,7 @@ %api_platform.formats% + diff --git a/src/Bridge/Symfony/Bundle/Resources/config/swagger.xml b/src/Bridge/Symfony/Bundle/Resources/config/swagger.xml index df28ac3f8fc..f41fb55c44e 100644 --- a/src/Bridge/Symfony/Bundle/Resources/config/swagger.xml +++ b/src/Bridge/Symfony/Bundle/Resources/config/swagger.xml @@ -33,6 +33,7 @@ %api_platform.collection.pagination.enabled_parameter_name% %api_platform.swagger.versions% + diff --git a/src/Bridge/Symfony/Routing/ApiLoader.php b/src/Bridge/Symfony/Routing/ApiLoader.php index 3bdad5ac363..17b886ac4c0 100644 --- a/src/Bridge/Symfony/Routing/ApiLoader.php +++ b/src/Bridge/Symfony/Routing/ApiLoader.php @@ -13,6 +13,7 @@ namespace ApiPlatform\Core\Bridge\Symfony\Routing; +use ApiPlatform\Core\Api\IdentifiersExtractorInterface; use ApiPlatform\Core\Api\OperationType; use ApiPlatform\Core\Exception\InvalidResourceException; use ApiPlatform\Core\Exception\RuntimeException; @@ -56,8 +57,9 @@ final class ApiLoader extends Loader private $graphQlPlaygroundEnabled; private $entrypointEnabled; private $docsEnabled; + private $identifiersExtractor; - public function __construct(KernelInterface $kernel, ResourceNameCollectionFactoryInterface $resourceNameCollectionFactory, ResourceMetadataFactoryInterface $resourceMetadataFactory, OperationPathResolverInterface $operationPathResolver, ContainerInterface $container, array $formats, array $resourceClassDirectories = [], SubresourceOperationFactoryInterface $subresourceOperationFactory = null, bool $graphqlEnabled = false, bool $entrypointEnabled = true, bool $docsEnabled = true, bool $graphiQlEnabled = false, bool $graphQlPlaygroundEnabled = false) + public function __construct(KernelInterface $kernel, ResourceNameCollectionFactoryInterface $resourceNameCollectionFactory, ResourceMetadataFactoryInterface $resourceMetadataFactory, OperationPathResolverInterface $operationPathResolver, ContainerInterface $container, array $formats, array $resourceClassDirectories = [], SubresourceOperationFactoryInterface $subresourceOperationFactory = null, bool $graphqlEnabled = false, bool $entrypointEnabled = true, bool $docsEnabled = true, bool $graphiQlEnabled = false, bool $graphQlPlaygroundEnabled = false, IdentifiersExtractorInterface $identifiersExtractor) { /** @var string[]|string $paths */ $paths = $kernel->locateResource('@ApiPlatformBundle/Resources/config/routing'); @@ -74,6 +76,7 @@ public function __construct(KernelInterface $kernel, ResourceNameCollectionFacto $this->graphQlPlaygroundEnabled = $graphQlPlaygroundEnabled; $this->entrypointEnabled = $entrypointEnabled; $this->docsEnabled = $docsEnabled; + $this->identifiersExtractor = $identifiersExtractor; } /** @@ -90,6 +93,7 @@ public function load($data, $type = null): RouteCollection foreach ($this->resourceNameCollectionFactory->create() as $resourceClass) { $resourceMetadata = $this->resourceMetadataFactory->create($resourceClass); + $resourceMetadata = $resourceMetadata->withAttributes(($resourceMetadata->getAttributes() ?: []) + ['identified_by' => $this->identifiersExtractor->getIdentifiersFromResourceClass($resourceClass)]); $resourceShortName = $resourceMetadata->getShortName(); if (null === $resourceShortName) { @@ -128,6 +132,8 @@ public function load($data, $type = null): RouteCollection '_format' => null, '_stateless' => $operation['stateless'] ?? $resourceMetadata->getAttribute('stateless'), '_api_resource_class' => $operation['resource_class'], + '_api_identified_by' => $operation['identified_by'], + '_api_has_composite_identifier' => false, '_api_subresource_operation_name' => $operation['route_name'], '_api_subresource_context' => [ 'property' => $operation['property'], @@ -222,6 +228,9 @@ private function addRoute(RouteCollection $routeCollection, string $resourceClas } } + $operation['identified_by'] = (array) ($operation['identified_by'] ?? $resourceMetadata->getAttribute('identified_by')); + $operation['has_composite_identifier'] = \count($operation['identified_by']) > 1 ? $resourceMetadata->getAttribute('composite_identifier', true) : false; + $path = trim(trim($resourceMetadata->getAttribute('route_prefix', '')), '/'); $path .= $this->operationPathResolver->resolveOperationPath($resourceShortName, $operation, $operationType, $operationName); @@ -232,6 +241,8 @@ private function addRoute(RouteCollection $routeCollection, string $resourceClas '_format' => null, '_stateless' => $operation['stateless'], '_api_resource_class' => $resourceClass, + '_api_identified_by' => $operation['identified_by'], + '_api_has_composite_identifier' => $operation['has_composite_identifier'], sprintf('_api_%s_operation_name', $operationType) => $operationName, ] + ($operation['defaults'] ?? []), $operation['requirements'] ?? [], diff --git a/src/Bridge/Symfony/Routing/IriConverter.php b/src/Bridge/Symfony/Routing/IriConverter.php index e0c8e5c7211..7f34c562de4 100644 --- a/src/Bridge/Symfony/Routing/IriConverter.php +++ b/src/Bridge/Symfony/Routing/IriConverter.php @@ -13,7 +13,6 @@ namespace ApiPlatform\Core\Bridge\Symfony\Routing; -use ApiPlatform\Core\Api\IdentifiersExtractor; use ApiPlatform\Core\Api\IdentifiersExtractorInterface; use ApiPlatform\Core\Api\IriConverterInterface; use ApiPlatform\Core\Api\OperationType; @@ -26,14 +25,11 @@ use ApiPlatform\Core\Exception\InvalidIdentifierException; use ApiPlatform\Core\Exception\ItemNotFoundException; use ApiPlatform\Core\Exception\RuntimeException; -use ApiPlatform\Core\Identifier\IdentifierConverterInterface; -use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface; -use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; +use ApiPlatform\Core\Identifier\CompositeIdentifierParser; +use ApiPlatform\Core\Identifier\IdentifierDenormalizerInterface; use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface; use ApiPlatform\Core\Util\AttributesExtractor; use ApiPlatform\Core\Util\ResourceClassInfoTrait; -use Symfony\Component\PropertyAccess\PropertyAccess; -use Symfony\Component\PropertyAccess\PropertyAccessorInterface; use Symfony\Component\Routing\Exception\ExceptionInterface as RoutingExceptionInterface; use Symfony\Component\Routing\RouterInterface; @@ -51,20 +47,16 @@ final class IriConverter implements IriConverterInterface private $router; private $identifiersExtractor; - public function __construct(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, ItemDataProviderInterface $itemDataProvider, RouteNameResolverInterface $routeNameResolver, RouterInterface $router, PropertyAccessorInterface $propertyAccessor = null, IdentifiersExtractorInterface $identifiersExtractor = null, SubresourceDataProviderInterface $subresourceDataProvider = null, IdentifierConverterInterface $identifierConverter = null, ResourceClassResolverInterface $resourceClassResolver = null, ResourceMetadataFactoryInterface $resourceMetadataFactory = null) + public function __construct(ItemDataProviderInterface $itemDataProvider, RouteNameResolverInterface $routeNameResolver, RouterInterface $router, IdentifiersExtractorInterface $identifiersExtractor, SubresourceDataProviderInterface $subresourceDataProvider = null, IdentifierDenormalizerInterface $identifierDenormalizer = null, ResourceClassResolverInterface $resourceClassResolver = null, ResourceMetadataFactoryInterface $resourceMetadataFactory = null) { $this->itemDataProvider = $itemDataProvider; $this->routeNameResolver = $routeNameResolver; $this->router = $router; $this->identifiersExtractor = $identifiersExtractor; $this->subresourceDataProvider = $subresourceDataProvider; - $this->identifierConverter = $identifierConverter; + $this->identifierDenormalizer = $identifierDenormalizer; $this->resourceClassResolver = $resourceClassResolver; - - if (null === $identifiersExtractor) { - @trigger_error(sprintf('Not injecting "%s" is deprecated since API Platform 2.1 and will not be possible anymore in API Platform 3', IdentifiersExtractorInterface::class), E_USER_DEPRECATED); - $this->identifiersExtractor = new IdentifiersExtractor($propertyNameCollectionFactory, $propertyMetadataFactory, $propertyAccessor ?? PropertyAccess::createPropertyAccessor()); - } + $this->identifiersExtractor = $identifiersExtractor; $this->resourceMetadataFactory = $resourceMetadataFactory; } @@ -95,10 +87,6 @@ public function getItemFromIri(string $iri, array $context = []) throw new InvalidArgumentException($e->getMessage(), $e->getCode(), $e); } - if ($this->identifierConverter) { - $context[IdentifierConverterInterface::HAS_IDENTIFIER_CONVERTER] = true; - } - if (isset($attributes['subresource_operation_name'])) { if (($item = $this->getSubresourceData($identifiers, $attributes, $context)) && !\is_array($item)) { return $item; @@ -148,11 +136,14 @@ public function getIriFromResourceClass(string $resourceClass, int $referenceTyp public function getItemIriFromResourceClass(string $resourceClass, array $identifiers, int $referenceType = null): string { $routeName = $this->routeNameResolver->getRouteName($resourceClass, OperationType::ITEM); + $metadata = $this->resourceMetadataFactory->create($resourceClass); - try { - $identifiers = $this->generateIdentifiersUrl($identifiers, $resourceClass); + if (\count($identifiers) > 1 && true === $metadata->getAttribute('composite_identifier', true)) { + $identifiers = ['id' => CompositeIdentifierParser::stringify($identifiers)]; + } - return $this->router->generate($routeName, ['id' => implode(';', $identifiers)], $this->getReferenceType($resourceClass, $referenceType)); + try { + return $this->router->generate($routeName, $identifiers, $this->getReferenceType($resourceClass, $referenceType)); } catch (RoutingExceptionInterface $e) { throw new InvalidArgumentException(sprintf('Unable to generate an IRI for "%s".', $resourceClass), $e->getCode(), $e); } @@ -170,30 +161,6 @@ public function getSubresourceIriFromResourceClass(string $resourceClass, array } } - /** - * Generate the identifier url. - * - * @throws InvalidArgumentException - * - * @return string[] - */ - private function generateIdentifiersUrl(array $identifiers, string $resourceClass): array - { - if (0 === \count($identifiers)) { - throw new InvalidArgumentException(sprintf('No identifiers defined for resource of type "%s"', $resourceClass)); - } - - if (1 === \count($identifiers)) { - return [(string) reset($identifiers)]; - } - - foreach ($identifiers as $name => $value) { - $identifiers[$name] = sprintf('%s=%s', $name, $value); - } - - return array_values($identifiers); - } - private function getReferenceType(string $resourceClass, ?int $referenceType): ?int { if (null === $referenceType && null !== $this->resourceMetadataFactory) { diff --git a/src/DataProvider/ChainItemDataProvider.php b/src/DataProvider/ChainItemDataProvider.php index defb799f7fa..35a20c7e8f2 100644 --- a/src/DataProvider/ChainItemDataProvider.php +++ b/src/DataProvider/ChainItemDataProvider.php @@ -40,7 +40,7 @@ public function __construct(iterable $dataProviders) /** * {@inheritdoc} */ - public function getItem(string $resourceClass, $id, string $operationName = null, array $context = []) + public function getItem(string $resourceClass, array $identifiers, string $operationName = null, array $context = []) { foreach ($this->dataProviders as $dataProvider) { try { @@ -49,17 +49,7 @@ public function getItem(string $resourceClass, $id, string $operationName = null continue; } - $identifier = $id; - if (!$dataProvider instanceof DenormalizedIdentifiersAwareItemDataProviderInterface && $identifier && \is_array($identifier)) { - if (\count($identifier) > 1) { - @trigger_error(sprintf('Receiving "$id" as non-array in an item data provider is deprecated in 2.3 in favor of implementing "%s".', DenormalizedIdentifiersAwareItemDataProviderInterface::class), E_USER_DEPRECATED); - $identifier = http_build_query($identifier, '', ';'); - } else { - $identifier = current($identifier); - } - } - - return $dataProvider->getItem($resourceClass, $identifier, $operationName, $context); + return $dataProvider->getItem($resourceClass, $identifiers, $operationName, $context); } catch (ResourceClassNotSupportedException $e) { @trigger_error(sprintf('Throwing a "%s" is deprecated in favor of implementing "%s"', \get_class($e), RestrictedDataProviderInterface::class), E_USER_DEPRECATED); continue; diff --git a/src/DataProvider/DenormalizedIdentifiersAwareItemDataProviderInterface.php b/src/DataProvider/DenormalizedIdentifiersAwareItemDataProviderInterface.php deleted file mode 100644 index 07067dc0b25..00000000000 --- a/src/DataProvider/DenormalizedIdentifiersAwareItemDataProviderInterface.php +++ /dev/null @@ -1,27 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Core\DataProvider; - -/** - * Marks data providers able to deal with complex identifiers denormalized as an array. - * - * @author Anthony GRASSIOT - */ -interface DenormalizedIdentifiersAwareItemDataProviderInterface extends ItemDataProviderInterface -{ - /** - * {@inheritdoc} - */ - public function getItem(string $resourceClass, /* array */ $id, string $operationName = null, array $context = []); -} diff --git a/src/DataProvider/ItemDataProviderInterface.php b/src/DataProvider/ItemDataProviderInterface.php index 050cdceee1b..aeba9e9797d 100644 --- a/src/DataProvider/ItemDataProviderInterface.php +++ b/src/DataProvider/ItemDataProviderInterface.php @@ -25,11 +25,9 @@ interface ItemDataProviderInterface /** * Retrieves an item. * - * @param array|int|string $id - * * @throws ResourceClassNotSupportedException * * @return object|null */ - public function getItem(string $resourceClass, $id, string $operationName = null, array $context = []); + public function getItem(string $resourceClass, array $identifiers, string $operationName = null, array $context = []); } diff --git a/src/DataProvider/OperationDataProviderTrait.php b/src/DataProvider/OperationDataProviderTrait.php index f08521ce379..a3e1cb7367f 100644 --- a/src/DataProvider/OperationDataProviderTrait.php +++ b/src/DataProvider/OperationDataProviderTrait.php @@ -15,7 +15,8 @@ use ApiPlatform\Core\Exception\InvalidIdentifierException; use ApiPlatform\Core\Exception\RuntimeException; -use ApiPlatform\Core\Identifier\IdentifierConverterInterface; +use ApiPlatform\Core\Identifier\CompositeIdentifierParser; +use ApiPlatform\Core\Identifier\IdentifierDenormalizerInterface; /** * @internal @@ -38,9 +39,9 @@ trait OperationDataProviderTrait private $subresourceDataProvider; /** - * @var IdentifierConverterInterface|null + * @var IdentifierDenormalizerInterface */ - private $identifierConverter; + private $identifierDenormalizer; /** * Retrieves data for a collection operation. @@ -75,7 +76,18 @@ private function getSubresourceData($identifiers, array $attributes, array $cont throw new RuntimeException('Subresources not supported'); } - return $this->subresourceDataProvider->getSubresource($attributes['resource_class'], $identifiers, $attributes['subresource_context'] + $context, $attributes['subresource_operation_name']); + $subresourceIdentifiers = []; + foreach ($attributes['subresource_context']['identifiers'] as list($identifier, , $isItem, $identifiedBy)) { + if ($isItem) { + $subresourceIdentifiers[$identifier] = [$identifiedBy[0] => current($identifiers)]; + next($identifiers); + continue; + } + + $subresourceIdentifiers[$identifier] = []; + } + + return $this->subresourceDataProvider->getSubresource($attributes['resource_class'], $subresourceIdentifiers, $attributes['subresource_context'] + $context, $attributes['subresource_operation_name']); } /** @@ -85,38 +97,38 @@ private function getSubresourceData($identifiers, array $attributes, array $cont */ private function extractIdentifiers(array $parameters, array $attributes) { - if (isset($attributes['item_operation_name'])) { - if (!isset($parameters['id'])) { - throw new InvalidIdentifierException('Parameter "id" not found'); - } - - $id = $parameters['id']; + $identifiersKeys = $attributes['identified_by'] ?? []; + $identifiers = []; - if (null !== $this->identifierConverter) { - return $this->identifierConverter->convert((string) $id, $attributes['resource_class']); + if (isset($attributes['subresource_context'])) { + $subresourceIdentifiersKeys = []; + $i = 0; + foreach ($attributes['subresource_context']['identifiers'] as list($identifier, , $isItem)) { + if ($isItem) { + $subresourceIdentifiersKeys[] = $identifiersKeys[$i++] ?? $identifier; + } } - - return $id; + $identifiersKeys = $subresourceIdentifiersKeys; } - if (!isset($attributes['subresource_context'])) { - throw new RuntimeException('Either "item_operation_name" or "collection_operation_name" must be defined, unless the "_api_receive" request attribute is set to false.'); - } + $identifiersNumber = \count($identifiersKeys); + foreach ($identifiersKeys as $identifier) { + if (!isset($parameters[$identifier])) { + if ($attributes['has_composite_identifier']) { + $identifiers = CompositeIdentifierParser::parse($parameters['id']); + if (($currentIdentifiersNumber = \count($identifiers)) !== $identifiersNumber) { + throw new InvalidIdentifierException(sprintf('Expected %d identifiers, got %d', $identifiersNumber, $currentIdentifiersNumber)); + } - $identifiers = []; + return $identifiers; + } - foreach ($attributes['subresource_context']['identifiers'] as $key => [$id, $resourceClass, $hasIdentifier]) { - if (false === $hasIdentifier) { - continue; + throw new InvalidIdentifierException(sprintf('Parameter "%s" not found', $identifier)); } - $identifiers[$id] = $parameters[$id]; - - if (null !== $this->identifierConverter) { - $identifiers[$id] = $this->identifierConverter->convert((string) $identifiers[$id], $resourceClass); - } + $identifiers[$identifier] = $parameters[$identifier]; } - return $identifiers; + return $this->identifierDenormalizer->denormalize($identifiers, $attributes['resource_class']); } } diff --git a/src/EventListener/ReadListener.php b/src/EventListener/ReadListener.php index 87dbe3e945f..c8a6963456e 100644 --- a/src/EventListener/ReadListener.php +++ b/src/EventListener/ReadListener.php @@ -19,7 +19,7 @@ use ApiPlatform\Core\DataProvider\SubresourceDataProviderInterface; use ApiPlatform\Core\Exception\InvalidIdentifierException; use ApiPlatform\Core\Exception\RuntimeException; -use ApiPlatform\Core\Identifier\IdentifierConverterInterface; +use ApiPlatform\Core\Identifier\IdentifierDenormalizerInterface; use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface; use ApiPlatform\Core\Metadata\Resource\ToggleableOperationAttributeTrait; use ApiPlatform\Core\Serializer\SerializerContextBuilderInterface; @@ -44,13 +44,13 @@ final class ReadListener private $serializerContextBuilder; - public function __construct(CollectionDataProviderInterface $collectionDataProvider, ItemDataProviderInterface $itemDataProvider, SubresourceDataProviderInterface $subresourceDataProvider = null, SerializerContextBuilderInterface $serializerContextBuilder = null, IdentifierConverterInterface $identifierConverter = null, ResourceMetadataFactoryInterface $resourceMetadataFactory = null) + public function __construct(CollectionDataProviderInterface $collectionDataProvider, ItemDataProviderInterface $itemDataProvider, SubresourceDataProviderInterface $subresourceDataProvider = null, SerializerContextBuilderInterface $serializerContextBuilder = null, IdentifierDenormalizerInterface $identifierDenormalizer = null, ResourceMetadataFactoryInterface $resourceMetadataFactory = null) { $this->collectionDataProvider = $collectionDataProvider; $this->itemDataProvider = $itemDataProvider; $this->subresourceDataProvider = $subresourceDataProvider; $this->serializerContextBuilder = $serializerContextBuilder; - $this->identifierConverter = $identifierConverter; + $this->identifierDenormalizer = $identifierDenormalizer; $this->resourceMetadataFactory = $resourceMetadataFactory; } @@ -91,10 +91,6 @@ public function onKernelRequest(RequestEvent $event): void $data = []; - if ($this->identifierConverter) { - $context[IdentifierConverterInterface::HAS_IDENTIFIER_CONVERTER] = true; - } - try { $identifiers = $this->extractIdentifiers($request->attributes->all(), $attributes); diff --git a/src/GraphQl/Serializer/ItemNormalizer.php b/src/GraphQl/Serializer/ItemNormalizer.php index e7c5e6c8a0c..1103c42f02f 100644 --- a/src/GraphQl/Serializer/ItemNormalizer.php +++ b/src/GraphQl/Serializer/ItemNormalizer.php @@ -45,9 +45,9 @@ final class ItemNormalizer extends BaseItemNormalizer private $identifiersExtractor; - public function __construct(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, IriConverterInterface $iriConverter, IdentifiersExtractorInterface $identifiersExtractor, ResourceClassResolverInterface $resourceClassResolver, PropertyAccessorInterface $propertyAccessor = null, NameConverterInterface $nameConverter = null, ClassMetadataFactoryInterface $classMetadataFactory = null, ItemDataProviderInterface $itemDataProvider = null, bool $allowPlainIdentifiers = false, LoggerInterface $logger = null, iterable $dataTransformers = [], ResourceMetadataFactoryInterface $resourceMetadataFactory = null) + public function __construct(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, IriConverterInterface $iriConverter, IdentifiersExtractorInterface $identifiersExtractor, ResourceClassResolverInterface $resourceClassResolver, PropertyAccessorInterface $propertyAccessor = null, NameConverterInterface $nameConverter = null, ClassMetadataFactoryInterface $classMetadataFactory = null, ItemDataProviderInterface $itemDataProvider = null, LoggerInterface $logger = null, iterable $dataTransformers = [], ResourceMetadataFactoryInterface $resourceMetadataFactory = null) { - parent::__construct($propertyNameCollectionFactory, $propertyMetadataFactory, $iriConverter, $resourceClassResolver, $propertyAccessor, $nameConverter, $classMetadataFactory, $itemDataProvider, $allowPlainIdentifiers, $logger ?: new NullLogger(), $dataTransformers, $resourceMetadataFactory); + parent::__construct($propertyNameCollectionFactory, $propertyMetadataFactory, $iriConverter, $resourceClassResolver, $propertyAccessor, $nameConverter, $classMetadataFactory, $itemDataProvider, $logger ?: new NullLogger(), $dataTransformers, $resourceMetadataFactory); $this->identifiersExtractor = $identifiersExtractor; } diff --git a/src/Identifier/CompositeIdentifierParser.php b/src/Identifier/CompositeIdentifierParser.php index 3aa72c79bde..80306a39f17 100644 --- a/src/Identifier/CompositeIdentifierParser.php +++ b/src/Identifier/CompositeIdentifierParser.php @@ -20,6 +20,8 @@ */ final class CompositeIdentifierParser { + public const COMPOSITE_IDENTIFIER_REGEXP = '/(\w+)=(?<=\w=)(.*?)(?=;\w+=)|(\w+)=([^;]*);?$/'; + private function __construct() { } @@ -33,7 +35,7 @@ public static function parse(string $identifier): array { $matches = []; $identifiers = []; - $num = preg_match_all('/(\w+)=(?<=\w=)(.*?)(?=;\w+=)|(\w+)=([^;]*);?$/', $identifier, $matches, PREG_SET_ORDER); + $num = preg_match_all(self::COMPOSITE_IDENTIFIER_REGEXP, $identifier, $matches, PREG_SET_ORDER); foreach ($matches as $i => $match) { if ($i === $num - 1) { @@ -45,4 +47,14 @@ public static function parse(string $identifier): array return $identifiers; } + + public static function stringify(array $identifiers): string + { + $composite = []; + foreach ($identifiers as $name => $value) { + $composite[] = sprintf('%s=%s', $name, $value); + } + + return implode(';', $composite); + } } diff --git a/src/Identifier/IdentifierConverter.php b/src/Identifier/IdentifierConverter.php deleted file mode 100644 index 9faafd8ef54..00000000000 --- a/src/Identifier/IdentifierConverter.php +++ /dev/null @@ -1,100 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Core\Identifier; - -use ApiPlatform\Core\Api\IdentifiersExtractorInterface; -use ApiPlatform\Core\Exception\InvalidIdentifierException; -use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface; -use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface; -use Symfony\Component\PropertyInfo\Type; -use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; - -/** - * Identifier converter that chains identifier denormalizers. - * - * @author Antoine Bluchet - */ -final class IdentifierConverter implements ContextAwareIdentifierConverterInterface -{ - private $propertyMetadataFactory; - private $identifiersExtractor; - private $identifierDenormalizers; - private $resourceMetadataFactory; - - /** - * @param iterable $identifierDenormalizers - */ - public function __construct(IdentifiersExtractorInterface $identifiersExtractor, PropertyMetadataFactoryInterface $propertyMetadataFactory, iterable $identifierDenormalizers, ResourceMetadataFactoryInterface $resourceMetadataFactory = null) - { - $this->propertyMetadataFactory = $propertyMetadataFactory; - $this->identifiersExtractor = $identifiersExtractor; - $this->identifierDenormalizers = $identifierDenormalizers; - $this->resourceMetadataFactory = $resourceMetadataFactory; - } - - /** - * {@inheritdoc} - */ - public function convert(string $data, string $class, array $context = []): array - { - if (null !== $this->resourceMetadataFactory) { - $resourceMetadata = $this->resourceMetadataFactory->create($class); - $class = $resourceMetadata->getOperationAttribute($context, 'output', ['class' => $class], true)['class']; - } - - $keys = $this->identifiersExtractor->getIdentifiersFromResourceClass($class); - - if (($numIdentifiers = \count($keys)) > 1) { - $identifiers = CompositeIdentifierParser::parse($data); - } elseif (0 === $numIdentifiers) { - throw new InvalidIdentifierException(sprintf('Resource "%s" has no identifiers.', $class)); - } else { - $identifiers = [$keys[0] => $data]; - } - - // Normalize every identifier (DateTime, UUID etc.) - foreach ($keys as $key) { - if (!isset($identifiers[$key])) { - throw new InvalidIdentifierException(sprintf('Invalid identifier "%1$s", "%1$s" was not found.', $key)); - } - - if (null === $type = $this->getIdentifierType($class, $key)) { - continue; - } - - foreach ($this->identifierDenormalizers as $identifierDenormalizer) { - if (!$identifierDenormalizer->supportsDenormalization($identifiers[$key], $type)) { - continue; - } - - try { - $identifiers[$key] = $identifierDenormalizer->denormalize($identifiers[$key], $type); - } catch (InvalidIdentifierException $e) { - throw new InvalidIdentifierException(sprintf('Identifier "%s" could not be denormalized.', $key), $e->getCode(), $e); - } - } - } - - return $identifiers; - } - - private function getIdentifierType(string $resourceClass, string $property): ?string - { - if (!$type = $this->propertyMetadataFactory->create($resourceClass, $property)->getType()) { - return null; - } - - return Type::BUILTIN_TYPE_OBJECT === ($builtinType = $type->getBuiltinType()) ? $type->getClassName() : $builtinType; - } -} diff --git a/src/Identifier/IdentifierConverterInterface.php b/src/Identifier/IdentifierConverterInterface.php deleted file mode 100644 index 93962a2d47d..00000000000 --- a/src/Identifier/IdentifierConverterInterface.php +++ /dev/null @@ -1,39 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Core\Identifier; - -use ApiPlatform\Core\Exception\InvalidIdentifierException; - -/** - * Identifier converter. - * - * @author Antoine Bluchet - */ -interface IdentifierConverterInterface -{ - /** - * @internal - */ - public const HAS_IDENTIFIER_CONVERTER = 'has_identifier_converter'; - - /** - * @param string $data Identifier to convert to php values - * @param string $class The class to which the identifiers belong - * - * @throws InvalidIdentifierException - * - * @return array Indexed by identifiers properties with their values denormalized - */ - public function convert(string $data, string $class): array; -} diff --git a/src/Identifier/IdentifierDenormalizer.php b/src/Identifier/IdentifierDenormalizer.php new file mode 100644 index 00000000000..0c86806bb63 --- /dev/null +++ b/src/Identifier/IdentifierDenormalizer.php @@ -0,0 +1,74 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Core\Identifier; + +use ApiPlatform\Core\Exception\InvalidIdentifierException; +use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface; +use Symfony\Component\PropertyInfo\Type; +use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; + +/** + * Identifier denormalizer. + * + * @author Antoine Bluchet + */ +final class IdentifierDenormalizer implements IdentifierDenormalizerInterface +{ + private $propertyMetadataFactory; + private $identifierDenormalizers; + + /** + * @param iterable $identifierDenormalizers + */ + public function __construct(PropertyMetadataFactoryInterface $propertyMetadataFactory, iterable $identifierDenormalizers) + { + $this->propertyMetadataFactory = $propertyMetadataFactory; + $this->identifierDenormalizers = $identifierDenormalizers; + } + + /* + * {@inheritdoc} + */ + public function denormalize($identifiers, $class, ?string $format = null, array $context = []): array + { + foreach ($identifiers as $identifier => $value) { + if (null === $type = $this->getIdentifierType($class, $identifier)) { + continue; + } + + foreach ($this->identifierDenormalizers as $identifierDenormalizer) { + if (!$identifierDenormalizer->supportsDenormalization($value, $type)) { + continue; + } + + try { + $identifiers[$identifier] = $identifierDenormalizer->denormalize($value, $type); + } catch (InvalidIdentifierException $e) { + throw new InvalidIdentifierException(sprintf('Identifier "%s" could not be denormalized.', $identifier), $e->getCode(), $e); + } + } + } + + return $identifiers; + } + + private function getIdentifierType(string $resourceClass, string $property): ?string + { + if (!$type = $this->propertyMetadataFactory->create($resourceClass, $property)->getType()) { + return null; + } + + return Type::BUILTIN_TYPE_OBJECT === ($builtinType = $type->getBuiltinType()) ? $type->getClassName() : $builtinType; + } +} diff --git a/src/Identifier/ContextAwareIdentifierConverterInterface.php b/src/Identifier/IdentifierDenormalizerInterface.php similarity index 51% rename from src/Identifier/ContextAwareIdentifierConverterInterface.php rename to src/Identifier/IdentifierDenormalizerInterface.php index 83675089eb0..ce595f18fb6 100644 --- a/src/Identifier/ContextAwareIdentifierConverterInterface.php +++ b/src/Identifier/IdentifierDenormalizerInterface.php @@ -13,15 +13,10 @@ namespace ApiPlatform\Core\Identifier; -/** - * Gives access to the context in the IdentifierConverter. - * - * @author Antoine Bluchet - */ -interface ContextAwareIdentifierConverterInterface extends IdentifierConverterInterface +interface IdentifierDenormalizerInterface { /** - * {@inheritdoc} + * Takes an array of identifiers and transform their values from strings to the expected type. */ - public function convert(string $data, string $class, array $context = []): array; + public function denormalize($identifiers, $class, ?string $format = null, array $context = []): array; } diff --git a/src/JsonApi/Serializer/ItemNormalizer.php b/src/JsonApi/Serializer/ItemNormalizer.php index ade96fe723c..3eada3db3ec 100644 --- a/src/JsonApi/Serializer/ItemNormalizer.php +++ b/src/JsonApi/Serializer/ItemNormalizer.php @@ -52,7 +52,7 @@ final class ItemNormalizer extends AbstractItemNormalizer public function __construct(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, IriConverterInterface $iriConverter, ResourceClassResolverInterface $resourceClassResolver, ?PropertyAccessorInterface $propertyAccessor, ?NameConverterInterface $nameConverter, ResourceMetadataFactoryInterface $resourceMetadataFactory, array $defaultContext = [], iterable $dataTransformers = [], ResourceAccessCheckerInterface $resourceAccessChecker = null) { - parent::__construct($propertyNameCollectionFactory, $propertyMetadataFactory, $iriConverter, $resourceClassResolver, $propertyAccessor, $nameConverter, null, null, false, $defaultContext, $dataTransformers, $resourceMetadataFactory, $resourceAccessChecker); + parent::__construct($propertyNameCollectionFactory, $propertyMetadataFactory, $iriConverter, $resourceClassResolver, $propertyAccessor, $nameConverter, null, null, $defaultContext, $dataTransformers, $resourceMetadataFactory, $resourceAccessChecker); } /** diff --git a/src/JsonLd/Serializer/ItemNormalizer.php b/src/JsonLd/Serializer/ItemNormalizer.php index 793eba67639..2fc276d1dec 100644 --- a/src/JsonLd/Serializer/ItemNormalizer.php +++ b/src/JsonLd/Serializer/ItemNormalizer.php @@ -46,7 +46,7 @@ final class ItemNormalizer extends AbstractItemNormalizer public function __construct(ResourceMetadataFactoryInterface $resourceMetadataFactory, PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, IriConverterInterface $iriConverter, ResourceClassResolverInterface $resourceClassResolver, ContextBuilderInterface $contextBuilder, PropertyAccessorInterface $propertyAccessor = null, NameConverterInterface $nameConverter = null, ClassMetadataFactoryInterface $classMetadataFactory = null, array $defaultContext = [], iterable $dataTransformers = [], ResourceAccessCheckerInterface $resourceAccessChecker = null) { - parent::__construct($propertyNameCollectionFactory, $propertyMetadataFactory, $iriConverter, $resourceClassResolver, $propertyAccessor, $nameConverter, $classMetadataFactory, null, false, $defaultContext, $dataTransformers, $resourceMetadataFactory, $resourceAccessChecker); + parent::__construct($propertyNameCollectionFactory, $propertyMetadataFactory, $iriConverter, $resourceClassResolver, $propertyAccessor, $nameConverter, $classMetadataFactory, null, $defaultContext, $dataTransformers, $resourceMetadataFactory, $resourceAccessChecker); $this->contextBuilder = $contextBuilder; } diff --git a/src/OpenApi/Factory/OpenApiFactory.php b/src/OpenApi/Factory/OpenApiFactory.php index 7ce8cc4d137..69238100fb1 100644 --- a/src/OpenApi/Factory/OpenApiFactory.php +++ b/src/OpenApi/Factory/OpenApiFactory.php @@ -14,6 +14,7 @@ namespace ApiPlatform\Core\OpenApi\Factory; use ApiPlatform\Core\Api\FilterLocatorTrait; +use ApiPlatform\Core\Api\IdentifiersExtractorInterface; use ApiPlatform\Core\Api\OperationType; use ApiPlatform\Core\DataProvider\PaginationOptions; use ApiPlatform\Core\JsonSchema\Schema; @@ -53,8 +54,9 @@ final class OpenApiFactory implements OpenApiFactoryInterface private $jsonSchemaTypeFactory; private $openApiOptions; private $paginationOptions; + private $identifiersExtractor; - public function __construct(ResourceNameCollectionFactoryInterface $resourceNameCollectionFactory, ResourceMetadataFactoryInterface $resourceMetadataFactory, PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, SchemaFactoryInterface $jsonSchemaFactory, TypeFactoryInterface $jsonSchemaTypeFactory, OperationPathResolverInterface $operationPathResolver, ContainerInterface $filterLocator, SubresourceOperationFactoryInterface $subresourceOperationFactory, array $formats = [], Options $openApiOptions, PaginationOptions $paginationOptions) + public function __construct(ResourceNameCollectionFactoryInterface $resourceNameCollectionFactory, ResourceMetadataFactoryInterface $resourceMetadataFactory, PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, SchemaFactoryInterface $jsonSchemaFactory, TypeFactoryInterface $jsonSchemaTypeFactory, OperationPathResolverInterface $operationPathResolver, ContainerInterface $filterLocator, SubresourceOperationFactoryInterface $subresourceOperationFactory, array $formats = [], Options $openApiOptions, PaginationOptions $paginationOptions, IdentifiersExtractorInterface $identifiersExtractor) { $this->resourceNameCollectionFactory = $resourceNameCollectionFactory; $this->jsonSchemaFactory = $jsonSchemaFactory; @@ -68,6 +70,7 @@ public function __construct(ResourceNameCollectionFactoryInterface $resourceName $this->openApiOptions = $openApiOptions; $this->paginationOptions = $paginationOptions; $this->subresourceOperationFactory = $subresourceOperationFactory; + $this->identifiersExtractor = $identifiersExtractor; } /** @@ -84,6 +87,7 @@ public function __invoke(array $context = []): OpenApi foreach ($this->resourceNameCollectionFactory->create() as $resourceClass) { $resourceMetadata = $this->resourceMetadataFactory->create($resourceClass); + $resourceMetadata = $resourceMetadata->withAttributes(($resourceMetadata->getAttributes() ?: []) + ['identified_by' => $this->identifiersExtractor->getIdentifiersFromResourceClass($resourceClass)]); $resourceShortName = $resourceMetadata->getShortName(); // Items needs to be parsed first to be able to reference the lines from the collection operation @@ -117,6 +121,13 @@ private function collectPaths(ResourceMetadata $resourceMetadata, string $resour } foreach ($operations as $operationName => $operation) { + $identifiers = (array) ($operation['identified_by'] ?? $resourceMetadata->getAttribute('identified_by')); + $hasCompositeIdentifiers = \count($identifiers) > 1 ? $resourceMetadata->getAttribute('composite_identifier', true) : false; + + if ($hasCompositeIdentifiers) { + $identifiers = ['id']; + } + $path = $this->getPath($resourceShortName, $operationName, $operation, $operationType); $method = $resourceMetadata->getTypedOperationAttribute($operationType, $operationName, 'method', 'GET'); list($requestMimeTypes, $responseMimeTypes) = $this->getMimeTypes($resourceClass, $operationName, $operationType, $resourceMetadata); @@ -136,7 +147,10 @@ private function collectPaths(ResourceMetadata $resourceMetadata, string $resour // Set up parameters if (OperationType::ITEM === $operationType) { - $parameters[] = new Model\Parameter('id', 'path', 'Resource identifier', true, false, false, ['type' => 'string']); + foreach ($identifiers as $identifier) { + $parameters[] = new Model\Parameter($identifier, 'path', 'Resource identifier', true, false, false, ['type' => 'string']); + } + $links[$operationId] = $this->getLink($resourceClass, $operationId, $path); } elseif (OperationType::COLLECTION === $operationType && 'GET' === $method) { $parameters = array_merge($parameters, $this->getPaginationParameters($resourceMetadata, $operationName), $this->getFiltersParameters($resourceMetadata, $operationName, $resourceClass)); diff --git a/src/Operation/Factory/SubresourceOperationFactory.php b/src/Operation/Factory/SubresourceOperationFactory.php index b6dbd4298e5..fe6c185dc2c 100644 --- a/src/Operation/Factory/SubresourceOperationFactory.php +++ b/src/Operation/Factory/SubresourceOperationFactory.php @@ -13,6 +13,7 @@ namespace ApiPlatform\Core\Operation\Factory; +use ApiPlatform\Core\Api\IdentifiersExtractorInterface; use ApiPlatform\Core\Bridge\Symfony\Routing\RouteNameGenerator; use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface; use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; @@ -32,13 +33,15 @@ final class SubresourceOperationFactory implements SubresourceOperationFactoryIn private $propertyNameCollectionFactory; private $propertyMetadataFactory; private $pathSegmentNameGenerator; + private $identifiersExtractor; - public function __construct(ResourceMetadataFactoryInterface $resourceMetadataFactory, PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, PathSegmentNameGeneratorInterface $pathSegmentNameGenerator) + public function __construct(ResourceMetadataFactoryInterface $resourceMetadataFactory, PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, PathSegmentNameGeneratorInterface $pathSegmentNameGenerator, IdentifiersExtractorInterface $identifiersExtractor) { $this->resourceMetadataFactory = $resourceMetadataFactory; $this->propertyNameCollectionFactory = $propertyNameCollectionFactory; $this->propertyMetadataFactory = $propertyMetadataFactory; $this->pathSegmentNameGenerator = $pathSegmentNameGenerator; + $this->identifiersExtractor = $identifiersExtractor; } /** @@ -75,6 +78,7 @@ private function computeSubresourceOperations(string $resourceClass, array &$tre $subresourceClass = $subresource->getResourceClass(); $subresourceMetadata = $this->resourceMetadataFactory->create($subresourceClass); + $subresourceMetadata = $subresourceMetadata->withAttributes(($subresourceMetadata->getAttributes() ?: []) + ['identified_by' => $this->identifiersExtractor->getIdentifiersFromResourceClass($subresourceClass)]); $isLastItem = ($parentOperation['resource_class'] ?? null) === $resourceClass && $propertyMetadata->isIdentifier(); // A subresource that is also an identifier can't be a start point @@ -93,17 +97,20 @@ private function computeSubresourceOperations(string $resourceClass, array &$tre } $rootResourceMetadata = $this->resourceMetadataFactory->create($rootResourceClass); + $rootResourceMetadata = $rootResourceMetadata->withAttributes(($rootResourceMetadata->getAttributes() ?: []) + ['identified_by' => $this->identifiersExtractor->getIdentifiersFromResourceClass($rootResourceClass)]); $operationName = 'get'; $operation = [ 'property' => $property, 'collection' => $subresource->isCollection(), + 'identified_by' => (array) $rootResourceMetadata->getAttribute('identified_by'), 'resource_class' => $subresourceClass, 'shortNames' => [$subresourceMetadata->getShortName()], ]; if (null === $parentOperation) { $rootShortname = $rootResourceMetadata->getShortName(); - $operation['identifiers'] = [['id', $rootResourceClass, true]]; + // TODO: mutliple identifiers for subresources? + $operation['identifiers'] = [[$operation['identified_by'][0], $rootResourceClass, true, $operation['identified_by']]]; $operation['operation_name'] = sprintf( '%s_%s%s', RouteNameGenerator::inflector($operation['property'], $operation['collection'] ?? false), @@ -126,9 +133,10 @@ private function computeSubresourceOperations(string $resourceClass, array &$tre } $operation['path'] = $subresourceOperation['path'] ?? sprintf( - '/%s%s/{id}/%s%s', + '/%s%s/{%s}/%s%s', $prefix, $this->pathSegmentNameGenerator->getSegmentName($rootShortname), + $operation['identified_by'][0], $this->pathSegmentNameGenerator->getSegmentName($operation['property'], $operation['collection']), self::FORMAT_SUFFIX ); @@ -139,7 +147,7 @@ private function computeSubresourceOperations(string $resourceClass, array &$tre } else { $resourceMetadata = $this->resourceMetadataFactory->create($resourceClass); $operation['identifiers'] = $parentOperation['identifiers']; - $operation['identifiers'][] = [$parentOperation['property'], $resourceClass, $isLastItem ? true : $parentOperation['collection']]; + $operation['identifiers'][] = [$parentOperation['property'], $resourceClass, $isLastItem ? true : $parentOperation['collection'], $operation['identified_by']]; $operation['operation_name'] = str_replace( 'get'.self::SUBRESOURCE_SUFFIX, RouteNameGenerator::inflector($isLastItem ? 'item' : $property, $operation['collection']).'_get'.self::SUBRESOURCE_SUFFIX, diff --git a/src/PathResolver/OperationPathResolver.php b/src/PathResolver/OperationPathResolver.php index aae217f00de..acf07ba6a33 100644 --- a/src/PathResolver/OperationPathResolver.php +++ b/src/PathResolver/OperationPathResolver.php @@ -50,7 +50,13 @@ public function resolveOperationPath(string $resourceShortName, array $operation $path = '/'.$this->pathSegmentNameGenerator->getSegmentName($resourceShortName); if (OperationType::ITEM === $operationType) { - $path .= '/{id}'; + if (isset($operation['identified_by']) && (\count($operation['identified_by']) <= 1 || false === ($operation['has_composite_identifier'] ?? true))) { + foreach ($operation['identified_by'] as $identifier) { + $path .= sprintf('/{%s}', $identifier); + } + } else { + $path .= '/{id}'; + } } $path .= '.{_format}'; diff --git a/src/Serializer/AbstractItemNormalizer.php b/src/Serializer/AbstractItemNormalizer.php index 7fa39d3d4ee..a0a5b9ed957 100644 --- a/src/Serializer/AbstractItemNormalizer.php +++ b/src/Serializer/AbstractItemNormalizer.php @@ -19,7 +19,6 @@ use ApiPlatform\Core\DataTransformer\DataTransformerInitializerInterface; use ApiPlatform\Core\DataTransformer\DataTransformerInterface; use ApiPlatform\Core\Exception\InvalidArgumentException; -use ApiPlatform\Core\Exception\InvalidValueException; use ApiPlatform\Core\Exception\ItemNotFoundException; use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface; use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; @@ -62,11 +61,10 @@ abstract class AbstractItemNormalizer extends AbstractObjectNormalizer protected $resourceAccessChecker; protected $propertyAccessor; protected $itemDataProvider; - protected $allowPlainIdentifiers; protected $dataTransformers = []; protected $localCache = []; - public function __construct(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, IriConverterInterface $iriConverter, ResourceClassResolverInterface $resourceClassResolver, PropertyAccessorInterface $propertyAccessor = null, NameConverterInterface $nameConverter = null, ClassMetadataFactoryInterface $classMetadataFactory = null, ItemDataProviderInterface $itemDataProvider = null, bool $allowPlainIdentifiers = false, array $defaultContext = [], iterable $dataTransformers = [], ResourceMetadataFactoryInterface $resourceMetadataFactory = null, ResourceAccessCheckerInterface $resourceAccessChecker = null) + public function __construct(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, IriConverterInterface $iriConverter, ResourceClassResolverInterface $resourceClassResolver, PropertyAccessorInterface $propertyAccessor = null, NameConverterInterface $nameConverter = null, ClassMetadataFactoryInterface $classMetadataFactory = null, ItemDataProviderInterface $itemDataProvider = null, array $defaultContext = [], iterable $dataTransformers = [], ResourceMetadataFactoryInterface $resourceMetadataFactory = null, ResourceAccessCheckerInterface $resourceAccessChecker = null) { if (!isset($defaultContext['circular_reference_handler'])) { $defaultContext['circular_reference_handler'] = function ($object) { @@ -85,7 +83,6 @@ public function __construct(PropertyNameCollectionFactoryInterface $propertyName $this->resourceClassResolver = $resourceClassResolver; $this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor(); $this->itemDataProvider = $itemDataProvider; - $this->allowPlainIdentifiers = $allowPlainIdentifiers; $this->dataTransformers = $dataTransformers; $this->resourceMetadataFactory = $resourceMetadataFactory; $this->resourceAccessChecker = $resourceAccessChecker; @@ -210,33 +207,18 @@ public function denormalize($data, $class, $format = null, array $context = []) return $dataTransformer->transform($denormalizedInput, $resourceClass, $dataTransformerContext); } - $supportsPlainIdentifiers = $this->supportsPlainIdentifiers(); - if (\is_string($data)) { try { return $this->iriConverter->getItemFromIri($data, $context + ['fetch_data' => true]); } catch (ItemNotFoundException $e) { - if (!$supportsPlainIdentifiers) { - throw new UnexpectedValueException($e->getMessage(), $e->getCode(), $e); - } + throw new UnexpectedValueException($e->getMessage(), $e->getCode(), $e); } catch (InvalidArgumentException $e) { - if (!$supportsPlainIdentifiers) { - throw new UnexpectedValueException(sprintf('Invalid IRI "%s".', $data), $e->getCode(), $e); - } + throw new UnexpectedValueException(sprintf('Invalid IRI "%s".', $data), $e->getCode(), $e); } } if (!\is_array($data)) { - if (!$supportsPlainIdentifiers) { - throw new UnexpectedValueException(sprintf('Expected IRI or document for resource "%s", "%s" given.', $resourceClass, \gettype($data))); - } - - $item = $this->itemDataProvider->getItem($resourceClass, $data, null, $context + ['fetch_data' => true]); - if (null === $item) { - throw new ItemNotFoundException(sprintf('Item not found for resource "%s" with id "%s".', $resourceClass, $data)); - } - - return $item; + throw new UnexpectedValueException(sprintf('Expected IRI or document for resource "%s", "%s" given.', $resourceClass, \gettype($data))); } return parent::denormalize($data, $resourceClass, $format, $context); @@ -448,19 +430,13 @@ protected function denormalizeCollection(string $attribute, PropertyMetadata $pr */ protected function denormalizeRelation(string $attributeName, PropertyMetadata $propertyMetadata, string $className, $value, ?string $format, array $context) { - $supportsPlainIdentifiers = $this->supportsPlainIdentifiers(); - if (\is_string($value)) { try { return $this->iriConverter->getItemFromIri($value, $context + ['fetch_data' => true]); } catch (ItemNotFoundException $e) { - if (!$supportsPlainIdentifiers) { - throw new UnexpectedValueException($e->getMessage(), $e->getCode(), $e); - } + throw new UnexpectedValueException($e->getMessage(), $e->getCode(), $e); } catch (InvalidArgumentException $e) { - if (!$supportsPlainIdentifiers) { - throw new UnexpectedValueException(sprintf('Invalid IRI "%s".', $value), $e->getCode(), $e); - } + throw new UnexpectedValueException(sprintf('Invalid IRI "%s".', $value), $e->getCode(), $e); } } @@ -471,28 +447,9 @@ protected function denormalizeRelation(string $attributeName, PropertyMetadata $ throw new LogicException(sprintf('The injected serializer must be an instance of "%s".', DenormalizerInterface::class)); } - try { - $item = $this->serializer->denormalize($value, $className, $format, $context); - if (!\is_object($item) && null !== $item) { - throw new \UnexpectedValueException('Expected item to be an object or null.'); - } - - return $item; - } catch (InvalidValueException $e) { - if (!$supportsPlainIdentifiers) { - throw $e; - } - } - } - - if (!\is_array($value)) { - if (!$supportsPlainIdentifiers) { - throw new UnexpectedValueException(sprintf('Expected IRI or nested document for attribute "%s", "%s" given.', $attributeName, \gettype($value))); - } - - $item = $this->itemDataProvider->getItem($className, $value, null, $context + ['fetch_data' => true]); - if (null === $item) { - throw new ItemNotFoundException(sprintf('Item not found for resource "%s" with id "%s".', $className, $value)); + $item = $this->serializer->denormalize($value, $className, $format, $context); + if (!\is_object($item) && null !== $item) { + throw new \UnexpectedValueException('Expected item to be an object or null.'); } return $item; @@ -778,9 +735,4 @@ private function setValue($object, string $attributeName, $value) // Properties not found are ignored } } - - private function supportsPlainIdentifiers(): bool - { - return $this->allowPlainIdentifiers && null !== $this->itemDataProvider; - } } diff --git a/src/Serializer/ItemNormalizer.php b/src/Serializer/ItemNormalizer.php index 293b7955766..74cdee39902 100644 --- a/src/Serializer/ItemNormalizer.php +++ b/src/Serializer/ItemNormalizer.php @@ -37,9 +37,9 @@ class ItemNormalizer extends AbstractItemNormalizer { private $logger; - public function __construct(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, IriConverterInterface $iriConverter, ResourceClassResolverInterface $resourceClassResolver, PropertyAccessorInterface $propertyAccessor = null, NameConverterInterface $nameConverter = null, ClassMetadataFactoryInterface $classMetadataFactory = null, ItemDataProviderInterface $itemDataProvider = null, bool $allowPlainIdentifiers = false, LoggerInterface $logger = null, iterable $dataTransformers = [], ResourceMetadataFactoryInterface $resourceMetadataFactory = null, ResourceAccessCheckerInterface $resourceAccessChecker = null) + public function __construct(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, IriConverterInterface $iriConverter, ResourceClassResolverInterface $resourceClassResolver, PropertyAccessorInterface $propertyAccessor = null, NameConverterInterface $nameConverter = null, ClassMetadataFactoryInterface $classMetadataFactory = null, ItemDataProviderInterface $itemDataProvider = null, LoggerInterface $logger = null, iterable $dataTransformers = [], ResourceMetadataFactoryInterface $resourceMetadataFactory = null, ResourceAccessCheckerInterface $resourceAccessChecker = null) { - parent::__construct($propertyNameCollectionFactory, $propertyMetadataFactory, $iriConverter, $resourceClassResolver, $propertyAccessor, $nameConverter, $classMetadataFactory, $itemDataProvider, $allowPlainIdentifiers, [], $dataTransformers, $resourceMetadataFactory, $resourceAccessChecker); + parent::__construct($propertyNameCollectionFactory, $propertyMetadataFactory, $iriConverter, $resourceClassResolver, $propertyAccessor, $nameConverter, $classMetadataFactory, $itemDataProvider, [], $dataTransformers, $resourceMetadataFactory, $resourceAccessChecker); $this->logger = $logger ?: new NullLogger(); } diff --git a/src/Swagger/Serializer/DocumentationNormalizer.php b/src/Swagger/Serializer/DocumentationNormalizer.php index 121bb11a172..11a3be3ecd5 100644 --- a/src/Swagger/Serializer/DocumentationNormalizer.php +++ b/src/Swagger/Serializer/DocumentationNormalizer.php @@ -16,6 +16,7 @@ use ApiPlatform\Core\Api\FilterCollection; use ApiPlatform\Core\Api\FilterLocatorTrait; use ApiPlatform\Core\Api\FormatsProviderInterface; +use ApiPlatform\Core\Api\IdentifiersExtractorInterface; use ApiPlatform\Core\Api\OperationAwareFormatsProviderInterface; use ApiPlatform\Core\Api\OperationMethodResolverInterface; use ApiPlatform\Core\Api\OperationType; @@ -85,6 +86,7 @@ final class DocumentationNormalizer implements NormalizerInterface, CacheableSup private $paginationClientEnabledParameterName; private $formats; private $formatsProvider; + private $identifiersExtractor; /** * @var SchemaFactoryInterface @@ -106,7 +108,7 @@ final class DocumentationNormalizer implements NormalizerInterface, CacheableSup * @param mixed|null $jsonSchemaTypeFactory * @param int[] $swaggerVersions */ - public function __construct(ResourceMetadataFactoryInterface $resourceMetadataFactory, PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, $jsonSchemaFactory = null, $jsonSchemaTypeFactory = null, OperationPathResolverInterface $operationPathResolver, UrlGeneratorInterface $urlGenerator = null, $filterLocator = null, NameConverterInterface $nameConverter = null, bool $oauthEnabled = false, string $oauthType = '', string $oauthFlow = '', string $oauthTokenUrl = '', string $oauthAuthorizationUrl = '', array $oauthScopes = [], array $apiKeys = [], SubresourceOperationFactoryInterface $subresourceOperationFactory = null, bool $paginationEnabled = true, string $paginationPageParameterName = 'page', bool $clientItemsPerPage = false, string $itemsPerPageParameterName = 'itemsPerPage', $formats = [], bool $paginationClientEnabled = false, string $paginationClientEnabledParameterName = 'pagination', array $defaultContext = [], array $swaggerVersions = [2, 3]) + public function __construct(ResourceMetadataFactoryInterface $resourceMetadataFactory, PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, $jsonSchemaFactory = null, $jsonSchemaTypeFactory = null, OperationPathResolverInterface $operationPathResolver, UrlGeneratorInterface $urlGenerator = null, $filterLocator = null, NameConverterInterface $nameConverter = null, bool $oauthEnabled = false, string $oauthType = '', string $oauthFlow = '', string $oauthTokenUrl = '', string $oauthAuthorizationUrl = '', array $oauthScopes = [], array $apiKeys = [], SubresourceOperationFactoryInterface $subresourceOperationFactory = null, bool $paginationEnabled = true, string $paginationPageParameterName = 'page', bool $clientItemsPerPage = false, string $itemsPerPageParameterName = 'itemsPerPage', $formats = [], bool $paginationClientEnabled = false, string $paginationClientEnabledParameterName = 'pagination', array $defaultContext = [], array $swaggerVersions = [2, 3], IdentifiersExtractorInterface $identifiersExtractor = null) { if ($jsonSchemaTypeFactory instanceof OperationMethodResolverInterface) { @trigger_error(sprintf('Passing an instance of %s to %s() is deprecated since version 2.5 and will be removed in 3.0.', OperationMethodResolverInterface::class, __METHOD__), E_USER_DEPRECATED); @@ -163,6 +165,7 @@ public function __construct(ResourceMetadataFactoryInterface $resourceMetadataFa $this->itemsPerPageParameterName = $itemsPerPageParameterName; $this->paginationClientEnabled = $paginationClientEnabled; $this->paginationClientEnabledParameterName = $paginationClientEnabledParameterName; + $this->identifiersExtractor = $identifiersExtractor; $this->defaultContext[self::SPEC_VERSION] = $swaggerVersions[0] ?? 2; @@ -183,6 +186,9 @@ public function normalize($object, $format = null, array $context = []) foreach ($object->getResourceNameCollection() as $resourceClass) { $resourceMetadata = $this->resourceMetadataFactory->create($resourceClass); $resourceShortName = $resourceMetadata->getShortName(); + if ($this->identifiersExtractor) { + $resourceMetadata = $resourceMetadata->withAttributes(($resourceMetadata->getAttributes() ?: []) + ['identified_by' => $this->identifiersExtractor->getIdentifiersFromResourceClass($resourceClass)]); + } // Items needs to be parsed first to be able to reference the lines from the collection operation $this->addPaths($v3, $paths, $definitions, $resourceClass, $resourceShortName, $resourceMetadata, OperationType::ITEM, $links); @@ -339,7 +345,7 @@ private function updateGetOperation(bool $v3, \ArrayObject $pathOperation, array $pathOperation['summary'] ?? $pathOperation['summary'] = sprintf('Retrieves a %s resource.', $resourceShortName); - $pathOperation = $this->addItemOperationParameters($v3, $pathOperation); + $pathOperation = $this->addItemOperationParameters($v3, $pathOperation, $operationType, $operationName, $resourceMetadata); $successResponse = ['description' => sprintf('%s resource response', $resourceShortName)]; [$successResponse] = $this->addSchemas($v3, $successResponse, $definitions, $resourceClass, $operationType, $operationName, $mimeTypes); @@ -485,7 +491,7 @@ private function updatePostOperation(bool $v3, \ArrayObject $pathOperation, arra $pathOperation['summary'] ?? $pathOperation['summary'] = sprintf('Creates a %s resource.', $resourceShortName); if (OperationType::ITEM === $operationType) { - $pathOperation = $this->addItemOperationParameters($v3, $pathOperation); + $pathOperation = $this->addItemOperationParameters($v3, $pathOperation, $operationType, $operationName, $resourceMetadata); } $successResponse = ['description' => sprintf('%s resource created', $resourceShortName)]; @@ -513,7 +519,7 @@ private function updatePutOperation(bool $v3, \ArrayObject $pathOperation, array $pathOperation['summary'] ?? $pathOperation['summary'] = sprintf('Replaces the %s resource.', $resourceShortName); - $pathOperation = $this->addItemOperationParameters($v3, $pathOperation); + $pathOperation = $this->addItemOperationParameters($v3, $pathOperation, $operationType, $operationName, $resourceMetadata); $successResponse = ['description' => sprintf('%s resource updated', $resourceShortName)]; [$successResponse] = $this->addSchemas($v3, $successResponse, $definitions, $resourceClass, $operationType, $operationName, $responseMimeTypes); @@ -575,18 +581,32 @@ private function updateDeleteOperation(bool $v3, \ArrayObject $pathOperation, st '404' => ['description' => 'Resource not found'], ]; - return $this->addItemOperationParameters($v3, $pathOperation); + return $this->addItemOperationParameters($v3, $pathOperation, $operationType, $operationName, $resourceMetadata); } - private function addItemOperationParameters(bool $v3, \ArrayObject $pathOperation): \ArrayObject + private function addItemOperationParameters(bool $v3, \ArrayObject $pathOperation, string $operationType, string $operationName, ResourceMetadata $resourceMetadata): \ArrayObject { - $parameter = [ - 'name' => 'id', - 'in' => 'path', - 'required' => true, - ]; - $v3 ? $parameter['schema'] = ['type' => 'string'] : $parameter['type'] = 'string'; - $pathOperation['parameters'] ?? $pathOperation['parameters'] = [$parameter]; + $identifiers = (array) $resourceMetadata + ->getTypedOperationAttribute(OperationType::ITEM, $operationName, 'identified_by', ['id'], true); + $hasCompositeIdentifiers = \count($identifiers) > 1 ? $resourceMetadata->getAttribute('composite_identifier', true) : false; + + if ($hasCompositeIdentifiers) { + $identifiers = ['id']; + } + + if (!($pathOperation['parameters'] ?? null)) { + $pathOperation['parameters'] = []; + } + + foreach ($identifiers as $identifier) { + $parameter = [ + 'name' => $identifier, + 'in' => 'path', + 'required' => true, + ]; + $v3 ? $parameter['schema'] = ['type' => 'string'] : $parameter['type'] = 'string'; + $pathOperation['parameters'][] = $parameter; + } return $pathOperation; } diff --git a/src/Util/AttributesExtractor.php b/src/Util/AttributesExtractor.php index fcb93a59242..438327f8bc4 100644 --- a/src/Util/AttributesExtractor.php +++ b/src/Util/AttributesExtractor.php @@ -34,7 +34,7 @@ private function __construct() */ public static function extractAttributes(array $attributes): array { - $result = ['resource_class' => $attributes['_api_resource_class'] ?? null]; + $result = ['resource_class' => $attributes['_api_resource_class'] ?? null, 'identified_by' => $attributes['_api_identified_by'] ?? null, 'has_composite_identifier' => $attributes['_api_has_composite_identifier'] ?? false]; if ($subresourceContext = $attributes['_api_subresource_context'] ?? null) { $result['subresource_context'] = $subresourceContext; } diff --git a/tests/Bridge/Doctrine/MongoDbOdm/ItemDataProviderTest.php b/tests/Bridge/Doctrine/MongoDbOdm/ItemDataProviderTest.php index 36f3780f987..2153c347fc6 100644 --- a/tests/Bridge/Doctrine/MongoDbOdm/ItemDataProviderTest.php +++ b/tests/Bridge/Doctrine/MongoDbOdm/ItemDataProviderTest.php @@ -18,7 +18,6 @@ use ApiPlatform\Core\Bridge\Doctrine\MongoDbOdm\ItemDataProvider; use ApiPlatform\Core\Exception\PropertyNotFoundException; use ApiPlatform\Core\Exception\RuntimeException; -use ApiPlatform\Core\Identifier\IdentifierConverterInterface; use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface; use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; use ApiPlatform\Core\Metadata\Property\PropertyMetadata; @@ -61,7 +60,7 @@ protected function setUp(): void public function testGetItemSingleIdentifier() { - $context = ['foo' => 'bar', 'fetch_data' => true, IdentifierConverterInterface::HAS_IDENTIFIER_CONVERTER => true]; + $context = ['foo' => 'bar', 'fetch_data' => true]; $matchProphecy = $this->prophesize(Match::class); $matchProphecy->field('id')->willReturn($matchProphecy)->shouldBeCalled(); @@ -94,7 +93,7 @@ public function testGetItemSingleIdentifier() public function testGetItemWithExecuteOptions() { - $context = ['foo' => 'bar', 'fetch_data' => true, IdentifierConverterInterface::HAS_IDENTIFIER_CONVERTER => true]; + $context = ['foo' => 'bar', 'fetch_data' => true]; $matchProphecy = $this->prophesize(Match::class); $matchProphecy->field('id')->willReturn($matchProphecy)->shouldBeCalled(); @@ -156,7 +155,7 @@ public function testGetItemDoubleIdentifier() $this->resourceMetadataFactoryProphecy->create(Dummy::class)->willReturn(new ResourceMetadata()); - $context = [IdentifierConverterInterface::HAS_IDENTIFIER_CONVERTER => true]; + $context = []; $extensionProphecy = $this->prophesize(AggregationItemExtensionInterface::class); $extensionProphecy->applyToItem($aggregationBuilder, Dummy::class, ['ida' => 1, 'idb' => 2], 'foo', $context)->shouldBeCalled(); @@ -170,6 +169,7 @@ public function testGetItemDoubleIdentifier() */ public function testGetItemWrongCompositeIdentifier() { + $this->markTestSkipped(); $this->expectException(PropertyNotFoundException::class); [$propertyNameCollectionFactory, $propertyMetadataFactory] = $this->getMetadataFactories(Dummy::class, [ @@ -204,7 +204,7 @@ public function testAggregationResultExtension() ]); $managerRegistry = $this->getManagerRegistry(Dummy::class, $aggregationBuilder); - $context = [IdentifierConverterInterface::HAS_IDENTIFIER_CONVERTER => true]; + $context = []; $extensionProphecy = $this->prophesize(AggregationResultItemExtensionInterface::class); $extensionProphecy->applyToItem($aggregationBuilder, Dummy::class, ['id' => 1], 'foo', $context)->shouldBeCalled(); $extensionProphecy->supportsResult(Dummy::class, 'foo', $context)->willReturn(true)->shouldBeCalled(); @@ -249,7 +249,7 @@ public function testCannotCreateAggregationBuilder() 'id', ]); - (new ItemDataProvider($managerRegistryProphecy->reveal(), $this->resourceMetadataFactoryProphecy->reveal(), $propertyNameCollectionFactory, $propertyMetadataFactory, [$extensionProphecy->reveal()]))->getItem(Dummy::class, 'foo', null, [IdentifierConverterInterface::HAS_IDENTIFIER_CONVERTER => true]); + (new ItemDataProvider($managerRegistryProphecy->reveal(), $this->resourceMetadataFactoryProphecy->reveal(), $propertyNameCollectionFactory, $propertyMetadataFactory, [$extensionProphecy->reveal()]))->getItem(Dummy::class, ['id' => 'foo'], null, []); } /** diff --git a/tests/Bridge/Doctrine/MongoDbOdm/SubresourceDataProviderTest.php b/tests/Bridge/Doctrine/MongoDbOdm/SubresourceDataProviderTest.php index ae58ddcb434..e19c8f225bf 100644 --- a/tests/Bridge/Doctrine/MongoDbOdm/SubresourceDataProviderTest.php +++ b/tests/Bridge/Doctrine/MongoDbOdm/SubresourceDataProviderTest.php @@ -17,7 +17,6 @@ use ApiPlatform\Core\Bridge\Doctrine\MongoDbOdm\SubresourceDataProvider; use ApiPlatform\Core\Exception\ResourceClassNotSupportedException; use ApiPlatform\Core\Exception\RuntimeException; -use ApiPlatform\Core\Identifier\IdentifierConverterInterface; use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface; use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; use ApiPlatform\Core\Metadata\Property\PropertyMetadata; @@ -167,7 +166,7 @@ public function testGetSubresource() $dataProvider = new SubresourceDataProvider($managerRegistryProphecy->reveal(), $this->resourceMetadataFactoryProphecy->reveal(), $propertyNameCollectionFactory, $propertyMetadataFactory); - $context = ['property' => 'relatedDummies', 'identifiers' => [['id', Dummy::class]], 'collection' => true, IdentifierConverterInterface::HAS_IDENTIFIER_CONVERTER => true]; + $context = ['property' => 'relatedDummies', 'identifiers' => [['id', Dummy::class]], 'collection' => true]; $this->assertEquals([], $dataProvider->getSubresource(RelatedDummy::class, ['id' => ['id' => 1]], $context)); } @@ -256,7 +255,7 @@ public function testGetSubSubresourceItem() $dataProvider = new SubresourceDataProvider($managerRegistryProphecy->reveal(), $this->resourceMetadataFactoryProphecy->reveal(), $propertyNameCollectionFactory, $propertyMetadataFactory); - $context = ['property' => 'thirdLevel', 'identifiers' => [['id', Dummy::class], ['relatedDummies', RelatedDummy::class]], 'collection' => false, IdentifierConverterInterface::HAS_IDENTIFIER_CONVERTER => true]; + $context = ['property' => 'thirdLevel', 'identifiers' => [['id', Dummy::class], ['relatedDummies', RelatedDummy::class]], 'collection' => false]; $this->assertEquals($result, $dataProvider->getSubresource(ThirdLevel::class, ['id' => ['id' => 1], 'relatedDummies' => ['id' => 1]], $context)); } @@ -353,7 +352,7 @@ public function testGetSubSubresourceItemWithExecuteOptions() $dataProvider = new SubresourceDataProvider($managerRegistryProphecy->reveal(), $this->resourceMetadataFactoryProphecy->reveal(), $propertyNameCollectionFactory, $propertyMetadataFactory); - $context = ['property' => 'thirdLevel', 'identifiers' => [['id', Dummy::class], ['relatedDummies', RelatedDummy::class]], 'collection' => false, IdentifierConverterInterface::HAS_IDENTIFIER_CONVERTER => true]; + $context = ['property' => 'thirdLevel', 'identifiers' => [['id', Dummy::class], ['relatedDummies', RelatedDummy::class]], 'collection' => false]; $this->assertEquals($result, $dataProvider->getSubresource(ThirdLevel::class, ['id' => ['id' => 1], 'relatedDummies' => ['id' => 1]], $context, 'third_level_operation_name')); } @@ -405,7 +404,7 @@ public function testGetSubresourceOneToOneOwningRelation() $dataProvider = new SubresourceDataProvider($managerRegistryProphecy->reveal(), $this->resourceMetadataFactoryProphecy->reveal(), $propertyNameCollectionFactory, $propertyMetadataFactory); - $context = ['property' => 'ownedDummy', 'identifiers' => [['id', Dummy::class]], 'collection' => false, IdentifierConverterInterface::HAS_IDENTIFIER_CONVERTER => true]; + $context = ['property' => 'ownedDummy', 'identifiers' => [['id', Dummy::class]], 'collection' => false]; $this->assertEquals($result, $dataProvider->getSubresource(RelatedOwningDummy::class, ['id' => ['id' => 1]], $context)); } @@ -458,7 +457,7 @@ public function testAggregationResultExtension() $dataProvider = new SubresourceDataProvider($managerRegistryProphecy->reveal(), $this->resourceMetadataFactoryProphecy->reveal(), $propertyNameCollectionFactory, $propertyMetadataFactory, [$extensionProphecy->reveal()]); - $context = ['property' => 'relatedDummies', 'identifiers' => [['id', Dummy::class]], 'collection' => true, IdentifierConverterInterface::HAS_IDENTIFIER_CONVERTER => true]; + $context = ['property' => 'relatedDummies', 'identifiers' => [['id', Dummy::class]], 'collection' => true]; $this->assertEquals([], $dataProvider->getSubresource(RelatedDummy::class, ['id' => ['id' => 1]], $context)); } @@ -539,7 +538,7 @@ public function testGetSubresourceCollectionItem() $dataProvider = new SubresourceDataProvider($managerRegistryProphecy->reveal(), $this->resourceMetadataFactoryProphecy->reveal(), $propertyNameCollectionFactory, $propertyMetadataFactory); - $context = ['property' => 'id', 'identifiers' => [['id', Dummy::class, true], ['relatedDummies', RelatedDummy::class, true]], 'collection' => false, IdentifierConverterInterface::HAS_IDENTIFIER_CONVERTER => true]; + $context = ['property' => 'id', 'identifiers' => [['id', Dummy::class, true], ['relatedDummies', RelatedDummy::class, true]], 'collection' => false]; $this->assertEquals($result, $dataProvider->getSubresource(RelatedDummy::class, ['id' => ['id' => 1], 'relatedDummies' => ['id' => 2]], $context)); } diff --git a/tests/Bridge/Doctrine/Orm/ItemDataProviderTest.php b/tests/Bridge/Doctrine/Orm/ItemDataProviderTest.php index e356de369ae..462ccadf758 100644 --- a/tests/Bridge/Doctrine/Orm/ItemDataProviderTest.php +++ b/tests/Bridge/Doctrine/Orm/ItemDataProviderTest.php @@ -17,13 +17,7 @@ use ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\QueryResultItemExtensionInterface; use ApiPlatform\Core\Bridge\Doctrine\Orm\ItemDataProvider; use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface; -use ApiPlatform\Core\Exception\PropertyNotFoundException; use ApiPlatform\Core\Exception\RuntimeException; -use ApiPlatform\Core\Identifier\IdentifierConverterInterface; -use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface; -use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; -use ApiPlatform\Core\Metadata\Property\PropertyMetadata; -use ApiPlatform\Core\Metadata\Property\PropertyNameCollection; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Dummy; use ApiPlatform\Core\Tests\ProphecyTrait; use Doctrine\DBAL\Connection; @@ -50,7 +44,7 @@ class ItemDataProviderTest extends TestCase public function testGetItemSingleIdentifier() { - $context = ['foo' => 'bar', 'fetch_data' => true, IdentifierConverterInterface::HAS_IDENTIFIER_CONVERTER => true]; + $context = ['foo' => 'bar', 'fetch_data' => true]; $queryProphecy = $this->prophesize(AbstractQuery::class); $queryProphecy->getOneOrNullResult()->willReturn([])->shouldBeCalled(); @@ -69,9 +63,6 @@ public function testGetItemSingleIdentifier() $queryBuilder = $queryBuilderProphecy->reveal(); - [$propertyNameCollectionFactory, $propertyMetadataFactory] = $this->getMetadataFactories(Dummy::class, [ - 'id', - ]); $managerRegistry = $this->getManagerRegistry(Dummy::class, [ 'id' => [ 'type' => DBALType::INTEGER, @@ -81,7 +72,7 @@ public function testGetItemSingleIdentifier() $extensionProphecy = $this->prophesize(QueryItemExtensionInterface::class); $extensionProphecy->applyToItem($queryBuilder, Argument::type(QueryNameGeneratorInterface::class), Dummy::class, ['id' => 1], 'foo', $context)->shouldBeCalled(); - $dataProvider = new ItemDataProvider($managerRegistry, $propertyNameCollectionFactory, $propertyMetadataFactory, [$extensionProphecy->reveal()]); + $dataProvider = new ItemDataProvider($managerRegistry, [$extensionProphecy->reveal()]); $this->assertEquals([], $dataProvider->getItem(Dummy::class, ['id' => 1], 'foo', $context)); } @@ -109,10 +100,6 @@ public function testGetItemDoubleIdentifier() $queryBuilder = $queryBuilderProphecy->reveal(); - [$propertyNameCollectionFactory, $propertyMetadataFactory] = $this->getMetadataFactories(Dummy::class, [ - 'ida', - 'idb', - ]); $managerRegistry = $this->getManagerRegistry(Dummy::class, [ 'ida' => [ 'type' => DBALType::INTEGER, @@ -122,37 +109,12 @@ public function testGetItemDoubleIdentifier() ], ], $queryBuilder); - $context = [IdentifierConverterInterface::HAS_IDENTIFIER_CONVERTER => true]; $extensionProphecy = $this->prophesize(QueryItemExtensionInterface::class); - $extensionProphecy->applyToItem($queryBuilder, Argument::type(QueryNameGeneratorInterface::class), Dummy::class, ['ida' => 1, 'idb' => 2], 'foo', $context)->shouldBeCalled(); - - $dataProvider = new ItemDataProvider($managerRegistry, $propertyNameCollectionFactory, $propertyMetadataFactory, [$extensionProphecy->reveal()]); - - $this->assertEquals([], $dataProvider->getItem(Dummy::class, ['ida' => 1, 'idb' => 2], 'foo', $context)); - } - - /** - * @group legacy - */ - public function testGetItemWrongCompositeIdentifier() - { - $this->expectException(PropertyNotFoundException::class); + $extensionProphecy->applyToItem($queryBuilder, Argument::type(QueryNameGeneratorInterface::class), Dummy::class, ['ida' => 1, 'idb' => 2], 'foo', [])->shouldBeCalled(); - [$propertyNameCollectionFactory, $propertyMetadataFactory] = $this->getMetadataFactories(Dummy::class, [ - 'ida', - 'idb', - ]); - $managerRegistry = $this->getManagerRegistry(Dummy::class, [ - 'ida' => [ - 'type' => DBALType::INTEGER, - ], - 'idb' => [ - 'type' => DBALType::INTEGER, - ], - ], $this->prophesize(QueryBuilder::class)->reveal()); + $dataProvider = new ItemDataProvider($managerRegistry, [$extensionProphecy->reveal()]); - $dataProvider = new ItemDataProvider($managerRegistry, $propertyNameCollectionFactory, $propertyMetadataFactory); - $dataProvider->getItem(Dummy::class, 'ida=1;', 'foo'); + $this->assertEquals([], $dataProvider->getItem(Dummy::class, ['ida' => 1, 'idb' => 2], 'foo')); } public function testQueryResultExtension() @@ -171,24 +133,20 @@ public function testQueryResultExtension() $queryBuilder = $queryBuilderProphecy->reveal(); - [$propertyNameCollectionFactory, $propertyMetadataFactory] = $this->getMetadataFactories(Dummy::class, [ - 'id', - ]); $managerRegistry = $this->getManagerRegistry(Dummy::class, [ 'id' => [ 'type' => DBALType::INTEGER, ], ], $queryBuilder); - $context = [IdentifierConverterInterface::HAS_IDENTIFIER_CONVERTER => true]; $extensionProphecy = $this->prophesize(QueryResultItemExtensionInterface::class); - $extensionProphecy->applyToItem($queryBuilder, Argument::type(QueryNameGeneratorInterface::class), Dummy::class, ['id' => 1], 'foo', $context)->shouldBeCalled(); - $extensionProphecy->supportsResult(Dummy::class, 'foo', $context)->willReturn(true)->shouldBeCalled(); - $extensionProphecy->getResult($queryBuilder, Dummy::class, 'foo', $context)->willReturn([])->shouldBeCalled(); + $extensionProphecy->applyToItem($queryBuilder, Argument::type(QueryNameGeneratorInterface::class), Dummy::class, ['id' => 1], 'foo', [])->shouldBeCalled(); + $extensionProphecy->supportsResult(Dummy::class, 'foo', [])->willReturn(true)->shouldBeCalled(); + $extensionProphecy->getResult($queryBuilder, Dummy::class, 'foo', [])->willReturn([])->shouldBeCalled(); - $dataProvider = new ItemDataProvider($managerRegistry, $propertyNameCollectionFactory, $propertyMetadataFactory, [$extensionProphecy->reveal()]); + $dataProvider = new ItemDataProvider($managerRegistry, [$extensionProphecy->reveal()]); - $this->assertEquals([], $dataProvider->getItem(Dummy::class, ['id' => 1], 'foo', $context)); + $this->assertEquals([], $dataProvider->getItem(Dummy::class, ['id' => 1], 'foo')); } public function testUnsupportedClass() @@ -198,11 +156,7 @@ public function testUnsupportedClass() $extensionProphecy = $this->prophesize(QueryItemExtensionInterface::class); - [$propertyNameCollectionFactory, $propertyMetadataFactory] = $this->getMetadataFactories(Dummy::class, [ - 'id', - ]); - - $dataProvider = new ItemDataProvider($managerRegistryProphecy->reveal(), $propertyNameCollectionFactory, $propertyMetadataFactory, [$extensionProphecy->reveal()]); + $dataProvider = new ItemDataProvider($managerRegistryProphecy->reveal(), [$extensionProphecy->reveal()]); $this->assertFalse($dataProvider->supports(Dummy::class, 'foo')); } @@ -233,38 +187,8 @@ public function testCannotCreateQueryBuilder() $extensionProphecy = $this->prophesize(QueryItemExtensionInterface::class); - [$propertyNameCollectionFactory, $propertyMetadataFactory] = $this->getMetadataFactories(Dummy::class, [ - 'id', - ]); - - $itemDataProvider = new ItemDataProvider($managerRegistryProphecy->reveal(), $propertyNameCollectionFactory, $propertyMetadataFactory, [$extensionProphecy->reveal()]); - $itemDataProvider->getItem(Dummy::class, ['id' => 1234], null, [IdentifierConverterInterface::HAS_IDENTIFIER_CONVERTER => true]); - } - - /** - * Gets mocked metadata factories. - */ - private function getMetadataFactories(string $resourceClass, array $identifiers): array - { - $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); - $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); - - $nameCollection = ['foobar']; - - foreach ($identifiers as $identifier) { - $metadata = new PropertyMetadata(); - $metadata = $metadata->withIdentifier(true); - $propertyMetadataFactoryProphecy->create($resourceClass, $identifier)->willReturn($metadata); - - $nameCollection[] = $identifier; - } - - //random property to prevent the use of non-identifiers metadata while looping - $propertyMetadataFactoryProphecy->create($resourceClass, 'foobar')->willReturn(new PropertyMetadata()); - - $propertyNameCollectionFactoryProphecy->create($resourceClass)->willReturn(new PropertyNameCollection($nameCollection)); - - return [$propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal()]; + $itemDataProvider = new ItemDataProvider($managerRegistryProphecy->reveal(), [$extensionProphecy->reveal()]); + $itemDataProvider->getItem(Dummy::class, ['id' => 1234], null); } /** diff --git a/tests/Bridge/Doctrine/Orm/SubresourceDataProviderTest.php b/tests/Bridge/Doctrine/Orm/SubresourceDataProviderTest.php index 58290ca51e4..a3236accd64 100644 --- a/tests/Bridge/Doctrine/Orm/SubresourceDataProviderTest.php +++ b/tests/Bridge/Doctrine/Orm/SubresourceDataProviderTest.php @@ -18,11 +18,6 @@ use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface; use ApiPlatform\Core\Exception\ResourceClassNotSupportedException; use ApiPlatform\Core\Exception\RuntimeException; -use ApiPlatform\Core\Identifier\IdentifierConverterInterface; -use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface; -use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; -use ApiPlatform\Core\Metadata\Property\PropertyMetadata; -use ApiPlatform\Core\Metadata\Property\PropertyNameCollection; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Dummy; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\RelatedDummy; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\RelatedOwningDummy; @@ -61,31 +56,6 @@ private function assertIdentifierManagerMethodCalls($managerProphecy) $managerProphecy->getConnection()->willReturn($connectionProphecy); } - private function getMetadataProphecies(array $resourceClassesIdentifiers) - { - $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); - $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); - - foreach ($resourceClassesIdentifiers as $resourceClass => $identifiers) { - $nameCollection = ['foobar']; - - foreach ($identifiers as $identifier) { - $metadata = new PropertyMetadata(); - $metadata = $metadata->withIdentifier(true); - $propertyMetadataFactoryProphecy->create($resourceClass, $identifier)->willReturn($metadata); - - $nameCollection[] = $identifier; - } - - //random property to prevent the use of non-identifiers metadata while looping - $propertyMetadataFactoryProphecy->create($resourceClass, 'foobar')->willReturn(new PropertyMetadata()); - - $propertyNameCollectionFactoryProphecy->create($resourceClass)->willReturn(new PropertyNameCollection($nameCollection)); - } - - return [$propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal()]; - } - private function getManagerRegistryProphecy(QueryBuilder $queryBuilder, array $identifiers, string $resourceClass) { $classMetadataProphecy = $this->prophesize(ClassMetadata::class); @@ -110,11 +80,10 @@ public function testNotASubresource() $this->expectExceptionMessage('The given resource class is not a subresource.'); $identifiers = ['id']; - [$propertyNameCollectionFactory, $propertyMetadataFactory] = $this->getMetadataProphecies([Dummy::class => $identifiers]); $queryBuilder = $this->prophesize(QueryBuilder::class)->reveal(); $managerRegistry = $this->getManagerRegistryProphecy($queryBuilder, $identifiers, Dummy::class); - $dataProvider = new SubresourceDataProvider($managerRegistry, $propertyNameCollectionFactory, $propertyMetadataFactory, []); + $dataProvider = new SubresourceDataProvider($managerRegistry, []); $dataProvider->getSubresource(Dummy::class, ['id' => 1], []); } @@ -168,11 +137,9 @@ public function testGetSubresource() $managerRegistryProphecy->getManagerForClass(RelatedDummy::class)->shouldBeCalled()->willReturn($managerProphecy->reveal()); $managerRegistryProphecy->getManagerForClass(Dummy::class)->shouldBeCalled()->willReturn($managerProphecy->reveal()); - [$propertyNameCollectionFactory, $propertyMetadataFactory] = $this->getMetadataProphecies([Dummy::class => $identifiers]); + $dataProvider = new SubresourceDataProvider($managerRegistryProphecy->reveal()); - $dataProvider = new SubresourceDataProvider($managerRegistryProphecy->reveal(), $propertyNameCollectionFactory, $propertyMetadataFactory); - - $context = ['property' => 'relatedDummies', 'identifiers' => [['id', Dummy::class]], 'collection' => true, IdentifierConverterInterface::HAS_IDENTIFIER_CONVERTER => true]; + $context = ['property' => 'relatedDummies', 'identifiers' => [['id', Dummy::class]], 'collection' => true]; $this->assertEquals([], $dataProvider->getSubresource(RelatedDummy::class, ['id' => ['id' => 1]], $context)); } @@ -262,11 +229,9 @@ public function testGetSubSubresourceItem() $managerRegistryProphecy->getManagerForClass(ThirdLevel::class)->shouldBeCalled()->willReturn($managerProphecy->reveal()); - [$propertyNameCollectionFactory, $propertyMetadataFactory] = $this->getMetadataProphecies([Dummy::class => $identifiers, RelatedDummy::class => $identifiers]); - - $dataProvider = new SubresourceDataProvider($managerRegistryProphecy->reveal(), $propertyNameCollectionFactory, $propertyMetadataFactory); + $dataProvider = new SubresourceDataProvider($managerRegistryProphecy->reveal()); - $context = ['property' => 'thirdLevel', 'identifiers' => [['id', Dummy::class], ['relatedDummies', RelatedDummy::class]], 'collection' => false, IdentifierConverterInterface::HAS_IDENTIFIER_CONVERTER => true]; + $context = ['property' => 'thirdLevel', 'identifiers' => [['id', Dummy::class], ['relatedDummies', RelatedDummy::class]], 'collection' => false]; $this->assertEquals($result, $dataProvider->getSubresource(ThirdLevel::class, ['id' => ['id' => 1], 'relatedDummies' => ['id' => 1]], $context)); } @@ -318,11 +283,9 @@ public function testGetSubresourceOneToOneOwningRelation() $managerRegistryProphecy->getManagerForClass(RelatedOwningDummy::class)->shouldBeCalled()->willReturn($managerProphecy->reveal()); $managerRegistryProphecy->getManagerForClass(Dummy::class)->shouldBeCalled()->willReturn($managerProphecy->reveal()); - [$propertyNameCollectionFactory, $propertyMetadataFactory] = $this->getMetadataProphecies([Dummy::class => $identifiers]); - - $dataProvider = new SubresourceDataProvider($managerRegistryProphecy->reveal(), $propertyNameCollectionFactory, $propertyMetadataFactory); + $dataProvider = new SubresourceDataProvider($managerRegistryProphecy->reveal()); - $context = ['property' => 'ownedDummy', 'identifiers' => [['id', Dummy::class]], 'collection' => false, IdentifierConverterInterface::HAS_IDENTIFIER_CONVERTER => true]; + $context = ['property' => 'ownedDummy', 'identifiers' => [['id', Dummy::class]], 'collection' => false]; $this->assertEquals([], $dataProvider->getSubresource(RelatedOwningDummy::class, ['id' => ['id' => 1]], $context)); } @@ -373,16 +336,14 @@ public function testQueryResultExtension() $managerRegistryProphecy->getManagerForClass(RelatedDummy::class)->shouldBeCalled()->willReturn($managerProphecy->reveal()); $managerRegistryProphecy->getManagerForClass(Dummy::class)->shouldBeCalled()->willReturn($managerProphecy->reveal()); - [$propertyNameCollectionFactory, $propertyMetadataFactory] = $this->getMetadataProphecies([Dummy::class => $identifiers]); - $extensionProphecy = $this->prophesize(QueryResultCollectionExtensionInterface::class); $extensionProphecy->applyToCollection($queryBuilder, Argument::type(QueryNameGeneratorInterface::class), RelatedDummy::class, null, Argument::type('array'))->shouldBeCalled(); $extensionProphecy->supportsResult(RelatedDummy::class, null, Argument::type('array'))->willReturn(true)->shouldBeCalled(); $extensionProphecy->getResult($queryBuilder, RelatedDummy::class, null, Argument::type('array'))->willReturn([])->shouldBeCalled(); - $dataProvider = new SubresourceDataProvider($managerRegistryProphecy->reveal(), $propertyNameCollectionFactory, $propertyMetadataFactory, [$extensionProphecy->reveal()]); + $dataProvider = new SubresourceDataProvider($managerRegistryProphecy->reveal(), [$extensionProphecy->reveal()]); - $context = ['property' => 'relatedDummies', 'identifiers' => [['id', Dummy::class]], 'collection' => true, IdentifierConverterInterface::HAS_IDENTIFIER_CONVERTER => true]; + $context = ['property' => 'relatedDummies', 'identifiers' => [['id', Dummy::class]], 'collection' => true]; $this->assertEquals([], $dataProvider->getSubresource(RelatedDummy::class, ['id' => ['id' => 1]], $context)); } @@ -401,9 +362,7 @@ public function testCannotCreateQueryBuilder() $managerRegistryProphecy = $this->prophesize(ManagerRegistry::class); $managerRegistryProphecy->getManagerForClass(Dummy::class)->willReturn($managerProphecy->reveal())->shouldBeCalled(); - [$propertyNameCollectionFactory, $propertyMetadataFactory] = $this->getMetadataProphecies([Dummy::class => $identifiers]); - - $dataProvider = new SubresourceDataProvider($managerRegistryProphecy->reveal(), $propertyNameCollectionFactory, $propertyMetadataFactory); + $dataProvider = new SubresourceDataProvider($managerRegistryProphecy->reveal()); $dataProvider->getSubresource(Dummy::class, ['id' => 1], []); } @@ -415,111 +374,10 @@ public function testThrowResourceClassNotSupportedException() $managerRegistryProphecy = $this->prophesize(ManagerRegistry::class); $managerRegistryProphecy->getManagerForClass(Dummy::class)->willReturn(null)->shouldBeCalled(); - [$propertyNameCollectionFactory, $propertyMetadataFactory] = $this->getMetadataProphecies([Dummy::class => $identifiers]); - - $dataProvider = new SubresourceDataProvider($managerRegistryProphecy->reveal(), $propertyNameCollectionFactory, $propertyMetadataFactory); + $dataProvider = new SubresourceDataProvider($managerRegistryProphecy->reveal()); $dataProvider->getSubresource(Dummy::class, ['id' => 1], []); } - /** - * @group legacy - */ - public function testGetSubSubresourceItemLegacy() - { - $managerRegistryProphecy = $this->prophesize(ManagerRegistry::class); - $identifiers = ['id']; - $funcProphecy = $this->prophesize(Func::class); - $func = $funcProphecy->reveal(); - - // First manager (Dummy) - $dummyDQL = 'SELECT relatedDummies_a3 FROM ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Dummy id_a2 INNER JOIN id_a2.relatedDummies relatedDummies_a3 WHERE id_a2.id = :id_p2'; - - $qb = $this->prophesize(QueryBuilder::class); - $qb->select('relatedDummies_a3')->shouldBeCalled()->willReturn($qb); - $qb->from(Dummy::class, 'id_a2')->shouldBeCalled()->willReturn($qb); - $qb->innerJoin('id_a2.relatedDummies', 'relatedDummies_a3')->shouldBeCalled()->willReturn($qb); - $qb->andWhere('id_a2.id = :id_p2')->shouldBeCalled()->willReturn($qb); - - $dummyFunc = new Func('in', ['any']); - - $dummyExpProphecy = $this->prophesize(Expr::class); - $dummyExpProphecy->in('relatedDummies_a1', $dummyDQL)->willReturn($dummyFunc)->shouldBeCalled(); - - $qb->expr()->shouldBeCalled()->willReturn($dummyExpProphecy->reveal()); - - $qb->getDQL()->shouldBeCalled()->willReturn($dummyDQL); - - $classMetadataProphecy = $this->prophesize(ClassMetadata::class); - $classMetadataProphecy->getIdentifier()->shouldBeCalled()->willReturn($identifiers); - $classMetadataProphecy->hasAssociation('relatedDummies')->willReturn(true)->shouldBeCalled(); - $classMetadataProphecy->getAssociationMapping('relatedDummies')->shouldBeCalled()->willReturn(['type' => ClassMetadata::MANY_TO_MANY]); - $classMetadataProphecy->getTypeOfField('id')->shouldBeCalled()->willReturn(DBALType::INTEGER); - - $dummyManagerProphecy = $this->prophesize(EntityManager::class); - $dummyManagerProphecy->createQueryBuilder()->shouldBeCalled()->willReturn($qb->reveal()); - $dummyManagerProphecy->getClassMetadata(Dummy::class)->shouldBeCalled()->willReturn($classMetadataProphecy->reveal()); - $this->assertIdentifierManagerMethodCalls($dummyManagerProphecy); - - $managerRegistryProphecy->getManagerForClass(Dummy::class)->shouldBeCalled()->willReturn($dummyManagerProphecy->reveal()); - - // Second manager (RelatedDummy) - $relatedDQL = 'SELECT IDENTITY(relatedDummies_a1.thirdLevel) FROM ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\RelatedDummy relatedDummies_a1 WHERE relatedDummies_a1.id = :id_p1 AND relatedDummies_a1 IN(SELECT relatedDummies_a3 FROM ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Dummy id_a2 INNER JOIN id_a2.relatedDummies relatedDummies_a3 WHERE id_a2.id = :id_p2)'; - - $rqb = $this->prophesize(QueryBuilder::class); - $rqb->select('IDENTITY(relatedDummies_a1.thirdLevel)')->shouldBeCalled()->willReturn($rqb); - $rqb->from(RelatedDummy::class, 'relatedDummies_a1')->shouldBeCalled()->willReturn($rqb); - $rqb->andWhere('relatedDummies_a1.id = :id_p1')->shouldBeCalled()->willReturn($rqb); - $rqb->andWhere($dummyFunc)->shouldBeCalled()->willReturn($rqb); - $rqb->getDQL()->shouldBeCalled()->willReturn($relatedDQL); - - $relatedExpProphecy = $this->prophesize(Expr::class); - $relatedExpProphecy->in('o', $relatedDQL)->willReturn($func)->shouldBeCalled(); - - $rqb->expr()->shouldBeCalled()->willReturn($relatedExpProphecy->reveal()); - - $rClassMetadataProphecy = $this->prophesize(ClassMetadata::class); - $rClassMetadataProphecy->getIdentifier()->shouldBeCalled()->willReturn($identifiers); - $rClassMetadataProphecy->getTypeOfField('id')->shouldBeCalled()->willReturn(DBALType::INTEGER); - $rClassMetadataProphecy->hasAssociation('thirdLevel')->shouldBeCalled()->willReturn(true); - $rClassMetadataProphecy->getAssociationMapping('thirdLevel')->shouldBeCalled()->willReturn(['type' => ClassMetadata::MANY_TO_ONE]); - - $rDummyManagerProphecy = $this->prophesize(EntityManager::class); - $rDummyManagerProphecy->createQueryBuilder()->shouldBeCalled()->willReturn($rqb->reveal()); - $rDummyManagerProphecy->getClassMetadata(RelatedDummy::class)->shouldBeCalled()->willReturn($rClassMetadataProphecy->reveal()); - $this->assertIdentifierManagerMethodCalls($rDummyManagerProphecy); - - $managerRegistryProphecy->getManagerForClass(RelatedDummy::class)->shouldBeCalled()->willReturn($rDummyManagerProphecy->reveal()); - - $result = new \stdClass(); - // Origin manager (ThirdLevel) - $queryProphecy = $this->prophesize(AbstractQuery::class); - $queryProphecy->getOneOrNullResult()->shouldBeCalled()->willReturn($result); - - $queryBuilder = $this->prophesize(QueryBuilder::class); - - $queryBuilder->andWhere($func)->shouldBeCalled()->willReturn($queryBuilder); - - $queryBuilder->getQuery()->shouldBeCalled()->willReturn($queryProphecy->reveal()); - $queryBuilder->setParameter('id_p1', 1, DBALType::INTEGER)->shouldBeCalled()->willReturn($queryBuilder); - $queryBuilder->setParameter('id_p2', 1, DBALType::INTEGER)->shouldBeCalled()->willReturn($queryBuilder); - - $repositoryProphecy = $this->prophesize(EntityRepository::class); - $repositoryProphecy->createQueryBuilder('o')->shouldBeCalled()->willReturn($queryBuilder->reveal()); - - $managerProphecy = $this->prophesize(ObjectManager::class); - $managerProphecy->getRepository(ThirdLevel::class)->shouldBeCalled()->willReturn($repositoryProphecy->reveal()); - - $managerRegistryProphecy->getManagerForClass(ThirdLevel::class)->shouldBeCalled()->willReturn($managerProphecy->reveal()); - - [$propertyNameCollectionFactory, $propertyMetadataFactory] = $this->getMetadataProphecies([Dummy::class => $identifiers, RelatedDummy::class => $identifiers]); - - $dataProvider = new SubresourceDataProvider($managerRegistryProphecy->reveal(), $propertyNameCollectionFactory, $propertyMetadataFactory); - - $context = ['property' => 'thirdLevel', 'identifiers' => [['id', Dummy::class], ['relatedDummies', RelatedDummy::class]], 'collection' => false]; - - $this->assertEquals($result, $dataProvider->getSubresource(ThirdLevel::class, ['id' => 1, 'relatedDummies' => 1], $context)); - } - public function testGetSubresourceCollectionItem() { $managerRegistryProphecy = $this->prophesize(ManagerRegistry::class); @@ -601,11 +459,9 @@ public function testGetSubresourceCollectionItem() $rDummyManagerProphecy->getRepository(RelatedDummy::class)->shouldBeCalled()->willReturn($repositoryProphecy->reveal()); - [$propertyNameCollectionFactory, $propertyMetadataFactory] = $this->getMetadataProphecies([Dummy::class => $identifiers, RelatedDummy::class => $identifiers]); - - $dataProvider = new SubresourceDataProvider($managerRegistryProphecy->reveal(), $propertyNameCollectionFactory, $propertyMetadataFactory); + $dataProvider = new SubresourceDataProvider($managerRegistryProphecy->reveal()); - $context = ['property' => 'id', 'identifiers' => [['id', Dummy::class, true], ['relatedDummies', RelatedDummy::class, true]], 'collection' => false, IdentifierConverterInterface::HAS_IDENTIFIER_CONVERTER => true]; + $context = ['property' => 'id', 'identifiers' => [['id', Dummy::class, true], ['relatedDummies', RelatedDummy::class, true]], 'collection' => false]; $this->assertEquals($result, $dataProvider->getSubresource(RelatedDummy::class, ['id' => ['id' => 1], 'relatedDummies' => ['id' => 2]], $context)); } diff --git a/tests/Bridge/Symfony/Bundle/DataCollector/RequestDataCollectorTest.php b/tests/Bridge/Symfony/Bundle/DataCollector/RequestDataCollectorTest.php index 52e482ff848..90e31dffbb5 100644 --- a/tests/Bridge/Symfony/Bundle/DataCollector/RequestDataCollectorTest.php +++ b/tests/Bridge/Symfony/Bundle/DataCollector/RequestDataCollectorTest.php @@ -142,6 +142,8 @@ public function testWithResource() $this->assertSame([ 'resource_class' => DummyEntity::class, + 'identified_by' => null, + 'has_composite_identifier' => false, 'item_operation_name' => 'get', 'receive' => true, 'respond' => true, @@ -275,13 +277,13 @@ private function getUsedItemDataProvider(): TraceableChainItemDataProvider { $itemDataProvider = new TraceableChainItemDataProvider(new ChainItemDataProvider([ new class() implements ItemDataProviderInterface { - public function getItem(string $resourceClass, $id, string $operationName = null, array $context = []) + public function getItem(string $resourceClass, array $identifiers, string $operationName = null, array $context = []) { return null; } }, ])); - $itemDataProvider->getItem('', '', null, ['item_context']); + $itemDataProvider->getItem('', [], null, ['item_context']); return $itemDataProvider; } diff --git a/tests/Bridge/Symfony/Bundle/DataProvider/TraceableChainItemDataCollectorTest.php b/tests/Bridge/Symfony/Bundle/DataProvider/TraceableChainItemDataCollectorTest.php index 8c610c497fd..f57249b1041 100644 --- a/tests/Bridge/Symfony/Bundle/DataProvider/TraceableChainItemDataCollectorTest.php +++ b/tests/Bridge/Symfony/Bundle/DataProvider/TraceableChainItemDataCollectorTest.php @@ -15,7 +15,6 @@ use ApiPlatform\Core\Bridge\Symfony\Bundle\DataProvider\TraceableChainItemDataProvider; use ApiPlatform\Core\DataProvider\ChainItemDataProvider; -use ApiPlatform\Core\DataProvider\DenormalizedIdentifiersAwareItemDataProviderInterface; use ApiPlatform\Core\DataProvider\ItemDataProviderInterface; use ApiPlatform\Core\DataProvider\RestrictedDataProviderInterface; use ApiPlatform\Core\Exception\ResourceClassNotSupportedException; @@ -30,7 +29,7 @@ class TraceableChainItemDataCollectorTest extends TestCase public function testGetItem($provider, $context, $expected) { $dataProvider = new TraceableChainItemDataProvider($provider); - $dataProvider->getItem('', '', null, $context); + $dataProvider->getItem('', [], null, $context); $result = $dataProvider->getProvidersResponse(); $this->assertCount(\count($expected), $result); @@ -48,7 +47,7 @@ public function testGetItem($provider, $context, $expected) public function testDeprecatedGetItem($provider, $context, $expected) { $dataProvider = new TraceableChainItemDataProvider($provider); - $dataProvider->getItem('', '', null, $context); + $dataProvider->getItem('', [], null, $context); $result = $dataProvider->getProvidersResponse(); $this->assertCount(\count($expected), $result); @@ -63,7 +62,7 @@ public function dataProviderProvider(): iterable { yield 'Not a ChainItemDataProvider' => [ new class() implements ItemDataProviderInterface { - public function getItem(string $resourceClass, $id, string $operationName = null, array $context = []) + public function getItem(string $resourceClass, array $identifiers, string $operationName = null, array $context = []) { return null; } @@ -86,13 +85,13 @@ public function supports(string $resourceClass, string $operationName = null, ar return false; } - public function getItem(string $resourceClass, $id, string $operationName = null, array $context = []) + public function getItem(string $resourceClass, array $identifiers, string $operationName = null, array $context = []) { return null; } }, - new class() implements RestrictedDataProviderInterface, DenormalizedIdentifiersAwareItemDataProviderInterface { - public function getItem(string $resourceClass, /* array */ $id, string $operationName = null, array $context = []) + new class() implements RestrictedDataProviderInterface { + public function getItem(string $resourceClass, array $identifiers, string $operationName = null, array $context = []) { return null; } @@ -103,7 +102,7 @@ public function supports(string $resourceClass, string $operationName = null, ar } }, new class() implements ItemDataProviderInterface { - public function getItem(string $resourceClass, $id, string $operationName = null, array $context = []) + public function getItem(string $resourceClass, array $identifiers, string $operationName = null, array $context = []) { return null; } @@ -119,13 +118,13 @@ public function deprecatedDataProviderProvider(): iterable yield 'deprecated ChainItemDataProvider - ResourceClassNotSupportedException' => [ new ChainItemDataProvider([ new class() implements ItemDataProviderInterface { - public function getItem(string $resourceClass, $id, string $operationName = null, array $context = []) + public function getItem(string $resourceClass, array $identifiers, string $operationName = null, array $context = []) { throw new ResourceClassNotSupportedException('nope'); } }, new class() implements ItemDataProviderInterface { - public function getItem(string $resourceClass, $id, string $operationName = null, array $context = []) + public function getItem(string $resourceClass, array $identifiers, string $operationName = null, array $context = []) { return null; } diff --git a/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php b/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php index 13198e2f1ea..63eb0d60053 100644 --- a/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php +++ b/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php @@ -861,7 +861,6 @@ private function getPartialContainerBuilderProphecy($configuration = null) 'api_platform.title' => 'title', 'api_platform.version' => 'version', 'api_platform.show_webby' => true, - 'api_platform.allow_plain_identifiers' => false, 'api_platform.eager_loading.enabled' => Argument::type('bool'), 'api_platform.eager_loading.max_joins' => 30, 'api_platform.eager_loading.force_eager' => true, @@ -931,7 +930,7 @@ private function getPartialContainerBuilderProphecy($configuration = null) 'api_platform.identifiers_extractor', 'api_platform.identifiers_extractor.cached', 'api_platform.iri_converter', - 'api_platform.identifier.converter', + 'api_platform.identifier.denormalizer', 'api_platform.identifier.date_normalizer', 'api_platform.identifier.integer', 'api_platform.identifier.uuid_normalizer', diff --git a/tests/Bridge/Symfony/Bundle/DependencyInjection/ConfigurationTest.php b/tests/Bridge/Symfony/Bundle/DependencyInjection/ConfigurationTest.php index 64ea917210b..9d41b067c8d 100644 --- a/tests/Bridge/Symfony/Bundle/DependencyInjection/ConfigurationTest.php +++ b/tests/Bridge/Symfony/Bundle/DependencyInjection/ConfigurationTest.php @@ -204,7 +204,6 @@ private function runDefaultConfigTests(array $doctrineIntegrationsToLoad = ['orm 'enabled' => true, 'hub_url' => null, ], - 'allow_plain_identifiers' => false, 'resource_class_directories' => [], ], $config); } diff --git a/tests/Bridge/Symfony/Routing/ApiLoaderTest.php b/tests/Bridge/Symfony/Routing/ApiLoaderTest.php index 629b4bd2995..1570e7e27fe 100644 --- a/tests/Bridge/Symfony/Routing/ApiLoaderTest.php +++ b/tests/Bridge/Symfony/Routing/ApiLoaderTest.php @@ -13,6 +13,7 @@ namespace ApiPlatform\Core\Tests\Bridge\Symfony\Routing; +use ApiPlatform\Core\Api\IdentifiersExtractorInterface; use ApiPlatform\Core\Bridge\Symfony\Routing\ApiLoader; use ApiPlatform\Core\Exception\InvalidResourceException; use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface; @@ -50,6 +51,7 @@ public function testApiLoader() { $resourceMetadata = new ResourceMetadata(); $resourceMetadata = $resourceMetadata->withShortName('dummy'); + $resourceMetadata = $resourceMetadata->withAttributes(['identified_by' => 'id']); //default operation based on OperationResourceMetadataFactory $resourceMetadata = $resourceMetadata->withItemOperations([ 'get' => ['method' => 'GET', 'requirements' => ['id' => '\d+'], 'defaults' => ['my_default' => 'default_value', '_controller' => 'should_not_be_overriden'], 'stateless' => null], @@ -105,7 +107,7 @@ public function testApiLoader() ); $this->assertEquals( - $this->getSubresourceRoute('/dummies/{id}/subresources.{_format}', 'api_platform.action.get_subresource', RelatedDummyEntity::class, 'api_dummies_subresources_get_subresource', ['property' => 'subresource', 'identifiers' => [['id', DummyEntity::class, true]], 'collection' => true, 'operationId' => 'api_dummies_subresources_get_subresource'], [], ['_stateless' => true]), + $this->getSubresourceRoute('/dummies/{id}/subresources.{_format}', 'api_platform.action.get_subresource', RelatedDummyEntity::class, 'api_dummies_subresources_get_subresource', ['property' => 'subresource', 'identifiers' => [['id', DummyEntity::class, true, ['id']]], 'collection' => true, 'operationId' => 'api_dummies_subresources_get_subresource'], [], ['_stateless' => true]), $routeCollection->get('api_dummies_subresources_get_subresource') ); } @@ -119,7 +121,7 @@ public function testApiLoaderWithPrefix() 'put' => ['method' => 'PUT', 'stateless' => null], 'delete' => ['method' => 'DELETE', 'stateless' => null], ]); - $resourceMetadata = $resourceMetadata->withAttributes(['route_prefix' => '/foobar-prefix']); + $resourceMetadata = $resourceMetadata->withAttributes(['route_prefix' => '/foobar-prefix', 'identified_by' => 'id']); $routeCollection = $this->getApiLoaderWithResourceMetadata($resourceMetadata)->load(null); @@ -200,32 +202,32 @@ public function testRecursiveSubresource() $routeCollection = $this->getApiLoaderWithResourceMetadata($resourceMetadata, true)->load(null); $this->assertEquals( - $this->getSubresourceRoute('/dummies/{id}/subresources.{_format}', 'api_platform.action.get_subresource', RelatedDummyEntity::class, 'api_dummies_subresources_get_subresource', ['property' => 'subresource', 'identifiers' => [['id', DummyEntity::class, true]], 'collection' => true, 'operationId' => 'api_dummies_subresources_get_subresource']), + $this->getSubresourceRoute('/dummies/{id}/subresources.{_format}', 'api_platform.action.get_subresource', RelatedDummyEntity::class, 'api_dummies_subresources_get_subresource', ['property' => 'subresource', 'identifiers' => [['id', DummyEntity::class, true, ['id']]], 'collection' => true, 'operationId' => 'api_dummies_subresources_get_subresource']), $routeCollection->get('api_dummies_subresources_get_subresource') ); $this->assertEquals( - $this->getSubresourceRoute('/related_dummies/{id}/recursivesubresource/subresources.{_format}', 'api_platform.action.get_subresource', RelatedDummyEntity::class, 'api_related_dummies_recursivesubresource_subresources_get_subresource', ['property' => 'subresource', 'identifiers' => [['id', RelatedDummyEntity::class, true], ['recursivesubresource', DummyEntity::class, false]], 'collection' => true, 'operationId' => 'api_related_dummies_recursivesubresource_subresources_get_subresource']), + $this->getSubresourceRoute('/related_dummies/{id}/recursivesubresource/subresources.{_format}', 'api_platform.action.get_subresource', RelatedDummyEntity::class, 'api_related_dummies_recursivesubresource_subresources_get_subresource', ['property' => 'subresource', 'identifiers' => [['id', RelatedDummyEntity::class, true, ['id']], ['recursivesubresource', DummyEntity::class, false, ['id']]], 'collection' => true, 'operationId' => 'api_related_dummies_recursivesubresource_subresources_get_subresource']), $routeCollection->get('api_related_dummies_recursivesubresource_subresources_get_subresource') ); $this->assertEquals( - $this->getSubresourceRoute('/related_dummies/{id}/recursivesubresource.{_format}', 'dummy_controller', DummyEntity::class, 'api_related_dummies_recursivesubresource_get_subresource', ['property' => 'recursivesubresource', 'identifiers' => [['id', RelatedDummyEntity::class, true]], 'collection' => false, 'operationId' => 'api_related_dummies_recursivesubresource_get_subresource']), + $this->getSubresourceRoute('/related_dummies/{id}/recursivesubresource.{_format}', 'dummy_controller', DummyEntity::class, 'api_related_dummies_recursivesubresource_get_subresource', ['property' => 'recursivesubresource', 'identifiers' => [['id', RelatedDummyEntity::class, true, ['id']]], 'collection' => false, 'operationId' => 'api_related_dummies_recursivesubresource_get_subresource']), $routeCollection->get('api_related_dummies_recursivesubresource_get_subresource') ); $this->assertEquals( - $this->getSubresourceRoute('/dummies/{id}/subresources/{subresource}/recursivesubresource.{_format}', 'api_platform.action.get_subresource', DummyEntity::class, 'api_dummies_subresources_recursivesubresource_get_subresource', ['property' => 'recursivesubresource', 'identifiers' => [['id', DummyEntity::class, true], ['subresource', RelatedDummyEntity::class, true]], 'collection' => false, 'operationId' => 'api_dummies_subresources_recursivesubresource_get_subresource']), + $this->getSubresourceRoute('/dummies/{id}/subresources/{subresource}/recursivesubresource.{_format}', 'api_platform.action.get_subresource', DummyEntity::class, 'api_dummies_subresources_recursivesubresource_get_subresource', ['property' => 'recursivesubresource', 'identifiers' => [['id', DummyEntity::class, true, ['id']], ['subresource', RelatedDummyEntity::class, true, ['id']]], 'collection' => false, 'operationId' => 'api_dummies_subresources_recursivesubresource_get_subresource']), $routeCollection->get('api_dummies_subresources_recursivesubresource_get_subresource') ); $this->assertEquals( - $this->getSubresourceRoute('/related_dummies/{id}/secondrecursivesubresource/subresources.{_format}', 'api_platform.action.get_subresource', RelatedDummyEntity::class, 'api_related_dummies_secondrecursivesubresource_subresources_get_subresource', ['property' => 'subresource', 'identifiers' => [['id', RelatedDummyEntity::class, true], ['secondrecursivesubresource', DummyEntity::class, false]], 'collection' => true, 'operationId' => 'api_related_dummies_secondrecursivesubresource_subresources_get_subresource']), + $this->getSubresourceRoute('/related_dummies/{id}/secondrecursivesubresource/subresources.{_format}', 'api_platform.action.get_subresource', RelatedDummyEntity::class, 'api_related_dummies_secondrecursivesubresource_subresources_get_subresource', ['property' => 'subresource', 'identifiers' => [['id', RelatedDummyEntity::class, true, ['id']], ['secondrecursivesubresource', DummyEntity::class, false, ['id']]], 'collection' => true, 'operationId' => 'api_related_dummies_secondrecursivesubresource_subresources_get_subresource']), $routeCollection->get('api_related_dummies_secondrecursivesubresource_subresources_get_subresource') ); $this->assertEquals( - $this->getSubresourceRoute('/related_dummies/{id}/secondrecursivesubresource.{_format}', 'api_platform.action.get_subresource', DummyEntity::class, 'api_related_dummies_secondrecursivesubresource_get_subresource', ['property' => 'secondrecursivesubresource', 'identifiers' => [['id', RelatedDummyEntity::class, true]], 'collection' => false, 'operationId' => 'api_related_dummies_secondrecursivesubresource_get_subresource']), + $this->getSubresourceRoute('/related_dummies/{id}/secondrecursivesubresource.{_format}', 'api_platform.action.get_subresource', DummyEntity::class, 'api_related_dummies_secondrecursivesubresource_get_subresource', ['property' => 'secondrecursivesubresource', 'identifiers' => [['id', RelatedDummyEntity::class, true, ['id']]], 'collection' => false, 'operationId' => 'api_related_dummies_secondrecursivesubresource_get_subresource']), $routeCollection->get('api_related_dummies_secondrecursivesubresource_get_subresource') ); } @@ -301,9 +303,13 @@ private function getApiLoaderWithResourceMetadata(ResourceMetadata $resourceMeta $resourceMetadataFactory = $resourceMetadataFactoryProphecy->reveal(); - $subresourceOperationFactory = new SubresourceOperationFactory($resourceMetadataFactory, $propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), new UnderscorePathSegmentNameGenerator()); + $identifiersExtractorProphecy = $this->prophesize(IdentifiersExtractorInterface::class); + $identifiersExtractorProphecy->getIdentifiersFromResourceClass(Argument::type('string'))->willReturn(['id']); + $identifiersExtractor = $identifiersExtractorProphecy->reveal(); - return new ApiLoader($kernelProphecy->reveal(), $resourceNameCollectionFactoryProphecy->reveal(), $resourceMetadataFactory, $operationPathResolver, $containerProphecy->reveal(), ['jsonld' => ['application/ld+json']], [], $subresourceOperationFactory, false, true, true, false, false); + $subresourceOperationFactory = new SubresourceOperationFactory($resourceMetadataFactory, $propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), new UnderscorePathSegmentNameGenerator(), $identifiersExtractor); + + return new ApiLoader($kernelProphecy->reveal(), $resourceNameCollectionFactoryProphecy->reveal(), $resourceMetadataFactory, $operationPathResolver, $containerProphecy->reveal(), ['jsonld' => ['application/ld+json']], [], $subresourceOperationFactory, false, true, true, false, false, $identifiersExtractor); } private function getRoute(string $path, string $controller, string $resourceClass, string $operationName, array $methods, bool $collection = false, array $requirements = [], array $extraDefaults = ['_stateless' => null], array $options = [], string $host = '', array $schemes = [], string $condition = ''): Route @@ -314,6 +320,8 @@ private function getRoute(string $path, string $controller, string $resourceClas '_controller' => $controller, '_format' => null, '_api_resource_class' => $resourceClass, + '_api_identified_by' => ['id'], + '_api_has_composite_identifier' => false, sprintf('_api_%s_operation_name', $collection ? 'collection' : 'item') => $operationName, ] + $extraDefaults, $requirements, @@ -335,6 +343,8 @@ private function getSubresourceRoute(string $path, string $controller, string $r '_api_resource_class' => $resourceClass, '_api_subresource_operation_name' => $operationName, '_api_subresource_context' => $context, + '_api_identified_by' => ['id'], + '_api_has_composite_identifier' => false, ] + $extraDefaults, $requirements, [], diff --git a/tests/Bridge/Symfony/Routing/IriConverterTest.php b/tests/Bridge/Symfony/Routing/IriConverterTest.php index dbf51036f9b..96cf660e91f 100644 --- a/tests/Bridge/Symfony/Routing/IriConverterTest.php +++ b/tests/Bridge/Symfony/Routing/IriConverterTest.php @@ -24,7 +24,7 @@ use ApiPlatform\Core\Exception\InvalidArgumentException; use ApiPlatform\Core\Exception\InvalidIdentifierException; use ApiPlatform\Core\Exception\ItemNotFoundException; -use ApiPlatform\Core\Identifier\IdentifierConverterInterface; +use ApiPlatform\Core\Identifier\IdentifierDenormalizerInterface; use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface; use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface; @@ -76,6 +76,7 @@ public function testGetItemFromIriCollectionRouteException() $routerProphecy->match('/users')->willReturn([ '_api_resource_class' => Dummy::class, '_api_collection_operation_name' => 'get', + '_api_identified_by' => ['id'], ])->shouldBeCalledTimes(1); $converter = $this->getIriConverter($routerProphecy); @@ -89,13 +90,14 @@ public function testGetItemFromIriItemNotFoundException() $itemDataProviderProphecy = $this->prophesize(ItemDataProviderInterface::class); $itemDataProviderProphecy - ->getItem(Dummy::class, 3, 'get', []) + ->getItem(Dummy::class, ['id' => 3], 'get', []) ->shouldBeCalled()->willReturn(null); $routerProphecy = $this->prophesize(RouterInterface::class); $routerProphecy->match('/users/3')->willReturn([ '_api_resource_class' => Dummy::class, '_api_item_operation_name' => 'get', + '_api_identified_by' => ['id'], 'id' => 3, ])->shouldBeCalledTimes(1); @@ -107,12 +109,13 @@ public function testGetItemFromIri() { $item = new \stdClass(); $itemDataProviderProphecy = $this->prophesize(ItemDataProviderInterface::class); - $itemDataProviderProphecy->getItem(Dummy::class, 3, 'get', ['fetch_data' => true])->shouldBeCalled()->willReturn($item); + $itemDataProviderProphecy->getItem(Dummy::class, ['id' => 3], 'get', ['fetch_data' => true])->shouldBeCalled()->willReturn($item); $routerProphecy = $this->prophesize(RouterInterface::class); $routerProphecy->match('/users/3')->willReturn([ '_api_resource_class' => Dummy::class, '_api_item_operation_name' => 'get', + '_api_identified_by' => ['id'], 'id' => 3, ])->shouldBeCalledTimes(1); @@ -123,7 +126,7 @@ public function testGetItemFromIri() public function testGetItemFromIriWithOperationName() { $itemDataProviderProphecy = $this->prophesize(ItemDataProviderInterface::class); - $itemDataProviderProphecy->getItem('AppBundle\Entity\User', '3', 'operation_name', ['fetch_data' => true]) + $itemDataProviderProphecy->getItem('AppBundle\Entity\User', ['id' => 3], 'operation_name', ['fetch_data' => true]) ->willReturn('foo') ->shouldBeCalledTimes(1); @@ -131,6 +134,7 @@ public function testGetItemFromIriWithOperationName() $routerProphecy->match('/users/3')->willReturn([ '_api_resource_class' => 'AppBundle\Entity\User', '_api_item_operation_name' => 'operation_name', + '_api_identified_by' => ['id'], 'id' => 3, ])->shouldBeCalledTimes(1); @@ -215,7 +219,10 @@ public function testGetItemIriFromResourceClass() $routerProphecy = $this->prophesize(RouterInterface::class); $routerProphecy->generate('api_dummies_get_item', ['id' => 1], UrlGeneratorInterface::ABS_PATH)->willReturn('/dummies/1'); - $converter = $this->getIriConverter($routerProphecy, $routeNameResolverProphecy); + $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class); + $resourceMetadataFactoryProphecy->create(Dummy::class)->willReturn((new ResourceMetadata())->withAttributes(['composite_identifier' => true])); + + $converter = $this->getIriConverter($routerProphecy, $routeNameResolverProphecy, null, null, null, $resourceMetadataFactoryProphecy->reveal()); $this->assertEquals($converter->getItemIriFromResourceClass(Dummy::class, ['id' => 1]), '/dummies/1'); } @@ -245,7 +252,10 @@ public function testNotAbleToGenerateGetItemIriFromResourceClass() $routerProphecy = $this->prophesize(RouterInterface::class); $routerProphecy->generate('dummies', ['id' => 1], UrlGeneratorInterface::ABS_PATH)->willThrow(new RouteNotFoundException()); - $converter = $this->getIriConverter($routerProphecy, $routeNameResolverProphecy); + $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class); + $resourceMetadataFactoryProphecy->create(Dummy::class)->willReturn((new ResourceMetadata())->withAttributes(['composite_identifier' => true])); + + $converter = $this->getIriConverter($routerProphecy, $routeNameResolverProphecy, null, null, null, $resourceMetadataFactoryProphecy->reveal()); $converter->getItemIriFromResourceClass(Dummy::class, ['id' => 1]); } @@ -253,24 +263,25 @@ public function testGetItemFromIriWithIdentifierConverter() { $item = new \stdClass(); $itemDataProviderProphecy = $this->prophesize(ItemDataProviderInterface::class); - $itemDataProviderProphecy->getItem(Dummy::class, ['id' => 3], 'get', ['fetch_data' => true, IdentifierConverterInterface::HAS_IDENTIFIER_CONVERTER => true])->shouldBeCalled()->willReturn($item); - $identifierConverterProphecy = $this->prophesize(IdentifierConverterInterface::class); - $identifierConverterProphecy->convert('3', Dummy::class)->shouldBeCalled()->willReturn(['id' => 3]); + $itemDataProviderProphecy->getItem(Dummy::class, ['id' => 3], 'get', ['fetch_data' => true])->shouldBeCalled()->willReturn($item); + $identifierDenormalizerProphecy = $this->prophesize(IdentifierDenormalizerInterface::class); + $identifierDenormalizerProphecy->denormalize(['id' => '3'], Dummy::class)->shouldBeCalled()->willReturn(['id' => 3]); $routerProphecy = $this->prophesize(RouterInterface::class); $routerProphecy->match('/users/3')->willReturn([ '_api_resource_class' => Dummy::class, '_api_item_operation_name' => 'get', + '_api_identified_by' => ['id'], 'id' => 3, ])->shouldBeCalledTimes(1); - $converter = $this->getIriConverter($routerProphecy, null, $itemDataProviderProphecy, null, $identifierConverterProphecy); + $converter = $this->getIriConverter($routerProphecy, null, $itemDataProviderProphecy, null, $identifierDenormalizerProphecy); $this->assertEquals($converter->getItemFromIri('/users/3', ['fetch_data' => true]), $item); } public function testGetItemFromIriWithSubresourceDataProvider() { $item = new \stdClass(); - $subresourceContext = ['identifiers' => [['id', Dummy::class, true]]]; + $subresourceContext = ['identifiers' => [['id', Dummy::class, true, ['id']]]]; $routeNameResolverProphecy = $this->prophesize(RouteNameResolverInterface::class); $routerProphecy = $this->prophesize(RouterInterface::class); $routerProphecy->match('/users/3/adresses')->willReturn([ @@ -280,7 +291,7 @@ public function testGetItemFromIriWithSubresourceDataProvider() 'id' => 3, ])->shouldBeCalledTimes(1); $subresourceDataProviderProphecy = $this->prophesize(SubresourceDataProviderInterface::class); - $subresourceDataProviderProphecy->getSubresource(Dummy::class, ['id' => 3], $subresourceContext + ['fetch_data' => true], 'get_subresource')->shouldBeCalled()->willReturn($item); + $subresourceDataProviderProphecy->getSubresource(Dummy::class, ['id' => ['id' => 3]], $subresourceContext + ['fetch_data' => true], 'get_subresource')->shouldBeCalled()->willReturn($item); $converter = $this->getIriConverter($routerProphecy, $routeNameResolverProphecy, null, $subresourceDataProviderProphecy); $this->assertEquals($converter->getItemFromIri('/users/3/adresses', ['fetch_data' => true]), $item); } @@ -290,7 +301,7 @@ public function testGetItemFromIriWithSubresourceDataProviderNotFound() $this->expectException(ItemNotFoundException::class); $this->expectExceptionMessage('Item not found for "/users/3/adresses".'); - $subresourceContext = ['identifiers' => [['id', Dummy::class, true]]]; + $subresourceContext = ['identifiers' => [['id', Dummy::class, true, ['id']]]]; $routeNameResolverProphecy = $this->prophesize(RouteNameResolverInterface::class); $routerProphecy = $this->prophesize(RouterInterface::class); $routerProphecy->match('/users/3/adresses')->willReturn([ @@ -299,11 +310,11 @@ public function testGetItemFromIriWithSubresourceDataProviderNotFound() '_api_subresource_operation_name' => 'get_subresource', 'id' => 3, ])->shouldBeCalledTimes(1); - $identifierConverterProphecy = $this->prophesize(IdentifierConverterInterface::class); - $identifierConverterProphecy->convert('3', Dummy::class)->shouldBeCalled()->willReturn(['id' => 3]); + $identifierDenormalizerProphecy = $this->prophesize(IdentifierDenormalizerInterface::class); + $identifierDenormalizerProphecy->denormalize(['id' => '3'], Dummy::class)->shouldBeCalled()->willReturn(['id' => 3]); $subresourceDataProviderProphecy = $this->prophesize(SubresourceDataProviderInterface::class); - $subresourceDataProviderProphecy->getSubresource(Dummy::class, ['id' => ['id' => 3]], $subresourceContext + ['fetch_data' => true, IdentifierConverterInterface::HAS_IDENTIFIER_CONVERTER => true], 'get_subresource')->shouldBeCalled()->willReturn(null); - $converter = $this->getIriConverter($routerProphecy, $routeNameResolverProphecy, null, $subresourceDataProviderProphecy, $identifierConverterProphecy); + $subresourceDataProviderProphecy->getSubresource(Dummy::class, ['id' => ['id' => 3]], $subresourceContext + ['fetch_data' => true], 'get_subresource')->shouldBeCalled()->willReturn(null); + $converter = $this->getIriConverter($routerProphecy, $routeNameResolverProphecy, null, $subresourceDataProviderProphecy, $identifierDenormalizerProphecy); $converter->getItemFromIri('/users/3/adresses', ['fetch_data' => true]); } @@ -318,52 +329,15 @@ public function testGetItemFromIriBadIdentifierException() $routerProphecy->match('/users/3')->willReturn([ '_api_resource_class' => Dummy::class, '_api_item_operation_name' => 'get_subresource', + '_api_identified_by' => ['id'], 'id' => 3, ])->shouldBeCalledTimes(1); - $identifierConverterProphecy = $this->prophesize(IdentifierConverterInterface::class); - $identifierConverterProphecy->convert('3', Dummy::class)->shouldBeCalled()->willThrow(new InvalidIdentifierException('Item not found for "/users/3".')); - $converter = $this->getIriConverter($routerProphecy, $routeNameResolverProphecy, null, null, $identifierConverterProphecy); + $identifierDenormalizerProphecy = $this->prophesize(IdentifierDenormalizerInterface::class); + $identifierDenormalizerProphecy->denormalize(['id' => 3], Dummy::class)->shouldBeCalled()->willThrow(new InvalidIdentifierException('Item not found for "/users/3".')); + $converter = $this->getIriConverter($routerProphecy, $routeNameResolverProphecy, null, null, $identifierDenormalizerProphecy); $this->assertEquals($converter->getItemFromIri('/users/3', ['fetch_data' => true]), $item); } - public function testNoIdentifiersException() - { - $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('No identifiers defined for resource of type "\App\Entity\Sample"'); - - $routeNameResolverProphecy = $this->prophesize(RouteNameResolverInterface::class); - $routerProphecy = $this->prophesize(RouterInterface::class); - - $converter = $this->getIriConverter($routerProphecy, $routeNameResolverProphecy); - - $method = new \ReflectionMethod(IriConverter::class, 'generateIdentifiersUrl'); - $method->setAccessible(true); - $method->invoke($converter, [], '\App\Entity\Sample'); - } - - /** - * @group legacy - * @expectedDeprecation Not injecting "ApiPlatform\Core\Api\IdentifiersExtractorInterface" is deprecated since API Platform 2.1 and will not be possible anymore in API Platform 3 - * @expectedDeprecation Not injecting ApiPlatform\Core\Api\ResourceClassResolverInterface in the IdentifiersExtractor might introduce cache issues with object identifiers. - */ - public function testLegacyConstructor() - { - $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); - $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); - $routerProphecy = $this->prophesize(RouterInterface::class); - $routeNameResolverProphecy = $this->prophesize(RouteNameResolverInterface::class); - $itemDataProviderProphecy = $this->prophesize(ItemDataProviderInterface::class); - - new IriConverter( - $propertyNameCollectionFactoryProphecy->reveal(), - $propertyMetadataFactoryProphecy->reveal(), - $itemDataProviderProphecy->reveal(), - $routeNameResolverProphecy->reveal(), - $routerProphecy->reveal(), - null - ); - } - private function getResourceClassResolver() { $resourceClassResolver = $this->prophesize(ResourceClassResolverInterface::class); @@ -374,7 +348,7 @@ private function getResourceClassResolver() return $resourceClassResolver->reveal(); } - private function getIriConverter($routerProphecy = null, $routeNameResolverProphecy = null, $itemDataProviderProphecy = null, $subresourceDataProviderProphecy = null, $identifierConverterProphecy = null, ResourceMetadataFactoryInterface $resourceMetadataFactory = null) + private function getIriConverter($routerProphecy = null, $routeNameResolverProphecy = null, $itemDataProviderProphecy = null, $subresourceDataProviderProphecy = null, $identifierDenormalizerProphecy = null, ResourceMetadataFactoryInterface $resourceMetadataFactory = null) { $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); @@ -392,16 +366,20 @@ private function getIriConverter($routerProphecy = null, $routeNameResolverProph $propertyNameCollectionFactory = $propertyNameCollectionFactoryProphecy->reveal(); $propertyMetadataFactory = $propertyMetadataFactoryProphecy->reveal(); + if (null === $identifierDenormalizerProphecy) { + $identifierDenormalizerProphecy = $this->prophesize(IdentifierDenormalizerInterface::class); + $identifierDenormalizerProphecy->denormalize(Argument::type('array'), Argument::type('string'))->will(function ($args) { + return $args[0]; + }); + } + return new IriConverter( - $propertyNameCollectionFactory, - $propertyMetadataFactory, $itemDataProvider->reveal(), $routeNameResolverProphecy->reveal(), $routerProphecy->reveal(), - null, new IdentifiersExtractor($propertyNameCollectionFactory, $propertyMetadataFactory, null, $this->getResourceClassResolver()), $subresourceDataProviderProphecy ? $subresourceDataProviderProphecy->reveal() : null, - $identifierConverterProphecy ? $identifierConverterProphecy->reveal() : null, + $identifierDenormalizerProphecy->reveal(), null, $resourceMetadataFactory ); diff --git a/tests/DataProvider/ChainItemDataProviderTest.php b/tests/DataProvider/ChainItemDataProviderTest.php index a226e5a3113..3bb53c40f60 100644 --- a/tests/DataProvider/ChainItemDataProviderTest.php +++ b/tests/DataProvider/ChainItemDataProviderTest.php @@ -14,11 +14,9 @@ namespace ApiPlatform\Core\Tests\DataProvider; use ApiPlatform\Core\DataProvider\ChainItemDataProvider; -use ApiPlatform\Core\DataProvider\DenormalizedIdentifiersAwareItemDataProviderInterface; use ApiPlatform\Core\DataProvider\ItemDataProviderInterface; use ApiPlatform\Core\DataProvider\RestrictedDataProviderInterface; use ApiPlatform\Core\Exception\ResourceClassNotSupportedException; -use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\CompositePrimitiveItem; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Dummy; use ApiPlatform\Core\Tests\ProphecyTrait; use PHPUnit\Framework\TestCase; @@ -37,34 +35,6 @@ public function testGetItem() $dummy = new Dummy(); $dummy->setName('Lucie'); - $firstDataProvider = $this->prophesize(DenormalizedIdentifiersAwareItemDataProviderInterface::class); - $firstDataProvider->willImplement(RestrictedDataProviderInterface::class); - $firstDataProvider->supports(Dummy::class, null, [])->willReturn(false); - - $secondDataProvider = $this->prophesize(DenormalizedIdentifiersAwareItemDataProviderInterface::class); - $secondDataProvider->willImplement(RestrictedDataProviderInterface::class); - $secondDataProvider->supports(Dummy::class, null, [])->willReturn(true); - $secondDataProvider->getItem(Dummy::class, ['id' => 1], null, [])->willReturn($dummy); - - $thirdDataProvider = $this->prophesize(DenormalizedIdentifiersAwareItemDataProviderInterface::class); - $thirdDataProvider->willImplement(RestrictedDataProviderInterface::class); - $thirdDataProvider->supports(Dummy::class, null, [])->willReturn(true); - $thirdDataProvider->getItem(Dummy::class, ['id' => 1], null, [])->willReturn(new \stdClass()); - - $chainItemDataProvider = new ChainItemDataProvider([ - $firstDataProvider->reveal(), - $secondDataProvider->reveal(), - $thirdDataProvider->reveal(), - ]); - - $this->assertEquals($dummy, $chainItemDataProvider->getItem(Dummy::class, ['id' => 1])); - } - - public function testGetItemWithoutDenormalizedIdentifiers() - { - $dummy = new Dummy(); - $dummy->setName('Lucie'); - $firstDataProvider = $this->prophesize(ItemDataProviderInterface::class); $firstDataProvider->willImplement(RestrictedDataProviderInterface::class); $firstDataProvider->supports(Dummy::class, null, [])->willReturn(false); @@ -72,12 +42,12 @@ public function testGetItemWithoutDenormalizedIdentifiers() $secondDataProvider = $this->prophesize(ItemDataProviderInterface::class); $secondDataProvider->willImplement(RestrictedDataProviderInterface::class); $secondDataProvider->supports(Dummy::class, null, [])->willReturn(true); - $secondDataProvider->getItem(Dummy::class, '1', null, [])->willReturn($dummy); + $secondDataProvider->getItem(Dummy::class, ['id' => 1], null, [])->willReturn($dummy); $thirdDataProvider = $this->prophesize(ItemDataProviderInterface::class); $thirdDataProvider->willImplement(RestrictedDataProviderInterface::class); $thirdDataProvider->supports(Dummy::class, null, [])->willReturn(true); - $thirdDataProvider->getItem(Dummy::class, 1, null, [])->willReturn(new \stdClass()); + $thirdDataProvider->getItem(Dummy::class, ['id' => 1], null, [])->willReturn(new \stdClass()); $chainItemDataProvider = new ChainItemDataProvider([ $firstDataProvider->reveal(), @@ -96,7 +66,7 @@ public function testGetItemExceptions() $chainItemDataProvider = new ChainItemDataProvider([$firstDataProvider->reveal()]); - $this->assertEquals('', $chainItemDataProvider->getItem('notfound', 1)); + $this->assertEquals('', $chainItemDataProvider->getItem('notfound', ['id' => 1])); } /** @@ -109,37 +79,17 @@ public function testLegacyGetItem() $dummy->setName('Lucie'); $firstDataProvider = $this->prophesize(ItemDataProviderInterface::class); - $firstDataProvider->getItem(Dummy::class, 1, null, [])->willThrow(ResourceClassNotSupportedException::class); + $firstDataProvider->getItem(Dummy::class, ['test' => 1], null, [])->willThrow(ResourceClassNotSupportedException::class); $secondDataProvider = $this->prophesize(ItemDataProviderInterface::class); - $secondDataProvider->getItem(Dummy::class, 1, null, [])->willReturn($dummy); + $secondDataProvider->getItem(Dummy::class, ['test' => 1], null, [])->willReturn($dummy); $thirdDataProvider = $this->prophesize(ItemDataProviderInterface::class); - $thirdDataProvider->getItem(Dummy::class, 1, null, [])->willReturn(new \stdClass()); + $thirdDataProvider->getItem(Dummy::class, ['test' => 1], null, [])->willReturn(new \stdClass()); $chainItemDataProvider = new ChainItemDataProvider([$firstDataProvider->reveal(), $secondDataProvider->reveal(), $thirdDataProvider->reveal()]); - $chainItemDataProvider->getItem(Dummy::class, 1); - } - - /** - * @group legacy - * @expectedDeprecation Receiving "$id" as non-array in an item data provider is deprecated in 2.3 in favor of implementing "ApiPlatform\Core\DataProvider\DenormalizedIdentifiersAwareItemDataProviderInterface". - */ - public function testLegacyGetItemWithoutDenormalizedIdentifiersAndCompositeIdentifier() - { - $dummy = new CompositePrimitiveItem('Lucie', 1984); - - $dataProvider = $this->prophesize(ItemDataProviderInterface::class); - $dataProvider->willImplement(RestrictedDataProviderInterface::class); - $dataProvider->supports(CompositePrimitiveItem::class, null, [])->willReturn(true); - $dataProvider->getItem(CompositePrimitiveItem::class, 'name=Lucie;year=1984', null, [])->willReturn($dummy); - - $chainItemDataProvider = new ChainItemDataProvider([ - $dataProvider->reveal(), - ]); - - $this->assertEquals($dummy, $chainItemDataProvider->getItem(CompositePrimitiveItem::class, ['name' => 'Lucie', 'year' => 1984])); + $chainItemDataProvider->getItem(Dummy::class, ['test' => 1]); } /** @@ -149,10 +99,10 @@ public function testLegacyGetItemWithoutDenormalizedIdentifiersAndCompositeIdent public function testLegacyGetItemExceptions() { $firstDataProvider = $this->prophesize(ItemDataProviderInterface::class); - $firstDataProvider->getItem('notfound', 1, null, [])->willThrow(ResourceClassNotSupportedException::class); + $firstDataProvider->getItem('notfound', ['test' => 1], null, [])->willThrow(ResourceClassNotSupportedException::class); $chainItemDataProvider = new ChainItemDataProvider([$firstDataProvider->reveal()]); - $this->assertEquals('', $chainItemDataProvider->getItem('notfound', 1)); + $this->assertEquals('', $chainItemDataProvider->getItem('notfound', ['test' => 1])); } } diff --git a/tests/EventListener/DeserializeListenerTest.php b/tests/EventListener/DeserializeListenerTest.php index 6563567b420..602ed2ee4eb 100644 --- a/tests/EventListener/DeserializeListenerTest.php +++ b/tests/EventListener/DeserializeListenerTest.php @@ -200,13 +200,7 @@ public function testDeserializeResourceClassSupportedFormat(string $method, bool public function testLegacyDeserializeResourceClassSupportedFormat(string $method, bool $populateObject): void { $formatsProviderProphecy = $this->prophesize(FormatsProviderInterface::class); - $formatsProviderProphecy->getFormatsFromAttributes([ - 'resource_class' => 'Foo', - 'collection_operation_name' => 'post', - 'receive' => true, - 'respond' => true, - 'persist' => true, - ])->willReturn(self::FORMATS)->shouldBeCalled(); + $formatsProviderProphecy->getFormatsFromAttributes(Argument::type('array'))->willReturn(self::FORMATS)->shouldBeCalled(); $this->doTestDeserializeResourceClassSupportedFormat($method, $populateObject, $formatsProviderProphecy->reveal()); } @@ -216,7 +210,7 @@ private function doTestDeserializeResourceClassSupportedFormat(string $method, b $result = $populateObject ? new \stdClass() : null; $eventProphecy = $this->prophesize(RequestEvent::class); - $request = new Request([], [], ['data' => $result, '_api_resource_class' => 'Foo', '_api_collection_operation_name' => 'post'], [], [], [], '{}'); + $request = new Request([], [], ['data' => $result, '_api_resource_class' => 'Foo', '_api_collection_operation_name' => 'post', '_api_identified_by'], [], [], [], '{}'); $request->setMethod($method); $request->headers->set('Content-Type', 'application/json'); $eventProphecy->getRequest()->willReturn($request)->shouldBeCalled(); diff --git a/tests/EventListener/ReadListenerTest.php b/tests/EventListener/ReadListenerTest.php index a809dfdd537..b07245d74e6 100644 --- a/tests/EventListener/ReadListenerTest.php +++ b/tests/EventListener/ReadListenerTest.php @@ -19,7 +19,7 @@ use ApiPlatform\Core\EventListener\ReadListener; use ApiPlatform\Core\Exception\InvalidIdentifierException; use ApiPlatform\Core\Exception\RuntimeException; -use ApiPlatform\Core\Identifier\IdentifierConverterInterface; +use ApiPlatform\Core\Identifier\IdentifierDenormalizerInterface; use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface; use ApiPlatform\Core\Metadata\Resource\ResourceMetadata; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Dummy; @@ -39,7 +39,7 @@ class ReadListenerTest extends TestCase public function testNotAnApiPlatformRequest() { - $identifierConverter = $this->prophesize(IdentifierConverterInterface::class); + $identifierConverter = $this->prophesize(IdentifierDenormalizerInterface::class); $collectionDataProvider = $this->prophesize(CollectionDataProviderInterface::class); $collectionDataProvider->getCollection()->shouldNotBeCalled(); @@ -89,7 +89,7 @@ public function testDoNotReadWhenReceiveFlagIsFalse() $subresourceDataProvider = $this->prophesize(SubresourceDataProviderInterface::class); $subresourceDataProvider->getSubresource(Argument::cetera())->shouldNotBeCalled(); - $identifierConverter = $this->prophesize(IdentifierConverterInterface::class); + $identifierConverter = $this->prophesize(IdentifierDenormalizerInterface::class); $request = new Request([], [], ['id' => 1, 'data' => new Dummy(), '_api_resource_class' => Dummy::class, '_api_item_operation_name' => 'put', '_api_receive' => false]); $request->setMethod('PUT'); @@ -112,7 +112,7 @@ public function testDoNotReadWhenDisabledInOperationAttribute() $subresourceDataProvider = $this->prophesize(SubresourceDataProviderInterface::class); $subresourceDataProvider->getSubresource(Argument::cetera())->shouldNotBeCalled(); - $identifierConverter = $this->prophesize(IdentifierConverterInterface::class); + $identifierConverter = $this->prophesize(IdentifierDenormalizerInterface::class); $resourceMetadata = new ResourceMetadata('Dummy', null, null, [ 'put' => [ @@ -135,7 +135,7 @@ public function testDoNotReadWhenDisabledInOperationAttribute() public function testRetrieveCollectionPost() { - $identifierConverter = $this->prophesize(IdentifierConverterInterface::class); + $identifierConverter = $this->prophesize(IdentifierDenormalizerInterface::class); $collectionDataProvider = $this->prophesize(CollectionDataProviderInterface::class); $collectionDataProvider->getCollection()->shouldNotBeCalled(); @@ -161,7 +161,7 @@ public function testRetrieveCollectionPost() public function testRetrieveCollectionGet() { - $identifierConverter = $this->prophesize(IdentifierConverterInterface::class); + $identifierConverter = $this->prophesize(IdentifierDenormalizerInterface::class); $collectionDataProvider = $this->prophesize(CollectionDataProviderInterface::class); $collectionDataProvider->getCollection('Foo', 'get', ['filters' => ['foo' => 'bar']])->willReturn([])->shouldBeCalled(); @@ -187,20 +187,20 @@ public function testRetrieveCollectionGet() public function testRetrieveItem() { - $identifierConverter = $this->prophesize(IdentifierConverterInterface::class); - $identifierConverter->convert('1', 'Foo')->shouldBeCalled()->willReturn(['id' => '1']); + $identifierConverter = $this->prophesize(IdentifierDenormalizerInterface::class); + $identifierConverter->denormalize(['id' => '1'], 'Foo')->shouldBeCalled()->willReturn(['id' => '1']); $collectionDataProvider = $this->prophesize(CollectionDataProviderInterface::class); $collectionDataProvider->getCollection()->shouldNotBeCalled(); $data = new \stdClass(); $itemDataProvider = $this->prophesize(ItemDataProviderInterface::class); - $itemDataProvider->getItem('Foo', ['id' => '1'], 'get', [IdentifierConverterInterface::HAS_IDENTIFIER_CONVERTER => true])->willReturn($data)->shouldBeCalled(); + $itemDataProvider->getItem('Foo', ['id' => '1'], 'get', [])->willReturn($data)->shouldBeCalled(); $subresourceDataProvider = $this->prophesize(SubresourceDataProviderInterface::class); $subresourceDataProvider->getSubresource()->shouldNotBeCalled(); - $request = new Request([], [], ['id' => 1, '_api_resource_class' => 'Foo', '_api_item_operation_name' => 'get', '_api_format' => 'json', '_api_mime_type' => 'application/json']); + $request = new Request([], [], ['id' => '1', '_api_resource_class' => 'Foo', '_api_item_operation_name' => 'get', '_api_identified_by' => ['id'], '_api_format' => 'json', '_api_mime_type' => 'application/json']); $request->setMethod('GET'); $event = $this->prophesize(RequestEvent::class); @@ -213,35 +213,10 @@ public function testRetrieveItem() $this->assertEquals($data, $request->attributes->get('previous_data')); } - public function testRetrieveItemNoIdentifier() - { - $this->expectException(NotFoundHttpException::class); - - $collectionDataProvider = $this->prophesize(CollectionDataProviderInterface::class); - $collectionDataProvider->getCollection()->shouldNotBeCalled(); - - $itemDataProvider = $this->prophesize(ItemDataProviderInterface::class); - $itemDataProvider->getItem()->shouldNotBeCalled(); - - $subresourceDataProvider = $this->prophesize(SubresourceDataProviderInterface::class); - $subresourceDataProvider->getSubresource()->shouldNotBeCalled(); - - $request = new Request([], [], ['_api_resource_class' => 'Foo', '_api_item_operation_name' => 'get', '_api_format' => 'json', '_api_mime_type' => 'application/json']); - $request->setMethod('GET'); - - $event = $this->prophesize(RequestEvent::class); - $event->getRequest()->willReturn($request)->shouldBeCalled(); - - $listener = new ReadListener($collectionDataProvider->reveal(), $itemDataProvider->reveal(), $subresourceDataProvider->reveal()); - $listener->onKernelRequest($event->reveal()); - - $request->attributes->get('data'); - } - public function testRetrieveSubresource() { - $identifierConverter = $this->prophesize(IdentifierConverterInterface::class); - $identifierConverter->convert('1', 'Bar')->shouldBeCalled()->willReturn(['id' => '1']); + $identifierConverter = $this->prophesize(IdentifierDenormalizerInterface::class); + $identifierConverter->denormalize(['id' => '1'], 'Foo')->shouldBeCalled()->willReturn(['id' => '1']); $collectionDataProvider = $this->prophesize(CollectionDataProviderInterface::class); $collectionDataProvider->getCollection()->shouldNotBeCalled(); @@ -251,9 +226,9 @@ public function testRetrieveSubresource() $data = [new \stdClass()]; $subresourceDataProvider = $this->prophesize(SubresourceDataProviderInterface::class); - $subresourceDataProvider->getSubresource('Foo', ['id' => ['id' => '1']], ['identifiers' => [['id', 'Bar', true]], 'property' => 'bar', IdentifierConverterInterface::HAS_IDENTIFIER_CONVERTER => true], 'get')->willReturn($data)->shouldBeCalled(); + $subresourceDataProvider->getSubresource('Foo', ['id' => ['id' => '1']], ['identifiers' => [['id', 'Bar', true, ['id']]], 'property' => 'bar'], 'get')->willReturn($data)->shouldBeCalled(); - $request = new Request([], [], ['id' => 1, '_api_resource_class' => 'Foo', '_api_subresource_operation_name' => 'get', '_api_format' => 'json', '_api_mime_type' => 'application/json', '_api_subresource_context' => ['identifiers' => [['id', 'Bar', true]], 'property' => 'bar']]); + $request = new Request([], [], ['id' => '1', '_api_resource_class' => 'Foo', '_api_subresource_operation_name' => 'get', '_api_format' => 'json', '_api_mime_type' => 'application/json', '_api_subresource_context' => ['identifiers' => [['id', 'Bar', true, ['id']]], 'property' => 'bar']]); $request->setMethod('GET'); $event = $this->prophesize(RequestEvent::class); @@ -269,6 +244,8 @@ public function testRetrieveSubresource() public function testRetrieveSubresourceNoDataProvider() { $this->expectException(RuntimeException::class); + $identifierConverter = $this->prophesize(IdentifierDenormalizerInterface::class); + $identifierConverter->denormalize(['id' => '1'], 'Foo')->shouldBeCalled()->willReturn(['id' => '1']); $collectionDataProvider = $this->prophesize(CollectionDataProviderInterface::class); $collectionDataProvider->getCollection()->shouldNotBeCalled(); @@ -276,13 +253,13 @@ public function testRetrieveSubresourceNoDataProvider() $itemDataProvider = $this->prophesize(ItemDataProviderInterface::class); $itemDataProvider->getItem()->shouldNotBeCalled(); - $request = new Request([], [], ['id' => 1, '_api_resource_class' => 'Foo', '_api_subresource_operation_name' => 'get', '_api_format' => 'json', '_api_mime_type' => 'application/json', '_api_subresource_context' => ['identifiers' => [['id', 'Bar', true]], 'property' => 'bar']]); + $request = new Request([], [], ['id' => '1', '_api_resource_class' => 'Foo', '_api_subresource_operation_name' => 'get', '_api_format' => 'json', '_api_mime_type' => 'application/json', '_api_subresource_context' => ['identifiers' => [['id', 'Bar', true, ['id']]], 'property' => 'bar']]); $request->setMethod('GET'); $event = $this->prophesize(RequestEvent::class); $event->getRequest()->willReturn($request)->shouldBeCalled(); - $listener = new ReadListener($collectionDataProvider->reveal(), $itemDataProvider->reveal()); + $listener = new ReadListener($collectionDataProvider->reveal(), $itemDataProvider->reveal(), null, null, $identifierConverter->reveal()); $listener->onKernelRequest($event->reveal()); $request->attributes->get('data'); @@ -290,8 +267,8 @@ public function testRetrieveSubresourceNoDataProvider() public function testRetrieveSubresourceNotFound() { - $identifierConverter = $this->prophesize(IdentifierConverterInterface::class); - $identifierConverter->convert('1', 'Bar')->willThrow(new InvalidIdentifierException())->shouldBeCalled(); + $identifierConverter = $this->prophesize(IdentifierDenormalizerInterface::class); + $identifierConverter->denormalize(['id' => '1'], 'Foo')->willThrow(new InvalidIdentifierException())->shouldBeCalled(); $this->expectException(NotFoundHttpException::class); $collectionDataProvider = $this->prophesize(CollectionDataProviderInterface::class); @@ -300,7 +277,7 @@ public function testRetrieveSubresourceNotFound() $itemDataProvider = $this->prophesize(ItemDataProviderInterface::class); $itemDataProvider->getItem()->shouldNotBeCalled(); - $request = new Request([], [], ['id' => 1, '_api_resource_class' => 'Foo', '_api_subresource_operation_name' => 'get', '_api_format' => 'json', '_api_mime_type' => 'application/json', '_api_subresource_context' => ['identifiers' => [['id', 'Bar', true]], 'property' => 'bar']]); + $request = new Request([], [], ['id' => '1', '_api_resource_class' => 'Foo', '_api_subresource_operation_name' => 'get', '_api_format' => 'json', '_api_mime_type' => 'application/json', '_api_subresource_context' => ['identifiers' => [['id', 'Bar', true, ['id']]], 'property' => 'bar']]); $request->setMethod('GET'); $event = $this->prophesize(RequestEvent::class); @@ -312,18 +289,18 @@ public function testRetrieveSubresourceNotFound() public function testRetrieveItemNotFound() { - $identifierConverter = $this->prophesize(IdentifierConverterInterface::class); - $identifierConverter->convert('22', 'Foo')->shouldBeCalled()->willReturn(['id' => 22]); + $identifierConverter = $this->prophesize(IdentifierDenormalizerInterface::class); + $identifierConverter->denormalize(['id' => '22'], 'Foo')->shouldBeCalled()->willReturn(['id' => 22]); $this->expectException(NotFoundHttpException::class); $collectionDataProvider = $this->prophesize(CollectionDataProviderInterface::class); $itemDataProvider = $this->prophesize(ItemDataProviderInterface::class); - $itemDataProvider->getItem('Foo', ['id' => 22], 'get', [IdentifierConverterInterface::HAS_IDENTIFIER_CONVERTER => true])->willReturn(null)->shouldBeCalled(); + $itemDataProvider->getItem('Foo', ['id' => 22], 'get', [])->willReturn(null)->shouldBeCalled(); $subresourceDataProvider = $this->prophesize(SubresourceDataProviderInterface::class); - $request = new Request([], [], ['id' => 22, '_api_resource_class' => 'Foo', '_api_item_operation_name' => 'get', '_api_format' => 'json', '_api_mime_type' => 'application/json']); + $request = new Request([], [], ['id' => '22', '_api_resource_class' => 'Foo', '_api_item_operation_name' => 'get', '_api_identified_by' => ['id'], '_api_format' => 'json', '_api_mime_type' => 'application/json']); $request->setMethod('GET'); $event = $this->prophesize(RequestEvent::class); @@ -337,14 +314,14 @@ public function testRetrieveBadItemNormalizedIdentifiers() { $this->expectException(NotFoundHttpException::class); - $identifierConverter = $this->prophesize(IdentifierConverterInterface::class); - $identifierConverter->convert('1', 'Foo')->shouldBeCalled()->willThrow(new InvalidIdentifierException()); + $identifierConverter = $this->prophesize(IdentifierDenormalizerInterface::class); + $identifierConverter->denormalize(['id' => '1'], 'Foo')->shouldBeCalled()->willThrow(new InvalidIdentifierException()); $collectionDataProvider = $this->prophesize(CollectionDataProviderInterface::class); $itemDataProvider = $this->prophesize(ItemDataProviderInterface::class); $subresourceDataProvider = $this->prophesize(SubresourceDataProviderInterface::class); - $request = new Request([], [], ['id' => 1, '_api_resource_class' => 'Foo', '_api_item_operation_name' => 'get', '_api_format' => 'json', '_api_mime_type' => 'application/json']); + $request = new Request([], [], ['id' => '1', '_api_resource_class' => 'Foo', '_api_item_operation_name' => 'get', '_api_identified_by' => ['id'], '_api_format' => 'json', '_api_mime_type' => 'application/json']); $request->setMethod(Request::METHOD_GET); $event = $this->prophesize(RequestEvent::class); @@ -358,8 +335,8 @@ public function testRetrieveBadSubresourceNormalizedIdentifiers() { $this->expectException(NotFoundHttpException::class); - $identifierConverter = $this->prophesize(IdentifierConverterInterface::class); - $identifierConverter->convert(Argument::type('string'), Argument::type('string'))->shouldBeCalled()->willThrow(new InvalidIdentifierException()); + $identifierConverter = $this->prophesize(IdentifierDenormalizerInterface::class); + $identifierConverter->denormalize(Argument::type('array'), Argument::type('string'))->shouldBeCalled()->willThrow(new InvalidIdentifierException()); $collectionDataProvider = $this->prophesize(CollectionDataProviderInterface::class); $collectionDataProvider->getCollection()->shouldNotBeCalled(); @@ -370,7 +347,7 @@ public function testRetrieveBadSubresourceNormalizedIdentifiers() $subresourceDataProvider = $this->prophesize(SubresourceDataProviderInterface::class); $subresourceDataProvider->getSubresource()->shouldNotBeCalled(); - $request = new Request([], [], ['id' => 1, '_api_resource_class' => 'Foo', '_api_subresource_operation_name' => 'get', '_api_format' => 'json', '_api_mime_type' => 'application/json', '_api_subresource_context' => ['identifiers' => [['id', 'Bar', true]], 'property' => 'bar']]); + $request = new Request([], [], ['id' => 1, '_api_resource_class' => 'Foo', '_api_subresource_operation_name' => 'get', '_api_format' => 'json', '_api_mime_type' => 'application/json', '_api_subresource_context' => ['identifiers' => [['id', 'Bar', true, ['id']]], 'property' => 'bar']]); $request->setMethod(Request::METHOD_GET); $event = $this->prophesize(RequestEvent::class); diff --git a/tests/Fixtures/TestBundle/Action/ConfigCustom.php b/tests/Fixtures/TestBundle/Action/ConfigCustom.php index c3e1b8811fa..32b5f21d83b 100644 --- a/tests/Fixtures/TestBundle/Action/ConfigCustom.php +++ b/tests/Fixtures/TestBundle/Action/ConfigCustom.php @@ -35,6 +35,6 @@ public function __invoke(Request $request, $id) { $attributes = RequestAttributesExtractor::extractAttributes($request); - return $this->dataProvider->getItem($attributes['resource_class'], $id); + return $this->dataProvider->getItem($attributes['resource_class'], ['id' => $id]); } } diff --git a/tests/Fixtures/TestBundle/Controller/MongoDbOdm/CustomActionController.php b/tests/Fixtures/TestBundle/Controller/MongoDbOdm/CustomActionController.php index 32baf7aa352..046834e2465 100644 --- a/tests/Fixtures/TestBundle/Controller/MongoDbOdm/CustomActionController.php +++ b/tests/Fixtures/TestBundle/Controller/MongoDbOdm/CustomActionController.php @@ -28,7 +28,7 @@ class CustomActionController extends AbstractController * methods={"GET"}, * name="custom_normalization", * path="/custom/{id}/normalization", - * defaults={"_api_resource_class"=CustomActionDummy::class, "_api_item_operation_name"="custom_normalization"} + * defaults={"_api_resource_class"=CustomActionDummy::class, "_api_item_operation_name"="custom_normalization", "_api_identified_by"={"id"}} * ) */ public function customNormalizationAction(CustomActionDummy $data) @@ -67,7 +67,7 @@ public function customDenormalizationAction(Request $request) * methods={"GET"}, * name="short_custom_normalization", * path="/short_custom/{id}/normalization", - * defaults={"_api_resource_class"=CustomActionDummy::class, "_api_item_operation_name"="custom_normalization"} + * defaults={"_api_resource_class"=CustomActionDummy::class, "_api_item_operation_name"="custom_normalization", "_api_identified_by"={"id"}} * ) */ public function shortCustomNormalizationAction(CustomActionDummy $data) diff --git a/tests/Fixtures/TestBundle/Controller/Orm/CustomActionController.php b/tests/Fixtures/TestBundle/Controller/Orm/CustomActionController.php index 33145ad9461..b8eab18bfba 100644 --- a/tests/Fixtures/TestBundle/Controller/Orm/CustomActionController.php +++ b/tests/Fixtures/TestBundle/Controller/Orm/CustomActionController.php @@ -28,7 +28,7 @@ class CustomActionController extends AbstractController * methods={"GET"}, * name="custom_normalization", * path="/custom/{id}/normalization", - * defaults={"_api_resource_class"=CustomActionDummy::class, "_api_item_operation_name"="custom_normalization"} + * defaults={"_api_resource_class"=CustomActionDummy::class, "_api_item_operation_name"="custom_normalization", "_api_identified_by"={"id"}} * ) */ public function customNormalizationAction(CustomActionDummy $data) @@ -67,7 +67,7 @@ public function customDenormalizationAction(Request $request) * methods={"GET"}, * name="short_custom_normalization", * path="/short_custom/{id}/normalization", - * defaults={"_api_resource_class"=CustomActionDummy::class, "_api_item_operation_name"="custom_normalization"} + * defaults={"_api_resource_class"=CustomActionDummy::class, "_api_item_operation_name"="custom_normalization", "_api_identified_by"={"id"}} * ) */ public function shortCustomNormalizationAction(CustomActionDummy $data) diff --git a/tests/Fixtures/TestBundle/DataProvider/ContainNonResourceItemDataProvider.php b/tests/Fixtures/TestBundle/DataProvider/ContainNonResourceItemDataProvider.php index e83e2826ee0..d1c277650d8 100644 --- a/tests/Fixtures/TestBundle/DataProvider/ContainNonResourceItemDataProvider.php +++ b/tests/Fixtures/TestBundle/DataProvider/ContainNonResourceItemDataProvider.php @@ -32,18 +32,18 @@ public function supports(string $resourceClass, string $operationName = null, ar /** * {@inheritdoc} */ - public function getItem(string $resourceClass, $id, string $operationName = null, array $context = []) + public function getItem(string $resourceClass, array $identifiers, string $operationName = null, array $context = []) { - if (!is_scalar($id)) { + if (!is_scalar($identifiers['id'])) { throw new \InvalidArgumentException('The id must be a scalar.'); } // Retrieve the blog post item from somewhere $cnr = new $resourceClass(); - $cnr->id = $id; + $cnr->id = $identifiers['id']; $cnr->notAResource = new NotAResource('f1', 'b1'); $cnr->nested = new $resourceClass(); - $cnr->nested->id = "$id-nested"; + $cnr->nested->id = "{$identifiers['id']}-nested"; $cnr->nested->notAResource = new NotAResource('f2', 'b2'); return $cnr; diff --git a/tests/Fixtures/TestBundle/DataProvider/ProductItemDataProvider.php b/tests/Fixtures/TestBundle/DataProvider/ProductItemDataProvider.php index 242539f9a02..2ffd1c0e7a6 100644 --- a/tests/Fixtures/TestBundle/DataProvider/ProductItemDataProvider.php +++ b/tests/Fixtures/TestBundle/DataProvider/ProductItemDataProvider.php @@ -42,10 +42,10 @@ public function supports(string $resourceClass, string $operationName = null, ar /** * {@inheritdoc} */ - public function getItem(string $resourceClass, $id, string $operationName = null, array $context = []) + public function getItem(string $resourceClass, array $identifiers, string $operationName = null, array $context = []) { return $this->managerRegistry->getRepository($this->orm ? Product::class : ProductDocument::class)->findOneBy([ - 'code' => $id, + 'code' => $identifiers['code'], ]); } } diff --git a/tests/Fixtures/TestBundle/DataProvider/ResourceInterfaceImplementationDataProvider.php b/tests/Fixtures/TestBundle/DataProvider/ResourceInterfaceImplementationDataProvider.php index dd994ac5f0c..db23d2a7ce4 100644 --- a/tests/Fixtures/TestBundle/DataProvider/ResourceInterfaceImplementationDataProvider.php +++ b/tests/Fixtures/TestBundle/DataProvider/ResourceInterfaceImplementationDataProvider.php @@ -26,9 +26,9 @@ public function supports(string $resourceClass, string $operationName = null, ar return ResourceInterface::class === $resourceClass; } - public function getItem(string $resourceClass, $id, string $operationName = null, array $context = []) + public function getItem(string $resourceClass, array $identifiers, string $operationName = null, array $context = []) { - if ('some-id' === $id) { + if ('some-id' === $identifiers['foo']) { return (new ResourceInterfaceImplementation())->setFoo('single item'); } diff --git a/tests/Fixtures/TestBundle/DataProvider/SerializableItemDataProvider.php b/tests/Fixtures/TestBundle/DataProvider/SerializableItemDataProvider.php index 6b5e8b8823a..b7686862fdf 100644 --- a/tests/Fixtures/TestBundle/DataProvider/SerializableItemDataProvider.php +++ b/tests/Fixtures/TestBundle/DataProvider/SerializableItemDataProvider.php @@ -29,7 +29,7 @@ class SerializableItemDataProvider implements ItemDataProviderInterface, Restric /** * {@inheritdoc} */ - public function getItem(string $resourceClass, $id, string $operationName = null, array $context = []) + public function getItem(string $resourceClass, array $identifiers, string $operationName = null, array $context = []) { return $this->getSerializer()->deserialize(<<<'JSON' { diff --git a/tests/Fixtures/TestBundle/DataProvider/TaxonItemDataProvider.php b/tests/Fixtures/TestBundle/DataProvider/TaxonItemDataProvider.php index 7e1c9d73f2b..68bd14acbe9 100644 --- a/tests/Fixtures/TestBundle/DataProvider/TaxonItemDataProvider.php +++ b/tests/Fixtures/TestBundle/DataProvider/TaxonItemDataProvider.php @@ -42,10 +42,10 @@ public function supports(string $resourceClass, string $operationName = null, ar /** * {@inheritdoc} */ - public function getItem(string $resourceClass, $id, string $operationName = null, array $context = []) + public function getItem(string $resourceClass, array $identifiers, string $operationName = null, array $context = []) { return $this->managerRegistry->getRepository($this->orm ? Taxon::class : TaxonDocument::class)->findOneBy([ - 'code' => $id, + 'code' => $identifiers['code'], ]); } } diff --git a/tests/Fixtures/TestBundle/Document/Book.php b/tests/Fixtures/TestBundle/Document/Book.php new file mode 100644 index 00000000000..098feacb68e --- /dev/null +++ b/tests/Fixtures/TestBundle/Document/Book.php @@ -0,0 +1,51 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Core\Tests\Fixtures\TestBundle\Document; + +use ApiPlatform\Core\Annotation\ApiResource; +use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; + +/** + * Book. + * + * @author Antoine Bluchet + * + * @ApiResource(collectionOperations={}, itemOperations={ + * "get", + * "get_by_isbn"={"method"="GET", "path"="/books/by_isbn/{isbn}.{_format}", "requirements"={"isbn"=".+"}, "identified_by"="isbn"} + * }) + * @ODM\Document + */ +class Book +{ + /** + * @ODM\Id(strategy="INCREMENT", type="integer") + */ + private $id; + + /** + * @ODM\Field(type="string", nullable=true) + */ + public $name; + + /** + * @ODM\Field(type="string") + */ + public $isbn; + + public function getId() + { + return $this->id; + } +} diff --git a/tests/Fixtures/TestBundle/Document/CustomMultipleIdentifierDummy.php b/tests/Fixtures/TestBundle/Document/CustomMultipleIdentifierDummy.php new file mode 100644 index 00000000000..4bdc10a52d8 --- /dev/null +++ b/tests/Fixtures/TestBundle/Document/CustomMultipleIdentifierDummy.php @@ -0,0 +1,79 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Core\Tests\Fixtures\TestBundle\Document; + +use ApiPlatform\Core\Annotation\ApiProperty; +use ApiPlatform\Core\Annotation\ApiResource; +use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; + +/** + * Custom Identifier Dummy. + * + * @ApiResource(compositeIdentifier=false) + * @ODM\Document + */ +class CustomMultipleIdentifierDummy +{ + /** + * @var int The custom identifier + * + * @ODM\Id(strategy="NONE", type="integer") + */ + private $firstId; + + /** + * @var int The custom identifier + * + * @ApiProperty(identifier=true) + * @ODM\Field(type="integer") + */ + private $secondId; + + /** + * @var string The dummy name + * + * @ODM\Field(type="string") + */ + private $name; + + public function getFirstId(): int + { + return $this->firstId; + } + + public function setFirstId(int $firstId) + { + $this->firstId = $firstId; + } + + public function getSecondId(): int + { + return $this->secondId; + } + + public function setSecondId(int $secondId) + { + $this->secondId = $secondId; + } + + public function getName(): string + { + return $this->name; + } + + public function setName(string $name) + { + $this->name = $name; + } +} diff --git a/tests/Fixtures/TestBundle/Entity/Book.php b/tests/Fixtures/TestBundle/Entity/Book.php new file mode 100644 index 00000000000..fc6fa14ff19 --- /dev/null +++ b/tests/Fixtures/TestBundle/Entity/Book.php @@ -0,0 +1,53 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity; + +use ApiPlatform\Core\Annotation\ApiResource; +use Doctrine\ORM\Mapping as ORM; + +/** + * Book. + * + * @author Antoine Bluchet + * + * @ApiResource(collectionOperations={}, itemOperations={ + * "get", + * "get_by_isbn"={"method"="GET", "path"="/books/by_isbn/{isbn}.{_format}", "requirements"={"isbn"=".+"}, "identified_by"="isbn"} + * }) + * @ORM\Entity + */ +class Book +{ + /** + * @ORM\Column(type="integer") + * @ORM\Id + * @ORM\GeneratedValue(strategy="AUTO") + */ + private $id; + + /** + * @ORM\Column + */ + public $name; + + /** + * @ORM\Column(unique=true) + */ + public $isbn; + + public function getId() + { + return $this->id; + } +} diff --git a/tests/Fixtures/TestBundle/Entity/CustomMultipleIdentifierDummy.php b/tests/Fixtures/TestBundle/Entity/CustomMultipleIdentifierDummy.php new file mode 100644 index 00000000000..82d77d8d2d0 --- /dev/null +++ b/tests/Fixtures/TestBundle/Entity/CustomMultipleIdentifierDummy.php @@ -0,0 +1,79 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity; + +use ApiPlatform\Core\Annotation\ApiResource; +use Doctrine\ORM\Mapping as ORM; + +/** + * Custom Identifier Dummy. + * + * @ApiResource(compositeIdentifier=false) + * @ORM\Entity + */ +class CustomMultipleIdentifierDummy +{ + /** + * @var int The custom identifier + * + * @ORM\Column(type="integer") + * @ORM\Id + */ + private $firstId; + + /** + * @var int The custom identifier + * + * @ORM\Column(type="integer") + * @ORM\Id + */ + private $secondId; + + /** + * @var string The dummy name + * + * @ORM\Column(length=30) + */ + private $name; + + public function getFirstId(): int + { + return $this->firstId; + } + + public function setFirstId(int $firstId) + { + $this->firstId = $firstId; + } + + public function getSecondId(): int + { + return $this->secondId; + } + + public function setSecondId(int $secondId) + { + $this->secondId = $secondId; + } + + public function getName(): string + { + return $this->name; + } + + public function setName(string $name) + { + $this->name = $name; + } +} diff --git a/tests/Fixtures/TestBundle/Entity/SlugParentDummy.php b/tests/Fixtures/TestBundle/Entity/SlugParentDummy.php index 722f5d94e93..926f6aac018 100644 --- a/tests/Fixtures/TestBundle/Entity/SlugParentDummy.php +++ b/tests/Fixtures/TestBundle/Entity/SlugParentDummy.php @@ -23,7 +23,7 @@ /** * Custom Identifier Dummy With Subresource. * - * @ApiResource + * @ApiResource(attributes={"identified_by"="slug"}) * @ORM\Entity */ class SlugParentDummy diff --git a/tests/Fixtures/TestBundle/Model/ProductInterface.php b/tests/Fixtures/TestBundle/Model/ProductInterface.php index 04b095817e5..74d2ab2e165 100644 --- a/tests/Fixtures/TestBundle/Model/ProductInterface.php +++ b/tests/Fixtures/TestBundle/Model/ProductInterface.php @@ -21,6 +21,7 @@ /** * @ApiResource( * shortName="Product", + * attributes={"identified_by"="code"}, * normalizationContext={ * "groups"={"product_read"}, * }, diff --git a/tests/Fixtures/TestBundle/Model/TaxonInterface.php b/tests/Fixtures/TestBundle/Model/TaxonInterface.php index e715e8d6348..de9543e92be 100644 --- a/tests/Fixtures/TestBundle/Model/TaxonInterface.php +++ b/tests/Fixtures/TestBundle/Model/TaxonInterface.php @@ -21,6 +21,7 @@ /** * @ApiResource( * shortName="Taxon", + * attributes={"identified_by"="code"}, * normalizationContext={ * "groups"={"taxon_read"}, * }, diff --git a/tests/Fixtures/app/config/config_common.yml b/tests/Fixtures/app/config/config_common.yml index dabcc50d797..3895668dfb4 100644 --- a/tests/Fixtures/app/config/config_common.yml +++ b/tests/Fixtures/app/config/config_common.yml @@ -44,7 +44,6 @@ api_platform: description: | This is a test API. Made with love - allow_plain_identifiers: true formats: jsonld: ['application/ld+json'] jsonhal: ['application/hal+json'] diff --git a/tests/GraphQl/Serializer/ItemNormalizerTest.php b/tests/GraphQl/Serializer/ItemNormalizerTest.php index 2ce80292c7a..ea5a2025bc2 100644 --- a/tests/GraphQl/Serializer/ItemNormalizerTest.php +++ b/tests/GraphQl/Serializer/ItemNormalizerTest.php @@ -107,7 +107,6 @@ public function testNormalize() null, null, null, - false, null, [], null @@ -161,7 +160,6 @@ public function testNormalizeNoResolverData(): void null, null, null, - false, null, [], null @@ -209,7 +207,6 @@ public function testDenormalize() null, null, null, - false, null, [], null diff --git a/tests/Hal/Serializer/ItemNormalizerTest.php b/tests/Hal/Serializer/ItemNormalizerTest.php index 4a11361625f..4a4febdf40e 100644 --- a/tests/Hal/Serializer/ItemNormalizerTest.php +++ b/tests/Hal/Serializer/ItemNormalizerTest.php @@ -151,7 +151,6 @@ public function testNormalize() $nameConverter->reveal(), null, null, - false, [], [], null @@ -218,7 +217,6 @@ public function testNormalizeWithoutCache() $nameConverter->reveal(), null, null, - false, [], [], null @@ -298,7 +296,6 @@ public function testMaxDepth() null, $classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader())), null, - false, [], [], null diff --git a/tests/Identifier/IdentifierConverterTest.php b/tests/Identifier/IdentifierConverterTest.php deleted file mode 100644 index effe2759411..00000000000 --- a/tests/Identifier/IdentifierConverterTest.php +++ /dev/null @@ -1,96 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Core\Tests\Identifier; - -use ApiPlatform\Core\Api\IdentifiersExtractorInterface; -use ApiPlatform\Core\Identifier\IdentifierConverter; -use ApiPlatform\Core\Identifier\Normalizer\DateTimeIdentifierDenormalizer; -use ApiPlatform\Core\Identifier\Normalizer\IntegerDenormalizer; -use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface; -use ApiPlatform\Core\Metadata\Property\PropertyMetadata; -use ApiPlatform\Core\Tests\ProphecyTrait; -use PHPUnit\Framework\TestCase; -use Symfony\Component\PropertyInfo\Type; - -/** - * @author Antoine Bluchet - */ -class IdentifierConverterTest extends TestCase -{ - use ProphecyTrait; - - public function testCompositeIdentifier() - { - $identifier = 'a=1;c=2;d=2015-04-05'; - $class = 'Dummy'; - - $integerPropertyMetadata = (new PropertyMetadata())->withIdentifier(true)->withType(new Type(Type::BUILTIN_TYPE_INT)); - $identifierPropertyMetadata = (new PropertyMetadata())->withIdentifier(true); - $dateIdentifierPropertyMetadata = (new PropertyMetadata())->withIdentifier(true)->withType(new Type(Type::BUILTIN_TYPE_OBJECT, false, \DateTime::class)); - - $propertyMetadataFactory = $this->prophesize(PropertyMetadataFactoryInterface::class); - $propertyMetadataFactory->create($class, 'a')->shouldBeCalled()->willReturn($integerPropertyMetadata); - $propertyMetadataFactory->create($class, 'c')->shouldBeCalled()->willReturn($identifierPropertyMetadata); - $propertyMetadataFactory->create($class, 'd')->shouldBeCalled()->willReturn($dateIdentifierPropertyMetadata); - - $identifiersExtractor = $this->prophesize(IdentifiersExtractorInterface::class); - $identifiersExtractor->getIdentifiersFromResourceClass($class)->willReturn(['a', 'c', 'd']); - - $identifierDenormalizers = [new IntegerDenormalizer(), new DateTimeIdentifierDenormalizer()]; - - $identifierDenormalizer = new IdentifierConverter($identifiersExtractor->reveal(), $propertyMetadataFactory->reveal(), $identifierDenormalizers); - - $result = $identifierDenormalizer->convert($identifier, $class); - $this->assertEquals(['a' => 1, 'c' => '2', 'd' => new \DateTime('2015-04-05')], $result); - $this->assertSame(1, $result['a']); - } - - public function testSingleDateIdentifier() - { - $identifier = '2015-04-05'; - $class = 'Dummy'; - - $dateIdentifierPropertyMetadata = (new PropertyMetadata())->withIdentifier(true)->withType(new Type(Type::BUILTIN_TYPE_OBJECT, false, \DateTime::class)); - - $propertyMetadataFactory = $this->prophesize(PropertyMetadataFactoryInterface::class); - $propertyMetadataFactory->create($class, 'funkyid')->shouldBeCalled()->willReturn($dateIdentifierPropertyMetadata); - - $identifiersExtractor = $this->prophesize(IdentifiersExtractorInterface::class); - $identifiersExtractor->getIdentifiersFromResourceClass($class)->willReturn(['funkyid']); - - $identifierDenormalizers = [new DateTimeIdentifierDenormalizer()]; - $identifierDenormalizer = new IdentifierConverter($identifiersExtractor->reveal(), $propertyMetadataFactory->reveal(), $identifierDenormalizers); - - $this->assertEquals($identifierDenormalizer->convert($identifier, $class), ['funkyid' => new \DateTime('2015-04-05')]); - } - - public function testIntegerIdentifier() - { - $identifier = '42'; - $class = 'Dummy'; - - $integerIdentifierPropertyMetadata = (new PropertyMetadata())->withIdentifier(true)->withType(new Type(Type::BUILTIN_TYPE_INT)); - - $propertyMetadataFactory = $this->prophesize(PropertyMetadataFactoryInterface::class); - $propertyMetadataFactory->create($class, 'id')->shouldBeCalled()->willReturn($integerIdentifierPropertyMetadata); - - $identifiersExtractor = $this->prophesize(IdentifiersExtractorInterface::class); - $identifiersExtractor->getIdentifiersFromResourceClass($class)->willReturn(['id']); - - $identifierDenormalizers = [new IntegerDenormalizer()]; - $identifierDenormalizer = new IdentifierConverter($identifiersExtractor->reveal(), $propertyMetadataFactory->reveal(), $identifierDenormalizers); - - $this->assertSame(['id' => 42], $identifierDenormalizer->convert($identifier, $class)); - } -} diff --git a/tests/Identifier/IdentifierDenormalizerTest.php b/tests/Identifier/IdentifierDenormalizerTest.php new file mode 100644 index 00000000000..bd0b3980ac4 --- /dev/null +++ b/tests/Identifier/IdentifierDenormalizerTest.php @@ -0,0 +1,60 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Core\Tests\Identifier; + +use ApiPlatform\Core\Identifier\IdentifierDenormalizer; +use ApiPlatform\Core\Identifier\Normalizer\DateTimeIdentifierDenormalizer; +use ApiPlatform\Core\Identifier\Normalizer\IntegerDenormalizer; +use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface; +use ApiPlatform\Core\Metadata\Property\PropertyMetadata; +use PHPUnit\Framework\TestCase; +use Symfony\Component\PropertyInfo\Type; + +/** + * @author Antoine Bluchet + */ +class IdentifierDenormalizerTest extends TestCase +{ + public function testSingleDateIdentifier() + { + $identifiers = ['funkyid' => '2015-04-05']; + $class = 'Dummy'; + + $dateIdentifierPropertyMetadata = (new PropertyMetadata())->withIdentifier(true)->withType(new Type(Type::BUILTIN_TYPE_OBJECT, false, \DateTime::class)); + + $propertyMetadataFactory = $this->prophesize(PropertyMetadataFactoryInterface::class); + $propertyMetadataFactory->create($class, 'funkyid')->shouldBeCalled()->willReturn($dateIdentifierPropertyMetadata); + + $identifierDenormalizers = [new DateTimeIdentifierDenormalizer()]; + $identifierDenormalizer = new IdentifierDenormalizer($propertyMetadataFactory->reveal(), $identifierDenormalizers); + + $this->assertEquals($identifierDenormalizer->denormalize($identifiers, $class), ['funkyid' => new \DateTime('2015-04-05')]); + } + + public function testIntegerIdentifier() + { + $identifiers = ['id' => '42']; + $class = 'Dummy'; + + $integerIdentifierPropertyMetadata = (new PropertyMetadata())->withIdentifier(true)->withType(new Type(Type::BUILTIN_TYPE_INT)); + + $propertyMetadataFactory = $this->prophesize(PropertyMetadataFactoryInterface::class); + $propertyMetadataFactory->create($class, 'id')->shouldBeCalled()->willReturn($integerIdentifierPropertyMetadata); + + $identifierDenormalizers = [new IntegerDenormalizer()]; + $identifierDenormalizer = new IdentifierDenormalizer($propertyMetadataFactory->reveal(), $identifierDenormalizers); + + $this->assertSame(['id' => 42], $identifierDenormalizer->denormalize($identifiers, $class)); + } +} diff --git a/tests/OpenApi/Factory/OpenApiFactoryTest.php b/tests/OpenApi/Factory/OpenApiFactoryTest.php index f960dc3a113..d7db82d2293 100644 --- a/tests/OpenApi/Factory/OpenApiFactoryTest.php +++ b/tests/OpenApi/Factory/OpenApiFactoryTest.php @@ -13,6 +13,7 @@ namespace ApiPlatform\Core\Tests\OpenApi\Factory; +use ApiPlatform\Core\Api\IdentifiersExtractorInterface; use ApiPlatform\Core\Bridge\Symfony\Routing\RouterOperationPathResolver; use ApiPlatform\Core\DataProvider\PaginationOptions; use ApiPlatform\Core\JsonSchema\Schema; @@ -148,6 +149,9 @@ public function testInvoke(): void $schemaFactory = new SchemaFactory($typeFactory, $resourceMetadataFactory, $propertyNameCollectionFactory, $propertyMetadataFactory, new CamelCaseToSnakeCaseNameConverter()); $typeFactory->setSchemaFactory($schemaFactory); + $identifiersExtractorProphecy = $this->prophesize(IdentifiersExtractorInterface::class); + $identifiersExtractorProphecy->getIdentifiersFromResourceClass(Argument::type('string'))->willReturn(['id']); + $factory = new OpenApiFactory( $resourceNameCollectionFactoryProphecy->reveal(), $resourceMetadataFactory, @@ -169,7 +173,8 @@ public function testInvoke(): void 'name' => 'key', ], ]), - new PaginationOptions(true, 'page', true, 'itemsPerPage', true, 'pagination') + new PaginationOptions(true, 'page', true, 'itemsPerPage', true, 'pagination'), + $identifiersExtractorProphecy->reveal() ); $dummySchema = new Schema('openapi'); @@ -524,6 +529,9 @@ public function testOverrideDocumentation() $schemaFactory = new SchemaFactory($typeFactory, $resourceMetadataFactory, $propertyNameCollectionFactory, $propertyMetadataFactory, new CamelCaseToSnakeCaseNameConverter()); $typeFactory->setSchemaFactory($schemaFactory); + $identifiersExtractorProphecy = $this->prophesize(IdentifiersExtractorInterface::class); + $identifiersExtractorProphecy->getIdentifiersFromResourceClass(Argument::type('string'))->willReturn(['id']); + $factory = new OpenApiFactory( $resourceNameCollectionFactoryProphecy->reveal(), $resourceMetadataFactory, @@ -545,7 +553,8 @@ public function testOverrideDocumentation() 'name' => 'key', ], ]), - new PaginationOptions(true, 'page', true, 'itemsPerPage', true, 'pagination') + new PaginationOptions(true, 'page', true, 'itemsPerPage', true, 'pagination'), + $identifiersExtractorProphecy->reveal() ); $openApi = $factory(['base_url' => '/app_dev.php/']); @@ -613,7 +622,11 @@ public function testSubresourceDocumentation() $propertyNameCollectionFactory = $propertyNameCollectionFactoryProphecy->reveal(); $propertyMetadataFactory = $propertyMetadataFactoryProphecy->reveal(); - $subresourceOperationFactory = new SubresourceOperationFactory($resourceMetadataFactory, $propertyNameCollectionFactory, $propertyMetadataFactory, new UnderscorePathSegmentNameGenerator()); + $identifiersExtractorProphecy = $this->prophesize(IdentifiersExtractorInterface::class); + $identifiersExtractorProphecy->getIdentifiersFromResourceClass(Argument::type('string'))->willReturn(['id']); + $identifiersExtractor = $identifiersExtractorProphecy->reveal(); + + $subresourceOperationFactory = new SubresourceOperationFactory($resourceMetadataFactory, $propertyNameCollectionFactory, $propertyMetadataFactory, new UnderscorePathSegmentNameGenerator(), $identifiersExtractor); $resourceNameCollectionFactoryProphecy = $this->prophesize(ResourceNameCollectionFactoryInterface::class); $resourceNameCollectionFactoryProphecy->create()->shouldBeCalled()->willReturn(new ResourceNameCollection([Question::class, Answer::class])); @@ -644,7 +657,8 @@ public function testSubresourceDocumentation() 'name' => 'key', ], ]), - new PaginationOptions(true, 'page', true, 'itemsPerPage', true, 'pagination') + new PaginationOptions(true, 'page', true, 'itemsPerPage', true, 'pagination'), + $identifiersExtractor ); $openApi = $factory(['base_url', '/app_dev.php/']); diff --git a/tests/OpenApi/Serializer/OpenApiNormalizerTest.php b/tests/OpenApi/Serializer/OpenApiNormalizerTest.php index 30444db0e4c..321b46dd9a2 100644 --- a/tests/OpenApi/Serializer/OpenApiNormalizerTest.php +++ b/tests/OpenApi/Serializer/OpenApiNormalizerTest.php @@ -13,6 +13,7 @@ namespace ApiPlatform\Core\Tests\OpenApi\Serializer; +use ApiPlatform\Core\Api\IdentifiersExtractorInterface; use ApiPlatform\Core\DataProvider\PaginationOptions; use ApiPlatform\Core\JsonSchema\SchemaFactory; use ApiPlatform\Core\JsonSchema\TypeFactory; @@ -95,6 +96,9 @@ public function testNormalize() $schemaFactory = new SchemaFactory($typeFactory, $resourceMetadataFactory, $propertyNameCollectionFactory, $propertyMetadataFactory, new CamelCaseToSnakeCaseNameConverter()); $typeFactory->setSchemaFactory($schemaFactory); + $identifiersExtractorProphecy = $this->prophesize(IdentifiersExtractorInterface::class); + $identifiersExtractorProphecy->getIdentifiersFromResourceClass(Argument::type('string'))->willReturn(['id']); + $factory = new OpenApiFactory( $resourceNameCollectionFactoryProphecy->reveal(), $resourceMetadataFactory, @@ -116,7 +120,8 @@ public function testNormalize() 'name' => 'key', ], ]), - new PaginationOptions(true, 'page', true, 'itemsPerPage', true, 'pagination') + new PaginationOptions(true, 'page', true, 'itemsPerPage', true, 'pagination'), + $identifiersExtractorProphecy->reveal() ); $openApi = $factory(['base_url' => '/app_dev.php/']); diff --git a/tests/Operation/Factory/SubresourceOperationFactoryTest.php b/tests/Operation/Factory/SubresourceOperationFactoryTest.php index c811b159097..8b53f873c4f 100644 --- a/tests/Operation/Factory/SubresourceOperationFactoryTest.php +++ b/tests/Operation/Factory/SubresourceOperationFactoryTest.php @@ -13,6 +13,7 @@ namespace ApiPlatform\Core\Tests\Operation\Factory; +use ApiPlatform\Core\Api\IdentifiersExtractorInterface; use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface; use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; use ApiPlatform\Core\Metadata\Property\PropertyMetadata; @@ -27,6 +28,7 @@ use ApiPlatform\Core\Tests\Fixtures\RelatedDummyEntity; use ApiPlatform\Core\Tests\ProphecyTrait; use PHPUnit\Framework\TestCase; +use Prophecy\Argument; /** * @author Antoine Bluchet @@ -62,7 +64,10 @@ public function testCreate() $pathSegmentNameGeneratorProphecy->getSegmentName('dummyEntity')->shouldBeCalled()->willReturn('dummy_entities'); $pathSegmentNameGeneratorProphecy->getSegmentName('anotherSubresource', false)->shouldBeCalled()->willReturn('another_subresource'); - $subresourceOperationFactory = new SubresourceOperationFactory($resourceMetadataFactoryProphecy->reveal(), $propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), $pathSegmentNameGeneratorProphecy->reveal()); + $identifiersExtractorProphecy = $this->prophesize(IdentifiersExtractorInterface::class); + $identifiersExtractorProphecy->getIdentifiersFromResourceClass(Argument::type('string'))->willReturn(['id']); + + $subresourceOperationFactory = new SubresourceOperationFactory($resourceMetadataFactoryProphecy->reveal(), $propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), $pathSegmentNameGeneratorProphecy->reveal(), $identifiersExtractorProphecy->reveal()); $this->assertEquals([ 'api_dummy_entities_subresource_get_subresource' => [ @@ -71,8 +76,9 @@ public function testCreate() 'resource_class' => RelatedDummyEntity::class, 'shortNames' => ['relatedDummyEntity', 'dummyEntity'], 'identifiers' => [ - ['id', DummyEntity::class, true], + ['id', DummyEntity::class, true, ['id']], ], + 'identified_by' => ['id'], 'route_name' => 'api_dummy_entities_subresource_get_subresource', 'path' => '/dummy_entities/{id}/subresource.{_format}', 'operation_name' => 'subresource_get_subresource', @@ -83,9 +89,10 @@ public function testCreate() 'resource_class' => DummyEntity::class, 'shortNames' => ['dummyEntity', 'relatedDummyEntity'], 'identifiers' => [ - ['id', DummyEntity::class, true], - ['subresource', RelatedDummyEntity::class, false], + ['id', DummyEntity::class, true, ['id']], + ['subresource', RelatedDummyEntity::class, false, ['id']], ], + 'identified_by' => ['id'], 'route_name' => 'api_dummy_entities_subresource_another_subresource_get_subresource', 'path' => '/dummy_entities/{id}/subresource/another_subresource.{_format}', 'operation_name' => 'subresource_another_subresource_get_subresource', @@ -96,10 +103,11 @@ public function testCreate() 'resource_class' => RelatedDummyEntity::class, 'shortNames' => ['relatedDummyEntity', 'dummyEntity'], 'identifiers' => [ - ['id', DummyEntity::class, true], - ['subresource', RelatedDummyEntity::class, false], - ['anotherSubresource', DummyEntity::class, false], + ['id', DummyEntity::class, true, ['id']], + ['subresource', RelatedDummyEntity::class, false, ['id']], + ['anotherSubresource', DummyEntity::class, false, ['id']], ], + 'identified_by' => ['id'], 'route_name' => 'api_dummy_entities_subresource_another_subresource_subcollections_get_subresource', 'path' => '/dummy_entities/{id}/subresource/another_subresource/subcollections.{_format}', 'operation_name' => 'subresource_another_subresource_subcollections_get_subresource', @@ -110,8 +118,9 @@ public function testCreate() 'resource_class' => RelatedDummyEntity::class, 'shortNames' => ['relatedDummyEntity', 'dummyEntity'], 'identifiers' => [ - ['id', DummyEntity::class, true], + ['id', DummyEntity::class, true, ['id']], ], + 'identified_by' => ['id'], 'route_name' => 'api_dummy_entities_subcollections_get_subresource', 'path' => '/dummy_entities/{id}/subcollections.{_format}', 'operation_name' => 'subcollections_get_subresource', @@ -122,9 +131,10 @@ public function testCreate() 'resource_class' => DummyEntity::class, 'shortNames' => ['dummyEntity', 'relatedDummyEntity'], 'identifiers' => [ - ['id', DummyEntity::class, true], - ['subcollection', RelatedDummyEntity::class, true], + ['id', DummyEntity::class, true, ['id']], + ['subcollection', RelatedDummyEntity::class, true, ['id']], ], + 'identified_by' => ['id'], 'route_name' => 'api_dummy_entities_subcollections_another_subresource_get_subresource', 'path' => '/dummy_entities/{id}/subcollections/{subcollection}/another_subresource.{_format}', 'operation_name' => 'subcollections_another_subresource_get_subresource', @@ -135,10 +145,11 @@ public function testCreate() 'resource_class' => RelatedDummyEntity::class, 'shortNames' => ['relatedDummyEntity', 'dummyEntity'], 'identifiers' => [ - ['id', DummyEntity::class, true], - ['subcollection', RelatedDummyEntity::class, true], - ['anotherSubresource', DummyEntity::class, false], + ['id', DummyEntity::class, true, ['id']], + ['subcollection', RelatedDummyEntity::class, true, ['id']], + ['anotherSubresource', DummyEntity::class, false, ['id']], ], + 'identified_by' => ['id'], 'route_name' => 'api_dummy_entities_subcollections_another_subresource_subresource_get_subresource', 'path' => '/dummy_entities/{id}/subcollections/{subcollection}/another_subresource/subresource.{_format}', 'operation_name' => 'subcollections_another_subresource_subresource_get_subresource', @@ -180,7 +191,10 @@ public function testCreateByOverriding() $pathSegmentNameGeneratorProphecy->getSegmentName('dummyEntity')->shouldBeCalled()->willReturn('dummy_entities'); $pathSegmentNameGeneratorProphecy->getSegmentName('anotherSubresource', false)->shouldBeCalled()->willReturn('another_subresource'); - $subresourceOperationFactory = new SubresourceOperationFactory($resourceMetadataFactoryProphecy->reveal(), $propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), $pathSegmentNameGeneratorProphecy->reveal()); + $identifiersExtractorProphecy = $this->prophesize(IdentifiersExtractorInterface::class); + $identifiersExtractorProphecy->getIdentifiersFromResourceClass(Argument::type('string'))->willReturn(['id']); + + $subresourceOperationFactory = new SubresourceOperationFactory($resourceMetadataFactoryProphecy->reveal(), $propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), $pathSegmentNameGeneratorProphecy->reveal(), $identifiersExtractorProphecy->reveal()); $this->assertEquals([ 'api_dummy_entities_subresource_get_subresource' => [ @@ -189,8 +203,9 @@ public function testCreateByOverriding() 'resource_class' => RelatedDummyEntity::class, 'shortNames' => ['relatedDummyEntity', 'dummyEntity'], 'identifiers' => [ - ['id', DummyEntity::class, true], + ['id', DummyEntity::class, true, ['id']], ], + 'identified_by' => ['id'], 'route_name' => 'api_dummy_entities_subresource_get_subresource', 'path' => '/dummy_entities/{id}/subresource.{_format}', 'operation_name' => 'subresource_get_subresource', @@ -201,9 +216,10 @@ public function testCreateByOverriding() 'resource_class' => DummyEntity::class, 'shortNames' => ['dummyEntity', 'relatedDummyEntity'], 'identifiers' => [ - ['id', DummyEntity::class, true], - ['subresource', RelatedDummyEntity::class, false], + ['id', DummyEntity::class, true, ['id']], + ['subresource', RelatedDummyEntity::class, false, ['id']], ], + 'identified_by' => ['id'], 'route_name' => 'api_dummy_entities_subresource_another_subresource_get_subresource', 'path' => '/dummy_entities/{id}/subresource/another_subresource.{_format}', 'operation_name' => 'subresource_another_subresource_get_subresource', @@ -214,10 +230,11 @@ public function testCreateByOverriding() 'resource_class' => RelatedDummyEntity::class, 'shortNames' => ['relatedDummyEntity', 'dummyEntity'], 'identifiers' => [ - ['id', DummyEntity::class, true], - ['subresource', RelatedDummyEntity::class, false], - ['anotherSubresource', DummyEntity::class, false], + ['id', DummyEntity::class, true, ['id']], + ['subresource', RelatedDummyEntity::class, false, ['id']], + ['anotherSubresource', DummyEntity::class, false, ['id']], ], + 'identified_by' => ['id'], 'route_name' => 'api_dummy_entities_subresource_another_subresource_subcollections_get_subresource', 'path' => '/dummy_entities/{id}/subresource/another_subresource/subcollections.{_format}', 'operation_name' => 'subresource_another_subresource_subcollections_get_subresource', @@ -228,8 +245,9 @@ public function testCreateByOverriding() 'resource_class' => RelatedDummyEntity::class, 'shortNames' => ['relatedDummyEntity', 'dummyEntity'], 'identifiers' => [ - ['id', DummyEntity::class, true], + ['id', DummyEntity::class, true, ['id']], ], + 'identified_by' => ['id'], 'route_name' => 'api_dummy_entities_subcollections_get_subresource', 'path' => '/dummy_entities/{id}/foobars', 'operation_name' => 'subcollections_get_subresource', @@ -240,9 +258,10 @@ public function testCreateByOverriding() 'resource_class' => DummyEntity::class, 'shortNames' => ['dummyEntity', 'relatedDummyEntity'], 'identifiers' => [ - ['id', DummyEntity::class, true], - ['subcollection', RelatedDummyEntity::class, true], + ['id', DummyEntity::class, true, ['id']], + ['subcollection', RelatedDummyEntity::class, true, ['id']], ], + 'identified_by' => ['id'], 'route_name' => 'api_dummy_entities_subcollections_another_subresource_get_subresource', 'path' => '/dummy_entities/{id}/foobars/{subcollection}/another_foobar.{_format}', 'operation_name' => 'subcollections_another_subresource_get_subresource', @@ -253,10 +272,11 @@ public function testCreateByOverriding() 'resource_class' => RelatedDummyEntity::class, 'shortNames' => ['relatedDummyEntity', 'dummyEntity'], 'identifiers' => [ - ['id', DummyEntity::class, true], - ['subcollection', RelatedDummyEntity::class, true], - ['anotherSubresource', DummyEntity::class, false], + ['id', DummyEntity::class, true, ['id']], + ['subcollection', RelatedDummyEntity::class, true, ['id']], + ['anotherSubresource', DummyEntity::class, false, ['id']], ], + 'identified_by' => ['id'], 'route_name' => 'api_dummy_entities_subcollections_another_subresource_subresource_get_subresource', 'path' => '/dummy_entities/{id}/foobars/{subcollection}/another_foobar/subresource.{_format}', 'operation_name' => 'subcollections_another_subresource_subresource_get_subresource', @@ -286,11 +306,15 @@ public function testCreateWithMaxDepth() $pathSegmentNameGeneratorProphecy->getSegmentName('dummyEntity')->shouldBeCalled()->willReturn('dummy_entities'); $pathSegmentNameGeneratorProphecy->getSegmentName('subresource', false)->shouldBeCalled()->willReturn('subresource'); + $identifiersExtractorProphecy = $this->prophesize(IdentifiersExtractorInterface::class); + $identifiersExtractorProphecy->getIdentifiersFromResourceClass(Argument::type('string'))->willReturn(['id']); + $subresourceOperationFactory = new SubresourceOperationFactory( $resourceMetadataFactoryProphecy->reveal(), $propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), - $pathSegmentNameGeneratorProphecy->reveal() + $pathSegmentNameGeneratorProphecy->reveal(), + $identifiersExtractorProphecy->reveal() ); $this->assertEquals([ @@ -300,8 +324,9 @@ public function testCreateWithMaxDepth() 'resource_class' => RelatedDummyEntity::class, 'shortNames' => ['relatedDummyEntity', 'dummyEntity'], 'identifiers' => [ - ['id', DummyEntity::class, true], + ['id', DummyEntity::class, true, ['id']], ], + 'identified_by' => ['id'], 'route_name' => 'api_dummy_entities_subresource_get_subresource', 'path' => '/dummy_entities/{id}/subresource.{_format}', 'operation_name' => 'subresource_get_subresource', @@ -346,11 +371,15 @@ public function testCreateWithMaxDepthMultipleSubresources() $pathSegmentNameGeneratorProphecy->getSegmentName('secondSubresource', false)->shouldBeCalled()->willReturn('second_subresources'); $pathSegmentNameGeneratorProphecy->getSegmentName('moreSubresource', false)->shouldBeCalled()->willReturn('mode_subresources'); + $identifiersExtractorProphecy = $this->prophesize(IdentifiersExtractorInterface::class); + $identifiersExtractorProphecy->getIdentifiersFromResourceClass(Argument::type('string'))->willReturn(['id']); + $subresourceOperationFactory = new SubresourceOperationFactory( $resourceMetadataFactoryProphecy->reveal(), $propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), - $pathSegmentNameGeneratorProphecy->reveal() + $pathSegmentNameGeneratorProphecy->reveal(), + $identifiersExtractorProphecy->reveal() ); $this->assertEquals([ @@ -360,8 +389,9 @@ public function testCreateWithMaxDepthMultipleSubresources() 'resource_class' => RelatedDummyEntity::class, 'shortNames' => ['relatedDummyEntity', 'dummyEntity'], 'identifiers' => [ - ['id', DummyEntity::class, true], + ['id', DummyEntity::class, true, ['id']], ], + 'identified_by' => ['id'], 'route_name' => 'api_dummy_entities_subresource_get_subresource', 'path' => '/dummy_entities/{id}/subresources.{_format}', 'operation_name' => 'subresource_get_subresource', @@ -372,8 +402,9 @@ public function testCreateWithMaxDepthMultipleSubresources() 'resource_class' => DummyValidatedEntity::class, 'shortNames' => ['dummyValidatedEntity', 'dummyEntity'], 'identifiers' => [ - ['id', DummyEntity::class, true], + ['id', DummyEntity::class, true, ['id']], ], + 'identified_by' => ['id'], 'route_name' => 'api_dummy_entities_second_subresource_get_subresource', 'path' => '/dummy_entities/{id}/second_subresources.{_format}', 'operation_name' => 'second_subresource_get_subresource', @@ -384,9 +415,10 @@ public function testCreateWithMaxDepthMultipleSubresources() 'resource_class' => RelatedDummyEntity::class, 'shortNames' => ['relatedDummyEntity', 'dummyValidatedEntity'], 'identifiers' => [ - ['id', DummyEntity::class, true], - ['secondSubresource', DummyValidatedEntity::class, false], + ['id', DummyEntity::class, true, ['id']], + ['secondSubresource', DummyValidatedEntity::class, false, ['id']], ], + 'identified_by' => ['id'], 'route_name' => 'api_dummy_entities_second_subresource_more_subresource_get_subresource', 'path' => '/dummy_entities/{id}/second_subresources/mode_subresources.{_format}', 'operation_name' => 'second_subresource_more_subresource_get_subresource', @@ -430,11 +462,15 @@ public function testCreateWithMaxDepthMultipleSubresourcesSameMaxDepth() $pathSegmentNameGeneratorProphecy->getSegmentName('subresource', false)->shouldBeCalled()->willReturn('subresources'); $pathSegmentNameGeneratorProphecy->getSegmentName('secondSubresource', false)->shouldBeCalled()->willReturn('second_subresources'); + $identifiersExtractorProphecy = $this->prophesize(IdentifiersExtractorInterface::class); + $identifiersExtractorProphecy->getIdentifiersFromResourceClass(Argument::type('string'))->willReturn(['id']); + $subresourceOperationFactory = new SubresourceOperationFactory( $resourceMetadataFactoryProphecy->reveal(), $propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), - $pathSegmentNameGeneratorProphecy->reveal() + $pathSegmentNameGeneratorProphecy->reveal(), + $identifiersExtractorProphecy->reveal() ); $this->assertEquals([ @@ -444,8 +480,9 @@ public function testCreateWithMaxDepthMultipleSubresourcesSameMaxDepth() 'resource_class' => RelatedDummyEntity::class, 'shortNames' => ['relatedDummyEntity', 'dummyEntity'], 'identifiers' => [ - ['id', DummyEntity::class, true], + ['id', DummyEntity::class, true, ['id']], ], + 'identified_by' => ['id'], 'route_name' => 'api_dummy_entities_subresource_get_subresource', 'path' => '/dummy_entities/{id}/subresources.{_format}', 'operation_name' => 'subresource_get_subresource', @@ -456,8 +493,9 @@ public function testCreateWithMaxDepthMultipleSubresourcesSameMaxDepth() 'resource_class' => DummyValidatedEntity::class, 'shortNames' => ['dummyValidatedEntity', 'dummyEntity'], 'identifiers' => [ - ['id', DummyEntity::class, true], + ['id', DummyEntity::class, true, ['id']], ], + 'identified_by' => ['id'], 'route_name' => 'api_dummy_entities_second_subresource_get_subresource', 'path' => '/dummy_entities/{id}/second_subresources.{_format}', 'operation_name' => 'second_subresource_get_subresource', @@ -485,11 +523,15 @@ public function testCreateSelfReferencingSubresources() $pathSegmentNameGeneratorProphecy->getSegmentName('dummyEntity')->shouldBeCalled()->willReturn('dummy_entities'); $pathSegmentNameGeneratorProphecy->getSegmentName('subresource', false)->shouldBeCalled()->willReturn('subresources'); + $identifiersExtractorProphecy = $this->prophesize(IdentifiersExtractorInterface::class); + $identifiersExtractorProphecy->getIdentifiersFromResourceClass(Argument::type('string'))->willReturn(['id']); + $subresourceOperationFactory = new SubresourceOperationFactory( $resourceMetadataFactoryProphecy->reveal(), $propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), - $pathSegmentNameGeneratorProphecy->reveal() + $pathSegmentNameGeneratorProphecy->reveal(), + $identifiersExtractorProphecy->reveal() ); $this->assertEquals([ @@ -499,8 +541,9 @@ public function testCreateSelfReferencingSubresources() 'resource_class' => DummyEntity::class, 'shortNames' => ['dummyEntity'], 'identifiers' => [ - ['id', DummyEntity::class, true], + ['id', DummyEntity::class, true, ['id']], ], + 'identified_by' => ['id'], 'route_name' => 'api_dummy_entities_subresource_get_subresource', 'path' => '/dummy_entities/{id}/subresources.{_format}', 'operation_name' => 'subresource_get_subresource', @@ -537,11 +580,15 @@ public function testCreateWithDifferentMaxDepthSelfReferencingSubresources() $pathSegmentNameGeneratorProphecy->getSegmentName('subresource', false)->shouldBeCalled()->willReturn('subresources'); $pathSegmentNameGeneratorProphecy->getSegmentName('secondSubresource', false)->shouldBeCalled()->willReturn('second_subresources'); + $identifiersExtractorProphecy = $this->prophesize(IdentifiersExtractorInterface::class); + $identifiersExtractorProphecy->getIdentifiersFromResourceClass(Argument::type('string'))->willReturn(['id']); + $subresourceOperationFactory = new SubresourceOperationFactory( $resourceMetadataFactoryProphecy->reveal(), $propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), - $pathSegmentNameGeneratorProphecy->reveal() + $pathSegmentNameGeneratorProphecy->reveal(), + $identifiersExtractorProphecy->reveal() ); $this->assertEquals([ @@ -551,8 +598,9 @@ public function testCreateWithDifferentMaxDepthSelfReferencingSubresources() 'resource_class' => DummyEntity::class, 'shortNames' => ['dummyEntity'], 'identifiers' => [ - ['id', DummyEntity::class, true], + ['id', DummyEntity::class, true, ['id']], ], + 'identified_by' => ['id'], 'route_name' => 'api_dummy_entities_subresource_get_subresource', 'path' => '/dummy_entities/{id}/subresources.{_format}', 'operation_name' => 'subresource_get_subresource', @@ -563,9 +611,10 @@ public function testCreateWithDifferentMaxDepthSelfReferencingSubresources() 'resource_class' => DummyEntity::class, 'shortNames' => ['dummyEntity'], 'identifiers' => [ - ['id', DummyEntity::class, true], - ['subresource', DummyEntity::class, false], + ['id', DummyEntity::class, true, ['id']], + ['subresource', DummyEntity::class, false, ['id']], ], + 'identified_by' => ['id'], 'route_name' => 'api_dummy_entities_subresource_second_subresource_get_subresource', 'path' => '/dummy_entities/{id}/subresources/second_subresources.{_format}', 'operation_name' => 'subresource_second_subresource_get_subresource', @@ -576,8 +625,9 @@ public function testCreateWithDifferentMaxDepthSelfReferencingSubresources() 'resource_class' => DummyEntity::class, 'shortNames' => ['dummyEntity'], 'identifiers' => [ - ['id', DummyEntity::class, true], + ['id', DummyEntity::class, true, ['id']], ], + 'identified_by' => ['id'], 'route_name' => 'api_dummy_entities_second_subresource_get_subresource', 'path' => '/dummy_entities/{id}/second_subresources.{_format}', 'operation_name' => 'second_subresource_get_subresource', @@ -606,11 +656,15 @@ public function testCreateWithEnd() $pathSegmentNameGeneratorProphecy->getSegmentName('dummyEntity')->shouldBeCalled()->willReturn('dummy_entities'); $pathSegmentNameGeneratorProphecy->getSegmentName('subresource', true)->shouldBeCalled()->willReturn('subresource'); + $identifiersExtractorProphecy = $this->prophesize(IdentifiersExtractorInterface::class); + $identifiersExtractorProphecy->getIdentifiersFromResourceClass(Argument::type('string'))->willReturn(['id']); + $subresourceOperationFactory = new SubresourceOperationFactory( $resourceMetadataFactoryProphecy->reveal(), $propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), - $pathSegmentNameGeneratorProphecy->reveal() + $pathSegmentNameGeneratorProphecy->reveal(), + $identifiersExtractorProphecy->reveal() ); $result = $subresourceOperationFactory->create(DummyEntity::class); @@ -621,8 +675,9 @@ public function testCreateWithEnd() 'resource_class' => RelatedDummyEntity::class, 'shortNames' => ['relatedDummyEntity', 'dummyEntity'], 'identifiers' => [ - ['id', DummyEntity::class, true], + ['id', DummyEntity::class, true, ['id']], ], + 'identified_by' => ['id'], 'route_name' => 'api_dummy_entities_subresources_get_subresource', 'path' => '/dummy_entities/{id}/subresource.{_format}', 'operation_name' => 'subresources_get_subresource', @@ -633,9 +688,10 @@ public function testCreateWithEnd() 'resource_class' => DummyEntity::class, 'shortNames' => ['dummyEntity', 'relatedDummyEntity'], 'identifiers' => [ - ['id', DummyEntity::class, true], - ['subresource', RelatedDummyEntity::class, true], + ['id', DummyEntity::class, true, ['id']], + ['subresource', RelatedDummyEntity::class, true, ['id']], ], + 'identified_by' => ['id'], 'route_name' => 'api_dummy_entities_subresources_item_get_subresource', 'path' => '/dummy_entities/{id}/subresource/{subresource}.{_format}', 'operation_name' => 'subresources_item_get_subresource', @@ -664,11 +720,15 @@ public function testCreateWithEndButNoCollection() $pathSegmentNameGeneratorProphecy->getSegmentName('dummyEntity')->shouldBeCalled()->willReturn('dummy_entities'); $pathSegmentNameGeneratorProphecy->getSegmentName('subresource', false)->shouldBeCalled()->willReturn('subresource'); + $identifiersExtractorProphecy = $this->prophesize(IdentifiersExtractorInterface::class); + $identifiersExtractorProphecy->getIdentifiersFromResourceClass(Argument::type('string'))->willReturn(['id']); + $subresourceOperationFactory = new SubresourceOperationFactory( $resourceMetadataFactoryProphecy->reveal(), $propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), - $pathSegmentNameGeneratorProphecy->reveal() + $pathSegmentNameGeneratorProphecy->reveal(), + $identifiersExtractorProphecy->reveal() ); $result = $subresourceOperationFactory->create(DummyEntity::class); @@ -679,8 +739,9 @@ public function testCreateWithEndButNoCollection() 'resource_class' => RelatedDummyEntity::class, 'shortNames' => ['relatedDummyEntity', 'dummyEntity'], 'identifiers' => [ - ['id', DummyEntity::class, true], + ['id', DummyEntity::class, true, ['id']], ], + 'identified_by' => ['id'], 'route_name' => 'api_dummy_entities_subresource_get_subresource', 'path' => '/dummy_entities/{id}/subresource.{_format}', 'operation_name' => 'subresource_get_subresource', @@ -710,11 +771,15 @@ public function testCreateWithRootResourcePrefix() $pathSegmentNameGeneratorProphecy->getSegmentName('dummyEntity')->shouldBeCalled()->willReturn('dummy_entities'); $pathSegmentNameGeneratorProphecy->getSegmentName('subresource', false)->shouldBeCalled()->willReturn('subresource'); + $identifiersExtractorProphecy = $this->prophesize(IdentifiersExtractorInterface::class); + $identifiersExtractorProphecy->getIdentifiersFromResourceClass(Argument::type('string'))->willReturn(['id']); + $subresourceOperationFactory = new SubresourceOperationFactory( $resourceMetadataFactoryProphecy->reveal(), $propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), - $pathSegmentNameGeneratorProphecy->reveal() + $pathSegmentNameGeneratorProphecy->reveal(), + $identifiersExtractorProphecy->reveal() ); $this->assertEquals([ @@ -724,8 +789,9 @@ public function testCreateWithRootResourcePrefix() 'resource_class' => RelatedDummyEntity::class, 'shortNames' => ['relatedDummyEntity', 'dummyEntity'], 'identifiers' => [ - ['id', DummyEntity::class, true], + ['id', DummyEntity::class, true, ['id']], ], + 'identified_by' => ['id'], 'route_name' => 'api_dummy_entities_subresource_get_subresource', 'path' => '/root_resource_prefix/dummy_entities/{id}/subresource.{_format}', 'operation_name' => 'subresource_get_subresource', @@ -759,11 +825,15 @@ public function testCreateSelfReferencingSubresourcesWithSubresources() $pathSegmentNameGeneratorProphecy->getSegmentName('subresource', false)->shouldBeCalled()->willReturn('subresources'); $pathSegmentNameGeneratorProphecy->getSegmentName('otherSubresource', false)->shouldBeCalled()->willReturn('other_subresources'); + $identifiersExtractorProphecy = $this->prophesize(IdentifiersExtractorInterface::class); + $identifiersExtractorProphecy->getIdentifiersFromResourceClass(Argument::type('string'))->willReturn(['id']); + $subresourceOperationFactory = new SubresourceOperationFactory( $resourceMetadataFactoryProphecy->reveal(), $propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), - $pathSegmentNameGeneratorProphecy->reveal() + $pathSegmentNameGeneratorProphecy->reveal(), + $identifiersExtractorProphecy->reveal() ); $this->assertEquals([ @@ -773,8 +843,9 @@ public function testCreateSelfReferencingSubresourcesWithSubresources() 'resource_class' => DummyEntity::class, 'shortNames' => ['dummyEntity'], 'identifiers' => [ - ['id', DummyEntity::class, true], + ['id', DummyEntity::class, true, ['id']], ], + 'identified_by' => ['id'], 'route_name' => 'api_dummy_entities_subresource_get_subresource', 'path' => '/dummy_entities/{id}/subresources.{_format}', 'operation_name' => 'subresource_get_subresource', @@ -785,8 +856,9 @@ public function testCreateSelfReferencingSubresourcesWithSubresources() 'resource_class' => RelatedDummyEntity::class, 'shortNames' => ['relatedDummyEntity', 'dummyEntity'], 'identifiers' => [ - ['id', DummyEntity::class, true], + ['id', DummyEntity::class, true, ['id']], ], + 'identified_by' => ['id'], 'route_name' => 'api_dummy_entities_other_subresource_get_subresource', 'path' => '/dummy_entities/{id}/other_subresources.{_format}', 'operation_name' => 'other_subresource_get_subresource', diff --git a/tests/PathResolver/OperationPathResolverTest.php b/tests/PathResolver/OperationPathResolverTest.php new file mode 100644 index 00000000000..c664cef58b5 --- /dev/null +++ b/tests/PathResolver/OperationPathResolverTest.php @@ -0,0 +1,34 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Core\Tests\PathResolver; + +use ApiPlatform\Core\Api\OperationType; +use ApiPlatform\Core\Operation\UnderscorePathSegmentNameGenerator; +use ApiPlatform\Core\PathResolver\OperationPathResolver; +use PHPUnit\Framework\TestCase; + +class OperationPathResolverTest extends TestCase +{ + public function testResolveItemOperationPath() + { + $operationPathResolver = new OperationPathResolver(new UnderscorePathSegmentNameGenerator()); + $this->assertEquals('/foos/{id}.{_format}', $operationPathResolver->resolveOperationPath('Foo', [], OperationType::ITEM, 'get')); + } + + public function testResolveItemOperationPathIdentifiedBy() + { + $operationPathResolver = new OperationPathResolver(new UnderscorePathSegmentNameGenerator()); + $this->assertSame('/short_names/{isbn}.{_format}', $operationPathResolver->resolveOperationPath('ShortName', ['identified_by' => ['isbn']], OperationType::ITEM, 'get')); + } +} diff --git a/tests/Serializer/AbstractItemNormalizerTest.php b/tests/Serializer/AbstractItemNormalizerTest.php index 7f81ac608df..d9309862707 100644 --- a/tests/Serializer/AbstractItemNormalizerTest.php +++ b/tests/Serializer/AbstractItemNormalizerTest.php @@ -113,7 +113,6 @@ public function testSupportNormalizationAndSupportDenormalization() null, null, null, - false, [], [], null, @@ -180,7 +179,6 @@ public function testNormalize() null, null, null, - false, [], [], null, @@ -242,7 +240,6 @@ public function testNormalizeWithSecuredProperty() null, null, null, - false, [], [], null, @@ -315,7 +312,6 @@ public function testNormalizeReadableLinks() null, null, null, - false, [], [], null, @@ -377,7 +373,6 @@ public function testDenormalize() null, null, null, - false, [], [], null, @@ -439,7 +434,7 @@ public function testCanDenormalizeInputClassWithDifferentFieldsThanResourceClass $serializerProphecy->willImplement(DenormalizerInterface::class); $serializerProphecy->denormalize($data, DummyForAdditionalFieldsInput::class, 'json', $cleanedContextWithObjectToPopulate)->willReturn($dummyInputDto); - $normalizer = new class($propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), $iriConverterProphecy->reveal(), $resourceClassResolverProphecy->reveal(), null, null, null, null, false, [], [$inputDataTransformerProphecy->reveal()], null, null) extends AbstractItemNormalizer { + $normalizer = new class($propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), $iriConverterProphecy->reveal(), $resourceClassResolverProphecy->reveal(), null, null, null, null, [], [$inputDataTransformerProphecy->reveal()], null, null) extends AbstractItemNormalizer { }; $normalizer->setSerializer($serializerProphecy->reveal()); @@ -494,7 +489,6 @@ public function testDenormalizeWritableLinks() null, null, null, - false, [], [], null, @@ -514,7 +508,7 @@ public function testDenormalizeWritableLinks() public function testBadRelationType() { $this->expectException(UnexpectedValueException::class); - $this->expectExceptionMessage('Expected IRI or nested document for attribute "relatedDummy", "integer" given.'); + $this->expectExceptionMessage('Nested documents for attribute "relatedDummy" are not allowed. Use IRIs instead.'); $data = [ 'relatedDummy' => 22, @@ -556,7 +550,6 @@ public function testBadRelationType() null, null, null, - false, [], [], null, @@ -614,7 +607,6 @@ public function testInnerDocumentNotAllowed() null, null, null, - false, [], [], null, @@ -659,7 +651,6 @@ public function testBadType() null, null, null, - false, [], [], null, @@ -701,7 +692,6 @@ public function testTypeChecksCanBeDisabled() null, null, null, - false, [], [], null, @@ -747,7 +737,6 @@ public function testJsonAllowIntAsFloat() null, null, null, - false, [], [], null, @@ -822,7 +811,6 @@ public function testDenormalizeBadKeyType() null, null, null, - false, [], [], null, @@ -864,7 +852,6 @@ public function testNullable() null, null, null, - false, [], [], null, @@ -924,7 +911,6 @@ public function testChildInheritedProperty(): void null, null, null, - false, [], [], null, @@ -940,6 +926,7 @@ public function testChildInheritedProperty(): void public function testDenormalizeRelationWithPlainId() { + static::markTestSkipped('Legacy plain identifiers.'); $data = [ 'relatedDummy' => 1, ]; @@ -976,7 +963,6 @@ public function testDenormalizeRelationWithPlainId() null, null, $itemDataProviderProphecy->reveal(), - true, [], [], null, @@ -993,6 +979,7 @@ public function testDenormalizeRelationWithPlainId() public function testDenormalizeRelationWithPlainIdNotFound() { + $this->markTestSkipped('Legacy plain identifiers.'); $this->expectException(ItemNotFoundException::class); $this->expectExceptionMessage(sprintf('Item not found for resource "%s" with id "1".', RelatedDummy::class)); @@ -1039,7 +1026,6 @@ public function testDenormalizeRelationWithPlainIdNotFound() null, null, $itemDataProviderProphecy->reveal(), - true, [], [], null, @@ -1052,6 +1038,7 @@ public function testDenormalizeRelationWithPlainIdNotFound() public function testDoNotDenormalizeRelationWithPlainIdWhenPlainIdentifiersAreNotAllowed() { + $this->markTestSkipped('Legacy plain identifiers.'); $this->expectException(UnexpectedValueException::class); $this->expectExceptionMessage('Expected IRI or nested document for attribute "relatedDummy", "integer" given.'); @@ -1097,7 +1084,6 @@ public function testDoNotDenormalizeRelationWithPlainIdWhenPlainIdentifiersAreNo null, null, $itemDataProviderProphecy->reveal(), - false, [], [], null, @@ -1184,7 +1170,6 @@ public function testNormalizationWithDataTransformer() null, null, $itemDataProviderProphecy->reveal(), - false, [], [$dataTransformerProphecy->reveal(), $secondDataTransformerProphecy->reveal()], $resourceMetadataFactoryProphecy->reveal(), diff --git a/tests/Serializer/ItemNormalizerTest.php b/tests/Serializer/ItemNormalizerTest.php index 7a097aae786..6da44661c18 100644 --- a/tests/Serializer/ItemNormalizerTest.php +++ b/tests/Serializer/ItemNormalizerTest.php @@ -105,7 +105,6 @@ public function testNormalize() null, null, null, - false, null, [], null @@ -144,7 +143,6 @@ public function testDenormalize() null, null, null, - false, null, [], null @@ -184,7 +182,6 @@ public function testDenormalizeWithIri() null, null, null, - false, null, [], null @@ -221,7 +218,6 @@ public function testDenormalizeWithIdAndUpdateNotAllowed() null, null, null, - false, null, [], null @@ -260,7 +256,6 @@ public function testDenormalizeWithIdAndNoResourceClass() null, null, null, - false, null, [], null @@ -305,7 +300,6 @@ public function testNormalizeWithDataTransformers() null, null, null, - false, null, [$dataTransformer->reveal()], null diff --git a/tests/Serializer/SerializerFilterContextBuilderTest.php b/tests/Serializer/SerializerFilterContextBuilderTest.php index 72f8dbe4495..6973c39a096 100644 --- a/tests/Serializer/SerializerFilterContextBuilderTest.php +++ b/tests/Serializer/SerializerFilterContextBuilderTest.php @@ -150,6 +150,8 @@ public function testCreateFromRequestWithoutAttributes() $attributes = [ 'resource_class' => DummyGroup::class, 'collection_operation_name' => 'get', + 'identified_by' => null, + 'has_composite_identifier' => false, 'receive' => true, 'respond' => true, 'persist' => true, diff --git a/tests/Swagger/Serializer/DocumentationNormalizerV2Test.php b/tests/Swagger/Serializer/DocumentationNormalizerV2Test.php index 30f1c0ffcb8..6106b145e07 100644 --- a/tests/Swagger/Serializer/DocumentationNormalizerV2Test.php +++ b/tests/Swagger/Serializer/DocumentationNormalizerV2Test.php @@ -14,6 +14,7 @@ namespace ApiPlatform\Core\Tests\Swagger\Serializer; use ApiPlatform\Core\Api\FilterCollection; +use ApiPlatform\Core\Api\IdentifiersExtractorInterface; use ApiPlatform\Core\Api\OperationAwareFormatsProviderInterface; use ApiPlatform\Core\Api\OperationMethodResolverInterface; use ApiPlatform\Core\Api\OperationType; @@ -2210,7 +2211,10 @@ private function doTestNormalizeWithSubResource(OperationAwareFormatsProviderInt $propertyNameCollectionFactory = $propertyNameCollectionFactoryProphecy->reveal(); $propertyMetadataFactory = $propertyMetadataFactoryProphecy->reveal(); - $subresourceOperationFactory = new SubresourceOperationFactory($resourceMetadataFactory, $propertyNameCollectionFactory, $propertyMetadataFactory, new UnderscorePathSegmentNameGenerator()); + $identifiersExtractorProphecy = $this->prophesize(IdentifiersExtractorInterface::class); + $identifiersExtractorProphecy->getIdentifiersFromResourceClass(Argument::type('string'))->willReturn(['id']); + + $subresourceOperationFactory = new SubresourceOperationFactory($resourceMetadataFactory, $propertyNameCollectionFactory, $propertyMetadataFactory, new UnderscorePathSegmentNameGenerator(), $identifiersExtractorProphecy->reveal()); $normalizer = new DocumentationNormalizer( $resourceMetadataFactory, diff --git a/tests/Swagger/Serializer/DocumentationNormalizerV3Test.php b/tests/Swagger/Serializer/DocumentationNormalizerV3Test.php index b900a0ae2c3..ff583940641 100644 --- a/tests/Swagger/Serializer/DocumentationNormalizerV3Test.php +++ b/tests/Swagger/Serializer/DocumentationNormalizerV3Test.php @@ -14,6 +14,7 @@ namespace ApiPlatform\Core\Tests\Swagger\Serializer; use ApiPlatform\Core\Api\FilterCollection; +use ApiPlatform\Core\Api\IdentifiersExtractorInterface; use ApiPlatform\Core\Api\OperationAwareFormatsProviderInterface; use ApiPlatform\Core\Api\OperationMethodResolverInterface; use ApiPlatform\Core\Api\OperationType; @@ -2242,7 +2243,10 @@ private function doTestNormalizeWithSubResource(OperationAwareFormatsProviderInt $propertyNameCollectionFactory = $propertyNameCollectionFactoryProphecy->reveal(); $propertyMetadataFactory = $propertyMetadataFactoryProphecy->reveal(); - $subresourceOperationFactory = new SubresourceOperationFactory($resourceMetadataFactory, $propertyNameCollectionFactory, $propertyMetadataFactory, new UnderscorePathSegmentNameGenerator()); + $identifiersExtractorProphecy = $this->prophesize(IdentifiersExtractorInterface::class); + $identifiersExtractorProphecy->getIdentifiersFromResourceClass(Argument::type('string'))->willReturn(['id']); + + $subresourceOperationFactory = new SubresourceOperationFactory($resourceMetadataFactory, $propertyNameCollectionFactory, $propertyMetadataFactory, new UnderscorePathSegmentNameGenerator(), $identifiersExtractorProphecy->reveal()); $normalizer = new DocumentationNormalizer( $resourceMetadataFactory, diff --git a/tests/Util/RequestAttributesExtractorTest.php b/tests/Util/RequestAttributesExtractorTest.php index 37b2d082467..248877244e3 100644 --- a/tests/Util/RequestAttributesExtractorTest.php +++ b/tests/Util/RequestAttributesExtractorTest.php @@ -33,6 +33,8 @@ public function testExtractCollectionAttributes() 'receive' => true, 'respond' => true, 'persist' => true, + 'identified_by' => null, + 'has_composite_identifier' => false, ], RequestAttributesExtractor::extractAttributes($request) ); @@ -49,6 +51,8 @@ public function testExtractItemAttributes() 'receive' => true, 'respond' => true, 'persist' => true, + 'identified_by' => null, + 'has_composite_identifier' => false, ], RequestAttributesExtractor::extractAttributes($request) ); @@ -65,6 +69,8 @@ public function testExtractReceive() 'receive' => false, 'respond' => true, 'persist' => true, + 'identified_by' => null, + 'has_composite_identifier' => false, ], RequestAttributesExtractor::extractAttributes($request) ); @@ -78,6 +84,8 @@ public function testExtractReceive() 'receive' => true, 'respond' => true, 'persist' => true, + 'identified_by' => null, + 'has_composite_identifier' => false, ], RequestAttributesExtractor::extractAttributes($request) ); @@ -91,6 +99,8 @@ public function testExtractReceive() 'receive' => true, 'respond' => true, 'persist' => true, + 'identified_by' => null, + 'has_composite_identifier' => false, ], RequestAttributesExtractor::extractAttributes($request) ); @@ -107,6 +117,8 @@ public function testExtractRespond() 'receive' => true, 'respond' => false, 'persist' => true, + 'identified_by' => null, + 'has_composite_identifier' => false, ], RequestAttributesExtractor::extractAttributes($request) ); @@ -120,6 +132,8 @@ public function testExtractRespond() 'receive' => true, 'respond' => true, 'persist' => true, + 'identified_by' => null, + 'has_composite_identifier' => false, ], RequestAttributesExtractor::extractAttributes($request) ); @@ -133,6 +147,8 @@ public function testExtractRespond() 'receive' => true, 'respond' => true, 'persist' => true, + 'identified_by' => null, + 'has_composite_identifier' => false, ], RequestAttributesExtractor::extractAttributes($request) ); @@ -149,6 +165,8 @@ public function testExtractPersist() 'receive' => true, 'respond' => true, 'persist' => false, + 'identified_by' => null, + 'has_composite_identifier' => false, ], RequestAttributesExtractor::extractAttributes($request) ); @@ -162,6 +180,8 @@ public function testExtractPersist() 'receive' => true, 'respond' => true, 'persist' => true, + 'identified_by' => null, + 'has_composite_identifier' => false, ], RequestAttributesExtractor::extractAttributes($request) ); @@ -175,6 +195,26 @@ public function testExtractPersist() 'receive' => true, 'respond' => true, 'persist' => true, + 'identified_by' => null, + 'has_composite_identifier' => false, + ], + RequestAttributesExtractor::extractAttributes($request) + ); + } + + public function testExtractIdentifiedBy() + { + $request = new Request([], [], ['_api_resource_class' => 'Foo', '_api_item_operation_name' => 'get', '_api_identified_by' => ['test'], '_api_has_composite_identifier' => true]); + + $this->assertEquals( + [ + 'resource_class' => 'Foo', + 'item_operation_name' => 'get', + 'receive' => true, + 'respond' => true, + 'persist' => true, + 'identified_by' => ['test'], + 'has_composite_identifier' => true, ], RequestAttributesExtractor::extractAttributes($request) );