diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index faccaa57ed5..d110e5a730c 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -522,7 +522,6 @@ jobs:
- name: Update project dependencies
run: |
composer update --no-interaction --no-progress --ansi
- # composer why doctrine/reflection
- name: Require Symfony Uid
run: composer require symfony/uid --dev --no-interaction --no-progress --ansi
- name: Install PHPUnit
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7ce9cb96450..e0bd71c6aeb 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,13 @@
# Changelog
+## 2.7.0
+
+* Doctrine: Better exception to find which resource is linked to an exception (#3965)
+* Doctrine: Allow mixed type value for date filter (notice if invalid) (#3870)
+* MongoDB: `date_immutable` support (#3940)
+* DataProvider: Add `TraversablePaginator` (#3783)
+* Swagger UI: Add `swagger_ui_extra_configuration` to Swagger / OpenAPI configuration (#3731)
+
## 2.6.3
* Identifiers: Re-allow `POST` operations even if no identifier is defined (#4052)
diff --git a/composer.json b/composer.json
index a4a2e4fa763..93f236ea7ad 100644
--- a/composer.json
+++ b/composer.json
@@ -37,7 +37,7 @@
"doctrine/common": "^2.11 || ^3.0",
"doctrine/data-fixtures": "^1.2.2",
"doctrine/doctrine-bundle": "^1.12 || ^2.0",
- "doctrine/mongodb-odm": "^2.0",
+ "doctrine/mongodb-odm": "^2.1",
"doctrine/mongodb-odm-bundle": "^4.0",
"doctrine/orm": "^2.6.4 || ^3.0",
"elasticsearch/elasticsearch": "^6.0 || ^7.0",
@@ -86,7 +86,7 @@
},
"conflict": {
"doctrine/common": "<2.7",
- "doctrine/mongodb-odm": "<2.0",
+ "doctrine/mongodb-odm": "<2.1",
"doctrine/persistence": "<1.3"
},
"suggest": {
diff --git a/docs/adr/0000-subresources-definition.md b/docs/adr/0000-subresources-definition.md
index 72cccb17e03..062711279fe 100644
--- a/docs/adr/0000-subresources-definition.md
+++ b/docs/adr/0000-subresources-definition.md
@@ -1,6 +1,6 @@
# Subresource Definition
-* Status: proposed
+* Status: superseded by [0002-resource-definition](0002-resource-definition.md)]
* Deciders: @dunglas, @vincentchalamon, @soyuka, @GregoireHebert, @Deuchnord
## Context and Problem Statement
diff --git a/docs/adr/0002-resource-definition.md b/docs/adr/0002-resource-definition.md
new file mode 100644
index 00000000000..7cdfd846950
--- /dev/null
+++ b/docs/adr/0002-resource-definition.md
@@ -0,0 +1,208 @@
+# Resource definition
+
+* Status: proposed
+* Deciders: @dunglas @soyuka @vincentchalamon @GregoireHebert
+
+## Context and Problem Statement
+
+The API Platform `@ApiResource` annotation was initially created to represent a Resource as defined in [Roy Fiedling's dissertation about REST](https://www.ics.uci.edu/~fielding/pubs/dissertation/rest_arch_style.htm#sec_5_2_1_1) in corelation with [RFC 7231 about HTTP Semantics](https://httpwg.org/specs/rfc7231.html#resources). This annotation brings some confusion as it mixes concepts of resources and operations. Here we discussed how we could revamp API Platform's resource definition using PHP8 attributes, beeing as close as we can to Roy Fiedling's thesis vocabulary.
+
+## Considered Options
+
+* Declare multiple ApiResource on a PHP Class [see Subresources definition](./0000-subresources-definition.md)
+* Declare operations in conjunction with resources using two attributes: `Resource` and `Operation`
+* Use HTTP Verb to represent operations with a syntax sugar for collections (`CGET`?)
+
+## Decision Outcome
+
+As Roy Fiedling's thesis states:
+
+> REST uses a resource identifier to identify the particular resource involved in an interaction between components. REST connectors provide a generic interface for accessing and manipulating the value set of a resource, regardless of how the membership function is defined or the type of software that is handling the request.
+
+In API Platform, this resource identifier is also named [IRI (Internationalized Resource Identifiers)](https://tools.ietf.org/html/rfc3987). Following these recommandation, applied to PHP, we came up with the following [PHP 8 attributes](https://www.php.net/manual/en/language.attributes.php):
+
+```php
+
+
+ |
+ Code
+ |
+
+ Operations
+ |
+
+
+
+
+#[Get]
+class User {}
+
+ |
+
+
+ |
+
+
+
+
+#[GetCollection]
+class User {}
+
+ |
+
+
+ |
+
+
+
+
+#[Get("/users")]
+class User {}
+
+ |
+
+
+ |
+
+
+
+
+#[Post("/users")]
+#[Patch("/users/{id}")]
+class User {}
+
+ |
+
+- POST /users
+- PATCH /users/{id}
+
|
+
+
+
+
+// See these as aliases to the `/users/{id}` Resource:
+#[Get("/companies/{companyId}/users/{id}")]
+#[Delete("/companies/{companyId}/users/{id}")]
+class User {}
+
+ |
+
+- GET /companies/{companyId}/users/{id}
+- DELETE /companies/{companyId}/users/{id}
+
|
+
+
+
+To link these operations with identifiers, refer to [Resource Identifiers decision record](./0001-resource-identifiers), for example:
+
+```php
+ [Company::class, "id"], "id" => [User::class, "id"]]])]
+class User {
+ #[ApiProperty(identifier=true)]
+ public $id;
+ public Company $company;
+}
+```
+
+The `Resource` attribute could be used to set defaults properties on operations:
+
+```php
+getLogger()->notice('Invalid filter ignored', [
+ 'exception' => new InvalidArgumentException(sprintf('Invalid value for "[%s]", expected string', $operator)),
+ ]);
+
+ return null;
+ }
+
+ return $value;
+ }
}
diff --git a/src/Bridge/Doctrine/Common/Util/IdentifierManagerTrait.php b/src/Bridge/Doctrine/Common/Util/IdentifierManagerTrait.php
index f33eca72ffa..d164fce4a7e 100644
--- a/src/Bridge/Doctrine/Common/Util/IdentifierManagerTrait.php
+++ b/src/Bridge/Doctrine/Common/Util/IdentifierManagerTrait.php
@@ -15,6 +15,7 @@
use ApiPlatform\Core\Exception\InvalidIdentifierException;
use ApiPlatform\Core\Exception\PropertyNotFoundException;
+use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
use Doctrine\DBAL\Types\ConversionException;
use Doctrine\DBAL\Types\Type as DBALType;
use Doctrine\ODM\MongoDB\DocumentManager;
@@ -29,6 +30,7 @@ trait IdentifierManagerTrait
{
private $propertyNameCollectionFactory;
private $propertyMetadataFactory;
+ private $resourceMetadataFactory;
/**
* Transform and check the identifier, composite or not.
@@ -74,7 +76,13 @@ private function normalizeIdentifiers($id, ObjectManager $manager, string $resou
$identifier = null === $identifiersMap ? $identifierValues[$i] ?? null : $identifiersMap[$propertyName] ?? null;
if (null === $identifier) {
- throw new PropertyNotFoundException(sprintf('Invalid identifier "%s", "%s" was not found.', $id, $propertyName));
+ $exceptionMessage = sprintf('Invalid identifier "%s", "%s" was not found', $id, $propertyName);
+ if ($this->resourceMetadataFactory instanceof ResourceMetadataFactoryInterface) {
+ $resourceMetadata = $this->resourceMetadataFactory->create($resourceClass);
+ $exceptionMessage .= sprintf(' for resource "%s"', $resourceMetadata->getShortName());
+ }
+
+ throw new PropertyNotFoundException($exceptionMessage.'.');
}
$doctrineTypeName = $doctrineClassMetadata->getTypeOfField($propertyName);
@@ -87,7 +95,13 @@ private function normalizeIdentifiers($id, ObjectManager $manager, string $resou
$identifier = MongoDbType::getType($doctrineTypeName)->convertToPHPValue($identifier);
}
} catch (ConversionException $e) {
- throw new InvalidIdentifierException(sprintf('Invalid value "%s" provided for an identifier.', $propertyName), $e->getCode(), $e);
+ $exceptionMessage = sprintf('Invalid value "%s" provided for an identifier', $propertyName);
+ if ($this->resourceMetadataFactory instanceof ResourceMetadataFactoryInterface) {
+ $resourceMetadata = $this->resourceMetadataFactory->create($resourceClass);
+ $exceptionMessage .= sprintf(' for resource "%s"', $resourceMetadata->getShortName());
+ }
+
+ throw new InvalidIdentifierException($exceptionMessage.'.', $e->getCode(), $e);
}
$identifiers[$propertyName] = $identifier;
diff --git a/src/Bridge/Doctrine/MongoDbOdm/Filter/DateFilter.php b/src/Bridge/Doctrine/MongoDbOdm/Filter/DateFilter.php
index 275a153e369..6c6abf3ccf7 100644
--- a/src/Bridge/Doctrine/MongoDbOdm/Filter/DateFilter.php
+++ b/src/Bridge/Doctrine/MongoDbOdm/Filter/DateFilter.php
@@ -34,6 +34,7 @@ class DateFilter extends AbstractFilter implements DateFilterInterface
public const DOCTRINE_DATE_TYPES = [
MongoDbType::DATE => true,
+ MongoDbType::DATE_IMMUTABLE => true,
];
/**
@@ -107,8 +108,14 @@ protected function filterProperty(string $property, $values, Builder $aggregatio
/**
* Adds the match stage according to the chosen null management.
*/
- private function addMatch(Builder $aggregationBuilder, string $field, string $operator, string $value, string $nullManagement = null): void
+ private function addMatch(Builder $aggregationBuilder, string $field, string $operator, $value, string $nullManagement = null): void
{
+ $value = $this->normalizeValue($value, $operator);
+
+ if (null === $value) {
+ return;
+ }
+
try {
$value = new \DateTime($value);
} catch (\Exception $e) {
diff --git a/src/Bridge/Doctrine/MongoDbOdm/Filter/SearchFilter.php b/src/Bridge/Doctrine/MongoDbOdm/Filter/SearchFilter.php
index 91cb23dc3cf..b75b2b310ea 100644
--- a/src/Bridge/Doctrine/MongoDbOdm/Filter/SearchFilter.php
+++ b/src/Bridge/Doctrine/MongoDbOdm/Filter/SearchFilter.php
@@ -202,6 +202,7 @@ protected function getType(string $doctrineType): string
case MongoDbType::BOOLEAN:
return 'bool';
case MongoDbType::DATE:
+ case MongoDbType::DATE_IMMUTABLE:
return \DateTimeInterface::class;
case MongoDbType::FLOAT:
return 'float';
diff --git a/src/Bridge/Doctrine/MongoDbOdm/PropertyInfo/DoctrineExtractor.php b/src/Bridge/Doctrine/MongoDbOdm/PropertyInfo/DoctrineExtractor.php
index 3325392b5da..6538bce9cf0 100644
--- a/src/Bridge/Doctrine/MongoDbOdm/PropertyInfo/DoctrineExtractor.php
+++ b/src/Bridge/Doctrine/MongoDbOdm/PropertyInfo/DoctrineExtractor.php
@@ -92,6 +92,8 @@ public function getTypes($class, $property, array $context = [])
switch ($typeOfField) {
case MongoDbType::DATE:
return [new Type(Type::BUILTIN_TYPE_OBJECT, $nullable, 'DateTime')];
+ case MongoDbType::DATE_IMMUTABLE:
+ return [new Type(Type::BUILTIN_TYPE_OBJECT, $nullable, 'DateTimeImmutable')];
case MongoDbType::HASH:
return [new Type(Type::BUILTIN_TYPE_ARRAY, $nullable, null, true)];
case MongoDbType::COLLECTION:
diff --git a/src/Bridge/Doctrine/Orm/Filter/DateFilter.php b/src/Bridge/Doctrine/Orm/Filter/DateFilter.php
index 0b619be243e..9970eadf3b4 100644
--- a/src/Bridge/Doctrine/Orm/Filter/DateFilter.php
+++ b/src/Bridge/Doctrine/Orm/Filter/DateFilter.php
@@ -128,9 +128,15 @@ protected function filterProperty(string $property, $values, QueryBuilder $query
*
* @param string|DBALType $type
*/
- protected function addWhere(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $alias, string $field, string $operator, string $value, string $nullManagement = null, $type = null)
+ protected function addWhere(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $alias, string $field, string $operator, $value, string $nullManagement = null, $type = null)
{
$type = (string) $type;
+ $value = $this->normalizeValue($value, $operator);
+
+ if (null === $value) {
+ return;
+ }
+
try {
$value = false === strpos($type, '_immutable') ? new \DateTime($value) : new \DateTimeImmutable($value);
} catch (\Exception $e) {
diff --git a/src/Bridge/Doctrine/Orm/ItemDataProvider.php b/src/Bridge/Doctrine/Orm/ItemDataProvider.php
index 9535b162427..e36cd8c79c8 100644
--- a/src/Bridge/Doctrine/Orm/ItemDataProvider.php
+++ b/src/Bridge/Doctrine/Orm/ItemDataProvider.php
@@ -23,6 +23,7 @@
use ApiPlatform\Core\Identifier\IdentifierConverterInterface;
use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
+use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\QueryBuilder;
use Doctrine\Persistence\ManagerRegistry;
@@ -45,11 +46,12 @@ class ItemDataProvider implements DenormalizedIdentifiersAwareItemDataProviderIn
/**
* @param QueryItemExtensionInterface[] $itemExtensions
*/
- public function __construct(ManagerRegistry $managerRegistry, PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, iterable $itemExtensions = [])
+ public function __construct(ManagerRegistry $managerRegistry, PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, iterable $itemExtensions = [], ResourceMetadataFactoryInterface $resourceMetadataFactory = null)
{
$this->managerRegistry = $managerRegistry;
$this->propertyNameCollectionFactory = $propertyNameCollectionFactory;
$this->propertyMetadataFactory = $propertyMetadataFactory;
+ $this->resourceMetadataFactory = $resourceMetadataFactory;
$this->itemExtensions = $itemExtensions;
}
diff --git a/src/Bridge/Doctrine/Orm/SubresourceDataProvider.php b/src/Bridge/Doctrine/Orm/SubresourceDataProvider.php
index e3571d9880b..264cf65d84c 100644
--- a/src/Bridge/Doctrine/Orm/SubresourceDataProvider.php
+++ b/src/Bridge/Doctrine/Orm/SubresourceDataProvider.php
@@ -26,6 +26,7 @@
use ApiPlatform\Core\Identifier\IdentifierConverterInterface;
use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
+use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Mapping\ClassMetadataInfo;
use Doctrine\ORM\QueryBuilder;
@@ -48,11 +49,12 @@ final class SubresourceDataProvider implements SubresourceDataProviderInterface
* @param QueryCollectionExtensionInterface[] $collectionExtensions
* @param QueryItemExtensionInterface[] $itemExtensions
*/
- public function __construct(ManagerRegistry $managerRegistry, PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, iterable $collectionExtensions = [], iterable $itemExtensions = [])
+ public function __construct(ManagerRegistry $managerRegistry, PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, iterable $collectionExtensions = [], iterable $itemExtensions = [], ResourceMetadataFactoryInterface $resourceMetadataFactory = null)
{
$this->managerRegistry = $managerRegistry;
$this->propertyNameCollectionFactory = $propertyNameCollectionFactory;
$this->propertyMetadataFactory = $propertyMetadataFactory;
+ $this->resourceMetadataFactory = $resourceMetadataFactory;
$this->collectionExtensions = $collectionExtensions;
$this->itemExtensions = $itemExtensions;
}
diff --git a/src/Bridge/Symfony/Bundle/Action/SwaggerUiAction.php b/src/Bridge/Symfony/Bundle/Action/SwaggerUiAction.php
index d84a1b2214a..5a03d2baffd 100644
--- a/src/Bridge/Symfony/Bundle/Action/SwaggerUiAction.php
+++ b/src/Bridge/Symfony/Bundle/Action/SwaggerUiAction.php
@@ -62,12 +62,13 @@ final class SwaggerUiAction
private $swaggerVersions;
private $swaggerUiAction;
private $assetPackage;
+ private $swaggerUiExtraConfiguration;
/**
* @param int[] $swaggerVersions
* @param mixed|null $assetPackage
*/
- public function __construct(ResourceNameCollectionFactoryInterface $resourceNameCollectionFactory, ResourceMetadataFactoryInterface $resourceMetadataFactory, NormalizerInterface $normalizer, TwigEnvironment $twig, UrlGeneratorInterface $urlGenerator, string $title = '', string $description = '', string $version = '', $formats = [], $oauthEnabled = false, $oauthClientId = '', $oauthClientSecret = '', $oauthType = '', $oauthFlow = '', $oauthTokenUrl = '', $oauthAuthorizationUrl = '', $oauthScopes = [], bool $showWebby = true, bool $swaggerUiEnabled = false, bool $reDocEnabled = false, bool $graphqlEnabled = false, bool $graphiQlEnabled = false, bool $graphQlPlaygroundEnabled = false, array $swaggerVersions = [2, 3], OpenApiSwaggerUiAction $swaggerUiAction = null, $assetPackage = null)
+ public function __construct(ResourceNameCollectionFactoryInterface $resourceNameCollectionFactory, ResourceMetadataFactoryInterface $resourceMetadataFactory, NormalizerInterface $normalizer, TwigEnvironment $twig, UrlGeneratorInterface $urlGenerator, string $title = '', string $description = '', string $version = '', $formats = [], $oauthEnabled = false, $oauthClientId = '', $oauthClientSecret = '', $oauthType = '', $oauthFlow = '', $oauthTokenUrl = '', $oauthAuthorizationUrl = '', $oauthScopes = [], bool $showWebby = true, bool $swaggerUiEnabled = false, bool $reDocEnabled = false, bool $graphqlEnabled = false, bool $graphiQlEnabled = false, bool $graphQlPlaygroundEnabled = false, array $swaggerVersions = [2, 3], OpenApiSwaggerUiAction $swaggerUiAction = null, $assetPackage = null, array $swaggerUiExtraConfiguration = [])
{
$this->resourceNameCollectionFactory = $resourceNameCollectionFactory;
$this->resourceMetadataFactory = $resourceMetadataFactory;
@@ -93,6 +94,7 @@ public function __construct(ResourceNameCollectionFactoryInterface $resourceName
$this->graphQlPlaygroundEnabled = $graphQlPlaygroundEnabled;
$this->swaggerVersions = $swaggerVersions;
$this->swaggerUiAction = $swaggerUiAction;
+ $this->swaggerUiExtraConfiguration = $swaggerUiExtraConfiguration;
$this->assetPackage = $assetPackage;
if (null === $this->swaggerUiAction) {
@@ -157,6 +159,7 @@ private function getContext(Request $request, Documentation $documentation): arr
$swaggerData = [
'url' => $this->urlGenerator->generate('api_doc', ['format' => 'json']),
'spec' => $this->normalizer->normalize($documentation, 'json', $swaggerContext),
+ 'extraConfiguration' => $this->swaggerUiExtraConfiguration,
];
$swaggerData['oauth'] = [
diff --git a/src/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php b/src/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php
index c330c90905f..8eee94a3990 100644
--- a/src/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php
+++ b/src/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php
@@ -397,6 +397,7 @@ private function registerSwaggerConfiguration(ContainerBuilder $container, array
$container->setParameter('api_platform.enable_swagger_ui', $config['enable_swagger_ui']);
$container->setParameter('api_platform.enable_re_doc', $config['enable_re_doc']);
$container->setParameter('api_platform.swagger.api_keys', $config['swagger']['api_keys']);
+ $container->setParameter('api_platform.swagger_ui.extra_configuration', $config['openapi']['swagger_ui_extra_configuration'] ?? $config['swagger']['swagger_ui_extra_configuration']);
if (true === $config['openapi']['backward_compatibility_layer']) {
$container->getDefinition('api_platform.swagger.normalizer.documentation')->addArgument($container->getDefinition('api_platform.openapi.normalizer'));
diff --git a/src/Bridge/Symfony/Bundle/DependencyInjection/Configuration.php b/src/Bridge/Symfony/Bundle/DependencyInjection/Configuration.php
index de9a01c80a8..d14ec4533ab 100644
--- a/src/Bridge/Symfony/Bundle/DependencyInjection/Configuration.php
+++ b/src/Bridge/Symfony/Bundle/DependencyInjection/Configuration.php
@@ -339,6 +339,14 @@ private function addSwaggerSection(ArrayNodeDefinition $rootNode): void
->end()
->end()
->end()
+ ->variableNode('swagger_ui_extra_configuration')
+ ->defaultValue([])
+ ->validate()
+ ->ifTrue(static function ($v) { return false === \is_array($v); })
+ ->thenInvalid('The swagger_ui_extra_configuration parameter must be an array.')
+ ->end()
+ ->info('To pass extra configuration to Swagger UI, like docExpansion or filter.')
+ ->end()
->end()
->end()
->end();
@@ -492,6 +500,14 @@ private function addOpenApiSection(ArrayNodeDefinition $rootNode): void
->scalarNode('url')->defaultNull()->info('URL to the license used for the API. MUST be in the format of a URL.')->end()
->end()
->end()
+ ->variableNode('swagger_ui_extra_configuration')
+ ->defaultValue([])
+ ->validate()
+ ->ifTrue(static function ($v) { return false === \is_array($v); })
+ ->thenInvalid('The swagger_ui_extra_configuration parameter must be an array.')
+ ->end()
+ ->info('To pass extra configuration to Swagger UI, like docExpansion or filter.')
+ ->end()
->end()
->end()
->end();
diff --git a/src/Bridge/Symfony/Bundle/Resources/config/doctrine_orm.xml b/src/Bridge/Symfony/Bundle/Resources/config/doctrine_orm.xml
index f60b7d564af..e18d751f2b6 100644
--- a/src/Bridge/Symfony/Bundle/Resources/config/doctrine_orm.xml
+++ b/src/Bridge/Symfony/Bundle/Resources/config/doctrine_orm.xml
@@ -25,6 +25,7 @@
+
@@ -33,6 +34,7 @@
+
diff --git a/src/Bridge/Symfony/Bundle/Resources/config/swagger-ui.xml b/src/Bridge/Symfony/Bundle/Resources/config/swagger-ui.xml
index c56d3a3440c..f882312a68d 100644
--- a/src/Bridge/Symfony/Bundle/Resources/config/swagger-ui.xml
+++ b/src/Bridge/Symfony/Bundle/Resources/config/swagger-ui.xml
@@ -38,6 +38,7 @@
%api_platform.swagger.versions%
%api_platform.asset_package%
+ %api_platform.swagger_ui.extra_configuration%
@@ -48,6 +49,7 @@
%api_platform.graphql.graphiql.enabled%
%api_platform.graphql.graphql_playground.enabled%
%api_platform.asset_package%
+ %api_platform.swagger_ui.extra_configuration%
diff --git a/src/Bridge/Symfony/Bundle/Resources/public/init-swagger-ui.js b/src/Bridge/Symfony/Bundle/Resources/public/init-swagger-ui.js
index 95f2b63ed5e..f248f92bbc2 100644
--- a/src/Bridge/Symfony/Bundle/Resources/public/init-swagger-ui.js
+++ b/src/Bridge/Symfony/Bundle/Resources/public/init-swagger-ui.js
@@ -41,7 +41,7 @@ window.onload = function() {
}).observe(document, {childList: true, subtree: true});
const data = JSON.parse(document.getElementById('swagger-data').innerText);
- const ui = SwaggerUIBundle({
+ const ui = SwaggerUIBundle(Object.assign({
spec: data.spec,
dom_id: '#swagger-ui',
validatorUrl: null,
@@ -54,7 +54,7 @@ window.onload = function() {
SwaggerUIBundle.plugins.DownloadUrl,
],
layout: 'StandaloneLayout',
- });
+ }, data.extraConfiguration));
if (data.oauth.enabled) {
ui.initOAuth({
diff --git a/src/Bridge/Symfony/Bundle/SwaggerUi/SwaggerUiAction.php b/src/Bridge/Symfony/Bundle/SwaggerUi/SwaggerUiAction.php
index 81bf60ebb39..c88c89993e1 100644
--- a/src/Bridge/Symfony/Bundle/SwaggerUi/SwaggerUiAction.php
+++ b/src/Bridge/Symfony/Bundle/SwaggerUi/SwaggerUiAction.php
@@ -86,6 +86,7 @@ public function __invoke(Request $request)
'clientId' => $this->oauthClientId,
'clientSecret' => $this->oauthClientSecret,
],
+ 'extraConfiguration' => $this->swaggerUiContext->getExtraConfiguration(),
];
if ($request->isMethodSafe() && null !== $resourceClass = $request->attributes->get('_api_resource_class')) {
diff --git a/src/Bridge/Symfony/Bundle/SwaggerUi/SwaggerUiContext.php b/src/Bridge/Symfony/Bundle/SwaggerUi/SwaggerUiContext.php
index 29aaf205600..d37f51fcfbe 100644
--- a/src/Bridge/Symfony/Bundle/SwaggerUi/SwaggerUiContext.php
+++ b/src/Bridge/Symfony/Bundle/SwaggerUi/SwaggerUiContext.php
@@ -22,8 +22,9 @@ final class SwaggerUiContext
private $graphiQlEnabled;
private $graphQlPlaygroundEnabled;
private $assetPackage;
+ private $extraConfiguration;
- public function __construct(bool $swaggerUiEnabled = false, bool $showWebby = true, bool $reDocEnabled = false, bool $graphQlEnabled = false, bool $graphiQlEnabled = false, bool $graphQlPlaygroundEnabled = false, $assetPackage = null)
+ public function __construct(bool $swaggerUiEnabled = false, bool $showWebby = true, bool $reDocEnabled = false, bool $graphQlEnabled = false, bool $graphiQlEnabled = false, bool $graphQlPlaygroundEnabled = false, $assetPackage = null, array $extraConfiguration = [])
{
$this->swaggerUiEnabled = $swaggerUiEnabled;
$this->showWebby = $showWebby;
@@ -32,6 +33,7 @@ public function __construct(bool $swaggerUiEnabled = false, bool $showWebby = tr
$this->graphiQlEnabled = $graphiQlEnabled;
$this->graphQlPlaygroundEnabled = $graphQlPlaygroundEnabled;
$this->assetPackage = $assetPackage;
+ $this->extraConfiguration = $extraConfiguration;
}
public function isSwaggerUiEnabled(): bool
@@ -68,4 +70,9 @@ public function getAssetPackage(): ?string
{
return $this->assetPackage;
}
+
+ public function getExtraConfiguration(): array
+ {
+ return $this->extraConfiguration;
+ }
}
diff --git a/src/DataProvider/TraversablePaginator.php b/src/DataProvider/TraversablePaginator.php
new file mode 100644
index 00000000000..91635bc1fb4
--- /dev/null
+++ b/src/DataProvider/TraversablePaginator.php
@@ -0,0 +1,90 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace ApiPlatform\Core\DataProvider;
+
+final class TraversablePaginator implements \IteratorAggregate, PaginatorInterface
+{
+ private $traversable;
+ private $currentPage;
+ private $itemsPerPage;
+ private $totalItems;
+
+ public function __construct(\Traversable $iterator, float $currentPage, float $itemsPerPage, float $totalItems)
+ {
+ $this->traversable = $iterator;
+ $this->currentPage = $currentPage;
+ $this->itemsPerPage = $itemsPerPage;
+ $this->totalItems = $totalItems;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getCurrentPage(): float
+ {
+ return $this->currentPage;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getLastPage(): float
+ {
+ if (0. >= $this->itemsPerPage) {
+ return 1.;
+ }
+
+ return max(ceil($this->totalItems / $this->itemsPerPage) ?: 1., 1.);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getItemsPerPage(): float
+ {
+ return $this->itemsPerPage;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getTotalItems(): float
+ {
+ return $this->totalItems;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function count(): int
+ {
+ if ($this->getCurrentPage() < $this->getLastPage()) {
+ return (int) ceil($this->itemsPerPage);
+ }
+
+ if (0. >= $this->itemsPerPage) {
+ return (int) ceil($this->totalItems);
+ }
+
+ return $this->totalItems % $this->itemsPerPage;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getIterator(): \Traversable
+ {
+ return $this->traversable;
+ }
+}
diff --git a/src/OpenApi/Model/PathItem.php b/src/OpenApi/Model/PathItem.php
index 5c07c9006c2..fae2c59a0ff 100644
--- a/src/OpenApi/Model/PathItem.php
+++ b/src/OpenApi/Model/PathItem.php
@@ -138,7 +138,7 @@ public function withDescription(string $description): self
return $clone;
}
- public function withGet(Operation $get): self
+ public function withGet(?Operation $get): self
{
$clone = clone $this;
$clone->get = $get;
@@ -146,7 +146,7 @@ public function withGet(Operation $get): self
return $clone;
}
- public function withPut(Operation $put): self
+ public function withPut(?Operation $put): self
{
$clone = clone $this;
$clone->put = $put;
@@ -154,7 +154,7 @@ public function withPut(Operation $put): self
return $clone;
}
- public function withPost(Operation $post): self
+ public function withPost(?Operation $post): self
{
$clone = clone $this;
$clone->post = $post;
@@ -162,7 +162,7 @@ public function withPost(Operation $post): self
return $clone;
}
- public function withDelete(Operation $delete): self
+ public function withDelete(?Operation $delete): self
{
$clone = clone $this;
$clone->delete = $delete;
@@ -186,7 +186,7 @@ public function withHead(Operation $head): self
return $clone;
}
- public function withPatch(Operation $patch): self
+ public function withPatch(?Operation $patch): self
{
$clone = clone $this;
$clone->patch = $patch;
diff --git a/tests/Behat/DoctrineContext.php b/tests/Behat/DoctrineContext.php
index a62255ed6b5..c57510af27d 100644
--- a/tests/Behat/DoctrineContext.php
+++ b/tests/Behat/DoctrineContext.php
@@ -45,6 +45,7 @@
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\DummyDtoOutputSameClass as DummyDtoOutputSameClassDocument;
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\DummyFriend as DummyFriendDocument;
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\DummyGroup as DummyGroupDocument;
+use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\DummyImmutableDate as DummyImmutableDateDocument;
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\DummyMercure as DummyMercureDocument;
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\DummyOffer as DummyOfferDocument;
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\DummyProduct as DummyProductDocument;
@@ -1271,7 +1272,7 @@ public function thereAreDummyImmutableDateObjectsWithDummyDate(int $nb)
{
for ($i = 1; $i <= $nb; ++$i) {
$date = new \DateTimeImmutable(sprintf('2015-04-%d', $i), new \DateTimeZone('UTC'));
- $dummy = new DummyImmutableDate();
+ $dummy = $this->buildDummyImmutableDate();
$dummy->dummyDate = $date;
$this->manager->persist($dummy);
@@ -1831,6 +1832,14 @@ private function buildDummyDate()
return $this->isOrm() ? new DummyDate() : new DummyDateDocument();
}
+ /**
+ * @return DummyImmutableDate|DummyImmutableDateDocument
+ */
+ private function buildDummyImmutableDate()
+ {
+ return $this->isOrm() ? new DummyImmutableDate() : new DummyImmutableDateDocument();
+ }
+
/**
* @return DummyDifferentGraphQlSerializationGroup|DummyDifferentGraphQlSerializationGroupDocument
*/
diff --git a/tests/Bridge/Doctrine/Common/Filter/DateFilterTestTrait.php b/tests/Bridge/Doctrine/Common/Filter/DateFilterTestTrait.php
index 490960d9bb8..7d680d26d54 100644
--- a/tests/Bridge/Doctrine/Common/Filter/DateFilterTestTrait.php
+++ b/tests/Bridge/Doctrine/Common/Filter/DateFilterTestTrait.php
@@ -220,6 +220,7 @@ private function provideApplyTestArguments(): array
[
'dummyDate' => [
'after' => '1932iur123ufqe',
+ 'before' => [],
],
],
],
diff --git a/tests/Bridge/Doctrine/Common/Util/IdentifierManagerTraitTest.php b/tests/Bridge/Doctrine/Common/Util/IdentifierManagerTraitTest.php
index 99c9d3d5abc..6e8fe0b97d9 100644
--- a/tests/Bridge/Doctrine/Common/Util/IdentifierManagerTraitTest.php
+++ b/tests/Bridge/Doctrine/Common/Util/IdentifierManagerTraitTest.php
@@ -20,6 +20,8 @@
use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
use ApiPlatform\Core\Metadata\Property\PropertyMetadata;
use ApiPlatform\Core\Metadata\Property\PropertyNameCollection;
+use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
+use ApiPlatform\Core\Metadata\Resource\ResourceMetadata;
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\Dummy as DummyDocument;
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Dummy;
use ApiPlatform\Core\Tests\ProphecyTrait;
@@ -39,17 +41,18 @@ class IdentifierManagerTraitTest extends TestCase
{
use ProphecyTrait;
- private function getIdentifierManagerTraitImpl(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory)
+ private function getIdentifierManagerTraitImpl(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, ResourceMetadataFactoryInterface $resourceMetadataFactory)
{
- return new class($propertyNameCollectionFactory, $propertyMetadataFactory) {
+ return new class($propertyNameCollectionFactory, $propertyMetadataFactory, $resourceMetadataFactory) {
use IdentifierManagerTrait {
IdentifierManagerTrait::normalizeIdentifiers as public;
}
- public function __construct(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory)
+ public function __construct(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, ResourceMetadataFactoryInterface $resourceMetadataFactory)
{
$this->propertyNameCollectionFactory = $propertyNameCollectionFactory;
$this->propertyMetadataFactory = $propertyMetadataFactory;
+ $this->resourceMetadataFactory = $resourceMetadataFactory;
}
};
}
@@ -59,7 +62,7 @@ public function __construct(PropertyNameCollectionFactoryInterface $propertyName
*/
public function testSingleIdentifier()
{
- [$propertyNameCollectionFactory, $propertyMetadataFactory] = $this->getMetadataFactories(Dummy::class, [
+ [$propertyNameCollectionFactory, $propertyMetadataFactory, $resourceMetadataFactory] = $this->getMetadataFactories(Dummy::class, [
'id',
]);
$objectManager = $this->getEntityManager(Dummy::class, [
@@ -68,7 +71,7 @@ public function testSingleIdentifier()
],
]);
- $identifierManager = $this->getIdentifierManagerTraitImpl($propertyNameCollectionFactory, $propertyMetadataFactory);
+ $identifierManager = $this->getIdentifierManagerTraitImpl($propertyNameCollectionFactory, $propertyMetadataFactory, $resourceMetadataFactory);
$this->assertEquals($identifierManager->normalizeIdentifiers(1, $objectManager, Dummy::class), ['id' => 1]);
}
@@ -79,7 +82,7 @@ public function testSingleIdentifier()
*/
public function testSingleDocumentIdentifier()
{
- [$propertyNameCollectionFactory, $propertyMetadataFactory] = $this->getMetadataFactories(DummyDocument::class, [
+ [$propertyNameCollectionFactory, $propertyMetadataFactory, $resourceMetadataFactory] = $this->getMetadataFactories(DummyDocument::class, [
'id',
]);
$objectManager = $this->getDocumentManager(DummyDocument::class, [
@@ -88,7 +91,7 @@ public function testSingleDocumentIdentifier()
],
]);
- $identifierManager = $this->getIdentifierManagerTraitImpl($propertyNameCollectionFactory, $propertyMetadataFactory);
+ $identifierManager = $this->getIdentifierManagerTraitImpl($propertyNameCollectionFactory, $propertyMetadataFactory, $resourceMetadataFactory);
$this->assertEquals($identifierManager->normalizeIdentifiers(1, $objectManager, DummyDocument::class), ['id' => 1]);
}
@@ -98,7 +101,7 @@ public function testSingleDocumentIdentifier()
*/
public function testCompositeIdentifier()
{
- [$propertyNameCollectionFactory, $propertyMetadataFactory] = $this->getMetadataFactories(Dummy::class, [
+ [$propertyNameCollectionFactory, $propertyMetadataFactory, $resourceMetadataFactory] = $this->getMetadataFactories(Dummy::class, [
'ida',
'idb',
]);
@@ -111,7 +114,7 @@ public function testCompositeIdentifier()
],
]);
- $identifierManager = $this->getIdentifierManagerTraitImpl($propertyNameCollectionFactory, $propertyMetadataFactory);
+ $identifierManager = $this->getIdentifierManagerTraitImpl($propertyNameCollectionFactory, $propertyMetadataFactory, $resourceMetadataFactory);
$this->assertEquals($identifierManager->normalizeIdentifiers('ida=1;idb=2', $objectManager, Dummy::class), ['ida' => 1, 'idb' => 2]);
}
@@ -122,9 +125,9 @@ public function testCompositeIdentifier()
public function testInvalidIdentifier()
{
$this->expectException(PropertyNotFoundException::class);
- $this->expectExceptionMessage('Invalid identifier "idbad=1;idb=2", "ida" was not found.');
+ $this->expectExceptionMessage('Invalid identifier "idbad=1;idb=2", "ida" was not found for resource "dummy".');
- [$propertyNameCollectionFactory, $propertyMetadataFactory] = $this->getMetadataFactories(Dummy::class, [
+ [$propertyNameCollectionFactory, $propertyMetadataFactory, $resourceMetadataFactory] = $this->getMetadataFactories(Dummy::class, [
'ida',
'idb',
]);
@@ -137,7 +140,7 @@ public function testInvalidIdentifier()
],
]);
- $identifierManager = $this->getIdentifierManagerTraitImpl($propertyNameCollectionFactory, $propertyMetadataFactory);
+ $identifierManager = $this->getIdentifierManagerTraitImpl($propertyNameCollectionFactory, $propertyMetadataFactory, $resourceMetadataFactory);
$identifierManager->normalizeIdentifiers('idbad=1;idb=2', $objectManager, Dummy::class);
}
@@ -149,6 +152,7 @@ private function getMetadataFactories(string $resourceClass, array $identifiers)
{
$propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class);
$propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class);
+ $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class);
$nameCollection = ['foobar'];
@@ -164,8 +168,9 @@ private function getMetadataFactories(string $resourceClass, array $identifiers)
$propertyMetadataFactoryProphecy->create($resourceClass, 'foobar')->willReturn(new PropertyMetadata());
$propertyNameCollectionFactoryProphecy->create($resourceClass)->willReturn(new PropertyNameCollection($nameCollection));
+ $resourceMetadataFactoryProphecy->create($resourceClass)->willReturn(new ResourceMetadata('dummy'));
- return [$propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal()];
+ return [$propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), $resourceMetadataFactoryProphecy->reveal()];
}
/**
@@ -218,9 +223,9 @@ public function testInvalidIdentifierConversion()
DBALType::addType('uuid', UuidType::class);
$this->expectException(InvalidIdentifierException::class);
- $this->expectExceptionMessage('Invalid value "ida" provided for an identifier.');
+ $this->expectExceptionMessage('Invalid value "ida" provided for an identifier for resource "dummy".');
- [$propertyNameCollectionFactory, $propertyMetadataFactory] = $this->getMetadataFactories(Dummy::class, [
+ [$propertyNameCollectionFactory, $propertyMetadataFactory, $resourceMetadataFactory] = $this->getMetadataFactories(Dummy::class, [
'ida',
]);
$objectManager = $this->getEntityManager(Dummy::class, [
@@ -229,7 +234,7 @@ public function testInvalidIdentifierConversion()
],
]);
- $identifierManager = $this->getIdentifierManagerTraitImpl($propertyNameCollectionFactory, $propertyMetadataFactory);
+ $identifierManager = $this->getIdentifierManagerTraitImpl($propertyNameCollectionFactory, $propertyMetadataFactory, $resourceMetadataFactory);
$identifierManager->normalizeIdentifiers('notanuuid', $objectManager, Dummy::class);
}
diff --git a/tests/Bridge/Doctrine/MongoDbOdm/PropertyInfo/DoctrineExtractorTest.php b/tests/Bridge/Doctrine/MongoDbOdm/PropertyInfo/DoctrineExtractorTest.php
index 36505c4dc9c..1638b933ea9 100644
--- a/tests/Bridge/Doctrine/MongoDbOdm/PropertyInfo/DoctrineExtractorTest.php
+++ b/tests/Bridge/Doctrine/MongoDbOdm/PropertyInfo/DoctrineExtractorTest.php
@@ -52,6 +52,7 @@ public function testGetProperties(): void
'binUuidRfc4122',
'timestamp',
'date',
+ 'dateImmutable',
'float',
'bool',
'boolean',
@@ -141,6 +142,7 @@ public function typesProvider(): array
['binUuidRfc4122', [new Type(Type::BUILTIN_TYPE_STRING)]],
['timestamp', [new Type(Type::BUILTIN_TYPE_STRING)]],
['date', [new Type(Type::BUILTIN_TYPE_OBJECT, false, 'DateTime')]],
+ ['dateImmutable', [new Type(Type::BUILTIN_TYPE_OBJECT, false, 'DateTimeImmutable')]],
['float', [new Type(Type::BUILTIN_TYPE_FLOAT)]],
['bool', [new Type(Type::BUILTIN_TYPE_BOOL)]],
['boolean', [new Type(Type::BUILTIN_TYPE_BOOL)]],
diff --git a/tests/Bridge/Doctrine/MongoDbOdm/PropertyInfo/Fixtures/DoctrineDummy.php b/tests/Bridge/Doctrine/MongoDbOdm/PropertyInfo/Fixtures/DoctrineDummy.php
index 64a34ed4f65..e54438a4190 100644
--- a/tests/Bridge/Doctrine/MongoDbOdm/PropertyInfo/Fixtures/DoctrineDummy.php
+++ b/tests/Bridge/Doctrine/MongoDbOdm/PropertyInfo/Fixtures/DoctrineDummy.php
@@ -91,6 +91,11 @@ class DoctrineDummy
*/
private $date;
+ /**
+ * @Field(type="date_immutable")
+ */
+ private $dateImmutable;
+
/**
* @Field(type="float")
*/
diff --git a/tests/Bridge/Doctrine/Orm/ItemDataProviderTest.php b/tests/Bridge/Doctrine/Orm/ItemDataProviderTest.php
index e356de369ae..a17dd694fdb 100644
--- a/tests/Bridge/Doctrine/Orm/ItemDataProviderTest.php
+++ b/tests/Bridge/Doctrine/Orm/ItemDataProviderTest.php
@@ -24,6 +24,8 @@
use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
use ApiPlatform\Core\Metadata\Property\PropertyMetadata;
use ApiPlatform\Core\Metadata\Property\PropertyNameCollection;
+use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
+use ApiPlatform\Core\Metadata\Resource\ResourceMetadata;
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Dummy;
use ApiPlatform\Core\Tests\ProphecyTrait;
use Doctrine\DBAL\Connection;
@@ -69,7 +71,7 @@ public function testGetItemSingleIdentifier()
$queryBuilder = $queryBuilderProphecy->reveal();
- [$propertyNameCollectionFactory, $propertyMetadataFactory] = $this->getMetadataFactories(Dummy::class, [
+ [$propertyNameCollectionFactory, $propertyMetadataFactory, $resourceMetadataFactory] = $this->getMetadataFactories(Dummy::class, [
'id',
]);
$managerRegistry = $this->getManagerRegistry(Dummy::class, [
@@ -81,7 +83,7 @@ public function testGetItemSingleIdentifier()
$extensionProphecy = $this->prophesize(QueryItemExtensionInterface::class);
$extensionProphecy->applyToItem($queryBuilder, Argument::type(QueryNameGeneratorInterface::class), Dummy::class, ['id' => 1], 'foo', $context)->shouldBeCalled();
- $dataProvider = new ItemDataProvider($managerRegistry, $propertyNameCollectionFactory, $propertyMetadataFactory, [$extensionProphecy->reveal()]);
+ $dataProvider = new ItemDataProvider($managerRegistry, $propertyNameCollectionFactory, $propertyMetadataFactory, [$extensionProphecy->reveal()], $resourceMetadataFactory);
$this->assertEquals([], $dataProvider->getItem(Dummy::class, ['id' => 1], 'foo', $context));
}
@@ -109,7 +111,7 @@ public function testGetItemDoubleIdentifier()
$queryBuilder = $queryBuilderProphecy->reveal();
- [$propertyNameCollectionFactory, $propertyMetadataFactory] = $this->getMetadataFactories(Dummy::class, [
+ [$propertyNameCollectionFactory, $propertyMetadataFactory, $resourceMetadataFactory] = $this->getMetadataFactories(Dummy::class, [
'ida',
'idb',
]);
@@ -126,7 +128,7 @@ public function testGetItemDoubleIdentifier()
$extensionProphecy = $this->prophesize(QueryItemExtensionInterface::class);
$extensionProphecy->applyToItem($queryBuilder, Argument::type(QueryNameGeneratorInterface::class), Dummy::class, ['ida' => 1, 'idb' => 2], 'foo', $context)->shouldBeCalled();
- $dataProvider = new ItemDataProvider($managerRegistry, $propertyNameCollectionFactory, $propertyMetadataFactory, [$extensionProphecy->reveal()]);
+ $dataProvider = new ItemDataProvider($managerRegistry, $propertyNameCollectionFactory, $propertyMetadataFactory, [$extensionProphecy->reveal()], $resourceMetadataFactory);
$this->assertEquals([], $dataProvider->getItem(Dummy::class, ['ida' => 1, 'idb' => 2], 'foo', $context));
}
@@ -138,7 +140,7 @@ public function testGetItemWrongCompositeIdentifier()
{
$this->expectException(PropertyNotFoundException::class);
- [$propertyNameCollectionFactory, $propertyMetadataFactory] = $this->getMetadataFactories(Dummy::class, [
+ [$propertyNameCollectionFactory, $propertyMetadataFactory, $resourceMetadataFactory] = $this->getMetadataFactories(Dummy::class, [
'ida',
'idb',
]);
@@ -151,7 +153,7 @@ public function testGetItemWrongCompositeIdentifier()
],
], $this->prophesize(QueryBuilder::class)->reveal());
- $dataProvider = new ItemDataProvider($managerRegistry, $propertyNameCollectionFactory, $propertyMetadataFactory);
+ $dataProvider = new ItemDataProvider($managerRegistry, $propertyNameCollectionFactory, $propertyMetadataFactory, [], $resourceMetadataFactory);
$dataProvider->getItem(Dummy::class, 'ida=1;', 'foo');
}
@@ -171,7 +173,7 @@ public function testQueryResultExtension()
$queryBuilder = $queryBuilderProphecy->reveal();
- [$propertyNameCollectionFactory, $propertyMetadataFactory] = $this->getMetadataFactories(Dummy::class, [
+ [$propertyNameCollectionFactory, $propertyMetadataFactory, $resourceMetadataFactory] = $this->getMetadataFactories(Dummy::class, [
'id',
]);
$managerRegistry = $this->getManagerRegistry(Dummy::class, [
@@ -186,7 +188,7 @@ public function testQueryResultExtension()
$extensionProphecy->supportsResult(Dummy::class, 'foo', $context)->willReturn(true)->shouldBeCalled();
$extensionProphecy->getResult($queryBuilder, Dummy::class, 'foo', $context)->willReturn([])->shouldBeCalled();
- $dataProvider = new ItemDataProvider($managerRegistry, $propertyNameCollectionFactory, $propertyMetadataFactory, [$extensionProphecy->reveal()]);
+ $dataProvider = new ItemDataProvider($managerRegistry, $propertyNameCollectionFactory, $propertyMetadataFactory, [$extensionProphecy->reveal()], $resourceMetadataFactory);
$this->assertEquals([], $dataProvider->getItem(Dummy::class, ['id' => 1], 'foo', $context));
}
@@ -198,11 +200,11 @@ public function testUnsupportedClass()
$extensionProphecy = $this->prophesize(QueryItemExtensionInterface::class);
- [$propertyNameCollectionFactory, $propertyMetadataFactory] = $this->getMetadataFactories(Dummy::class, [
+ [$propertyNameCollectionFactory, $propertyMetadataFactory, $resourceMetadataFactory] = $this->getMetadataFactories(Dummy::class, [
'id',
]);
- $dataProvider = new ItemDataProvider($managerRegistryProphecy->reveal(), $propertyNameCollectionFactory, $propertyMetadataFactory, [$extensionProphecy->reveal()]);
+ $dataProvider = new ItemDataProvider($managerRegistryProphecy->reveal(), $propertyNameCollectionFactory, $propertyMetadataFactory, [$extensionProphecy->reveal()], $resourceMetadataFactory);
$this->assertFalse($dataProvider->supports(Dummy::class, 'foo'));
}
@@ -233,11 +235,11 @@ public function testCannotCreateQueryBuilder()
$extensionProphecy = $this->prophesize(QueryItemExtensionInterface::class);
- [$propertyNameCollectionFactory, $propertyMetadataFactory] = $this->getMetadataFactories(Dummy::class, [
+ [$propertyNameCollectionFactory, $propertyMetadataFactory, $resourceMetadataFactory] = $this->getMetadataFactories(Dummy::class, [
'id',
]);
- $itemDataProvider = new ItemDataProvider($managerRegistryProphecy->reveal(), $propertyNameCollectionFactory, $propertyMetadataFactory, [$extensionProphecy->reveal()]);
+ $itemDataProvider = new ItemDataProvider($managerRegistryProphecy->reveal(), $propertyNameCollectionFactory, $propertyMetadataFactory, [$extensionProphecy->reveal()], $resourceMetadataFactory);
$itemDataProvider->getItem(Dummy::class, ['id' => 1234], null, [IdentifierConverterInterface::HAS_IDENTIFIER_CONVERTER => true]);
}
@@ -248,6 +250,7 @@ private function getMetadataFactories(string $resourceClass, array $identifiers)
{
$propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class);
$propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class);
+ $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class);
$nameCollection = ['foobar'];
@@ -263,8 +266,9 @@ private function getMetadataFactories(string $resourceClass, array $identifiers)
$propertyMetadataFactoryProphecy->create($resourceClass, 'foobar')->willReturn(new PropertyMetadata());
$propertyNameCollectionFactoryProphecy->create($resourceClass)->willReturn(new PropertyNameCollection($nameCollection));
+ $resourceMetadataFactoryProphecy->create($resourceClass)->willReturn(new ResourceMetadata('dummy'));
- return [$propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal()];
+ return [$propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), $resourceMetadataFactoryProphecy->reveal()];
}
/**
diff --git a/tests/Bridge/Doctrine/Orm/SubresourceDataProviderTest.php b/tests/Bridge/Doctrine/Orm/SubresourceDataProviderTest.php
index d9e49037bef..cf8791f18ce 100644
--- a/tests/Bridge/Doctrine/Orm/SubresourceDataProviderTest.php
+++ b/tests/Bridge/Doctrine/Orm/SubresourceDataProviderTest.php
@@ -23,6 +23,8 @@
use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
use ApiPlatform\Core\Metadata\Property\PropertyMetadata;
use ApiPlatform\Core\Metadata\Property\PropertyNameCollection;
+use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
+use ApiPlatform\Core\Metadata\Resource\ResourceMetadata;
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Dummy;
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\RelatedDummy;
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\RelatedOwningDummy;
@@ -67,6 +69,7 @@ private function getMetadataProphecies(array $resourceClassesIdentifiers)
{
$propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class);
$propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class);
+ $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class);
foreach ($resourceClassesIdentifiers as $resourceClass => $identifiers) {
$nameCollection = ['foobar'];
@@ -81,11 +84,12 @@ private function getMetadataProphecies(array $resourceClassesIdentifiers)
//random property to prevent the use of non-identifiers metadata while looping
$propertyMetadataFactoryProphecy->create($resourceClass, 'foobar')->willReturn(new PropertyMetadata());
+ $resourceMetadataFactoryProphecy->create($resourceClass)->willReturn(new ResourceMetadata('dummy'));
$propertyNameCollectionFactoryProphecy->create($resourceClass)->willReturn(new PropertyNameCollection($nameCollection));
}
- return [$propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal()];
+ return [$propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), $resourceMetadataFactoryProphecy->reveal()];
}
private function getManagerRegistryProphecy(QueryBuilder $queryBuilder, array $identifiers, string $resourceClass)
@@ -112,11 +116,11 @@ public function testNotASubresource()
$this->expectExceptionMessage('The given resource class is not a subresource.');
$identifiers = ['id'];
- [$propertyNameCollectionFactory, $propertyMetadataFactory] = $this->getMetadataProphecies([Dummy::class => $identifiers]);
+ [$propertyNameCollectionFactory, $propertyMetadataFactory, $resourceMetadataFactory] = $this->getMetadataProphecies([Dummy::class => $identifiers]);
$queryBuilder = $this->prophesize(QueryBuilder::class)->reveal();
$managerRegistry = $this->getManagerRegistryProphecy($queryBuilder, $identifiers, Dummy::class);
- $dataProvider = new SubresourceDataProvider($managerRegistry, $propertyNameCollectionFactory, $propertyMetadataFactory, []);
+ $dataProvider = new SubresourceDataProvider($managerRegistry, $propertyNameCollectionFactory, $propertyMetadataFactory, [], [], $resourceMetadataFactory);
$dataProvider->getSubresource(Dummy::class, ['id' => 1], []);
}
@@ -170,9 +174,9 @@ public function testGetSubresource()
$managerRegistryProphecy->getManagerForClass(RelatedDummy::class)->shouldBeCalled()->willReturn($managerProphecy->reveal());
$managerRegistryProphecy->getManagerForClass(Dummy::class)->shouldBeCalled()->willReturn($managerProphecy->reveal());
- [$propertyNameCollectionFactory, $propertyMetadataFactory] = $this->getMetadataProphecies([Dummy::class => $identifiers]);
+ [$propertyNameCollectionFactory, $propertyMetadataFactory, $resourceMetadataFactory] = $this->getMetadataProphecies([Dummy::class => $identifiers]);
- $dataProvider = new SubresourceDataProvider($managerRegistryProphecy->reveal(), $propertyNameCollectionFactory, $propertyMetadataFactory);
+ $dataProvider = new SubresourceDataProvider($managerRegistryProphecy->reveal(), $propertyNameCollectionFactory, $propertyMetadataFactory, [], [], $resourceMetadataFactory);
$context = ['property' => 'relatedDummies', 'identifiers' => ['id' => [Dummy::class, 'id']], 'collection' => true, IdentifierConverterInterface::HAS_IDENTIFIER_CONVERTER => true];
@@ -264,9 +268,9 @@ public function testGetSubSubresourceItem()
$managerRegistryProphecy->getManagerForClass(ThirdLevel::class)->shouldBeCalled()->willReturn($managerProphecy->reveal());
- [$propertyNameCollectionFactory, $propertyMetadataFactory] = $this->getMetadataProphecies([Dummy::class => $identifiers, RelatedDummy::class => $identifiers]);
+ [$propertyNameCollectionFactory, $propertyMetadataFactory, $resourceMetadataFactory] = $this->getMetadataProphecies([Dummy::class => $identifiers, RelatedDummy::class => $identifiers]);
- $dataProvider = new SubresourceDataProvider($managerRegistryProphecy->reveal(), $propertyNameCollectionFactory, $propertyMetadataFactory);
+ $dataProvider = new SubresourceDataProvider($managerRegistryProphecy->reveal(), $propertyNameCollectionFactory, $propertyMetadataFactory, [], [], $resourceMetadataFactory);
$context = ['property' => 'thirdLevel', 'identifiers' => ['id' => [Dummy::class, 'id'], 'relatedDummies' => [RelatedDummy::class, 'id']], 'collection' => false, IdentifierConverterInterface::HAS_IDENTIFIER_CONVERTER => true];
@@ -320,9 +324,9 @@ public function testGetSubresourceOneToOneOwningRelation()
$managerRegistryProphecy->getManagerForClass(RelatedOwningDummy::class)->shouldBeCalled()->willReturn($managerProphecy->reveal());
$managerRegistryProphecy->getManagerForClass(Dummy::class)->shouldBeCalled()->willReturn($managerProphecy->reveal());
- [$propertyNameCollectionFactory, $propertyMetadataFactory] = $this->getMetadataProphecies([Dummy::class => $identifiers]);
+ [$propertyNameCollectionFactory, $propertyMetadataFactory, $resourceMetadataFactory] = $this->getMetadataProphecies([Dummy::class => $identifiers]);
- $dataProvider = new SubresourceDataProvider($managerRegistryProphecy->reveal(), $propertyNameCollectionFactory, $propertyMetadataFactory);
+ $dataProvider = new SubresourceDataProvider($managerRegistryProphecy->reveal(), $propertyNameCollectionFactory, $propertyMetadataFactory, [], [], $resourceMetadataFactory);
$context = ['property' => 'ownedDummy', 'identifiers' => ['id' => [Dummy::class, 'id']], 'collection' => false, IdentifierConverterInterface::HAS_IDENTIFIER_CONVERTER => true];
@@ -375,14 +379,14 @@ public function testQueryResultExtension()
$managerRegistryProphecy->getManagerForClass(RelatedDummy::class)->shouldBeCalled()->willReturn($managerProphecy->reveal());
$managerRegistryProphecy->getManagerForClass(Dummy::class)->shouldBeCalled()->willReturn($managerProphecy->reveal());
- [$propertyNameCollectionFactory, $propertyMetadataFactory] = $this->getMetadataProphecies([Dummy::class => $identifiers]);
+ [$propertyNameCollectionFactory, $propertyMetadataFactory, $resourceMetadataFactory] = $this->getMetadataProphecies([Dummy::class => $identifiers]);
$extensionProphecy = $this->prophesize(QueryResultCollectionExtensionInterface::class);
$extensionProphecy->applyToCollection($queryBuilder, Argument::type(QueryNameGeneratorInterface::class), RelatedDummy::class, null, Argument::type('array'))->shouldBeCalled();
$extensionProphecy->supportsResult(RelatedDummy::class, null, Argument::type('array'))->willReturn(true)->shouldBeCalled();
$extensionProphecy->getResult($queryBuilder, RelatedDummy::class, null, Argument::type('array'))->willReturn([])->shouldBeCalled();
- $dataProvider = new SubresourceDataProvider($managerRegistryProphecy->reveal(), $propertyNameCollectionFactory, $propertyMetadataFactory, [$extensionProphecy->reveal()]);
+ $dataProvider = new SubresourceDataProvider($managerRegistryProphecy->reveal(), $propertyNameCollectionFactory, $propertyMetadataFactory, [$extensionProphecy->reveal()], [], $resourceMetadataFactory);
$context = ['property' => 'relatedDummies', 'identifiers' => ['id' => [Dummy::class, 'id']], 'collection' => true, IdentifierConverterInterface::HAS_IDENTIFIER_CONVERTER => true];
@@ -403,9 +407,9 @@ public function testCannotCreateQueryBuilder()
$managerRegistryProphecy = $this->prophesize(ManagerRegistry::class);
$managerRegistryProphecy->getManagerForClass(Dummy::class)->willReturn($managerProphecy->reveal())->shouldBeCalled();
- [$propertyNameCollectionFactory, $propertyMetadataFactory] = $this->getMetadataProphecies([Dummy::class => $identifiers]);
+ [$propertyNameCollectionFactory, $propertyMetadataFactory, $resourceMetadataFactory] = $this->getMetadataProphecies([Dummy::class => $identifiers]);
- $dataProvider = new SubresourceDataProvider($managerRegistryProphecy->reveal(), $propertyNameCollectionFactory, $propertyMetadataFactory);
+ $dataProvider = new SubresourceDataProvider($managerRegistryProphecy->reveal(), $propertyNameCollectionFactory, $propertyMetadataFactory, [], [], $resourceMetadataFactory);
$dataProvider->getSubresource(Dummy::class, ['id' => 1], []);
}
@@ -417,9 +421,9 @@ public function testThrowResourceClassNotSupportedException()
$managerRegistryProphecy = $this->prophesize(ManagerRegistry::class);
$managerRegistryProphecy->getManagerForClass(Dummy::class)->willReturn(null)->shouldBeCalled();
- [$propertyNameCollectionFactory, $propertyMetadataFactory] = $this->getMetadataProphecies([Dummy::class => $identifiers]);
+ [$propertyNameCollectionFactory, $propertyMetadataFactory, $resourceMetadataFactory] = $this->getMetadataProphecies([Dummy::class => $identifiers]);
- $dataProvider = new SubresourceDataProvider($managerRegistryProphecy->reveal(), $propertyNameCollectionFactory, $propertyMetadataFactory);
+ $dataProvider = new SubresourceDataProvider($managerRegistryProphecy->reveal(), $propertyNameCollectionFactory, $propertyMetadataFactory, [], [], $resourceMetadataFactory);
$dataProvider->getSubresource(Dummy::class, ['id' => 1], []);
}
@@ -514,9 +518,9 @@ public function testGetSubSubresourceItemLegacy()
$managerRegistryProphecy->getManagerForClass(ThirdLevel::class)->shouldBeCalled()->willReturn($managerProphecy->reveal());
- [$propertyNameCollectionFactory, $propertyMetadataFactory] = $this->getMetadataProphecies([Dummy::class => $identifiers, RelatedDummy::class => $identifiers]);
+ [$propertyNameCollectionFactory, $propertyMetadataFactory, $resourceMetadataFactory] = $this->getMetadataProphecies([Dummy::class => $identifiers, RelatedDummy::class => $identifiers]);
- $dataProvider = new SubresourceDataProvider($managerRegistryProphecy->reveal(), $propertyNameCollectionFactory, $propertyMetadataFactory);
+ $dataProvider = new SubresourceDataProvider($managerRegistryProphecy->reveal(), $propertyNameCollectionFactory, $propertyMetadataFactory, [], [], $resourceMetadataFactory);
$context = ['property' => 'thirdLevel', 'identifiers' => [['id', Dummy::class], ['relatedDummies', RelatedDummy::class]], 'collection' => false];
@@ -604,9 +608,9 @@ public function testGetSubresourceCollectionItem()
$rDummyManagerProphecy->getRepository(RelatedDummy::class)->shouldBeCalled()->willReturn($repositoryProphecy->reveal());
- [$propertyNameCollectionFactory, $propertyMetadataFactory] = $this->getMetadataProphecies([Dummy::class => $identifiers, RelatedDummy::class => $identifiers]);
+ [$propertyNameCollectionFactory, $propertyMetadataFactory, $resourceMetadataFactory] = $this->getMetadataProphecies([Dummy::class => $identifiers, RelatedDummy::class => $identifiers]);
- $dataProvider = new SubresourceDataProvider($managerRegistryProphecy->reveal(), $propertyNameCollectionFactory, $propertyMetadataFactory);
+ $dataProvider = new SubresourceDataProvider($managerRegistryProphecy->reveal(), $propertyNameCollectionFactory, $propertyMetadataFactory, [], [], $resourceMetadataFactory);
$context = ['property' => 'id', 'identifiers' => ['id' => [Dummy::class, 'id', true], 'relatedDummies' => [RelatedDummy::class, 'id', true]], 'collection' => false, IdentifierConverterInterface::HAS_IDENTIFIER_CONVERTER => true];
diff --git a/tests/Bridge/Symfony/Bundle/Action/SwaggerUiActionTest.php b/tests/Bridge/Symfony/Bundle/Action/SwaggerUiActionTest.php
index ae1c6d3391d..ef9e674cc24 100644
--- a/tests/Bridge/Symfony/Bundle/Action/SwaggerUiActionTest.php
+++ b/tests/Bridge/Symfony/Bundle/Action/SwaggerUiActionTest.php
@@ -89,6 +89,7 @@ public function getInvokeParameters()
'swagger_data' => [
'url' => '/url',
'spec' => self::SPEC,
+ 'extraConfiguration' => [],
'oauth' => [
'enabled' => false,
'clientId' => '',
@@ -123,6 +124,7 @@ public function getInvokeParameters()
'swagger_data' => [
'url' => '/url',
'spec' => self::SPEC,
+ 'extraConfiguration' => [],
'oauth' => [
'enabled' => false,
'clientId' => '',
@@ -180,6 +182,7 @@ public function testDoNotRunCurrentRequest(Request $request)
'swagger_data' => [
'url' => '/url',
'spec' => self::SPEC,
+ 'extraConfiguration' => [],
'oauth' => [
'enabled' => false,
'clientId' => '',
diff --git a/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php b/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php
index 38f379dd726..0b000383d13 100644
--- a/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php
+++ b/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php
@@ -244,6 +244,7 @@ public function testSetNameConverter()
$containerBuilderProphecy->hasParameter('kernel.debug')->willReturn(true);
$containerBuilderProphecy->getParameter('kernel.debug')->willReturn(false);
$containerBuilderProphecy->setAlias('api_platform.name_converter', $nameConverterId)->shouldBeCalled();
+ $containerBuilderProphecy->setParameter('api_platform.swagger_ui.extra_configuration', [])->shouldBeCalled();
$containerBuilder = $containerBuilderProphecy->reveal();
@@ -1172,6 +1173,7 @@ private function getBaseContainerBuilderProphecy(array $doctrineIntegrationsToLo
if ($hasSwagger) {
$parameters['api_platform.swagger.versions'] = [2, 3];
$parameters['api_platform.swagger.api_keys'] = [];
+ $parameters['api_platform.swagger_ui.extra_configuration'] = [];
} else {
$parameters['api_platform.swagger.versions'] = [];
}
diff --git a/tests/Bridge/Symfony/Bundle/DependencyInjection/ConfigurationTest.php b/tests/Bridge/Symfony/Bundle/DependencyInjection/ConfigurationTest.php
index 1df2208bb80..4f544652f20 100644
--- a/tests/Bridge/Symfony/Bundle/DependencyInjection/ConfigurationTest.php
+++ b/tests/Bridge/Symfony/Bundle/DependencyInjection/ConfigurationTest.php
@@ -150,6 +150,7 @@ private function runDefaultConfigTests(array $doctrineIntegrationsToLoad = ['orm
'swagger' => [
'versions' => [2, 3],
'api_keys' => [],
+ 'swagger_ui_extra_configuration' => [],
],
'eager_loading' => [
'enabled' => true,
@@ -219,6 +220,7 @@ private function runDefaultConfigTests(array $doctrineIntegrationsToLoad = ['orm
'url' => null,
],
'backward_compatibility_layer' => true,
+ 'swagger_ui_extra_configuration' => [],
],
], $config);
}
diff --git a/tests/Bridge/Symfony/Bundle/SwaggerUi/SwaggerUiActionTest.php b/tests/Bridge/Symfony/Bundle/SwaggerUi/SwaggerUiActionTest.php
index 126a3b8bc10..d00df4ba2ec 100644
--- a/tests/Bridge/Symfony/Bundle/SwaggerUi/SwaggerUiActionTest.php
+++ b/tests/Bridge/Symfony/Bundle/SwaggerUi/SwaggerUiActionTest.php
@@ -102,6 +102,7 @@ public function getInvokeParameters()
'authorizationUrl' => '',
'scopes' => [],
],
+ 'extraConfiguration' => [],
'shortName' => 'F',
'operationId' => 'getFCollection',
'id' => null,
@@ -136,6 +137,7 @@ public function getInvokeParameters()
'authorizationUrl' => '',
'scopes' => [],
],
+ 'extraConfiguration' => [],
'shortName' => 'F',
'operationId' => 'getFItem',
'id' => null,
@@ -188,6 +190,7 @@ public function testDoNotRunCurrentRequest(Request $request)
'authorizationUrl' => '',
'scopes' => [],
],
+ 'extraConfiguration' => [],
],
])->shouldBeCalled()->willReturn('');
diff --git a/tests/DataProvider/TraversablePaginatorTest.php b/tests/DataProvider/TraversablePaginatorTest.php
new file mode 100644
index 00000000000..ac96262a102
--- /dev/null
+++ b/tests/DataProvider/TraversablePaginatorTest.php
@@ -0,0 +1,56 @@
+
+ *
+ * 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\DataProvider;
+
+use ApiPlatform\Core\DataProvider\TraversablePaginator;
+use ArrayIterator;
+use PHPUnit\Framework\TestCase;
+
+class TraversablePaginatorTest extends TestCase
+{
+ /**
+ * @dataProvider initializeProvider
+ */
+ public function testInitialize(
+ array $results,
+ float $currentPage,
+ float $perPage,
+ float $totalItems,
+ float $lastPage,
+ float $currentItems
+ ): void {
+ $traversable = new ArrayIterator($results);
+
+ $paginator = new TraversablePaginator($traversable, $currentPage, $perPage, $totalItems);
+
+ self::assertEquals($totalItems, $paginator->getTotalItems());
+ self::assertEquals($currentPage, $paginator->getCurrentPage());
+ self::assertEquals($lastPage, $paginator->getLastPage());
+ self::assertEquals($perPage, $paginator->getItemsPerPage());
+ self::assertEquals($currentItems, $paginator->count());
+
+ self::assertSame($results, iterator_to_array($paginator));
+ }
+
+ public function initializeProvider(): array
+ {
+ return [
+ 'First of three pages of 3 items each' => [[0, 1, 2, 3, 4, 5, 6], 1, 3, 7, 3, 3],
+ 'Second of two pages of 3 items for the first page and 2 for the second' => [[0, 1, 2, 3, 4], 2, 3, 5, 2, 2],
+ 'Empty results' => [[], 1, 2, 0, 1, 0],
+ '0 items per page' => [[0, 1, 2, 3], 1, 0, 4, 1, 4],
+ 'Total items less than items per page' => [[0, 1, 2], 1, 4, 3, 1, 3],
+ ];
+ }
+}
diff --git a/tests/Fixtures/TestBundle/Document/DummyImmutableDate.php b/tests/Fixtures/TestBundle/Document/DummyImmutableDate.php
new file mode 100644
index 00000000000..d9893b94a3e
--- /dev/null
+++ b/tests/Fixtures/TestBundle/Document/DummyImmutableDate.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\Document;
+
+use ApiPlatform\Core\Annotation\ApiResource;
+use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM;
+
+/**
+ * Dummy Date Immutable.
+ *
+ * @ApiResource(attributes={
+ * "filters"={"my_dummy_immutable_date.mongodb.date"}
+ * })
+ *
+ * @ODM\Document
+ */
+class DummyImmutableDate
+{
+ /**
+ * @var int The id
+ *
+ * @ODM\Id(strategy="INCREMENT", type="int")
+ */
+ private $id;
+
+ /**
+ * @var \DateTimeImmutable The dummy date
+ *
+ * @ODM\Field(type="date_immutable")
+ */
+ public $dummyDate;
+
+ /**
+ * Get id.
+ *
+ * @return int
+ */
+ public function getId()
+ {
+ return $this->id;
+ }
+}
diff --git a/tests/Fixtures/app/config/config_mongodb.yml b/tests/Fixtures/app/config/config_mongodb.yml
index 15565a806d3..747bd44724e 100644
--- a/tests/Fixtures/app/config/config_mongodb.yml
+++ b/tests/Fixtures/app/config/config_mongodb.yml
@@ -60,6 +60,10 @@ services:
parent: 'api_platform.doctrine_mongodb.odm.date_filter'
arguments: [ { 'dummyDate': ~ } ]
tags: [ { name: 'api_platform.filter', id: 'my_dummy_date.mongodb.date' } ]
+ app.my_dummy_immutable_date_resource.mongodb.date_filter:
+ parent: 'api_platform.doctrine_mongodb.odm.date_filter'
+ arguments: [ { 'dummyDate': ~ } ]
+ tags: [ { name: 'api_platform.filter', id: 'my_dummy_immutable_date.mongodb.date' } ]
app.related_dummy_to_friend_resource.mongodb.search_filter:
parent: 'api_platform.doctrine_mongodb.odm.search_filter'
arguments: [ { 'name': 'ipartial', 'description': 'ipartial' } ]