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 1c26db087c7..24dda9b8746 100644
--- a/features/main/operation.feature
+++ b/features/main/operation.feature
@@ -64,3 +64,22 @@ 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/src/Annotation/ApiResource.php b/src/Annotation/ApiResource.php
index 1208080d692..75635f88c19 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"),
@@ -217,7 +218,8 @@ public function __construct(
?string $sunset = null,
?array $swaggerContext = null,
?array $validationGroups = null,
- ?int $urlGenerationStrategy = null
+ ?int $urlGenerationStrategy = null,
+ ?bool $compositeIdentifier = null
) {
if (!\is_array($description)) { // @phpstan-ignore-line Doctrine annotations support
[$publicProperties, $configurableAttributes] = self::getConfigMetadata();
diff --git a/src/Bridge/Doctrine/MongoDbOdm/SubresourceDataProvider.php b/src/Bridge/Doctrine/MongoDbOdm/SubresourceDataProvider.php
index e263466320f..2d5b2d87fd5 100644
--- a/src/Bridge/Doctrine/MongoDbOdm/SubresourceDataProvider.php
+++ b/src/Bridge/Doctrine/MongoDbOdm/SubresourceDataProvider.php
@@ -121,8 +121,16 @@ private function buildAggregation(array $identifiers, array $context, array $exe
$topAggregationBuilder = $topAggregationBuilder ?? $previousAggregationBuilder;
- [$identifier, $identifierResourceClass] = $context['identifiers'][$remainingIdentifiers - 1];
- $previousAssociationProperty = $context['identifiers'][$remainingIdentifiers][0] ?? $context['property'];
+ if (\is_string(key($context['identifiers']))) {
+ $contextIdentifiers = array_keys($context['identifiers']);
+ $identifier = $contextIdentifiers[$remainingIdentifiers - 1];
+ $identifierResourceClass = $context['identifiers'][$identifier][0];
+ $previousAssociationProperty = $contextIdentifiers[$remainingIdentifiers] ?? $context['property'];
+ } else {
+ @trigger_error('Identifiers should match the convention introduced in ADR 0001-resource-identifiers, this behavior will be removed in 3.0.', E_USER_DEPRECATED);
+ [$identifier, $identifierResourceClass] = $context['identifiers'][$remainingIdentifiers - 1];
+ $previousAssociationProperty = $context['identifiers'][$remainingIdentifiers][0] ?? $context['property'];
+ }
$manager = $this->managerRegistry->getManagerForClass($identifierResourceClass);
if (!$manager instanceof DocumentManager) {
diff --git a/src/Bridge/Doctrine/Orm/SubresourceDataProvider.php b/src/Bridge/Doctrine/Orm/SubresourceDataProvider.php
index 9438a708fde..76b9cab0a5d 100644
--- a/src/Bridge/Doctrine/Orm/SubresourceDataProvider.php
+++ b/src/Bridge/Doctrine/Orm/SubresourceDataProvider.php
@@ -130,8 +130,16 @@ private function buildQuery(array $identifiers, array $context, QueryNameGenerat
$topQueryBuilder = $topQueryBuilder ?? $previousQueryBuilder;
- [$identifier, $identifierResourceClass] = $context['identifiers'][$remainingIdentifiers - 1];
- $previousAssociationProperty = $context['identifiers'][$remainingIdentifiers][0] ?? $context['property'];
+ if (\is_string(key($context['identifiers']))) {
+ $contextIdentifiers = array_keys($context['identifiers']);
+ $identifier = $contextIdentifiers[$remainingIdentifiers - 1];
+ $identifierResourceClass = $context['identifiers'][$identifier][0];
+ $previousAssociationProperty = $contextIdentifiers[$remainingIdentifiers] ?? $context['property'];
+ } else {
+ @trigger_error('Identifiers should match the convention introduced in ADR 0001-resource-identifiers, this behavior will be removed in 3.0.', E_USER_DEPRECATED);
+ [$identifier, $identifierResourceClass] = $context['identifiers'][$remainingIdentifiers - 1];
+ $previousAssociationProperty = $context['identifiers'][$remainingIdentifiers][0] ?? $context['property'];
+ }
$manager = $this->managerRegistry->getManagerForClass($identifierResourceClass);
diff --git a/src/Bridge/Symfony/Bundle/Resources/config/api.xml b/src/Bridge/Symfony/Bundle/Resources/config/api.xml
index f5b94eb917d..3dac225ca80 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%
+
@@ -288,6 +289,7 @@
+
diff --git a/src/Bridge/Symfony/Bundle/Resources/config/openapi.xml b/src/Bridge/Symfony/Bundle/Resources/config/openapi.xml
index 065dc4d33ad..f512cfd7f0b 100644
--- a/src/Bridge/Symfony/Bundle/Resources/config/openapi.xml
+++ b/src/Bridge/Symfony/Bundle/Resources/config/openapi.xml
@@ -42,6 +42,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..c2e6228de00 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;
}
/**
@@ -128,6 +131,8 @@ public function load($data, $type = null): RouteCollection
'_format' => null,
'_stateless' => $operation['stateless'] ?? $resourceMetadata->getAttribute('stateless'),
'_api_resource_class' => $operation['resource_class'],
+ '_api_identifiers' => $operation['identifiers'],
+ '_api_has_composite_identifier' => false,
'_api_subresource_operation_name' => $operation['route_name'],
'_api_subresource_context' => [
'property' => $operation['property'],
@@ -222,6 +227,8 @@ private function addRoute(RouteCollection $routeCollection, string $resourceClas
}
}
+ $operation['identifiers'] = (array) ($operation['identifiers'] ?? $resourceMetadata->getAttribute('identifiers', $this->identifiersExtractor ? $this->identifiersExtractor->getIdentifiersFromResourceClass($resourceClass) : ['id']));
+ $operation['has_composite_identifier'] = \count($operation['identifiers']) > 1 ? $resourceMetadata->getAttribute('composite_identifier', true) : false;
$path = trim(trim($resourceMetadata->getAttribute('route_prefix', '')), '/');
$path .= $this->operationPathResolver->resolveOperationPath($resourceShortName, $operation, $operationType, $operationName);
@@ -232,6 +239,8 @@ private function addRoute(RouteCollection $routeCollection, string $resourceClas
'_format' => null,
'_stateless' => $operation['stateless'],
'_api_resource_class' => $resourceClass,
+ '_api_identifiers' => $operation['identifiers'],
+ '_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..61de1d562bf 100644
--- a/src/Bridge/Symfony/Routing/IriConverter.php
+++ b/src/Bridge/Symfony/Routing/IriConverter.php
@@ -26,6 +26,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;
@@ -60,11 +61,7 @@ public function __construct(PropertyNameCollectionFactoryInterface $propertyName
$this->subresourceDataProvider = $subresourceDataProvider;
$this->identifierConverter = $identifierConverter;
$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 ?: new IdentifiersExtractor($propertyNameCollectionFactory, $propertyMetadataFactory, $propertyAccessor ?? PropertyAccess::createPropertyAccessor());
$this->resourceMetadataFactory = $resourceMetadataFactory;
}
@@ -148,11 +145,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 +170,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/Bridge/Symfony/Routing/RouteNameResolver.php b/src/Bridge/Symfony/Routing/RouteNameResolver.php
index e17f5e7eb52..b3ba719ffd9 100644
--- a/src/Bridge/Symfony/Routing/RouteNameResolver.php
+++ b/src/Bridge/Symfony/Routing/RouteNameResolver.php
@@ -67,8 +67,8 @@ private function isSameSubresource(array $context, array $currentContext): bool
$subresources = array_keys($context['subresource_resources']);
$currentSubresources = [];
- foreach ($currentContext['identifiers'] as $identifierContext) {
- $currentSubresources[] = $identifierContext[1];
+ foreach ($currentContext['identifiers'] as [$class]) {
+ $currentSubresources[] = $class;
}
return $currentSubresources === $subresources;
diff --git a/src/DataProvider/OperationDataProviderTrait.php b/src/DataProvider/OperationDataProviderTrait.php
index f08521ce379..a2754f92610 100644
--- a/src/DataProvider/OperationDataProviderTrait.php
+++ b/src/DataProvider/OperationDataProviderTrait.php
@@ -15,6 +15,7 @@
use ApiPlatform\Core\Exception\InvalidIdentifierException;
use ApiPlatform\Core\Exception\RuntimeException;
+use ApiPlatform\Core\Identifier\CompositeIdentifierParser;
use ApiPlatform\Core\Identifier\IdentifierConverterInterface;
/**
@@ -75,7 +76,15 @@ 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']);
+ // TODO: SubresourceDataProvider wants: ['id' => ['id' => 1], 'relatedDummies' => ['id' => 2]], identifiers is ['id' => 1, 'relatedDummies' => 2]
+ $subresourceIdentifiers = [];
+ foreach ($attributes['identifiers'] as $parameterName => [$class, $property]) {
+ if (false !== ($attributes['identifiers'][$parameterName][2] ?? null)) {
+ $subresourceIdentifiers[$parameterName] = [$property => $identifiers[$parameterName]];
+ }
+ }
+
+ return $this->subresourceDataProvider->getSubresource($attributes['resource_class'], $subresourceIdentifiers, $attributes['subresource_context'] + $context, $attributes['subresource_operation_name']);
}
/**
@@ -85,38 +94,32 @@ 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']);
- }
+ $identifiersKeys = $attributes['identifiers'] ?? ['id' => [$attributes['resource_class'], 'id']];
+ $identifiers = [];
- return $id;
- }
+ $identifiersNumber = \count($identifiersKeys);
+ foreach ($identifiersKeys as $parameterName => $identifiedBy) {
+ if (!isset($parameters[$parameterName])) {
+ 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));
+ }
- 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.');
- }
+ return $identifiers;
+ }
- $identifiers = [];
+ // TODO: Subresources tuple may have a third item representing if it is a "collection", this behavior will be removed in 3.0
+ if (false === ($identifiedBy[2] ?? null)) {
+ continue;
+ }
- foreach ($attributes['subresource_context']['identifiers'] as $key => [$id, $resourceClass, $hasIdentifier]) {
- if (false === $hasIdentifier) {
- continue;
+ throw new InvalidIdentifierException(sprintf('Parameter "%s" not found', $parameterName));
}
- $identifiers[$id] = $parameters[$id];
-
- if (null !== $this->identifierConverter) {
- $identifiers[$id] = $this->identifierConverter->convert((string) $identifiers[$id], $resourceClass);
- }
+ $identifiers[$parameterName] = $parameters[$parameterName];
}
- return $identifiers;
+ return $this->identifierConverter->convert($identifiers, $attributes['resource_class']);
}
}
diff --git a/src/GraphQl/Resolver/Stage/ReadStage.php b/src/GraphQl/Resolver/Stage/ReadStage.php
index 213976035c5..d5f87effef9 100644
--- a/src/GraphQl/Resolver/Stage/ReadStage.php
+++ b/src/GraphQl/Resolver/Stage/ReadStage.php
@@ -172,7 +172,7 @@ private function getSubresource(string $rootResolvedClass, array $rootResolvedFi
$resolvedIdentifiers = [];
$rootIdentifiers = array_keys($rootResolvedFields);
foreach ($rootIdentifiers as $rootIdentifier) {
- $resolvedIdentifiers[] = [$rootIdentifier, $rootResolvedClass];
+ $resolvedIdentifiers[$rootIdentifier] = [$rootResolvedClass, $rootIdentifier];
}
return $this->subresourceDataProvider->getSubresource($subresourceClass, $rootResolvedFields, $normalizationContext + [
diff --git a/src/Hydra/Serializer/DocumentationNormalizer.php b/src/Hydra/Serializer/DocumentationNormalizer.php
index be501d49e1a..a9ef38556f9 100644
--- a/src/Hydra/Serializer/DocumentationNormalizer.php
+++ b/src/Hydra/Serializer/DocumentationNormalizer.php
@@ -236,7 +236,7 @@ private function getHydraOperations(string $resourceClass, ResourceMetadata $res
if (null !== $this->subresourceOperationFactory) {
foreach ($this->subresourceOperationFactory->create($resourceClass) as $operationId => $operation) {
$subresourceMetadata = $this->resourceMetadataFactory->create($operation['resource_class']);
- $propertyMetadata = $this->propertyMetadataFactory->create(end($operation['identifiers'])[1], $operation['property']);
+ $propertyMetadata = $this->propertyMetadataFactory->create(end($operation['identifiers'])[0], $operation['property']);
$hydraOperations[] = $this->getHydraOperation($resourceClass, $subresourceMetadata, $operation['route_name'], $operation, "#{$subresourceMetadata->getShortName()}", OperationType::SUBRESOURCE, $propertyMetadata->getSubresource());
}
}
diff --git a/src/Identifier/CompositeIdentifierParser.php b/src/Identifier/CompositeIdentifierParser.php
index 3aa72c79bde..5d6fce92c74 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,17 @@ public static function parse(string $identifier): array
return $identifiers;
}
+
+ /**
+ * Renders composite identifiers to string using: key=value;key2=value2.
+ */
+ 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/ContextAwareIdentifierConverterInterface.php b/src/Identifier/ContextAwareIdentifierConverterInterface.php
index 83675089eb0..76f33ad81f0 100644
--- a/src/Identifier/ContextAwareIdentifierConverterInterface.php
+++ b/src/Identifier/ContextAwareIdentifierConverterInterface.php
@@ -23,5 +23,5 @@ interface ContextAwareIdentifierConverterInterface extends IdentifierConverterIn
/**
* {@inheritdoc}
*/
- public function convert(string $data, string $class, array $context = []): array;
+ public function convert($data, string $class, array $context = []): array;
}
diff --git a/src/Identifier/IdentifierConverter.php b/src/Identifier/IdentifierConverter.php
index c60651b1064..9d8f53620e0 100644
--- a/src/Identifier/IdentifierConverter.php
+++ b/src/Identifier/IdentifierConverter.php
@@ -33,6 +33,8 @@ final class IdentifierConverter implements ContextAwareIdentifierConverterInterf
private $resourceMetadataFactory;
/**
+ * TODO: rename identifierDenormalizers to identifierTransformers in 3.0 and change their interfaces to a IdentifierTransformerInterface.
+ *
* @param iterable $identifierDenormalizers
*/
public function __construct(IdentifiersExtractorInterface $identifiersExtractor, PropertyMetadataFactoryInterface $propertyMetadataFactory, iterable $identifierDenormalizers, ResourceMetadataFactoryInterface $resourceMetadataFactory = null)
@@ -46,43 +48,29 @@ public function __construct(IdentifiersExtractorInterface $identifiersExtractor,
/**
* {@inheritdoc}
*/
- public function convert(string $data, string $class, array $context = []): array
+ public function convert($data, string $class, array $context = []): array
{
- if (null !== $this->resourceMetadataFactory) {
- $resourceMetadata = $this->resourceMetadataFactory->create($class);
- $class = $resourceMetadata->getOperationAttribute($context, 'output', ['class' => $class], true)['class'];
+ if (!\is_array($data)) {
+ @trigger_error(sprintf('Not using an array as the first argument of "%s->convert" is deprecated since API Platform 2.6 and will not be possible anymore in API Platform 3', self::class), E_USER_DEPRECATED);
+ $data = ['id' => $data];
}
- $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)) {
+ $identifiers = $data;
+ foreach ($data as $identifier => $value) {
+ if (null === $type = $this->getIdentifierType($class, $identifier)) {
continue;
}
- foreach ($this->identifierDenormalizers as $identifierDenormalizer) {
- if (!$identifierDenormalizer->supportsDenormalization($identifiers[$key], $type)) {
+ /* @var DenormalizerInterface[] */
+ foreach ($this->identifierDenormalizers as $identifierTransformer) {
+ if (!$identifierTransformer->supportsDenormalization($value, $type)) {
continue;
}
try {
- $identifiers[$key] = $identifierDenormalizer->denormalize($identifiers[$key], $type);
- break;
+ $identifiers[$identifier] = $identifierTransformer->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/IdentifierConverterInterface.php b/src/Identifier/IdentifierConverterInterface.php
index 93962a2d47d..e642df9562c 100644
--- a/src/Identifier/IdentifierConverterInterface.php
+++ b/src/Identifier/IdentifierConverterInterface.php
@@ -28,12 +28,14 @@ interface IdentifierConverterInterface
public const HAS_IDENTIFIER_CONVERTER = 'has_identifier_converter';
/**
- * @param string $data Identifier to convert to php values
+ * Takes an array of strings representing identifiers and transform their values to the expected type.
+ *
+ * @param mixed $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;
+ public function convert($data, string $class): array;
}
diff --git a/src/OpenApi/Factory/OpenApiFactory.php b/src/OpenApi/Factory/OpenApiFactory.php
index 208299f814c..bc67eb98e6c 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;
@@ -54,8 +55,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, IdentifiersExtractorInterface $identifiersExtractor = null, array $formats = [], Options $openApiOptions = null, PaginationOptions $paginationOptions = null)
{
$this->resourceNameCollectionFactory = $resourceNameCollectionFactory;
$this->jsonSchemaFactory = $jsonSchemaFactory;
@@ -66,9 +68,10 @@ public function __construct(ResourceNameCollectionFactoryInterface $resourceName
$this->propertyNameCollectionFactory = $propertyNameCollectionFactory;
$this->propertyMetadataFactory = $propertyMetadataFactory;
$this->operationPathResolver = $operationPathResolver;
- $this->openApiOptions = $openApiOptions;
- $this->paginationOptions = $paginationOptions;
$this->subresourceOperationFactory = $subresourceOperationFactory;
+ $this->identifiersExtractor = $identifiersExtractor;
+ $this->openApiOptions = $openApiOptions ?: new Options('API Platform');
+ $this->paginationOptions = $paginationOptions ?: new PaginationOptions();
}
/**
@@ -121,6 +124,11 @@ private function collectPaths(ResourceMetadata $resourceMetadata, string $resour
$rootResourceClass = $resourceClass;
foreach ($operations as $operationName => $operation) {
+ $identifiers = (array) ($operation['identifiers'] ?? $resourceMetadata->getAttribute('identifiers', null === $this->identifiersExtractor ? ['id'] : $this->identifiersExtractor->getIdentifiersFromResourceClass($resourceClass)));
+ if (\count($identifiers) > 1 ? $resourceMetadata->getAttribute('composite_identifier', true) : false) {
+ $identifiers = ['id'];
+ }
+
$resourceClass = $operation['resource_class'] ?? $rootResourceClass;
$path = $this->getPath($resourceShortName, $operationName, $operation, $operationType);
$method = $resourceMetadata->getTypedOperationAttribute($operationType, $operationName, 'method', 'GET');
@@ -142,20 +150,15 @@ 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 $parameterName => $identifier) {
+ $parameters[] = new Model\Parameter(\is_string($parameterName) ? $parameterName : $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));
} elseif (OperationType::SUBRESOURCE === $operationType) {
- // FIXME: In SubresourceOperationFactory identifiers may happen twice
- $added = [];
- foreach ($operation['identifiers'] as $identifier) {
- if (\in_array($identifier[0], $added, true)) {
- continue;
- }
- $added[] = $identifier[0];
- $parameterShortname = $this->resourceMetadataFactory->create($identifier[1])->getShortName();
- $parameters[] = new Model\Parameter($identifier[0], 'path', $parameterShortname.' identifier', true, false, false, ['type' => 'string']);
+ foreach ($operation['identifiers'] as $parameterName => [$class, $property]) {
+ $parameters[] = new Model\Parameter($parameterName, 'path', $this->resourceMetadataFactory->create($class)->getShortName().' identifier', true, false, false, ['type' => 'string']);
}
if ($operation['collection']) {
diff --git a/src/Operation/Factory/SubresourceOperationFactory.php b/src/Operation/Factory/SubresourceOperationFactory.php
index b6dbd4298e5..0986b3c75f6 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 = null)
{
$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() ?: []) + ['identifiers' => !$this->identifiersExtractor ? [$property] : $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,6 +97,7 @@ private function computeSubresourceOperations(string $resourceClass, array &$tre
}
$rootResourceMetadata = $this->resourceMetadataFactory->create($rootResourceClass);
+ $rootResourceMetadata = $rootResourceMetadata->withAttributes(($rootResourceMetadata->getAttributes() ?: []) + ['identifiers' => !$this->identifiersExtractor ? ['id'] : $this->identifiersExtractor->getIdentifiersFromResourceClass($rootResourceClass)]);
$operationName = 'get';
$operation = [
'property' => $property,
@@ -102,8 +107,10 @@ private function computeSubresourceOperations(string $resourceClass, array &$tre
];
if (null === $parentOperation) {
+ $identifiers = (array) $rootResourceMetadata->getAttribute('identifiers');
$rootShortname = $rootResourceMetadata->getShortName();
- $operation['identifiers'] = [['id', $rootResourceClass, true]];
+ $identifier = \is_string($key = array_key_first($identifiers)) ? $key : $identifiers[0];
+ $operation['identifiers'][$identifier] = [$rootResourceClass, $identifiers[$identifier][1] ?? $identifier, true];
$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),
+ $identifier,
$this->pathSegmentNameGenerator->getSegmentName($operation['property'], $operation['collection']),
self::FORMAT_SUFFIX
);
@@ -138,8 +146,11 @@ private function computeSubresourceOperations(string $resourceClass, array &$tre
}
} else {
$resourceMetadata = $this->resourceMetadataFactory->create($resourceClass);
+ $identifiers = (array) $resourceMetadata->getAttribute('identifiers', null === $this->identifiersExtractor ? ['id'] : $this->identifiersExtractor->getIdentifiersFromResourceClass($resourceClass));
+ $identifier = \is_string($key = array_key_first($identifiers)) ? $key : $identifiers[0];
$operation['identifiers'] = $parentOperation['identifiers'];
- $operation['identifiers'][] = [$parentOperation['property'], $resourceClass, $isLastItem ? true : $parentOperation['collection']];
+ $operation['identifiers'][$parentOperation['property']] = [$resourceClass, $identifiers[$identifier][1] ?? $identifier, $isLastItem ? true : $parentOperation['collection']];
+
$operation['operation_name'] = str_replace(
'get'.self::SUBRESOURCE_SUFFIX,
RouteNameGenerator::inflector($isLastItem ? 'item' : $property, $operation['collection']).'_get'.self::SUBRESOURCE_SUFFIX,
@@ -159,8 +170,7 @@ private function computeSubresourceOperations(string $resourceClass, array &$tre
$operation['path'] = str_replace(self::FORMAT_SUFFIX, '', (string) $parentOperation['path']);
if ($parentOperation['collection']) {
- [$key] = end($operation['identifiers']);
- $operation['path'] .= sprintf('/{%s}', $key);
+ $operation['path'] .= sprintf('/{%s}', array_key_last($operation['identifiers']));
}
if ($isLastItem) {
diff --git a/src/PathResolver/OperationPathResolver.php b/src/PathResolver/OperationPathResolver.php
index aae217f00de..ad7ceddfc67 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['identifiers']) && (\count($operation['identifiers']) <= 1 || false === ($operation['has_composite_identifier'] ?? true))) {
+ foreach ($operation['identifiers'] as $parameterName => $identifier) {
+ $path .= sprintf('/{%s}', \is_string($parameterName) ? $parameterName : $identifier);
+ }
+ } else {
+ $path .= '/{id}';
+ }
}
$path .= '.{_format}';
diff --git a/src/Serializer/SerializerContextBuilder.php b/src/Serializer/SerializerContextBuilder.php
index d280b751a14..04a26f485c1 100644
--- a/src/Serializer/SerializerContextBuilder.php
+++ b/src/Serializer/SerializerContextBuilder.php
@@ -85,12 +85,12 @@ public function createFromRequest(Request $request, bool $normalization, array $
if (isset($attributes['subresource_context'])) {
$context['subresource_identifiers'] = [];
- foreach ($attributes['subresource_context']['identifiers'] as $key => [$id, $resourceClass]) {
+ foreach ($attributes['subresource_context']['identifiers'] as $parameterName => [$resourceClass]) {
if (!isset($context['subresource_resources'][$resourceClass])) {
$context['subresource_resources'][$resourceClass] = [];
}
- $context['subresource_identifiers'][$id] = $context['subresource_resources'][$resourceClass][$id] = $request->attributes->get($id);
+ $context['subresource_identifiers'][$parameterName] = $context['subresource_resources'][$resourceClass][$parameterName] = $request->attributes->get($parameterName);
}
}
diff --git a/src/Swagger/Serializer/DocumentationNormalizer.php b/src/Swagger/Serializer/DocumentationNormalizer.php
index a6a1085b845..77962bdd765 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;
@@ -99,6 +100,8 @@ final class DocumentationNormalizer implements NormalizerInterface, CacheableSup
ApiGatewayNormalizer::API_GATEWAY => false,
];
+ private $identifiersExtractor;
+
/**
* @param SchemaFactoryInterface|ResourceClassResolverInterface|null $jsonSchemaFactory
* @param ContainerInterface|FilterCollection|null $filterLocator
@@ -106,7 +109,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 = null, 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 = null, 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);
@@ -167,6 +170,7 @@ public function __construct(ResourceMetadataFactoryInterface $resourceMetadataFa
$this->defaultContext[self::SPEC_VERSION] = $swaggerVersions[0] ?? 2;
$this->defaultContext = array_merge($this->defaultContext, $defaultContext);
+ $this->identifiersExtractor = $identifiersExtractor;
}
/**
@@ -182,6 +186,9 @@ public function normalize($object, $format = null, array $context = [])
foreach ($object->getResourceNameCollection() as $resourceClass) {
$resourceMetadata = $this->resourceMetadataFactory->create($resourceClass);
+ if ($this->identifiersExtractor) {
+ $resourceMetadata = $resourceMetadata->withAttributes(($resourceMetadata->getAttributes() ?: []) + ['identifiers' => $this->identifiersExtractor->getIdentifiersFromResourceClass($resourceClass)]);
+ }
$resourceShortName = $resourceMetadata->getShortName();
// Items needs to be parsed first to be able to reference the lines from the collection operation
@@ -341,7 +348,7 @@ private function updateGetOperation(bool $v3, \ArrayObject $pathOperation, array
$outputResourseShortName = $resourceMetadata->getItemOperations()[$operationName]['output']['name'] ?? $resourceShortName;
$pathOperation['summary'] ?? $pathOperation['summary'] = sprintf('Retrieves a %s resource.', $outputResourseShortName);
- $pathOperation = $this->addItemOperationParameters($v3, $pathOperation);
+ $pathOperation = $this->addItemOperationParameters($v3, $pathOperation, $operationType, $operationName, $resourceMetadata);
$successResponse = ['description' => sprintf('%s resource response', $outputResourseShortName)];
[$successResponse] = $this->addSchemas($v3, $successResponse, $definitions, $resourceClass, $operationType, $operationName, $mimeTypes);
@@ -454,14 +461,17 @@ private function addSubresourceOperation(bool $v3, array $subresourceOperation,
// Avoid duplicates parameters when there is a filter on a subresource identifier
$parametersMemory = [];
$pathOperation['parameters'] = [];
- foreach ($subresourceOperation['identifiers'] as list($identifier, , $hasIdentifier)) {
- if (true === $hasIdentifier) {
- $parameter = ['name' => $identifier, 'in' => 'path', 'required' => true];
- $v3 ? $parameter['schema'] = ['type' => 'string'] : $parameter['type'] = 'string';
- $pathOperation['parameters'][] = $parameter;
- $parametersMemory[] = $identifier;
+ foreach ($subresourceOperation['identifiers'] as $parameterName => [$class, $identifier, $hasIdentifier]) {
+ if (false === strpos($subresourceOperation['path'], sprintf('{%s}', $parameterName))) {
+ continue;
}
+
+ $parameter = ['name' => $parameterName, 'in' => 'path', 'required' => true];
+ $v3 ? $parameter['schema'] = ['type' => 'string'] : $parameter['type'] = 'string';
+ $pathOperation['parameters'][] = $parameter;
+ $parametersMemory[] = $parameterName;
}
+
if ($parameters = $this->getFiltersParameters($v3, $subresourceOperation['resource_class'], $operationName, $subResourceMetadata)) {
foreach ($parameters as $parameter) {
if (!\in_array($parameter['name'], $parametersMemory, true)) {
@@ -487,7 +497,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)];
@@ -515,7 +525,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);
@@ -577,18 +587,30 @@ 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, 'identifiers', ['id'], true);
+ if (\count($identifiers) > 1 ? $resourceMetadata->getAttribute('composite_identifier', true) : false) {
+ $identifiers = ['id'];
+ }
+
+ if (!isset($pathOperation['parameters'])) {
+ $pathOperation['parameters'] = [];
+ }
+
+ foreach ($identifiers as $parameterName => $identifier) {
+ $parameter = [
+ 'name' => \is_string($parameterName) ? $parameterName : $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 56b73217b9d..3871abba58e 100644
--- a/src/Util/AttributesExtractor.php
+++ b/src/Util/AttributesExtractor.php
@@ -34,11 +34,23 @@ 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, 'has_composite_identifier' => $attributes['_api_has_composite_identifier'] ?? false];
if ($subresourceContext = $attributes['_api_subresource_context'] ?? null) {
$result['subresource_context'] = $subresourceContext;
}
+ // Normalizing identifiers tuples
+ $identifiers = [];
+ foreach (($attributes['_api_identifiers'] ?? ['id']) as $parameterName => $identifiedBy) {
+ if (\is_string($identifiedBy)) {
+ $identifiers[$identifiedBy] = [$result['resource_class'], $identifiedBy];
+ } else {
+ $identifiers[$parameterName] = $identifiedBy;
+ }
+ }
+
+ $result['identifiers'] = $identifiers;
+
if (null === $result['resource_class']) {
return [];
}
diff --git a/tests/Behat/DoctrineContext.php b/tests/Behat/DoctrineContext.php
index a3e8c367268..3d5754c4f34 100644
--- a/tests/Behat/DoctrineContext.php
+++ b/tests/Behat/DoctrineContext.php
@@ -17,6 +17,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;
@@ -28,6 +29,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;
@@ -82,6 +84,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;
@@ -93,6 +96,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;
@@ -1673,6 +1677,32 @@ public function thereIsAPatchDummyRelation()
$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;
@@ -2114,4 +2144,20 @@ private function buildPatchDummyRelation()
{
return $this->isOrm() ? new PatchDummyRelation() : new PatchDummyRelationDocument();
}
+
+ /**
+ * @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/tests/Bridge/Doctrine/Orm/SubresourceDataProviderTest.php b/tests/Bridge/Doctrine/Orm/SubresourceDataProviderTest.php
index 58290ca51e4..d9e49037bef 100644
--- a/tests/Bridge/Doctrine/Orm/SubresourceDataProviderTest.php
+++ b/tests/Bridge/Doctrine/Orm/SubresourceDataProviderTest.php
@@ -43,12 +43,14 @@
use Doctrine\Persistence\ObjectRepository;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
+use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait;
/**
* @author Kévin Dunglas
*/
class SubresourceDataProviderTest extends TestCase
{
+ use ExpectDeprecationTrait;
use ProphecyTrait;
private function assertIdentifierManagerMethodCalls($managerProphecy)
@@ -172,7 +174,7 @@ public function testGetSubresource()
$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, 'id']], 'collection' => true, IdentifierConverterInterface::HAS_IDENTIFIER_CONVERTER => true];
$this->assertEquals([], $dataProvider->getSubresource(RelatedDummy::class, ['id' => ['id' => 1]], $context));
}
@@ -266,7 +268,7 @@ public function testGetSubSubresourceItem()
$dataProvider = new SubresourceDataProvider($managerRegistryProphecy->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, 'id'], 'relatedDummies' => [RelatedDummy::class, 'id']], 'collection' => false, IdentifierConverterInterface::HAS_IDENTIFIER_CONVERTER => true];
$this->assertEquals($result, $dataProvider->getSubresource(ThirdLevel::class, ['id' => ['id' => 1], 'relatedDummies' => ['id' => 1]], $context));
}
@@ -322,7 +324,7 @@ public function testGetSubresourceOneToOneOwningRelation()
$dataProvider = new SubresourceDataProvider($managerRegistryProphecy->reveal(), $propertyNameCollectionFactory, $propertyMetadataFactory);
- $context = ['property' => 'ownedDummy', 'identifiers' => [['id', Dummy::class]], 'collection' => false, IdentifierConverterInterface::HAS_IDENTIFIER_CONVERTER => true];
+ $context = ['property' => 'ownedDummy', 'identifiers' => ['id' => [Dummy::class, 'id']], 'collection' => false, IdentifierConverterInterface::HAS_IDENTIFIER_CONVERTER => true];
$this->assertEquals([], $dataProvider->getSubresource(RelatedOwningDummy::class, ['id' => ['id' => 1]], $context));
}
@@ -382,7 +384,7 @@ public function testQueryResultExtension()
$dataProvider = new SubresourceDataProvider($managerRegistryProphecy->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, 'id']], 'collection' => true, IdentifierConverterInterface::HAS_IDENTIFIER_CONVERTER => true];
$this->assertEquals([], $dataProvider->getSubresource(RelatedDummy::class, ['id' => ['id' => 1]], $context));
}
@@ -426,6 +428,7 @@ public function testThrowResourceClassNotSupportedException()
*/
public function testGetSubSubresourceItemLegacy()
{
+ $this->expectDeprecation('Identifiers should match the convention introduced in ADR 0001-resource-identifiers, this behavior will be removed in 3.0.');
$managerRegistryProphecy = $this->prophesize(ManagerRegistry::class);
$identifiers = ['id'];
$funcProphecy = $this->prophesize(Func::class);
@@ -605,7 +608,7 @@ public function testGetSubresourceCollectionItem()
$dataProvider = new SubresourceDataProvider($managerRegistryProphecy->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, 'id', true], 'relatedDummies' => [RelatedDummy::class, 'id', true]], 'collection' => false, IdentifierConverterInterface::HAS_IDENTIFIER_CONVERTER => true];
$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 4875acf1b0c..980add90322 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,
+ 'has_composite_identifier' => false,
+ 'identifiers' => ['id' => [DummyEntity::class, 'id']],
'item_operation_name' => 'get',
'receive' => true,
'respond' => true,
@@ -282,7 +284,7 @@ public function getItem(string $resourceClass, $id, string $operationName = null
}
},
]));
- $itemDataProvider->getItem('', '', null, ['item_context']);
+ $itemDataProvider->getItem('', [], null, ['item_context']);
return $itemDataProvider;
}
diff --git a/tests/Bridge/Symfony/Routing/ApiLoaderTest.php b/tests/Bridge/Symfony/Routing/ApiLoaderTest.php
index 629b4bd2995..aae3a3b66eb 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(['identifiers' => '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, 'id', true]], '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', 'identifiers' => '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, 'id', true]], '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, 'id', true], 'recursivesubresource' => [DummyEntity::class, 'id', false]], '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, 'id', true]], '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, 'id', true], 'subresource' => [RelatedDummyEntity::class, 'id', true]], '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, 'id', true], 'secondrecursivesubresource' => [DummyEntity::class, 'id', false]], '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, 'id', true]], '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_identifiers' => ['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_identifiers' => $context['identifiers'],
+ '_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..763d7c3499e 100644
--- a/tests/Bridge/Symfony/Routing/IriConverterTest.php
+++ b/tests/Bridge/Symfony/Routing/IriConverterTest.php
@@ -34,6 +34,7 @@
use ApiPlatform\Core\Tests\ProphecyTrait;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
+use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait;
use Symfony\Component\Routing\Exception\RouteNotFoundException;
use Symfony\Component\Routing\RouterInterface;
@@ -43,6 +44,7 @@
class IriConverterTest extends TestCase
{
use ProphecyTrait;
+ use ExpectDeprecationTrait;
public function testGetItemFromIriNoRouteException()
{
@@ -76,6 +78,7 @@ public function testGetItemFromIriCollectionRouteException()
$routerProphecy->match('/users')->willReturn([
'_api_resource_class' => Dummy::class,
'_api_collection_operation_name' => 'get',
+ '_api_identifiers' => ['id'],
])->shouldBeCalledTimes(1);
$converter = $this->getIriConverter($routerProphecy);
@@ -89,13 +92,14 @@ public function testGetItemFromIriItemNotFoundException()
$itemDataProviderProphecy = $this->prophesize(ItemDataProviderInterface::class);
$itemDataProviderProphecy
- ->getItem(Dummy::class, 3, 'get', [])
+ ->getItem(Dummy::class, ['id' => 3], 'get', [IdentifierConverterInterface::HAS_IDENTIFIER_CONVERTER => true])
->shouldBeCalled()->willReturn(null);
$routerProphecy = $this->prophesize(RouterInterface::class);
$routerProphecy->match('/users/3')->willReturn([
'_api_resource_class' => Dummy::class,
'_api_item_operation_name' => 'get',
+ '_api_identifiers' => ['id'],
'id' => 3,
])->shouldBeCalledTimes(1);
@@ -107,12 +111,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, IdentifierConverterInterface::HAS_IDENTIFIER_CONVERTER => 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_identifiers' => ['id'],
'id' => 3,
])->shouldBeCalledTimes(1);
@@ -123,7 +128,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, IdentifierConverterInterface::HAS_IDENTIFIER_CONVERTER => true])
->willReturn('foo')
->shouldBeCalledTimes(1);
@@ -131,6 +136,7 @@ public function testGetItemFromIriWithOperationName()
$routerProphecy->match('/users/3')->willReturn([
'_api_resource_class' => 'AppBundle\Entity\User',
'_api_item_operation_name' => 'operation_name',
+ '_api_identifiers' => ['id'],
'id' => 3,
])->shouldBeCalledTimes(1);
@@ -215,7 +221,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 +254,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]);
}
@@ -255,11 +267,12 @@ public function testGetItemFromIriWithIdentifierConverter()
$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]);
+ $identifierConverterProphecy->convert(['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_identifiers' => ['id' => [Dummy::class, 'id']],
'id' => 3,
])->shouldBeCalledTimes(1);
@@ -270,17 +283,18 @@ public function testGetItemFromIriWithIdentifierConverter()
public function testGetItemFromIriWithSubresourceDataProvider()
{
$item = new \stdClass();
- $subresourceContext = ['identifiers' => [['id', Dummy::class, true]]];
+ $subresourceContext = ['identifiers' => ['id' => [Dummy::class, 'id', true]]];
$routeNameResolverProphecy = $this->prophesize(RouteNameResolverInterface::class);
$routerProphecy = $this->prophesize(RouterInterface::class);
$routerProphecy->match('/users/3/adresses')->willReturn([
'_api_resource_class' => Dummy::class,
'_api_subresource_context' => $subresourceContext,
'_api_subresource_operation_name' => 'get_subresource',
+ '_api_identifiers' => $subresourceContext['identifiers'],
'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, IdentifierConverterInterface::HAS_IDENTIFIER_CONVERTER => 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,17 +304,18 @@ 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, 'id', true]]];
$routeNameResolverProphecy = $this->prophesize(RouteNameResolverInterface::class);
$routerProphecy = $this->prophesize(RouterInterface::class);
$routerProphecy->match('/users/3/adresses')->willReturn([
'_api_resource_class' => Dummy::class,
'_api_subresource_context' => $subresourceContext,
'_api_subresource_operation_name' => 'get_subresource',
+ '_api_identifiers' => $subresourceContext['identifiers'],
'id' => 3,
])->shouldBeCalledTimes(1);
$identifierConverterProphecy = $this->prophesize(IdentifierConverterInterface::class);
- $identifierConverterProphecy->convert('3', Dummy::class)->shouldBeCalled()->willReturn(['id' => 3]);
+ $identifierConverterProphecy->convert(['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);
@@ -318,16 +333,19 @@ public function testGetItemFromIriBadIdentifierException()
$routerProphecy->match('/users/3')->willReturn([
'_api_resource_class' => Dummy::class,
'_api_item_operation_name' => 'get_subresource',
+ '_api_identifiers' => ['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".'));
+ $identifierConverterProphecy->convert(['id' => '3'], Dummy::class)->shouldBeCalled()->willThrow(new InvalidIdentifierException('Item not found for "/users/3".'));
$converter = $this->getIriConverter($routerProphecy, $routeNameResolverProphecy, null, null, $identifierConverterProphecy);
$this->assertEquals($converter->getItemFromIri('/users/3', ['fetch_data' => true]), $item);
}
public function testNoIdentifiersException()
{
+ $this->markTestSkipped('The method "generateIdentifiersUrl" has been removed.');
+ /* @phpstan-ignore-next-line */
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('No identifiers defined for resource of type "\App\Entity\Sample"');
@@ -343,11 +361,10 @@ public function testNoIdentifiersException()
/**
* @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()
{
+ $this->expectDeprecation('Not injecting ApiPlatform\Core\Api\ResourceClassResolverInterface in the IdentifiersExtractor might introduce cache issues with object identifiers.');
$propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class);
$propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class);
$routerProphecy = $this->prophesize(RouterInterface::class);
@@ -392,6 +409,13 @@ private function getIriConverter($routerProphecy = null, $routeNameResolverProph
$propertyNameCollectionFactory = $propertyNameCollectionFactoryProphecy->reveal();
$propertyMetadataFactory = $propertyMetadataFactoryProphecy->reveal();
+ if (null === $identifierConverterProphecy) {
+ $identifierConverterProphecy = $this->prophesize(IdentifierConverterInterface::class);
+ $identifierConverterProphecy->convert(Argument::type('array'), Argument::type('string'))->will(function ($args) {
+ return $args[0];
+ });
+ }
+
return new IriConverter(
$propertyNameCollectionFactory,
$propertyMetadataFactory,
@@ -401,7 +425,7 @@ private function getIriConverter($routerProphecy = null, $routeNameResolverProph
null,
new IdentifiersExtractor($propertyNameCollectionFactory, $propertyMetadataFactory, null, $this->getResourceClassResolver()),
$subresourceDataProviderProphecy ? $subresourceDataProviderProphecy->reveal() : null,
- $identifierConverterProphecy ? $identifierConverterProphecy->reveal() : null,
+ $identifierConverterProphecy->reveal(),
null,
$resourceMetadataFactory
);
diff --git a/tests/Bridge/Symfony/Routing/RouteNameResolverTest.php b/tests/Bridge/Symfony/Routing/RouteNameResolverTest.php
index ae602f62582..7f7ba96d33b 100644
--- a/tests/Bridge/Symfony/Routing/RouteNameResolverTest.php
+++ b/tests/Bridge/Symfony/Routing/RouteNameResolverTest.php
@@ -172,12 +172,12 @@ public function testGetRouteNameForSubresourceRoute()
$routeCollection->add('a_some_subresource_route', new Route('/a/some/item/path/{id}', [
'_api_resource_class' => 'AppBundle\Entity\User',
'_api_subresource_operation_name' => 'some_other_item_op',
- '_api_subresource_context' => ['identifiers' => [[1, 'bar']]],
+ '_api_subresource_context' => ['identifiers' => ['id' => ['Bar', 'id']]],
]));
$routeCollection->add('b_some_subresource_route', new Route('/b/some/item/path/{id}', [
'_api_resource_class' => 'AppBundle\Entity\User',
'_api_subresource_operation_name' => 'some_item_op',
- '_api_subresource_context' => ['identifiers' => [[1, 'foo']]],
+ '_api_subresource_context' => ['identifiers' => ['id' => ['Foo', 'id']]],
]));
$routeCollection->add('some_collection_route', new Route('/some/collection/path', [
'_api_resource_class' => 'AppBundle\Entity\User',
@@ -188,7 +188,7 @@ public function testGetRouteNameForSubresourceRoute()
$routerProphecy->getRouteCollection()->willReturn($routeCollection);
$routeNameResolver = new RouteNameResolver($routerProphecy->reveal());
- $actual = $routeNameResolver->getRouteName('AppBundle\Entity\User', OperationType::SUBRESOURCE, ['subresource_resources' => ['foo' => 1]]);
+ $actual = $routeNameResolver->getRouteName('AppBundle\Entity\User', OperationType::SUBRESOURCE, ['subresource_resources' => ['Foo' => 1]]);
$this->assertSame('b_some_subresource_route', $actual);
}
diff --git a/tests/EventListener/DeserializeListenerTest.php b/tests/EventListener/DeserializeListenerTest.php
index 6563567b420..f35f30d3542 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());
}
diff --git a/tests/EventListener/ReadListenerTest.php b/tests/EventListener/ReadListenerTest.php
index a809dfdd537..06b5c05502c 100644
--- a/tests/EventListener/ReadListenerTest.php
+++ b/tests/EventListener/ReadListenerTest.php
@@ -188,7 +188,7 @@ public function testRetrieveCollectionGet()
public function testRetrieveItem()
{
$identifierConverter = $this->prophesize(IdentifierConverterInterface::class);
- $identifierConverter->convert('1', 'Foo')->shouldBeCalled()->willReturn(['id' => '1']);
+ $identifierConverter->convert(['id' => '1'], 'Foo')->shouldBeCalled()->willReturn(['id' => '1']);
$collectionDataProvider = $this->prophesize(CollectionDataProviderInterface::class);
$collectionDataProvider->getCollection()->shouldNotBeCalled();
@@ -200,7 +200,7 @@ public function testRetrieveItem()
$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_format' => 'json', '_api_mime_type' => 'application/json']);
$request->setMethod('GET');
$event = $this->prophesize(RequestEvent::class);
@@ -241,7 +241,7 @@ public function testRetrieveItemNoIdentifier()
public function testRetrieveSubresource()
{
$identifierConverter = $this->prophesize(IdentifierConverterInterface::class);
- $identifierConverter->convert('1', 'Bar')->shouldBeCalled()->willReturn(['id' => '1']);
+ $identifierConverter->convert(['id' => '1'], 'Foo')->shouldBeCalled()->willReturn(['id' => '1']);
$collectionDataProvider = $this->prophesize(CollectionDataProviderInterface::class);
$collectionDataProvider->getCollection()->shouldNotBeCalled();
@@ -251,9 +251,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', IdentifierConverterInterface::HAS_IDENTIFIER_CONVERTER => true], '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);
@@ -270,19 +270,22 @@ public function testRetrieveSubresourceNoDataProvider()
{
$this->expectException(RuntimeException::class);
+ $identifierConverter = $this->prophesize(IdentifierConverterInterface::class);
+ $identifierConverter->convert(['id' => '1'], 'Foo')->shouldBeCalled()->willReturn(['id' => '1']);
+
$collectionDataProvider = $this->prophesize(CollectionDataProviderInterface::class);
$collectionDataProvider->getCollection()->shouldNotBeCalled();
$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');
@@ -291,7 +294,7 @@ public function testRetrieveSubresourceNoDataProvider()
public function testRetrieveSubresourceNotFound()
{
$identifierConverter = $this->prophesize(IdentifierConverterInterface::class);
- $identifierConverter->convert('1', 'Bar')->willThrow(new InvalidIdentifierException())->shouldBeCalled();
+ $identifierConverter->convert(['id' => '1'], 'Foo')->willThrow(new InvalidIdentifierException())->shouldBeCalled();
$this->expectException(NotFoundHttpException::class);
$collectionDataProvider = $this->prophesize(CollectionDataProviderInterface::class);
@@ -300,7 +303,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);
@@ -313,7 +316,7 @@ public function testRetrieveSubresourceNotFound()
public function testRetrieveItemNotFound()
{
$identifierConverter = $this->prophesize(IdentifierConverterInterface::class);
- $identifierConverter->convert('22', 'Foo')->shouldBeCalled()->willReturn(['id' => 22]);
+ $identifierConverter->convert(['id' => '22'], 'Foo')->shouldBeCalled()->willReturn(['id' => 22]);
$this->expectException(NotFoundHttpException::class);
$collectionDataProvider = $this->prophesize(CollectionDataProviderInterface::class);
@@ -323,7 +326,7 @@ public function testRetrieveItemNotFound()
$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_format' => 'json', '_api_mime_type' => 'application/json']);
$request->setMethod('GET');
$event = $this->prophesize(RequestEvent::class);
@@ -338,13 +341,13 @@ public function testRetrieveBadItemNormalizedIdentifiers()
$this->expectException(NotFoundHttpException::class);
$identifierConverter = $this->prophesize(IdentifierConverterInterface::class);
- $identifierConverter->convert('1', 'Foo')->shouldBeCalled()->willThrow(new InvalidIdentifierException());
+ $identifierConverter->convert(['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_format' => 'json', '_api_mime_type' => 'application/json']);
$request->setMethod(Request::METHOD_GET);
$event = $this->prophesize(RequestEvent::class);
@@ -359,7 +362,7 @@ 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->convert(Argument::type('array'), Argument::type('string'))->shouldBeCalled()->willThrow(new InvalidIdentifierException());
$collectionDataProvider = $this->prophesize(CollectionDataProviderInterface::class);
$collectionDataProvider->getCollection()->shouldNotBeCalled();
@@ -370,7 +373,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/Document/Book.php b/tests/Fixtures/TestBundle/Document/Book.php
new file mode 100644
index 00000000000..32e880d6855
--- /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"=".+"}, "identifiers"="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/Document/SlugParentDummy.php b/tests/Fixtures/TestBundle/Document/SlugParentDummy.php
index 7c96edd1882..730233c51b6 100644
--- a/tests/Fixtures/TestBundle/Document/SlugParentDummy.php
+++ b/tests/Fixtures/TestBundle/Document/SlugParentDummy.php
@@ -23,7 +23,7 @@
/**
* Custom Identifier Dummy With Subresource.
*
- * @ApiResource
+ * @ApiResource(attributes={"identifiers"="slug"})
* @ODM\Document
*/
class SlugParentDummy
diff --git a/tests/Fixtures/TestBundle/Entity/Book.php b/tests/Fixtures/TestBundle/Entity/Book.php
new file mode 100644
index 00000000000..e2e874edbad
--- /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"=".+"}, "identifiers"="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..c24e784cc91 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={"identifiers"="slug"})
* @ORM\Entity
*/
class SlugParentDummy
diff --git a/tests/Fixtures/TestBundle/Model/ProductInterface.php b/tests/Fixtures/TestBundle/Model/ProductInterface.php
index 04b095817e5..ba482a0d6c3 100644
--- a/tests/Fixtures/TestBundle/Model/ProductInterface.php
+++ b/tests/Fixtures/TestBundle/Model/ProductInterface.php
@@ -21,6 +21,7 @@
/**
* @ApiResource(
* shortName="Product",
+ * attributes={"identifiers"="code"},
* normalizationContext={
* "groups"={"product_read"},
* },
diff --git a/tests/Fixtures/TestBundle/Model/TaxonInterface.php b/tests/Fixtures/TestBundle/Model/TaxonInterface.php
index e715e8d6348..b3386d45f90 100644
--- a/tests/Fixtures/TestBundle/Model/TaxonInterface.php
+++ b/tests/Fixtures/TestBundle/Model/TaxonInterface.php
@@ -20,6 +20,7 @@
/**
* @ApiResource(
+ * attributes={"identifiers"="code"},
* shortName="Taxon",
* normalizationContext={
* "groups"={"taxon_read"},
diff --git a/tests/GraphQl/Resolver/Stage/ReadStageTest.php b/tests/GraphQl/Resolver/Stage/ReadStageTest.php
index 981d3a56693..1facf8d56a7 100644
--- a/tests/GraphQl/Resolver/Stage/ReadStageTest.php
+++ b/tests/GraphQl/Resolver/Stage/ReadStageTest.php
@@ -212,7 +212,7 @@ public function testApplyCollection(array $args, ?string $rootClass, ?array $sou
$normalizationContext = ['normalization' => true];
$this->serializerContextBuilderProphecy->create($resourceClass, $operationName, $context, true)->shouldBeCalled()->willReturn($normalizationContext);
- $this->subresourceDataProviderProphecy->getSubresource($resourceClass, ['id' => 3], $normalizationContext + ['filters' => $expectedFilters, 'property' => $fieldName, 'identifiers' => [['id', $resourceClass]], 'collection' => true], $operationName)->willReturn(['subresource']);
+ $this->subresourceDataProviderProphecy->getSubresource($resourceClass, ['id' => 3], $normalizationContext + ['filters' => $expectedFilters, 'property' => $fieldName, 'identifiers' => ['id' => [$resourceClass, 'id']], 'collection' => true], $operationName)->willReturn(['subresource']);
$this->collectionDataProviderProphecy->getCollection($resourceClass, $operationName, $normalizationContext + ['filters' => $expectedFilters])->willReturn([]);
diff --git a/tests/Hydra/Serializer/DocumentationNormalizerTest.php b/tests/Hydra/Serializer/DocumentationNormalizerTest.php
index b0ca173a402..e75d39186e8 100644
--- a/tests/Hydra/Serializer/DocumentationNormalizerTest.php
+++ b/tests/Hydra/Serializer/DocumentationNormalizerTest.php
@@ -95,7 +95,7 @@ private function doTestNormalize(OperationMethodResolverInterface $operationMeth
'resource_class' => 'relatedDummy',
'shortNames' => ['relatedDummy'],
'identifiers' => [
- ['id', 'dummy', true],
+ 'id' => ['dummy', 'id', true],
],
'route_name' => 'api_dummies_subresource_get_related_dummy',
'path' => '/dummies/{id}/related_dummy.{_format}',
diff --git a/tests/Identifier/CompositeIdentifierParserTest.php b/tests/Identifier/CompositeIdentifierParserTest.php
index c78f00c2847..79e87b99e0d 100644
--- a/tests/Identifier/CompositeIdentifierParserTest.php
+++ b/tests/Identifier/CompositeIdentifierParserTest.php
@@ -41,4 +41,27 @@ public function variousIdentifiers(): array
'foo=test=bar;bar=' => ['foo' => 'test=bar', 'bar' => ''],
]]];
}
+
+ /**
+ * @dataProvider compositeIdentifiers
+ */
+ public function testStringify(array $identifiers)
+ {
+ foreach ($identifiers as $string => $arr) {
+ $this->assertEquals(CompositeIdentifierParser::stringify($arr), $string);
+ }
+ }
+
+ public function compositeIdentifiers(): array
+ {
+ return [[[
+ 'a=bd;dc=d' => ['a' => 'bd', 'dc' => 'd'],
+ 'a=b;c=d foo;d23i=e' => ['a' => 'b', 'c' => 'd foo', 'd23i' => 'e'],
+ 'a=1;c=2;d=10-30-24' => ['a' => '1', 'c' => '2', 'd' => '10-30-24'],
+ 'a=test;b=bar;foo;c=123' => ['a' => 'test', 'b' => 'bar;foo', 'c' => '123'],
+ 'foo=test=bar;;bar=bazzz' => ['foo' => 'test=bar;', 'bar' => 'bazzz'],
+ 'foo=test=bar;bar=;test=foo' => ['foo' => 'test=bar', 'bar' => '', 'test' => 'foo'],
+ 'foo=test=bar;bar=' => ['foo' => 'test=bar', 'bar' => ''],
+ ]]];
+ }
}
diff --git a/tests/Identifier/IdentifierConverterTest.php b/tests/Identifier/IdentifierConverterTest.php
index effe2759411..00251d928d7 100644
--- a/tests/Identifier/IdentifierConverterTest.php
+++ b/tests/Identifier/IdentifierConverterTest.php
@@ -32,6 +32,8 @@ class IdentifierConverterTest extends TestCase
public function testCompositeIdentifier()
{
+ $this->markTestSkipped('This behavior is now external to the identifier converter.');
+ /** @phpstan-ignore-next-line */
$identifier = 'a=1;c=2;d=2015-04-05';
$class = 'Dummy';
@@ -58,7 +60,7 @@ public function testCompositeIdentifier()
public function testSingleDateIdentifier()
{
- $identifier = '2015-04-05';
+ $identifier = ['funkyid' => '2015-04-05'];
$class = 'Dummy';
$dateIdentifierPropertyMetadata = (new PropertyMetadata())->withIdentifier(true)->withType(new Type(Type::BUILTIN_TYPE_OBJECT, false, \DateTime::class));
@@ -77,7 +79,7 @@ public function testSingleDateIdentifier()
public function testIntegerIdentifier()
{
- $identifier = '42';
+ $identifier = ['id' => '42'];
$class = 'Dummy';
$integerIdentifierPropertyMetadata = (new PropertyMetadata())->withIdentifier(true)->withType(new Type(Type::BUILTIN_TYPE_INT));
diff --git a/tests/OpenApi/Factory/OpenApiFactoryTest.php b/tests/OpenApi/Factory/OpenApiFactoryTest.php
index 3c8953f301f..8b66422febf 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,
@@ -158,6 +162,7 @@ public function testInvoke(): void
$operationPathResolver,
$filterLocatorProphecy->reveal(),
$subresourceOperationFactoryProphecy->reveal(),
+ $identifiersExtractorProphecy->reveal(),
[],
new Options('Test API', 'This is a test API.', '1.2.3', true, 'oauth2', 'authorizationCode', '/oauth/v2/token', '/oauth/v2/auth', '/oauth/v2/refresh', ['scope param'], [
'header' => [
@@ -525,6 +530,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,
@@ -535,6 +543,7 @@ public function testOverrideDocumentation()
$operationPathResolver,
$filterLocatorProphecy->reveal(),
$subresourceOperationFactoryProphecy->reveal(),
+ $identifiersExtractorProphecy->reveal(),
[],
new Options('Test API', 'This is a test API.', '1.2.3', true, 'oauth2', 'authorizationCode', '/oauth/v2/token', '/oauth/v2/auth', '/oauth/v2/refresh', ['scope param'], [
'header' => [
@@ -614,7 +623,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]));
@@ -634,6 +647,7 @@ public function testSubresourceDocumentation()
$operationPathResolver,
$filterLocatorProphecy->reveal(),
$subresourceOperationFactory,
+ $identifiersExtractor,
['jsonld' => ['application/ld+json']],
new Options('Test API', 'This is a test API.', '1.2.3', true, 'oauth2', 'authorizationCode', '/oauth/v2/token', '/oauth/v2/auth', '/oauth/v2/refresh', ['scope param'], [
'header' => [
diff --git a/tests/OpenApi/Serializer/OpenApiNormalizerTest.php b/tests/OpenApi/Serializer/OpenApiNormalizerTest.php
index bed21b8eca2..8ba832a5007 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,
@@ -105,6 +109,7 @@ public function testNormalize()
$operationPathResolver,
$filterLocatorProphecy->reveal(),
$subresourceOperationFactoryProphecy->reveal(),
+ $identifiersExtractorProphecy->reveal(),
[],
new Options('Test API', 'This is a test API.', '1.2.3', true, 'oauth2', 'authorizationCode', '/oauth/v2/token', '/oauth/v2/auth', '/oauth/v2/refresh', ['scope param'], [
'header' => [
diff --git a/tests/Operation/Factory/SubresourceOperationFactoryTest.php b/tests/Operation/Factory/SubresourceOperationFactoryTest.php
index c811b159097..59e644a14d1 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,7 +76,7 @@ public function testCreate()
'resource_class' => RelatedDummyEntity::class,
'shortNames' => ['relatedDummyEntity', 'dummyEntity'],
'identifiers' => [
- ['id', DummyEntity::class, true],
+ 'id' => [DummyEntity::class, 'id', true],
],
'route_name' => 'api_dummy_entities_subresource_get_subresource',
'path' => '/dummy_entities/{id}/subresource.{_format}',
@@ -83,8 +88,8 @@ public function testCreate()
'resource_class' => DummyEntity::class,
'shortNames' => ['dummyEntity', 'relatedDummyEntity'],
'identifiers' => [
- ['id', DummyEntity::class, true],
- ['subresource', RelatedDummyEntity::class, false],
+ 'id' => [DummyEntity::class, 'id', true],
+ 'subresource' => [RelatedDummyEntity::class, 'id', false],
],
'route_name' => 'api_dummy_entities_subresource_another_subresource_get_subresource',
'path' => '/dummy_entities/{id}/subresource/another_subresource.{_format}',
@@ -96,9 +101,9 @@ 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, 'id', true],
+ 'subresource' => [RelatedDummyEntity::class, 'id', false],
+ 'anotherSubresource' => [DummyEntity::class, 'id', false],
],
'route_name' => 'api_dummy_entities_subresource_another_subresource_subcollections_get_subresource',
'path' => '/dummy_entities/{id}/subresource/another_subresource/subcollections.{_format}',
@@ -110,7 +115,7 @@ public function testCreate()
'resource_class' => RelatedDummyEntity::class,
'shortNames' => ['relatedDummyEntity', 'dummyEntity'],
'identifiers' => [
- ['id', DummyEntity::class, true],
+ 'id' => [DummyEntity::class, 'id', true],
],
'route_name' => 'api_dummy_entities_subcollections_get_subresource',
'path' => '/dummy_entities/{id}/subcollections.{_format}',
@@ -122,8 +127,8 @@ public function testCreate()
'resource_class' => DummyEntity::class,
'shortNames' => ['dummyEntity', 'relatedDummyEntity'],
'identifiers' => [
- ['id', DummyEntity::class, true],
- ['subcollection', RelatedDummyEntity::class, true],
+ 'id' => [DummyEntity::class, 'id', true],
+ 'subcollection' => [RelatedDummyEntity::class, 'id', true],
],
'route_name' => 'api_dummy_entities_subcollections_another_subresource_get_subresource',
'path' => '/dummy_entities/{id}/subcollections/{subcollection}/another_subresource.{_format}',
@@ -135,9 +140,9 @@ 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, 'id', true],
+ 'subcollection' => [RelatedDummyEntity::class, 'id', true],
+ 'anotherSubresource' => [DummyEntity::class, 'id', false],
],
'route_name' => 'api_dummy_entities_subcollections_another_subresource_subresource_get_subresource',
'path' => '/dummy_entities/{id}/subcollections/{subcollection}/another_subresource/subresource.{_format}',
@@ -180,7 +185,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,7 +197,7 @@ public function testCreateByOverriding()
'resource_class' => RelatedDummyEntity::class,
'shortNames' => ['relatedDummyEntity', 'dummyEntity'],
'identifiers' => [
- ['id', DummyEntity::class, true],
+ 'id' => [DummyEntity::class, 'id', true],
],
'route_name' => 'api_dummy_entities_subresource_get_subresource',
'path' => '/dummy_entities/{id}/subresource.{_format}',
@@ -201,8 +209,8 @@ public function testCreateByOverriding()
'resource_class' => DummyEntity::class,
'shortNames' => ['dummyEntity', 'relatedDummyEntity'],
'identifiers' => [
- ['id', DummyEntity::class, true],
- ['subresource', RelatedDummyEntity::class, false],
+ 'id' => [DummyEntity::class, 'id', true],
+ 'subresource' => [RelatedDummyEntity::class, 'id', false],
],
'route_name' => 'api_dummy_entities_subresource_another_subresource_get_subresource',
'path' => '/dummy_entities/{id}/subresource/another_subresource.{_format}',
@@ -214,9 +222,9 @@ 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, 'id', true],
+ 'subresource' => [RelatedDummyEntity::class, 'id', false],
+ 'anotherSubresource' => [DummyEntity::class, 'id', false],
],
'route_name' => 'api_dummy_entities_subresource_another_subresource_subcollections_get_subresource',
'path' => '/dummy_entities/{id}/subresource/another_subresource/subcollections.{_format}',
@@ -228,7 +236,7 @@ public function testCreateByOverriding()
'resource_class' => RelatedDummyEntity::class,
'shortNames' => ['relatedDummyEntity', 'dummyEntity'],
'identifiers' => [
- ['id', DummyEntity::class, true],
+ 'id' => [DummyEntity::class, 'id', true],
],
'route_name' => 'api_dummy_entities_subcollections_get_subresource',
'path' => '/dummy_entities/{id}/foobars',
@@ -240,8 +248,8 @@ public function testCreateByOverriding()
'resource_class' => DummyEntity::class,
'shortNames' => ['dummyEntity', 'relatedDummyEntity'],
'identifiers' => [
- ['id', DummyEntity::class, true],
- ['subcollection', RelatedDummyEntity::class, true],
+ 'id' => [DummyEntity::class, 'id', true],
+ 'subcollection' => [RelatedDummyEntity::class, 'id', true],
],
'route_name' => 'api_dummy_entities_subcollections_another_subresource_get_subresource',
'path' => '/dummy_entities/{id}/foobars/{subcollection}/another_foobar.{_format}',
@@ -253,9 +261,9 @@ 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, 'id', true],
+ 'subcollection' => [RelatedDummyEntity::class, 'id', true],
+ 'anotherSubresource' => [DummyEntity::class, 'id', false],
],
'route_name' => 'api_dummy_entities_subcollections_another_subresource_subresource_get_subresource',
'path' => '/dummy_entities/{id}/foobars/{subcollection}/another_foobar/subresource.{_format}',
@@ -286,11 +294,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,7 +312,7 @@ public function testCreateWithMaxDepth()
'resource_class' => RelatedDummyEntity::class,
'shortNames' => ['relatedDummyEntity', 'dummyEntity'],
'identifiers' => [
- ['id', DummyEntity::class, true],
+ 'id' => [DummyEntity::class, 'id', true],
],
'route_name' => 'api_dummy_entities_subresource_get_subresource',
'path' => '/dummy_entities/{id}/subresource.{_format}',
@@ -346,11 +358,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,7 +376,7 @@ public function testCreateWithMaxDepthMultipleSubresources()
'resource_class' => RelatedDummyEntity::class,
'shortNames' => ['relatedDummyEntity', 'dummyEntity'],
'identifiers' => [
- ['id', DummyEntity::class, true],
+ 'id' => [DummyEntity::class, 'id', true],
],
'route_name' => 'api_dummy_entities_subresource_get_subresource',
'path' => '/dummy_entities/{id}/subresources.{_format}',
@@ -372,7 +388,7 @@ public function testCreateWithMaxDepthMultipleSubresources()
'resource_class' => DummyValidatedEntity::class,
'shortNames' => ['dummyValidatedEntity', 'dummyEntity'],
'identifiers' => [
- ['id', DummyEntity::class, true],
+ 'id' => [DummyEntity::class, 'id', true],
],
'route_name' => 'api_dummy_entities_second_subresource_get_subresource',
'path' => '/dummy_entities/{id}/second_subresources.{_format}',
@@ -384,8 +400,8 @@ public function testCreateWithMaxDepthMultipleSubresources()
'resource_class' => RelatedDummyEntity::class,
'shortNames' => ['relatedDummyEntity', 'dummyValidatedEntity'],
'identifiers' => [
- ['id', DummyEntity::class, true],
- ['secondSubresource', DummyValidatedEntity::class, false],
+ 'id' => [DummyEntity::class, 'id', true],
+ 'secondSubresource' => [DummyValidatedEntity::class, 'id', false],
],
'route_name' => 'api_dummy_entities_second_subresource_more_subresource_get_subresource',
'path' => '/dummy_entities/{id}/second_subresources/mode_subresources.{_format}',
@@ -430,11 +446,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,7 +464,7 @@ public function testCreateWithMaxDepthMultipleSubresourcesSameMaxDepth()
'resource_class' => RelatedDummyEntity::class,
'shortNames' => ['relatedDummyEntity', 'dummyEntity'],
'identifiers' => [
- ['id', DummyEntity::class, true],
+ 'id' => [DummyEntity::class, 'id', true],
],
'route_name' => 'api_dummy_entities_subresource_get_subresource',
'path' => '/dummy_entities/{id}/subresources.{_format}',
@@ -456,7 +476,7 @@ public function testCreateWithMaxDepthMultipleSubresourcesSameMaxDepth()
'resource_class' => DummyValidatedEntity::class,
'shortNames' => ['dummyValidatedEntity', 'dummyEntity'],
'identifiers' => [
- ['id', DummyEntity::class, true],
+ 'id' => [DummyEntity::class, 'id', true],
],
'route_name' => 'api_dummy_entities_second_subresource_get_subresource',
'path' => '/dummy_entities/{id}/second_subresources.{_format}',
@@ -485,11 +505,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,7 +523,7 @@ public function testCreateSelfReferencingSubresources()
'resource_class' => DummyEntity::class,
'shortNames' => ['dummyEntity'],
'identifiers' => [
- ['id', DummyEntity::class, true],
+ 'id' => [DummyEntity::class, 'id', true],
],
'route_name' => 'api_dummy_entities_subresource_get_subresource',
'path' => '/dummy_entities/{id}/subresources.{_format}',
@@ -537,11 +561,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,7 +579,7 @@ public function testCreateWithDifferentMaxDepthSelfReferencingSubresources()
'resource_class' => DummyEntity::class,
'shortNames' => ['dummyEntity'],
'identifiers' => [
- ['id', DummyEntity::class, true],
+ 'id' => [DummyEntity::class, 'id', true],
],
'route_name' => 'api_dummy_entities_subresource_get_subresource',
'path' => '/dummy_entities/{id}/subresources.{_format}',
@@ -563,8 +591,8 @@ public function testCreateWithDifferentMaxDepthSelfReferencingSubresources()
'resource_class' => DummyEntity::class,
'shortNames' => ['dummyEntity'],
'identifiers' => [
- ['id', DummyEntity::class, true],
- ['subresource', DummyEntity::class, false],
+ 'id' => [DummyEntity::class, 'id', true],
+ 'subresource' => [DummyEntity::class, 'id', false],
],
'route_name' => 'api_dummy_entities_subresource_second_subresource_get_subresource',
'path' => '/dummy_entities/{id}/subresources/second_subresources.{_format}',
@@ -576,7 +604,7 @@ public function testCreateWithDifferentMaxDepthSelfReferencingSubresources()
'resource_class' => DummyEntity::class,
'shortNames' => ['dummyEntity'],
'identifiers' => [
- ['id', DummyEntity::class, true],
+ 'id' => [DummyEntity::class, 'id', true],
],
'route_name' => 'api_dummy_entities_second_subresource_get_subresource',
'path' => '/dummy_entities/{id}/second_subresources.{_format}',
@@ -606,11 +634,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,7 +653,7 @@ public function testCreateWithEnd()
'resource_class' => RelatedDummyEntity::class,
'shortNames' => ['relatedDummyEntity', 'dummyEntity'],
'identifiers' => [
- ['id', DummyEntity::class, true],
+ 'id' => [DummyEntity::class, 'id', true],
],
'route_name' => 'api_dummy_entities_subresources_get_subresource',
'path' => '/dummy_entities/{id}/subresource.{_format}',
@@ -633,8 +665,8 @@ public function testCreateWithEnd()
'resource_class' => DummyEntity::class,
'shortNames' => ['dummyEntity', 'relatedDummyEntity'],
'identifiers' => [
- ['id', DummyEntity::class, true],
- ['subresource', RelatedDummyEntity::class, true],
+ 'id' => [DummyEntity::class, 'id', true],
+ 'subresource' => [RelatedDummyEntity::class, 'id', true],
],
'route_name' => 'api_dummy_entities_subresources_item_get_subresource',
'path' => '/dummy_entities/{id}/subresource/{subresource}.{_format}',
@@ -664,11 +696,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,7 +715,7 @@ public function testCreateWithEndButNoCollection()
'resource_class' => RelatedDummyEntity::class,
'shortNames' => ['relatedDummyEntity', 'dummyEntity'],
'identifiers' => [
- ['id', DummyEntity::class, true],
+ 'id' => [DummyEntity::class, 'id', true],
],
'route_name' => 'api_dummy_entities_subresource_get_subresource',
'path' => '/dummy_entities/{id}/subresource.{_format}',
@@ -710,11 +746,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,7 +764,7 @@ public function testCreateWithRootResourcePrefix()
'resource_class' => RelatedDummyEntity::class,
'shortNames' => ['relatedDummyEntity', 'dummyEntity'],
'identifiers' => [
- ['id', DummyEntity::class, true],
+ 'id' => [DummyEntity::class, 'id', true],
],
'route_name' => 'api_dummy_entities_subresource_get_subresource',
'path' => '/root_resource_prefix/dummy_entities/{id}/subresource.{_format}',
@@ -759,11 +799,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,7 +817,7 @@ public function testCreateSelfReferencingSubresourcesWithSubresources()
'resource_class' => DummyEntity::class,
'shortNames' => ['dummyEntity'],
'identifiers' => [
- ['id', DummyEntity::class, true],
+ 'id' => [DummyEntity::class, 'id', true],
],
'route_name' => 'api_dummy_entities_subresource_get_subresource',
'path' => '/dummy_entities/{id}/subresources.{_format}',
@@ -785,7 +829,7 @@ public function testCreateSelfReferencingSubresourcesWithSubresources()
'resource_class' => RelatedDummyEntity::class,
'shortNames' => ['relatedDummyEntity', 'dummyEntity'],
'identifiers' => [
- ['id', DummyEntity::class, true],
+ 'id' => [DummyEntity::class, 'id', true],
],
'route_name' => 'api_dummy_entities_other_subresource_get_subresource',
'path' => '/dummy_entities/{id}/other_subresources.{_format}',
diff --git a/tests/PathResolver/OperationPathResolverTest.php b/tests/PathResolver/OperationPathResolverTest.php
new file mode 100644
index 00000000000..81c9b77f38f
--- /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', ['identifiers' => ['isbn']], OperationType::ITEM, 'get'));
+ }
+}
diff --git a/tests/Serializer/SerializerContextBuilderTest.php b/tests/Serializer/SerializerContextBuilderTest.php
index d73238214f2..565ed59a82a 100644
--- a/tests/Serializer/SerializerContextBuilderTest.php
+++ b/tests/Serializer/SerializerContextBuilderTest.php
@@ -99,6 +99,11 @@ public function testCreateFromRequest()
$request->attributes->replace(['_api_resource_class' => 'FooWithPatch', '_api_item_operation_name' => 'patch', '_api_format' => 'json', '_api_mime_type' => 'application/json']);
$expected = ['item_operation_name' => 'patch', 'resource_class' => 'FooWithPatch', 'request_uri' => '/foowithpatch/1', 'operation_type' => 'item', 'api_allow_update' => true, 'uri' => 'http://localhost/foowithpatch/1', 'output' => null, 'input' => null, 'deep_object_to_populate' => true, 'skip_null_values' => true, 'iri_only' => false];
$this->assertEquals($expected, $this->builder->createFromRequest($request, false));
+
+ $request = Request::create('/bars/1/foos');
+ $request->attributes->replace(['_api_resource_class' => 'Foo', '_api_subresource_operation_name' => 'get', '_api_format' => 'xml', '_api_mime_type' => 'text/xml', '_api_subresource_context' => ['identifiers' => ['id' => ['Foo', 'id']]], 'id' => '1']);
+ $expected = ['bar' => 'baz', 'subresource_operation_name' => 'get', 'resource_class' => 'Foo', 'request_uri' => '/bars/1/foos', 'operation_type' => 'subresource', 'api_allow_update' => false, 'uri' => 'http://localhost/bars/1/foos', 'output' => null, 'input' => null, 'iri_only' => false, 'subresource_identifiers' => ['id' => '1'], 'subresource_resources' => ['Foo' => ['id' => '1']]];
+ $this->assertEquals($expected, $this->builder->createFromRequest($request, false));
}
public function testThrowExceptionOnInvalidRequest()
diff --git a/tests/Serializer/SerializerFilterContextBuilderTest.php b/tests/Serializer/SerializerFilterContextBuilderTest.php
index 72f8dbe4495..bae2bf5d5d4 100644
--- a/tests/Serializer/SerializerFilterContextBuilderTest.php
+++ b/tests/Serializer/SerializerFilterContextBuilderTest.php
@@ -150,6 +150,10 @@ public function testCreateFromRequestWithoutAttributes()
$attributes = [
'resource_class' => DummyGroup::class,
'collection_operation_name' => 'get',
+ 'identifiers' => [
+ 'id' => [DummyGroup::class, 'id'],
+ ],
+ '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 3bd73151eb7..d40722e9a2c 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;
@@ -50,6 +51,7 @@
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Psr\Container\ContainerInterface;
+use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait;
use Symfony\Component\PropertyInfo\Type;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection;
@@ -65,6 +67,7 @@
class DocumentationNormalizerV2Test extends TestCase
{
use ProphecyTrait;
+ use ExpectDeprecationTrait;
private const OPERATION_FORMATS = [
'input_formats' => ['jsonld' => ['application/ld+json']],
@@ -73,10 +76,11 @@ class DocumentationNormalizerV2Test extends TestCase
/**
* @group legacy
- * @expectedDeprecation Passing an instance of ApiPlatform\Core\Api\UrlGeneratorInterface to ApiPlatform\Core\Swagger\Serializer\DocumentationNormalizer::__construct() is deprecated since version 2.1 and will be removed in 3.0.
*/
public function testLegacyConstruct(): void
{
+ $this->expectDeprecation('Passing an instance of ApiPlatform\Core\Api\UrlGeneratorInterface to ApiPlatform\Core\Swagger\Serializer\DocumentationNormalizer::__construct() is deprecated since version 2.1 and will be removed in 3.0.');
+
$normalizer = new DocumentationNormalizer(
$this->prophesize(ResourceMetadataFactoryInterface::class)->reveal(),
$this->prophesize(PropertyNameCollectionFactoryInterface::class)->reveal(),
@@ -142,13 +146,37 @@ private function doTestNormalize(OperationMethodResolverInterface $operationMeth
$operationPathResolver = new CustomOperationPathResolver(new OperationPathResolver(new UnderscorePathSegmentNameGenerator()));
+ $identifiersExtractorProphecy = $this->prophesize(IdentifiersExtractorInterface::class);
+ $identifiersExtractorProphecy->getIdentifiersFromResourceClass(Argument::type('string'))->willReturn(['id']);
+
$normalizer = new DocumentationNormalizer(
$resourceMetadataFactoryProphecy->reveal(),
$propertyNameCollectionFactoryProphecy->reveal(),
$propertyMetadataFactoryProphecy->reveal(),
null,
$operationMethodResolver,
- $operationPathResolver
+ $operationPathResolver,
+ null,
+ null,
+ null,
+ false,
+ '',
+ '',
+ '',
+ '',
+ [],
+ [],
+ null,
+ true,
+ 'page',
+ false,
+ 'itemsPerPage',
+ [],
+ false,
+ 'pagination',
+ [],
+ [2, 3],
+ $identifiersExtractorProphecy->reveal()
);
$expected = [
@@ -409,6 +437,9 @@ private function doTestNormalizeWithNameConverter(bool $legacy = false): void
$typeFactory->setSchemaFactory($schemaFactory);
}
+ $identifiersExtractorProphecy = $this->prophesize(IdentifiersExtractorInterface::class);
+ $identifiersExtractorProphecy->getIdentifiersFromResourceClass(Argument::type('string'))->willReturn(['id']);
+
$normalizer = new DocumentationNormalizer(
$resourceMetadataFactoryProphecy->reveal(),
$propertyNameCollectionFactoryProphecy->reveal(),
@@ -424,7 +455,19 @@ private function doTestNormalizeWithNameConverter(bool $legacy = false): void
'application',
'/oauth/v2/token',
'/oauth/v2/auth',
- ['scope param']
+ ['scope param'],
+ [],
+ null,
+ true,
+ 'page',
+ false,
+ 'itemsPerPage',
+ [],
+ false,
+ 'pagination',
+ [],
+ [2, 3],
+ $identifiersExtractorProphecy->reveal()
);
$expected = [
@@ -521,6 +564,9 @@ public function testNormalizeWithApiKeysEnabled(): void
],
];
+ $identifiersExtractorProphecy = $this->prophesize(IdentifiersExtractorInterface::class);
+ $identifiersExtractorProphecy->getIdentifiersFromResourceClass(Argument::type('string'))->willReturn(['id']);
+
$normalizer = new DocumentationNormalizer(
$resourceMetadataFactoryProphecy->reveal(),
$propertyNameCollectionFactoryProphecy->reveal(),
@@ -537,7 +583,18 @@ public function testNormalizeWithApiKeysEnabled(): void
'',
'',
[],
- $apiKeysConfiguration
+ $apiKeysConfiguration,
+ null,
+ true,
+ 'page',
+ false,
+ 'itemsPerPage',
+ [],
+ false,
+ 'pagination',
+ [],
+ [2, 3],
+ $identifiersExtractorProphecy->reveal()
);
$expected = [
@@ -648,13 +705,37 @@ public function testNormalizeWithOnlyNormalizationGroups(): void
$operationPathResolver = new CustomOperationPathResolver(new OperationPathResolver(new UnderscorePathSegmentNameGenerator()));
+ $identifiersExtractorProphecy = $this->prophesize(IdentifiersExtractorInterface::class);
+ $identifiersExtractorProphecy->getIdentifiersFromResourceClass(Argument::type('string'))->willReturn(['id']);
+
$normalizer = new DocumentationNormalizer(
$resourceMetadataFactoryProphecy->reveal(),
$propertyNameCollectionFactoryProphecy->reveal(),
$propertyMetadataFactoryProphecy->reveal(),
null,
null,
- $operationPathResolver
+ $operationPathResolver,
+ null,
+ null,
+ null,
+ false,
+ '',
+ '',
+ '',
+ '',
+ [],
+ [],
+ null,
+ true,
+ 'page',
+ false,
+ 'itemsPerPage',
+ [],
+ false,
+ 'pagination',
+ [],
+ [2, 3],
+ $identifiersExtractorProphecy->reveal()
);
$expected = [
@@ -848,13 +929,37 @@ public function testNormalizeNotAddExtraBodyParameters(): void
$operationPathResolver = new CustomOperationPathResolver(new OperationPathResolver(new UnderscorePathSegmentNameGenerator()));
+ $identifiersExtractorProphecy = $this->prophesize(IdentifiersExtractorInterface::class);
+ $identifiersExtractorProphecy->getIdentifiersFromResourceClass(Argument::type('string'))->willReturn(['id']);
+
$normalizer = new DocumentationNormalizer(
$resourceMetadataFactoryProphecy->reveal(),
$propertyNameCollectionFactoryProphecy->reveal(),
$propertyMetadataFactoryProphecy->reveal(),
null,
null,
- $operationPathResolver
+ $operationPathResolver,
+ null,
+ null,
+ null,
+ false,
+ '',
+ '',
+ '',
+ '',
+ [],
+ [],
+ null,
+ true,
+ 'page',
+ false,
+ 'itemsPerPage',
+ [],
+ false,
+ 'pagination',
+ [],
+ [2, 3],
+ $identifiersExtractorProphecy->reveal()
);
$expected = [
@@ -1025,13 +1130,37 @@ public function testNormalizeWithSwaggerDefinitionName(): void
$operationPathResolver = new CustomOperationPathResolver(new OperationPathResolver(new UnderscorePathSegmentNameGenerator()));
+ $identifiersExtractorProphecy = $this->prophesize(IdentifiersExtractorInterface::class);
+ $identifiersExtractorProphecy->getIdentifiersFromResourceClass(Argument::type('string'))->willReturn(['id']);
+
$normalizer = new DocumentationNormalizer(
$resourceMetadataFactoryProphecy->reveal(),
$propertyNameCollectionFactoryProphecy->reveal(),
$propertyMetadataFactoryProphecy->reveal(),
null,
null,
- $operationPathResolver
+ $operationPathResolver,
+ null,
+ null,
+ null,
+ false,
+ '',
+ '',
+ '',
+ '',
+ [],
+ [],
+ null,
+ true,
+ 'page',
+ false,
+ 'itemsPerPage',
+ [],
+ false,
+ 'pagination',
+ [],
+ [2, 3],
+ $identifiersExtractorProphecy->reveal()
);
$expected = [
@@ -1123,13 +1252,37 @@ public function testNormalizeWithOnlyDenormalizationGroups(): void
$operationPathResolver = new CustomOperationPathResolver(new OperationPathResolver(new UnderscorePathSegmentNameGenerator()));
+ $identifiersExtractorProphecy = $this->prophesize(IdentifiersExtractorInterface::class);
+ $identifiersExtractorProphecy->getIdentifiersFromResourceClass(Argument::type('string'))->willReturn(['id']);
+
$normalizer = new DocumentationNormalizer(
$resourceMetadataFactoryProphecy->reveal(),
$propertyNameCollectionFactoryProphecy->reveal(),
$propertyMetadataFactoryProphecy->reveal(),
null,
null,
- $operationPathResolver
+ $operationPathResolver,
+ null,
+ null,
+ null,
+ false,
+ '',
+ '',
+ '',
+ '',
+ [],
+ [],
+ null,
+ true,
+ 'page',
+ false,
+ 'itemsPerPage',
+ [],
+ false,
+ 'pagination',
+ [],
+ [2, 3],
+ $identifiersExtractorProphecy->reveal()
);
$expected = [
@@ -1311,13 +1464,37 @@ public function testNormalizeWithNormalizationAndDenormalizationGroups(): void
$operationPathResolver = new CustomOperationPathResolver(new OperationPathResolver(new UnderscorePathSegmentNameGenerator()));
+ $identifiersExtractorProphecy = $this->prophesize(IdentifiersExtractorInterface::class);
+ $identifiersExtractorProphecy->getIdentifiersFromResourceClass(Argument::type('string'))->willReturn(['id']);
+
$normalizer = new DocumentationNormalizer(
$resourceMetadataFactoryProphecy->reveal(),
$propertyNameCollectionFactoryProphecy->reveal(),
$propertyMetadataFactoryProphecy->reveal(),
null,
null,
- $operationPathResolver
+ $operationPathResolver,
+ null,
+ null,
+ null,
+ false,
+ '',
+ '',
+ '',
+ '',
+ [],
+ [],
+ null,
+ true,
+ 'page',
+ false,
+ 'itemsPerPage',
+ [],
+ false,
+ 'pagination',
+ [],
+ [2, 3],
+ $identifiersExtractorProphecy->reveal()
);
$expected = [
@@ -1491,13 +1668,37 @@ public function testNormalizeSkipsNotReadableAndNotWritableProperties(): void
$operationPathResolver = new CustomOperationPathResolver(new OperationPathResolver(new UnderscorePathSegmentNameGenerator()));
+ $identifiersExtractorProphecy = $this->prophesize(IdentifiersExtractorInterface::class);
+ $identifiersExtractorProphecy->getIdentifiersFromResourceClass(Argument::type('string'))->willReturn(['id']);
+
$normalizer = new DocumentationNormalizer(
$resourceMetadataFactoryProphecy->reveal(),
$propertyNameCollectionFactoryProphecy->reveal(),
$propertyMetadataFactoryProphecy->reveal(),
null,
null,
- $operationPathResolver
+ $operationPathResolver,
+ null,
+ null,
+ null,
+ false,
+ '',
+ '',
+ '',
+ '',
+ [],
+ [],
+ null,
+ true,
+ 'page',
+ false,
+ 'itemsPerPage',
+ [],
+ false,
+ 'pagination',
+ [],
+ [2, 3],
+ $identifiersExtractorProphecy->reveal()
);
$expected = [
@@ -1721,7 +1922,26 @@ public function testConstructWithInvalidFilterLocator(): void
null,
$this->prophesize(OperationPathResolverInterface::class)->reveal(),
null,
- new \ArrayObject()
+ new \ArrayObject(),
+ null,
+ false,
+ '',
+ '',
+ '',
+ '',
+ [],
+ [],
+ null,
+ true,
+ 'page',
+ false,
+ 'itemsPerPage',
+ [],
+ false,
+ 'pagination',
+ [],
+ [2, 3],
+ $this->prophesize(IdentifiersExtractorInterface::class)->reveal()
);
}
@@ -1732,13 +1952,37 @@ public function testSupports(): void
$propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class);
$operationPathResolver = new CustomOperationPathResolver(new OperationPathResolver(new UnderscorePathSegmentNameGenerator()));
+ $identifiersExtractorProphecy = $this->prophesize(IdentifiersExtractorInterface::class);
+ $identifiersExtractorProphecy->getIdentifiersFromResourceClass(Argument::type('string'))->willReturn(['id']);
+
$normalizer = new DocumentationNormalizer(
$resourceMetadataFactoryProphecy->reveal(),
$propertyNameCollectionFactoryProphecy->reveal(),
$propertyMetadataFactoryProphecy->reveal(),
null,
null,
- $operationPathResolver
+ $operationPathResolver,
+ null,
+ null,
+ null,
+ false,
+ '',
+ '',
+ '',
+ '',
+ [],
+ [],
+ null,
+ true,
+ 'page',
+ false,
+ 'itemsPerPage',
+ [],
+ false,
+ 'pagination',
+ [],
+ [2, 3],
+ $identifiersExtractorProphecy->reveal()
);
$documentation = new Documentation(new ResourceNameCollection([Dummy::class]), 'Test API', 'This is a test API.', '1.2.3');
@@ -1768,13 +2012,37 @@ public function testNormalizeWithNoOperations(): void
$operationPathResolver = new CustomOperationPathResolver(new OperationPathResolver(new UnderscorePathSegmentNameGenerator()));
+ $identifiersExtractorProphecy = $this->prophesize(IdentifiersExtractorInterface::class);
+ $identifiersExtractorProphecy->getIdentifiersFromResourceClass(Argument::type('string'))->willReturn(['id']);
+
$normalizer = new DocumentationNormalizer(
$resourceMetadataFactoryProphecy->reveal(),
$propertyNameCollectionFactoryProphecy->reveal(),
$propertyMetadataFactoryProphecy->reveal(),
null,
null,
- $operationPathResolver
+ $operationPathResolver,
+ null,
+ null,
+ null,
+ false,
+ '',
+ '',
+ '',
+ '',
+ [],
+ [],
+ null,
+ true,
+ 'page',
+ false,
+ 'itemsPerPage',
+ [],
+ false,
+ 'pagination',
+ [],
+ [2, 3],
+ $identifiersExtractorProphecy->reveal()
);
$expected = [
@@ -1810,13 +2078,37 @@ public function testNormalizeWithCustomMethod(): void
$operationPathResolver = new CustomOperationPathResolver(new OperationPathResolver(new UnderscorePathSegmentNameGenerator()));
+ $identifiersExtractorProphecy = $this->prophesize(IdentifiersExtractorInterface::class);
+ $identifiersExtractorProphecy->getIdentifiersFromResourceClass(Argument::type('string'))->willReturn(['id']);
+
$normalizer = new DocumentationNormalizer(
$resourceMetadataFactoryProphecy->reveal(),
$propertyNameCollectionFactoryProphecy->reveal(),
$propertyMetadataFactoryProphecy->reveal(),
null,
null,
- $operationPathResolver
+ $operationPathResolver,
+ null,
+ null,
+ null,
+ false,
+ '',
+ '',
+ '',
+ '',
+ [],
+ [],
+ null,
+ true,
+ 'page',
+ false,
+ 'itemsPerPage',
+ [],
+ false,
+ 'pagination',
+ [],
+ [2, 3],
+ $identifiersExtractorProphecy->reveal()
);
$expected = [
@@ -1894,13 +2186,37 @@ public function testNormalizeWithNestedNormalizationGroups(): void
$operationPathResolver = new CustomOperationPathResolver(new OperationPathResolver(new UnderscorePathSegmentNameGenerator()));
+ $identifiersExtractorProphecy = $this->prophesize(IdentifiersExtractorInterface::class);
+ $identifiersExtractorProphecy->getIdentifiersFromResourceClass(Argument::type('string'))->willReturn(['id']);
+
$normalizer = new DocumentationNormalizer(
$resourceMetadataFactoryProphecy->reveal(),
$propertyNameCollectionFactoryProphecy->reveal(),
$propertyMetadataFactoryProphecy->reveal(),
null,
null,
- $operationPathResolver
+ $operationPathResolver,
+ null,
+ null,
+ null,
+ false,
+ '',
+ '',
+ '',
+ '',
+ [],
+ [],
+ null,
+ true,
+ 'page',
+ false,
+ 'itemsPerPage',
+ [],
+ false,
+ 'pagination',
+ [],
+ [2, 3],
+ $identifiersExtractorProphecy->reveal()
);
$expected = [
@@ -2081,6 +2397,9 @@ private function doTestNormalizeWithFilters($filterLocator): void
$operationPathResolver = new CustomOperationPathResolver(new OperationPathResolver(new UnderscorePathSegmentNameGenerator()));
+ $identifiersExtractorProphecy = $this->prophesize(IdentifiersExtractorInterface::class);
+ $identifiersExtractorProphecy->getIdentifiersFromResourceClass(Argument::type('string'))->willReturn(['id']);
+
$normalizer = new DocumentationNormalizer(
$resourceMetadataFactoryProphecy->reveal(),
$propertyNameCollectionFactoryProphecy->reveal(),
@@ -2089,7 +2408,26 @@ private function doTestNormalizeWithFilters($filterLocator): void
null,
$operationPathResolver,
null,
- $filterLocator
+ $filterLocator,
+ null,
+ false,
+ '',
+ '',
+ '',
+ '',
+ [],
+ [],
+ null,
+ true,
+ 'page',
+ false,
+ 'itemsPerPage',
+ [],
+ false,
+ 'pagination',
+ [],
+ [2, 3],
+ $identifiersExtractorProphecy->reveal()
);
$expected = [
@@ -2227,7 +2565,13 @@ 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());
+
+ $identifiersExtractorProphecy = $this->prophesize(IdentifiersExtractorInterface::class);
+ $identifiersExtractorProphecy->getIdentifiersFromResourceClass(Argument::type('string'))->willReturn(['id']);
$normalizer = new DocumentationNormalizer(
$resourceMetadataFactory,
@@ -2251,7 +2595,12 @@ private function doTestNormalizeWithSubResource(OperationAwareFormatsProviderInt
'page',
false,
'itemsPerPage',
- $formatsProvider ?? ['json' => ['application/json'], 'csv' => ['text/csv']]
+ $formatsProvider ?? ['json' => ['application/json'], 'csv' => ['text/csv']],
+ false,
+ 'pagination',
+ [],
+ [2, 3],
+ $identifiersExtractorProphecy->reveal()
);
$expected = [
@@ -2365,13 +2714,37 @@ public function testNormalizeWithPropertySwaggerContext(): void
$operationPathResolver = new CustomOperationPathResolver(new OperationPathResolver(new UnderscorePathSegmentNameGenerator()));
+ $identifiersExtractorProphecy = $this->prophesize(IdentifiersExtractorInterface::class);
+ $identifiersExtractorProphecy->getIdentifiersFromResourceClass(Argument::type('string'))->willReturn(['id']);
+
$normalizer = new DocumentationNormalizer(
$resourceMetadataFactoryProphecy->reveal(),
$propertyNameCollectionFactoryProphecy->reveal(),
$propertyMetadataFactoryProphecy->reveal(),
null,
null,
- $operationPathResolver
+ $operationPathResolver,
+ null,
+ null,
+ null,
+ false,
+ '',
+ '',
+ '',
+ '',
+ [],
+ [],
+ null,
+ true,
+ 'page',
+ false,
+ 'itemsPerPage',
+ [],
+ false,
+ 'pagination',
+ [],
+ [2, 3],
+ $identifiersExtractorProphecy->reveal()
);
$expected = [
@@ -2456,13 +2829,37 @@ public function testNormalizeWithPaginationClientEnabled(): void
$operationPathResolver = new CustomOperationPathResolver(new OperationPathResolver(new UnderscorePathSegmentNameGenerator()));
+ $identifiersExtractorProphecy = $this->prophesize(IdentifiersExtractorInterface::class);
+ $identifiersExtractorProphecy->getIdentifiersFromResourceClass(Argument::type('string'))->willReturn(['id']);
+
$normalizer = new DocumentationNormalizer(
$resourceMetadataFactoryProphecy->reveal(),
$propertyNameCollectionFactoryProphecy->reveal(),
$propertyMetadataFactoryProphecy->reveal(),
null,
null,
- $operationPathResolver
+ $operationPathResolver,
+ null,
+ null,
+ null,
+ false,
+ '',
+ '',
+ '',
+ '',
+ [],
+ [],
+ null,
+ true,
+ 'page',
+ false,
+ 'itemsPerPage',
+ [],
+ false,
+ 'pagination',
+ [],
+ [2, 3],
+ $identifiersExtractorProphecy->reveal()
);
$expected = [
@@ -2577,6 +2974,9 @@ private function doTestNormalizeWithCustomFormatsDefinedAtOperationLevel(Operati
$operationPathResolver = new OperationPathResolver(new UnderscorePathSegmentNameGenerator());
+ $identifiersExtractorProphecy = $this->prophesize(IdentifiersExtractorInterface::class);
+ $identifiersExtractorProphecy->getIdentifiersFromResourceClass(Argument::type('string'))->willReturn(['id']);
+
$normalizer = new DocumentationNormalizer(
$resourceMetadataFactoryProphecy->reveal(),
$propertyNameCollectionFactoryProphecy->reveal(),
@@ -2598,7 +2998,12 @@ private function doTestNormalizeWithCustomFormatsDefinedAtOperationLevel(Operati
'page',
false,
'itemsPerPage',
- $formatProvider
+ $formatProvider,
+ false,
+ 'pagination',
+ [],
+ [2, 3],
+ $identifiersExtractorProphecy->reveal()
);
$expected = [
@@ -2771,13 +3176,37 @@ private function doTestNormalizeWithInputAndOutputClass(): void
$operationPathResolver = new CustomOperationPathResolver(new OperationPathResolver(new UnderscorePathSegmentNameGenerator()));
+ $identifiersExtractorProphecy = $this->prophesize(IdentifiersExtractorInterface::class);
+ $identifiersExtractorProphecy->getIdentifiersFromResourceClass(Argument::type('string'))->willReturn(['id']);
+
$normalizer = new DocumentationNormalizer(
$resourceMetadataFactoryProphecy->reveal(),
$propertyNameCollectionFactoryProphecy->reveal(),
$propertyMetadataFactoryProphecy->reveal(),
null,
null,
- $operationPathResolver
+ $operationPathResolver,
+ null,
+ null,
+ null,
+ false,
+ '',
+ '',
+ '',
+ '',
+ [],
+ [],
+ null,
+ true,
+ 'page',
+ false,
+ 'itemsPerPage',
+ [],
+ false,
+ '',
+ [],
+ [],
+ $identifiersExtractorProphecy->reveal()
);
$expected = [
@@ -2977,13 +3406,37 @@ public function testNormalizeWithDefaultProperty($expectedDefault, $expectedExam
$operationPathResolver = new CustomOperationPathResolver(new OperationPathResolver(new UnderscorePathSegmentNameGenerator()));
+ $identifiersExtractorProphecy = $this->prophesize(IdentifiersExtractorInterface::class);
+ $identifiersExtractorProphecy->getIdentifiersFromResourceClass(Argument::type('string'))->willReturn(['id']);
+
$normalizer = new DocumentationNormalizer(
$resourceMetadataFactoryProphecy->reveal(),
$propertyNameCollectionFactoryProphecy->reveal(),
$propertyMetadataFactoryProphecy->reveal(),
null,
null,
- $operationPathResolver
+ $operationPathResolver,
+ null,
+ null,
+ null,
+ false,
+ '',
+ '',
+ '',
+ '',
+ [],
+ [],
+ null,
+ true,
+ 'page',
+ false,
+ 'itemsPerPage',
+ [],
+ false,
+ '',
+ [],
+ [],
+ $identifiersExtractorProphecy->reveal()
);
$result = $normalizer->normalize($documentation, DocumentationNormalizer::FORMAT);
diff --git a/tests/Swagger/Serializer/DocumentationNormalizerV3Test.php b/tests/Swagger/Serializer/DocumentationNormalizerV3Test.php
index 71aaebe162c..d09cc53a429 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;
@@ -120,6 +121,9 @@ private function doTestNormalize(OperationMethodResolverInterface $operationMeth
$operationPathResolver = new CustomOperationPathResolver(new OperationPathResolver(new UnderscorePathSegmentNameGenerator()));
+ $identifiersExtractorProphecy = $this->prophesize(IdentifiersExtractorInterface::class);
+ $identifiersExtractorProphecy->getIdentifiersFromResourceClass(Argument::type('string'))->willReturn(['id']);
+
$normalizer = new DocumentationNormalizer(
$resourceMetadataFactoryProphecy->reveal(),
$propertyNameCollectionFactoryProphecy->reveal(),
@@ -145,7 +149,9 @@ private function doTestNormalize(OperationMethodResolverInterface $operationMeth
[],
false,
'pagination',
- ['spec_version' => 3]
+ ['spec_version' => 3],
+ [2, 3],
+ $identifiersExtractorProphecy->reveal()
);
$expected = [
@@ -477,6 +483,9 @@ private function doTestNormalizeWithNameConverter(bool $legacy = false): void
$typeFactory->setSchemaFactory($schemaFactory);
}
+ $identifiersExtractorProphecy = $this->prophesize(IdentifiersExtractorInterface::class);
+ $identifiersExtractorProphecy->getIdentifiersFromResourceClass(Argument::type('string'))->willReturn(['id']);
+
$normalizer = new DocumentationNormalizer(
$resourceMetadataFactory,
$propertyNameCollectionFactory,
@@ -502,7 +511,9 @@ private function doTestNormalizeWithNameConverter(bool $legacy = false): void
[],
false,
'pagination',
- ['spec_version' => 3]
+ ['spec_version' => 3],
+ [2, 3],
+ $identifiersExtractorProphecy->reveal()
);
$expected = [
@@ -611,6 +622,9 @@ public function testNormalizeWithApiKeysEnabled(): void
],
];
+ $identifiersExtractorProphecy = $this->prophesize(IdentifiersExtractorInterface::class);
+ $identifiersExtractorProphecy->getIdentifiersFromResourceClass(Argument::type('string'))->willReturn(['id']);
+
$normalizer = new DocumentationNormalizer(
$resourceMetadataFactoryProphecy->reveal(),
$propertyNameCollectionFactoryProphecy->reveal(),
@@ -636,7 +650,9 @@ public function testNormalizeWithApiKeysEnabled(): void
[],
false,
'pagination',
- ['spec_version' => 3]
+ ['spec_version' => 3],
+ [2, 3],
+ $identifiersExtractorProphecy->reveal()
);
$expected = [
@@ -752,6 +768,9 @@ public function testNormalizeWithOnlyNormalizationGroups(): void
$operationPathResolver = new CustomOperationPathResolver(new OperationPathResolver(new UnderscorePathSegmentNameGenerator()));
+ $identifiersExtractorProphecy = $this->prophesize(IdentifiersExtractorInterface::class);
+ $identifiersExtractorProphecy->getIdentifiersFromResourceClass(Argument::type('string'))->willReturn(['id']);
+
$normalizer = new DocumentationNormalizer(
$resourceMetadataFactoryProphecy->reveal(),
$propertyNameCollectionFactoryProphecy->reveal(),
@@ -777,7 +796,9 @@ public function testNormalizeWithOnlyNormalizationGroups(): void
[],
false,
'pagination',
- ['spec_version' => 3]
+ ['spec_version' => 3],
+ [2, 3],
+ $identifiersExtractorProphecy->reveal()
);
$expected = [
@@ -968,6 +989,9 @@ public function testNormalizeWithOpenApiDefinitionName(): void
$operationPathResolver = new CustomOperationPathResolver(new OperationPathResolver(new UnderscorePathSegmentNameGenerator()));
+ $identifiersExtractorProphecy = $this->prophesize(IdentifiersExtractorInterface::class);
+ $identifiersExtractorProphecy->getIdentifiersFromResourceClass(Argument::type('string'))->willReturn(['id']);
+
$normalizer = new DocumentationNormalizer(
$resourceMetadataFactoryProphecy->reveal(),
$propertyNameCollectionFactoryProphecy->reveal(),
@@ -993,7 +1017,9 @@ public function testNormalizeWithOpenApiDefinitionName(): void
[],
false,
'pagination',
- ['spec_version' => 3]
+ ['spec_version' => 3],
+ [2, 3],
+ $identifiersExtractorProphecy->reveal()
);
$expected = [
@@ -1090,6 +1116,9 @@ public function testNormalizeWithOnlyDenormalizationGroups(): void
$operationPathResolver = new CustomOperationPathResolver(new OperationPathResolver(new UnderscorePathSegmentNameGenerator()));
+ $identifiersExtractorProphecy = $this->prophesize(IdentifiersExtractorInterface::class);
+ $identifiersExtractorProphecy->getIdentifiersFromResourceClass(Argument::type('string'))->willReturn(['id']);
+
$normalizer = new DocumentationNormalizer(
$resourceMetadataFactoryProphecy->reveal(),
$propertyNameCollectionFactoryProphecy->reveal(),
@@ -1115,7 +1144,9 @@ public function testNormalizeWithOnlyDenormalizationGroups(): void
[],
false,
'pagination',
- ['spec_version' => 3]
+ ['spec_version' => 3],
+ [2, 3],
+ $identifiersExtractorProphecy->reveal()
);
$expected = [
@@ -1315,6 +1346,9 @@ public function testNormalizeWithNormalizationAndDenormalizationGroups(): void
$operationPathResolver = new CustomOperationPathResolver(new OperationPathResolver(new UnderscorePathSegmentNameGenerator()));
+ $identifiersExtractorProphecy = $this->prophesize(IdentifiersExtractorInterface::class);
+ $identifiersExtractorProphecy->getIdentifiersFromResourceClass(Argument::type('string'))->willReturn(['id']);
+
$normalizer = new DocumentationNormalizer(
$resourceMetadataFactoryProphecy->reveal(),
$propertyNameCollectionFactoryProphecy->reveal(),
@@ -1340,7 +1374,9 @@ public function testNormalizeWithNormalizationAndDenormalizationGroups(): void
[],
false,
'pagination',
- ['spec_version' => 3]
+ ['spec_version' => 3],
+ [2, 3],
+ $identifiersExtractorProphecy->reveal()
);
$expected = [
@@ -1615,7 +1651,9 @@ public function testConstructWithInvalidFilterLocator(): void
[],
false,
'pagination',
- ['spec_version' => 3]
+ ['spec_version' => 3],
+ [2, 3],
+ $this->prophesize(IdentifiersExtractorInterface::class)->reveal()
);
}
@@ -1625,6 +1663,7 @@ public function testSupports(): void
$propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class);
$propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class);
$operationPathResolver = new CustomOperationPathResolver(new OperationPathResolver(new UnderscorePathSegmentNameGenerator()));
+ $identifiersExtractorProphecy = $this->prophesize(IdentifiersExtractorInterface::class);
$normalizer = new DocumentationNormalizer(
$resourceMetadataFactoryProphecy->reveal(),
@@ -1651,7 +1690,9 @@ public function testSupports(): void
[],
false,
'pagination',
- ['spec_version' => 3]
+ ['spec_version' => 3],
+ [2, 3],
+ $identifiersExtractorProphecy->reveal()
);
$documentation = new Documentation(new ResourceNameCollection([Dummy::class]), 'Test API', 'This is a test API.', '1.2.3');
@@ -1685,6 +1726,9 @@ public function testNormalizeWithNoOperations(): void
$operationPathResolver = new CustomOperationPathResolver(new OperationPathResolver(new UnderscorePathSegmentNameGenerator()));
+ $identifiersExtractorProphecy = $this->prophesize(IdentifiersExtractorInterface::class);
+ $identifiersExtractorProphecy->getIdentifiersFromResourceClass(Argument::type('string'))->willReturn(['id']);
+
$normalizer = new DocumentationNormalizer(
$resourceMetadataFactoryProphecy->reveal(),
$propertyNameCollectionFactoryProphecy->reveal(),
@@ -1710,7 +1754,9 @@ public function testNormalizeWithNoOperations(): void
[],
false,
'pagination',
- ['spec_version' => 3]
+ ['spec_version' => 3],
+ [2, 3],
+ $identifiersExtractorProphecy->reveal()
);
$expected = [
@@ -1745,6 +1791,9 @@ public function testNormalizeWithCustomMethod(): void
$operationPathResolver = new CustomOperationPathResolver(new OperationPathResolver(new UnderscorePathSegmentNameGenerator()));
+ $identifiersExtractorProphecy = $this->prophesize(IdentifiersExtractorInterface::class);
+ $identifiersExtractorProphecy->getIdentifiersFromResourceClass(Argument::type('string'))->willReturn(['id']);
+
$normalizer = new DocumentationNormalizer(
$resourceMetadataFactoryProphecy->reveal(),
$propertyNameCollectionFactoryProphecy->reveal(),
@@ -1770,7 +1819,9 @@ public function testNormalizeWithCustomMethod(): void
[],
false,
'pagination',
- ['spec_version' => 3]
+ ['spec_version' => 3],
+ [2, 3],
+ $identifiersExtractorProphecy->reveal()
);
$expected = [
@@ -1847,6 +1898,9 @@ public function testNormalizeWithNestedNormalizationGroups(): void
$operationPathResolver = new CustomOperationPathResolver(new OperationPathResolver(new UnderscorePathSegmentNameGenerator()));
+ $identifiersExtractorProphecy = $this->prophesize(IdentifiersExtractorInterface::class);
+ $identifiersExtractorProphecy->getIdentifiersFromResourceClass(Argument::type('string'))->willReturn(['id']);
+
$normalizer = new DocumentationNormalizer(
$resourceMetadataFactoryProphecy->reveal(),
$propertyNameCollectionFactoryProphecy->reveal(),
@@ -1872,7 +1926,9 @@ public function testNormalizeWithNestedNormalizationGroups(): void
[],
false,
'pagination',
- ['spec_version' => 3]
+ ['spec_version' => 3],
+ [2, 3],
+ $identifiersExtractorProphecy->reveal()
);
$expected = [
@@ -2074,6 +2130,9 @@ private function doTestNormalizeWithFilters($filterLocator): void
$operationPathResolver = new CustomOperationPathResolver(new OperationPathResolver(new UnderscorePathSegmentNameGenerator()));
+ $identifiersExtractorProphecy = $this->prophesize(IdentifiersExtractorInterface::class);
+ $identifiersExtractorProphecy->getIdentifiersFromResourceClass(Argument::type('string'))->willReturn(['id']);
+
$normalizer = new DocumentationNormalizer(
$resourceMetadataFactoryProphecy->reveal(),
$propertyNameCollectionFactoryProphecy->reveal(),
@@ -2099,7 +2158,9 @@ private function doTestNormalizeWithFilters($filterLocator): void
[],
false,
'pagination',
- ['spec_version' => 3]
+ ['spec_version' => 3],
+ [2, 3],
+ $identifiersExtractorProphecy->reveal()
);
$expected = [
@@ -2256,7 +2317,13 @@ 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());
+
+ $identifiersExtractorProphecy = $this->prophesize(IdentifiersExtractorInterface::class);
+ $identifiersExtractorProphecy->getIdentifiersFromResourceClass(Argument::type('string'))->willReturn(['id']);
$normalizer = new DocumentationNormalizer(
$resourceMetadataFactory,
@@ -2283,7 +2350,9 @@ private function doTestNormalizeWithSubResource(OperationAwareFormatsProviderInt
$formatProvider ?? [],
false,
'pagination',
- ['spec_version' => 3]
+ ['spec_version' => 3],
+ [2, 3],
+ $identifiersExtractorProphecy->reveal()
);
$expected = [
@@ -2407,6 +2476,9 @@ public function testNormalizeWithPropertyOpenApiContext(): void
$operationPathResolver = new CustomOperationPathResolver(new OperationPathResolver(new UnderscorePathSegmentNameGenerator()));
+ $identifiersExtractorProphecy = $this->prophesize(IdentifiersExtractorInterface::class);
+ $identifiersExtractorProphecy->getIdentifiersFromResourceClass(Argument::type('string'))->willReturn(['id']);
+
$normalizer = new DocumentationNormalizer(
$resourceMetadataFactoryProphecy->reveal(),
$propertyNameCollectionFactoryProphecy->reveal(),
@@ -2432,7 +2504,9 @@ public function testNormalizeWithPropertyOpenApiContext(): void
[],
false,
'pagination',
- ['spec_version' => 3]
+ ['spec_version' => 3],
+ [2, 3],
+ $identifiersExtractorProphecy->reveal()
);
$expected = [
@@ -2522,6 +2596,9 @@ public function testNormalizeWithPaginationClientEnabled(): void
$operationPathResolver = new CustomOperationPathResolver(new OperationPathResolver(new UnderscorePathSegmentNameGenerator()));
+ $identifiersExtractorProphecy = $this->prophesize(IdentifiersExtractorInterface::class);
+ $identifiersExtractorProphecy->getIdentifiersFromResourceClass(Argument::type('string'))->willReturn(['id']);
+
$normalizer = new DocumentationNormalizer(
$resourceMetadataFactoryProphecy->reveal(),
$propertyNameCollectionFactoryProphecy->reveal(),
@@ -2547,7 +2624,9 @@ public function testNormalizeWithPaginationClientEnabled(): void
[],
false,
'pagination',
- ['spec_version' => 3]
+ ['spec_version' => 3],
+ [2, 3],
+ $identifiersExtractorProphecy->reveal()
);
$expected = [
@@ -2651,6 +2730,9 @@ public function testNormalizeWithPaginationCustomDefaultAndMaxItemsPerPage(): vo
$operationPathResolver = new CustomOperationPathResolver(new OperationPathResolver(new UnderscorePathSegmentNameGenerator()));
+ $identifiersExtractorProphecy = $this->prophesize(IdentifiersExtractorInterface::class);
+ $identifiersExtractorProphecy->getIdentifiersFromResourceClass(Argument::type('string'))->willReturn(['id']);
+
$normalizer = new DocumentationNormalizer(
$resourceMetadataFactoryProphecy->reveal(),
$propertyNameCollectionFactoryProphecy->reveal(),
@@ -2676,7 +2758,9 @@ public function testNormalizeWithPaginationCustomDefaultAndMaxItemsPerPage(): vo
[],
false,
'pagination',
- ['spec_version' => 3]
+ ['spec_version' => 3],
+ [2, 3],
+ $identifiersExtractorProphecy->reveal()
);
$expected = [
@@ -2789,6 +2873,9 @@ public function testLegacyNormalizeWithPaginationCustomDefaultAndMaxItemsPerPage
$operationPathResolver = new CustomOperationPathResolver(new OperationPathResolver(new UnderscorePathSegmentNameGenerator()));
+ $identifiersExtractorProphecy = $this->prophesize(IdentifiersExtractorInterface::class);
+ $identifiersExtractorProphecy->getIdentifiersFromResourceClass(Argument::type('string'))->willReturn(['id']);
+
$normalizer = new DocumentationNormalizer(
$resourceMetadataFactoryProphecy->reveal(),
$propertyNameCollectionFactoryProphecy->reveal(),
@@ -2814,7 +2901,9 @@ public function testLegacyNormalizeWithPaginationCustomDefaultAndMaxItemsPerPage
[],
false,
'pagination',
- ['spec_version' => 3]
+ ['spec_version' => 3],
+ [2, 3],
+ $identifiersExtractorProphecy->reveal()
);
$expected = [
@@ -2943,6 +3032,9 @@ private function doTestNormalizeWithCustomFormatsDefinedAtOperationLevel(Operati
$operationPathResolver = new OperationPathResolver(new UnderscorePathSegmentNameGenerator());
+ $identifiersExtractorProphecy = $this->prophesize(IdentifiersExtractorInterface::class);
+ $identifiersExtractorProphecy->getIdentifiersFromResourceClass(Argument::type('string'))->willReturn(['id']);
+
$normalizer = new DocumentationNormalizer(
$resourceMetadataFactoryProphecy->reveal(),
$propertyNameCollectionFactoryProphecy->reveal(),
@@ -2967,7 +3059,9 @@ private function doTestNormalizeWithCustomFormatsDefinedAtOperationLevel(Operati
$formatsProvider ?? [],
false,
'pagination',
- ['spec_version' => 3]
+ ['spec_version' => 3],
+ [2, 3],
+ $identifiersExtractorProphecy->reveal()
);
$expected = [
diff --git a/tests/Util/RequestAttributesExtractorTest.php b/tests/Util/RequestAttributesExtractorTest.php
index 9288ba0986c..ed0b52705ec 100644
--- a/tests/Util/RequestAttributesExtractorTest.php
+++ b/tests/Util/RequestAttributesExtractorTest.php
@@ -33,6 +33,8 @@ public function testExtractCollectionAttributes()
'receive' => true,
'respond' => true,
'persist' => true,
+ 'identifiers' => ['id' => ['Foo', 'id']],
+ 'has_composite_identifier' => false,
],
RequestAttributesExtractor::extractAttributes($request)
);
@@ -49,6 +51,8 @@ public function testExtractItemAttributes()
'receive' => true,
'respond' => true,
'persist' => true,
+ 'identifiers' => ['id' => ['Foo', 'id']],
+ 'has_composite_identifier' => false,
],
RequestAttributesExtractor::extractAttributes($request)
);
@@ -65,6 +69,8 @@ public function testExtractReceive()
'receive' => false,
'respond' => true,
'persist' => true,
+ 'identifiers' => ['id' => ['Foo', 'id']],
+ 'has_composite_identifier' => false,
],
RequestAttributesExtractor::extractAttributes($request)
);
@@ -78,6 +84,8 @@ public function testExtractReceive()
'receive' => true,
'respond' => true,
'persist' => true,
+ 'identifiers' => ['id' => ['Foo', 'id']],
+ 'has_composite_identifier' => false,
],
RequestAttributesExtractor::extractAttributes($request)
);
@@ -91,6 +99,8 @@ public function testExtractReceive()
'receive' => true,
'respond' => true,
'persist' => true,
+ 'identifiers' => ['id' => ['Foo', 'id']],
+ 'has_composite_identifier' => false,
],
RequestAttributesExtractor::extractAttributes($request)
);
@@ -107,6 +117,8 @@ public function testExtractRespond()
'receive' => true,
'respond' => false,
'persist' => true,
+ 'identifiers' => ['id' => ['Foo', 'id']],
+ 'has_composite_identifier' => false,
],
RequestAttributesExtractor::extractAttributes($request)
);
@@ -120,6 +132,8 @@ public function testExtractRespond()
'receive' => true,
'respond' => true,
'persist' => true,
+ 'identifiers' => ['id' => ['Foo', 'id']],
+ 'has_composite_identifier' => false,
],
RequestAttributesExtractor::extractAttributes($request)
);
@@ -133,6 +147,8 @@ public function testExtractRespond()
'receive' => true,
'respond' => true,
'persist' => true,
+ 'identifiers' => ['id' => ['Foo', 'id']],
+ 'has_composite_identifier' => false,
],
RequestAttributesExtractor::extractAttributes($request)
);
@@ -149,6 +165,8 @@ public function testExtractPersist()
'receive' => true,
'respond' => true,
'persist' => false,
+ 'identifiers' => ['id' => ['Foo', 'id']],
+ 'has_composite_identifier' => false,
],
RequestAttributesExtractor::extractAttributes($request)
);
@@ -162,6 +180,8 @@ public function testExtractPersist()
'receive' => true,
'respond' => true,
'persist' => true,
+ 'identifiers' => ['id' => ['Foo', 'id']],
+ 'has_composite_identifier' => false,
],
RequestAttributesExtractor::extractAttributes($request)
);
@@ -175,6 +195,8 @@ public function testExtractPersist()
'receive' => true,
'respond' => true,
'persist' => true,
+ 'identifiers' => ['id' => ['Foo', 'id']],
+ 'has_composite_identifier' => false,
],
RequestAttributesExtractor::extractAttributes($request)
);
@@ -203,6 +225,26 @@ public function testExtractPreviousDataAttributes()
'respond' => true,
'persist' => true,
'previous_data' => $object,
+ 'identifiers' => ['id' => ['Foo', 'id']],
+ 'has_composite_identifier' => false,
+ ],
+ RequestAttributesExtractor::extractAttributes($request)
+ );
+ }
+
+ public function testExtractIdentifiers()
+ {
+ $request = new Request([], [], ['_api_resource_class' => 'Foo', '_api_item_operation_name' => 'get', '_api_identifiers' => ['test'], '_api_has_composite_identifier' => true]);
+
+ $this->assertEquals(
+ [
+ 'resource_class' => 'Foo',
+ 'item_operation_name' => 'get',
+ 'receive' => true,
+ 'respond' => true,
+ 'persist' => true,
+ 'identifiers' => ['test' => ['Foo', 'test']],
+ 'has_composite_identifier' => true,
],
RequestAttributesExtractor::extractAttributes($request)
);