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)
);