diff --git a/CHANGELOG.md b/CHANGELOG.md
index 29d113ffdb4..4d782466bd6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,6 +8,7 @@
* 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)
* Doctrine: Add `nulls_always_first` and `nulls_always_last` to `nulls_comparison` in order filter (#4103)
+* Doctrine: Add a global `order_nulls_comparison` configuration (#3117)
* MongoDB: `date_immutable` support (#3940)
* DataProvider: Add `TraversablePaginator` (#3783)
* JSON:API: Support inclusion of resources from path (#3288)
diff --git a/src/Bridge/Doctrine/Orm/Filter/OrderFilter.php b/src/Bridge/Doctrine/Orm/Filter/OrderFilter.php
index c1643ea8cdd..cde61b93368 100644
--- a/src/Bridge/Doctrine/Orm/Filter/OrderFilter.php
+++ b/src/Bridge/Doctrine/Orm/Filter/OrderFilter.php
@@ -43,7 +43,9 @@ class OrderFilter extends AbstractContextAwareFilter implements OrderFilterInter
{
use OrderFilterTrait;
- public function __construct(ManagerRegistry $managerRegistry, ?RequestStack $requestStack = null, string $orderParameterName = 'order', LoggerInterface $logger = null, array $properties = null, NameConverterInterface $nameConverter = null)
+ private $orderNullsComparison;
+
+ public function __construct(ManagerRegistry $managerRegistry, ?RequestStack $requestStack = null, string $orderParameterName = 'order', LoggerInterface $logger = null, array $properties = null, NameConverterInterface $nameConverter = null, ?string $orderNullsComparison = null)
{
if (null !== $properties) {
$properties = array_map(static function ($propertyOptions) {
@@ -61,6 +63,7 @@ public function __construct(ManagerRegistry $managerRegistry, ?RequestStack $req
parent::__construct($managerRegistry, $requestStack, $logger, $properties, $nameConverter);
$this->orderParameterName = $orderParameterName;
+ $this->orderNullsComparison = $orderNullsComparison;
}
/**
@@ -104,7 +107,7 @@ protected function filterProperty(string $property, $direction, QueryBuilder $qu
[$alias, $field] = $this->addJoinsForNestedProperty($property, $alias, $queryBuilder, $queryNameGenerator, $resourceClass, Join::LEFT_JOIN);
}
- if (null !== $nullsComparison = $this->properties[$property]['nulls_comparison'] ?? null) {
+ if (null !== $nullsComparison = $this->properties[$property]['nulls_comparison'] ?? $this->orderNullsComparison) {
$nullsDirection = self::NULLS_DIRECTION_MAP[$nullsComparison][$direction];
$nullRankHiddenField = sprintf('_%s_%s_null_rank', $alias, str_replace('.', '_', $field));
diff --git a/src/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php b/src/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php
index faa80f5c069..fe910c407a6 100644
--- a/src/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php
+++ b/src/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php
@@ -185,6 +185,7 @@ private function registerCommonConfiguration(ContainerBuilder $container, array
$container->setParameter('api_platform.collection.exists_parameter_name', $config['collection']['exists_parameter_name']);
$container->setParameter('api_platform.collection.order', $config['defaults']['order'] ?? $config['collection']['order']);
$container->setParameter('api_platform.collection.order_parameter_name', $config['collection']['order_parameter_name']);
+ $container->setParameter('api_platform.collection.order_nulls_comparison', $config['collection']['order_nulls_comparison']);
$container->setParameter('api_platform.collection.pagination.enabled', $config['defaults']['pagination_enabled'] ?? $this->isConfigEnabled($container, $config['collection']['pagination']));
$container->setParameter('api_platform.collection.pagination.partial', $config['defaults']['pagination_partial'] ?? $config['collection']['pagination']['partial']);
$container->setParameter('api_platform.collection.pagination.client_enabled', $config['defaults']['pagination_client_enabled'] ?? $config['collection']['pagination']['client_enabled']);
diff --git a/src/Bridge/Symfony/Bundle/DependencyInjection/Configuration.php b/src/Bridge/Symfony/Bundle/DependencyInjection/Configuration.php
index a7689079fe5..46c20c9ca51 100644
--- a/src/Bridge/Symfony/Bundle/DependencyInjection/Configuration.php
+++ b/src/Bridge/Symfony/Bundle/DependencyInjection/Configuration.php
@@ -14,6 +14,7 @@
namespace ApiPlatform\Core\Bridge\Symfony\Bundle\DependencyInjection;
use ApiPlatform\Core\Annotation\ApiResource;
+use ApiPlatform\Core\Bridge\Doctrine\Common\Filter\OrderFilterInterface;
use ApiPlatform\Core\Bridge\Elasticsearch\Metadata\Document\DocumentMetadata;
use ApiPlatform\Core\Exception\FilterValidationException;
use ApiPlatform\Core\Exception\InvalidArgumentException;
@@ -132,6 +133,7 @@ public function getConfigTreeBuilder()
->scalarNode('exists_parameter_name')->defaultValue('exists')->cannotBeEmpty()->info('The name of the query parameter to filter on nullable field values.')->end()
->scalarNode('order')->defaultValue('ASC')->info('The default order of results.')->end() // Default ORDER is required for postgresql and mysql >= 5.7 when using LIMIT/OFFSET request
->scalarNode('order_parameter_name')->defaultValue('order')->cannotBeEmpty()->info('The name of the query parameter to order results.')->end()
+ ->enumNode('order_nulls_comparison')->defaultNull()->values(array_merge(array_keys(OrderFilterInterface::NULLS_DIRECTION_MAP), [null]))->info('The nulls comparison strategy.')->end()
->arrayNode('pagination')
->canBeDisabled()
->addDefaultsIfNotSet()
diff --git a/src/Bridge/Symfony/Bundle/Resources/config/doctrine_orm.xml b/src/Bridge/Symfony/Bundle/Resources/config/doctrine_orm.xml
index ec3b07b4c50..86d1d6b31a8 100644
--- a/src/Bridge/Symfony/Bundle/Resources/config/doctrine_orm.xml
+++ b/src/Bridge/Symfony/Bundle/Resources/config/doctrine_orm.xml
@@ -68,6 +68,7 @@
%api_platform.collection.order_parameter_name%
+ %api_platform.collection.order_nulls_comparison%
diff --git a/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php b/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php
index d08d64d7a39..4a9c065f3b9 100644
--- a/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php
+++ b/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php
@@ -883,6 +883,7 @@ private function getPartialContainerBuilderProphecy($configuration = null)
'api_platform.collection.exists_parameter_name' => 'exists',
'api_platform.collection.order' => 'ASC',
'api_platform.collection.order_parameter_name' => 'order',
+ 'api_platform.collection.order_nulls_comparison' => null,
'api_platform.description' => 'description',
'api_platform.error_formats' => ['jsonproblem' => ['application/problem+json'], 'jsonld' => ['application/ld+json']],
'api_platform.formats' => null === $configuration ? ['jsonld' => ['application/ld+json'], 'jsonhal' => ['application/hal+json']] : $this->getFormatsFromConfiguration($configuration['api_platform']['formats']) ?? [],
diff --git a/tests/Bridge/Symfony/Bundle/DependencyInjection/ConfigurationTest.php b/tests/Bridge/Symfony/Bundle/DependencyInjection/ConfigurationTest.php
index adf0d3a4098..a5cabd4c39f 100644
--- a/tests/Bridge/Symfony/Bundle/DependencyInjection/ConfigurationTest.php
+++ b/tests/Bridge/Symfony/Bundle/DependencyInjection/ConfigurationTest.php
@@ -162,6 +162,7 @@ private function runDefaultConfigTests(array $doctrineIntegrationsToLoad = ['orm
'exists_parameter_name' => 'exists',
'order' => 'ASC',
'order_parameter_name' => 'order',
+ 'order_nulls_comparison' => null,
'pagination' => [
'enabled' => true,
'partial' => false,