diff --git a/features/bootstrap/DoctrineContext.php b/features/bootstrap/DoctrineContext.php index 3901c90bc66..bdc32e8b5aa 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; @@ -77,6 +78,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; @@ -1589,6 +1591,18 @@ public function thereAreNetworkPathDummies(int $nb) $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(); + } + private function isOrm(): bool { return null !== $this->schemaTool; @@ -2006,4 +2020,12 @@ private function buildNetworkPathRelationDummy() { return $this->isOrm() ? new NetworkPathRelationDummy() : new NetworkPathRelationDummyDocument(); } + + /** + * @return BookDocument | Book + */ + private function buildBook() + { + return $this->isOrm() ? new Book() : new BookDocument(); + } } diff --git a/features/main/composite.feature b/features/main/composite.feature index 6fd4e9721ce..b4f0cdc5b8f 100644 --- a/features/main/composite.feature +++ b/features/main/composite.feature @@ -119,10 +119,11 @@ Feature: Retrieve data with Composite identifiers } """ - Scenario: Get the first composite relation with a missing identifier - Given there are Composite identifier objects - When I send a "GET" request to "/composite_relations/compositeLabel=1;" - Then the response status code should be 404 + # Deprecate ? + # Scenario: Get the first composite relation with a missing identifier + # Given there are Composite identifier objects + # When I send a "GET" request to "/composite_relations/compositeLabel=1;" + # Then the response status code should be 404 Scenario: Get first composite item Given there are Composite identifier objects diff --git a/features/main/operation.feature b/features/main/operation.feature index 8c70d1c2b99..3bdf02b2d9f 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 it's 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/src/Bridge/Doctrine/Orm/ItemDataProvider.php b/src/Bridge/Doctrine/Orm/ItemDataProvider.php index 980f2ca6e38..c6018d66eed 100644 --- a/src/Bridge/Doctrine/Orm/ItemDataProvider.php +++ b/src/Bridge/Doctrine/Orm/ItemDataProvider.php @@ -20,7 +20,6 @@ use ApiPlatform\Core\DataProvider\DenormalizedIdentifiersAwareItemDataProviderInterface; 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; @@ -65,19 +64,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, $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); diff --git a/src/Bridge/Symfony/Bundle/Resources/config/api.xml b/src/Bridge/Symfony/Bundle/Resources/config/api.xml index ffa00199f6d..7d1641df838 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% + diff --git a/src/Bridge/Symfony/Routing/ApiLoader.php b/src/Bridge/Symfony/Routing/ApiLoader.php index aaa093a1cfb..7cf0b1d27b1 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 = null) { /** @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; } /** @@ -221,6 +224,10 @@ private function addRoute(RouteCollection $routeCollection, string $resourceClas } } + if (!isset($operation['identifiedBy'])) { + $operation['identifiedBy'] = null !== $this->identifiersExtractor ? $this->identifiersExtractor->getIdentifiersFromResourceClass($resourceClass) : ['id']; + } + $path = trim(trim($resourceMetadata->getAttribute('route_prefix', '')), '/'); $path .= $this->operationPathResolver->resolveOperationPath($resourceShortName, $operation, $operationType, $operationName); @@ -230,6 +237,7 @@ private function addRoute(RouteCollection $routeCollection, string $resourceClas '_controller' => $controller, '_format' => null, '_api_resource_class' => $resourceClass, + '_api_identified_by' => \is_array($operation['identifiedBy']) ? $operation['identifiedBy'] : [$operation['identifiedBy']], 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 1e031609f8a..cfc8f6bcc71 100644 --- a/src/Bridge/Symfony/Routing/IriConverter.php +++ b/src/Bridge/Symfony/Routing/IriConverter.php @@ -25,6 +25,7 @@ use ApiPlatform\Core\Exception\InvalidIdentifierException; use ApiPlatform\Core\Exception\ItemNotFoundException; use ApiPlatform\Core\Exception\RuntimeException; +use ApiPlatform\Core\Identifier\CompositeIdentifierParser; use ApiPlatform\Core\Identifier\IdentifierConverterInterface; use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface; use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; @@ -94,10 +95,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,10 +145,16 @@ public function getItemIriFromResourceClass(string $resourceClass, array $identi { $routeName = $this->routeNameResolver->getRouteName($resourceClass, OperationType::ITEM); - try { - $identifiers = $this->generateIdentifiersUrl($identifiers, $resourceClass); + // Make composite identifiers work, there's only one identifier but the Resources has several: + if (1 < \count($identifiers)) { + $route = $this->router->getRouteCollection()->get($routeName); + if (1 === \count($identifiedBy = $route->getDefault('_api_identified_by'))) { + $identifiers = [$identifiedBy[0] => 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); } @@ -169,30 +172,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/OperationDataProviderTrait.php b/src/DataProvider/OperationDataProviderTrait.php index f08521ce379..80f4fc1dac8 100644 --- a/src/DataProvider/OperationDataProviderTrait.php +++ b/src/DataProvider/OperationDataProviderTrait.php @@ -85,38 +85,16 @@ 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']; - - if (null !== $this->identifierConverter) { - return $this->identifierConverter->convert((string) $id, $attributes['resource_class']); - } - - return $id; - } - - 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.'); - } - + $identifiersKeys = $attributes['identified_by'] ?? $attributes['subresource_context']['identifiers']; $identifiers = []; - - foreach ($attributes['subresource_context']['identifiers'] as $key => [$id, $resourceClass, $hasIdentifier]) { - if (false === $hasIdentifier) { - continue; + foreach ($identifiersKeys as $identifier) { + if (!isset($parameters[$identifier])) { + 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->identifierConverter->denormalize($identifiers, $attributes['resource_class']); } } 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 index 9faafd8ef54..a1628454256 100644 --- a/src/Identifier/IdentifierConverter.php +++ b/src/Identifier/IdentifierConverter.php @@ -25,7 +25,7 @@ * * @author Antoine Bluchet */ -final class IdentifierConverter implements ContextAwareIdentifierConverterInterface +final class IdentifierConverter implements NormalizeIdentifierConverterInterface { private $propertyMetadataFactory; private $identifiersExtractor; @@ -56,6 +56,7 @@ public function convert(string $data, string $class, array $context = []): array $keys = $this->identifiersExtractor->getIdentifiersFromResourceClass($class); if (($numIdentifiers = \count($keys)) > 1) { + // todo put this in normalizer $identifiers = CompositeIdentifierParser::parse($data); } elseif (0 === $numIdentifiers) { throw new InvalidIdentifierException(sprintf('Resource "%s" has no identifiers.', $class)); @@ -63,25 +64,32 @@ public function convert(string $data, string $class, array $context = []): array $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)); - } + return $this->denormalize($identifiers, $class); + } - if (null === $type = $this->getIdentifierType($class, $key)) { + /** + * {@inheritdoc} + */ + public function denormalize($identifiers, $class, $format = null, array $context = []): array + { + // Normalize every identifier (DateTime, UUID etc.) + foreach ($identifiers as $identifier => $value) { + if (null === $type = $this->getIdentifierType($class, $identifier)) { + if (preg_match_all(CompositeIdentifierParser::COMPOSITE_IDENTIFIER_REGEXP, $value)) { + return CompositeIdentifierParser::parse($value); + } continue; } foreach ($this->identifierDenormalizers as $identifierDenormalizer) { - if (!$identifierDenormalizer->supportsDenormalization($identifiers[$key], $type)) { + if (!$identifierDenormalizer->supportsDenormalization($value, $type)) { continue; } try { - $identifiers[$key] = $identifierDenormalizer->denormalize($identifiers[$key], $type); + $identifiers[$identifier] = $identifierDenormalizer->denormalize($value, $type); } catch (InvalidIdentifierException $e) { - throw new InvalidIdentifierException(sprintf('Identifier "%s" could not be denormalized.', $key), $e->getCode(), $e); + throw new InvalidIdentifierException(sprintf('Identifier "%s" could not be denormalized.', $identifier), $e->getCode(), $e); } } } diff --git a/src/Identifier/NormalizeIdentifierConverterInterface.php b/src/Identifier/NormalizeIdentifierConverterInterface.php new file mode 100644 index 00000000000..898fee3a312 --- /dev/null +++ b/src/Identifier/NormalizeIdentifierConverterInterface.php @@ -0,0 +1,27 @@ + + * + * 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; + +/** + * Gives access to the context in the IdentifierConverter. + * + * @author Antoine Bluchet + */ +interface NormalizeIdentifierConverterInterface extends ContextAwareIdentifierConverterInterface +{ + /** + * {@inheritdoc} + */ + public function denormalize(array $identifiers, string $class, string $format = null, array $context = []): array; +} diff --git a/src/PathResolver/OperationPathResolver.php b/src/PathResolver/OperationPathResolver.php index aae217f00de..23f382dd06b 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['identifiedBy'])) { + foreach ($operation['identifiedBy'] as $identifier) { + $path .= sprintf('/{%s}', $identifier); + } + } else { + $path .= '/{id}'; + } } $path .= '.{_format}'; diff --git a/src/Util/AttributesExtractor.php b/src/Util/AttributesExtractor.php index fcb93a59242..329b85c406d 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]; if ($subresourceContext = $attributes['_api_subresource_context'] ?? null) { $result['subresource_context'] = $subresourceContext; } diff --git a/tests/Bridge/Symfony/Bundle/DataCollector/RequestDataCollectorTest.php b/tests/Bridge/Symfony/Bundle/DataCollector/RequestDataCollectorTest.php index 7b3c867d7c0..9440b766049 100644 --- a/tests/Bridge/Symfony/Bundle/DataCollector/RequestDataCollectorTest.php +++ b/tests/Bridge/Symfony/Bundle/DataCollector/RequestDataCollectorTest.php @@ -138,6 +138,7 @@ public function testWithResource() $this->assertSame([ 'resource_class' => DummyEntity::class, + 'identified_by' => null, 'item_operation_name' => 'get', 'receive' => true, 'respond' => true, diff --git a/tests/Bridge/Symfony/Routing/ApiLoaderTest.php b/tests/Bridge/Symfony/Routing/ApiLoaderTest.php index 032b0cf2cbd..a70543cb678 100644 --- a/tests/Bridge/Symfony/Routing/ApiLoaderTest.php +++ b/tests/Bridge/Symfony/Routing/ApiLoaderTest.php @@ -302,6 +302,7 @@ private function getRoute(string $path, string $controller, string $resourceClas '_controller' => $controller, '_format' => null, '_api_resource_class' => $resourceClass, + '_api_identified_by' => ['id'], sprintf('_api_%s_operation_name', $collection ? 'collection' : 'item') => $operationName, ] + $extraDefaults, $requirements, diff --git a/tests/EventListener/DeserializeListenerTest.php b/tests/EventListener/DeserializeListenerTest.php index 25b4e3b5507..d65ab65b18f 100644 --- a/tests/EventListener/DeserializeListenerTest.php +++ b/tests/EventListener/DeserializeListenerTest.php @@ -200,6 +200,7 @@ public function testLegacyDeserializeResourceClassSupportedFormat(string $method $formatsProviderProphecy->getFormatsFromAttributes([ 'resource_class' => 'Foo', 'collection_operation_name' => 'post', + 'identified_by' => null, 'receive' => true, 'respond' => true, 'persist' => true, 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/Document/Book.php b/tests/Fixtures/TestBundle/Document/Book.php new file mode 100644 index 00000000000..5ceeb323f10 --- /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"=".+"}, "identifiedBy"="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/Entity/Book.php b/tests/Fixtures/TestBundle/Entity/Book.php new file mode 100644 index 00000000000..675abb06ecf --- /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"=".+"}, "identifiedBy"="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/Serializer/SerializerFilterContextBuilderTest.php b/tests/Serializer/SerializerFilterContextBuilderTest.php index 08a9583f0a0..3c0a0315dc0 100644 --- a/tests/Serializer/SerializerFilterContextBuilderTest.php +++ b/tests/Serializer/SerializerFilterContextBuilderTest.php @@ -147,6 +147,7 @@ public function testCreateFromRequestWithoutAttributes() $attributes = [ 'resource_class' => DummyGroup::class, 'collection_operation_name' => 'get', + 'identified_by' => null, 'receive' => true, 'respond' => true, 'persist' => true, diff --git a/tests/Util/RequestAttributesExtractorTest.php b/tests/Util/RequestAttributesExtractorTest.php index 37b2d082467..6ba3bd37a2a 100644 --- a/tests/Util/RequestAttributesExtractorTest.php +++ b/tests/Util/RequestAttributesExtractorTest.php @@ -33,6 +33,7 @@ public function testExtractCollectionAttributes() 'receive' => true, 'respond' => true, 'persist' => true, + 'identified_by' => null, ], RequestAttributesExtractor::extractAttributes($request) ); @@ -49,6 +50,7 @@ public function testExtractItemAttributes() 'receive' => true, 'respond' => true, 'persist' => true, + 'identified_by' => null, ], RequestAttributesExtractor::extractAttributes($request) ); @@ -65,6 +67,7 @@ public function testExtractReceive() 'receive' => false, 'respond' => true, 'persist' => true, + 'identified_by' => null, ], RequestAttributesExtractor::extractAttributes($request) ); @@ -78,6 +81,7 @@ public function testExtractReceive() 'receive' => true, 'respond' => true, 'persist' => true, + 'identified_by' => null, ], RequestAttributesExtractor::extractAttributes($request) ); @@ -91,6 +95,7 @@ public function testExtractReceive() 'receive' => true, 'respond' => true, 'persist' => true, + 'identified_by' => null, ], RequestAttributesExtractor::extractAttributes($request) ); @@ -107,6 +112,7 @@ public function testExtractRespond() 'receive' => true, 'respond' => false, 'persist' => true, + 'identified_by' => null, ], RequestAttributesExtractor::extractAttributes($request) ); @@ -120,6 +126,7 @@ public function testExtractRespond() 'receive' => true, 'respond' => true, 'persist' => true, + 'identified_by' => null, ], RequestAttributesExtractor::extractAttributes($request) ); @@ -133,6 +140,7 @@ public function testExtractRespond() 'receive' => true, 'respond' => true, 'persist' => true, + 'identified_by' => null, ], RequestAttributesExtractor::extractAttributes($request) ); @@ -149,6 +157,7 @@ public function testExtractPersist() 'receive' => true, 'respond' => true, 'persist' => false, + 'identified_by' => null, ], RequestAttributesExtractor::extractAttributes($request) ); @@ -162,6 +171,7 @@ public function testExtractPersist() 'receive' => true, 'respond' => true, 'persist' => true, + 'identified_by' => null, ], RequestAttributesExtractor::extractAttributes($request) ); @@ -175,6 +185,24 @@ public function testExtractPersist() 'receive' => true, 'respond' => true, 'persist' => true, + 'identified_by' => null, + ], + RequestAttributesExtractor::extractAttributes($request) + ); + } + + public function testExtractIdentifiedBy() + { + $request = new Request([], [], ['_api_resource_class' => 'Foo', '_api_item_operation_name' => 'get', '_api_identified_by' => ['test']]); + + $this->assertEquals( + [ + 'resource_class' => 'Foo', + 'item_operation_name' => 'get', + 'receive' => true, + 'respond' => true, + 'persist' => true, + 'identified_by' => ['test'], ], RequestAttributesExtractor::extractAttributes($request) );