Skip to content

Commit 94e66e8

Browse files
committed
Allow ordering on nested property values
1 parent 76cfbf1 commit 94e66e8

File tree

5 files changed

+108
-13
lines changed

5 files changed

+108
-13
lines changed

features/doctrine/order_filter.feature

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -830,3 +830,55 @@ Feature: Order filter on collections
830830
}
831831
}
832832
"""
833+
834+
@createSchema
835+
Scenario: Get collection ordered in descending order on a related property
836+
Given there are 2 dummy objects with relatedDummy
837+
When I send a "GET" request to "/dummies?order[relatedDummy.name]=desc"
838+
Then the response status code should be 200
839+
And the response should be in JSON
840+
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
841+
And the JSON should be valid according to this schema:
842+
"""
843+
{
844+
"type": "object",
845+
"properties": {
846+
"@context": {"pattern": "^/contexts/Dummy$"},
847+
"@id": {"pattern": "^/dummies$"},
848+
"@type": {"pattern": "^hydra:Collection$"},
849+
"hydra:member": {
850+
"type": "array",
851+
"items": [
852+
{
853+
"type": "object",
854+
"properties": {
855+
"@id": {
856+
"type": "string",
857+
"pattern": "^/dummies/2$"
858+
}
859+
}
860+
},
861+
{
862+
"type": "object",
863+
"properties": {
864+
"@id": {
865+
"type": "string",
866+
"pattern": "^/dummies/1$"
867+
}
868+
}
869+
}
870+
],
871+
"additionalItems": false,
872+
"maxItems": 2,
873+
"minItems": 2
874+
},
875+
"hydra:view": {
876+
"type": "object",
877+
"properties": {
878+
"@id": {"pattern": "^/dummies\\?order%5BrelatedDummy.name%5D=desc"},
879+
"@type": {"pattern": "^hydra:PartialCollectionView$"}
880+
}
881+
}
882+
}
883+
}
884+
"""

features/graphql/filters.feature

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,6 @@ Feature: Collections filtering
9696
When I add "Accept" header equal to "application/hal+json"
9797
And I send a "GET" request to "/dummies?relatedDummies.name=RelatedDummy31"
9898
Then the response status code should be 200
99-
And the response should be in JSON
10099
And the JSON node "_embedded.item" should have 1 element
101100
When I send the following GraphQL request:
102101
"""
@@ -112,3 +111,27 @@ Feature: Collections filtering
112111
"""
113112
And the response status code should be 200
114113
And the JSON node "data.dummies.edges" should have 1 element
114+
115+
@createSchema
116+
Scenario: Retrieve a collection ordered using nested properties
117+
Given there are 2 dummy objects with relatedDummy
118+
When I send the following GraphQL request:
119+
"""
120+
{
121+
dummies(order: {relatedDummy_name: "DESC"}) {
122+
edges {
123+
node {
124+
name
125+
relatedDummy {
126+
id
127+
name
128+
}
129+
}
130+
}
131+
}
132+
}
133+
"""
134+
Then the response status code should be 200
135+
And the header "Content-Type" should be equal to "application/json"
136+
And the JSON node "data.dummies.edges[0].node.name" should be equal to "Dummy #2"
137+
And the JSON node "data.dummies.edges[1].node.name" should be equal to "Dummy #1"

src/Bridge/Symfony/Bundle/Resources/config/graphql.xml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
<argument type="service" id="api_platform.security.resource_access_checker" on-invalid="null" />
1919
<argument type="service" id="request_stack" />
2020
<argument>%api_platform.collection.pagination.enabled%</argument>
21+
<argument>%api_platform.collection.order_parameter_name%</argument>
2122
</service>
2223

2324
<service id="api_platform.graphql.resolver.factory.item_mutation" class="ApiPlatform\Core\GraphQl\Resolver\Factory\ItemMutationResolverFactory" public="false">
@@ -51,6 +52,7 @@
5152
<argument type="service" id="api_platform.graphql.resolver.resource_field" />
5253
<argument type="service" id="api_platform.filter_locator" />
5354
<argument>%api_platform.collection.pagination.enabled%</argument>
55+
<argument>%api_platform.collection.order_parameter_name%</argument>
5456
</service>
5557

5658
<!-- Action -->

src/GraphQl/Resolver/Factory/CollectionResolverFactory.php

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,9 @@ final class CollectionResolverFactory implements ResolverFactoryInterface
4747
private $requestStack;
4848
private $paginationEnabled;
4949
private $resourceMetadataFactory;
50+
private $orderParameterName;
5051

51-
public function __construct(CollectionDataProviderInterface $collectionDataProvider, SubresourceDataProviderInterface $subresourceDataProvider, NormalizerInterface $normalizer, IdentifiersExtractorInterface $identifiersExtractor, ResourceMetadataFactoryInterface $resourceMetadataFactory, ResourceAccessCheckerInterface $resourceAccessChecker = null, RequestStack $requestStack = null, bool $paginationEnabled = false)
52+
public function __construct(CollectionDataProviderInterface $collectionDataProvider, SubresourceDataProviderInterface $subresourceDataProvider, NormalizerInterface $normalizer, IdentifiersExtractorInterface $identifiersExtractor, ResourceMetadataFactoryInterface $resourceMetadataFactory, ResourceAccessCheckerInterface $resourceAccessChecker = null, RequestStack $requestStack = null, bool $paginationEnabled = false, string $orderParameterName = null)
5253
{
5354
$this->subresourceDataProvider = $subresourceDataProvider;
5455
$this->collectionDataProvider = $collectionDataProvider;
@@ -58,6 +59,7 @@ public function __construct(CollectionDataProviderInterface $collectionDataProvi
5859
$this->requestStack = $requestStack;
5960
$this->paginationEnabled = $paginationEnabled;
6061
$this->resourceMetadataFactory = $resourceMetadataFactory;
62+
$this->orderParameterName = $orderParameterName;
6163
}
6264

6365
public function __invoke(string $resourceClass = null, string $rootClass = null, string $operationName = null): callable
@@ -77,14 +79,7 @@ public function __invoke(string $resourceClass = null, string $rootClass = null,
7779
$resourceMetadata = $this->resourceMetadataFactory->create($resourceClass);
7880
$dataProviderContext = $resourceMetadata->getGraphqlAttribute('query', 'normalization_context', [], true);
7981
$dataProviderContext['attributes'] = $this->fieldsToAttributes($info);
80-
$filters = $args;
81-
foreach ($filters as $name => $value) {
82-
if (strpos($name, '_')) {
83-
// Gives a chance to relations/nested fields
84-
$filters[str_replace('_', '.', $name)] = $value;
85-
}
86-
}
87-
$dataProviderContext['filters'] = $filters;
82+
$dataProviderContext['filters'] = $this->getNormalizedFilters($args);
8883

8984
if (isset($rootClass, $source[$rootProperty = $info->fieldName], $source[ItemNormalizer::ITEM_KEY])) {
9085
$rootResolvedFields = $this->identifiersExtractor->getIdentifiersFromItem(unserialize($source[ItemNormalizer::ITEM_KEY]));
@@ -171,4 +166,22 @@ private function getSubresource(string $rootClass, array $rootResolvedFields, ar
171166
'collection' => $isCollection,
172167
]);
173168
}
169+
170+
private function getNormalizedFilters($args)
171+
{
172+
$filters = $args;
173+
foreach ($filters as $name => &$value) {
174+
if ($name === $this->orderParameterName && \is_array($value)) {
175+
$filters[$this->orderParameterName] = $this->getNormalizedFilters($value);
176+
continue;
177+
}
178+
179+
if (strpos($name, '_')) {
180+
// Gives a chance to relations/nested fields
181+
$filters[str_replace('_', '.', $name)] = $value;
182+
}
183+
}
184+
185+
return $filters;
186+
}
174187
}

src/GraphQl/Type/SchemaBuilder.php

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,9 @@ final class SchemaBuilder implements SchemaBuilderInterface
5959
private $filterLocator;
6060
private $paginationEnabled;
6161
private $graphqlTypes = [];
62+
private $orderParameterName;
6263

63-
public function __construct(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, ResourceNameCollectionFactoryInterface $resourceNameCollectionFactory, ResourceMetadataFactoryInterface $resourceMetadataFactory, ResolverFactoryInterface $collectionResolverFactory, ResolverFactoryInterface $itemMutationResolverFactory, callable $itemResolver, callable $defaultFieldResolver, ContainerInterface $filterLocator = null, bool $paginationEnabled = true)
64+
public function __construct(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, ResourceNameCollectionFactoryInterface $resourceNameCollectionFactory, ResourceMetadataFactoryInterface $resourceMetadataFactory, ResolverFactoryInterface $collectionResolverFactory, ResolverFactoryInterface $itemMutationResolverFactory, callable $itemResolver, callable $defaultFieldResolver, ContainerInterface $filterLocator = null, bool $paginationEnabled = true, string $orderParameterName = null)
6465
{
6566
$this->propertyNameCollectionFactory = $propertyNameCollectionFactory;
6667
$this->propertyMetadataFactory = $propertyMetadataFactory;
@@ -72,6 +73,7 @@ public function __construct(PropertyNameCollectionFactoryInterface $propertyName
7273
$this->defaultFieldResolver = $defaultFieldResolver;
7374
$this->filterLocator = $filterLocator;
7475
$this->paginationEnabled = $paginationEnabled;
76+
$this->orderParameterName = $orderParameterName;
7577
}
7678

7779
public function getSchema(): Schema
@@ -289,9 +291,12 @@ private function mergeFilterArgs(array $args, array $parsed, ResourceMetadata $r
289291
return $args;
290292
}
291293

292-
private function convertFilterArgsToTypes(array $args): array
294+
private function convertFilterArgsToTypes(array $args, bool $normalizeKeys = false): array
293295
{
294296
foreach ($args as $key => $value) {
297+
if ($normalizeKeys && (strpos($key, '.'))) {
298+
$args[str_replace('.', '_', $key)] = $value;
299+
}
295300
if (!\is_array($value) || !isset($value['#name'])) {
296301
continue;
297302
}
@@ -306,7 +311,7 @@ private function convertFilterArgsToTypes(array $args): array
306311

307312
$this->graphqlTypes[$name] = $args[$key] = new InputObjectType([
308313
'name' => $name,
309-
'fields' => $this->convertFilterArgsToTypes($value),
314+
'fields' => $this->convertFilterArgsToTypes($value, $key === $this->orderParameterName),
310315
]);
311316
}
312317

0 commit comments

Comments
 (0)