Skip to content

Commit 45c537f

Browse files
authored
Merge pull request #1710 from antograssiot/maintain-order-in-filters
Ensure that OrderFilter preserves the query string parameters order
2 parents d5591e0 + fb7cc47 commit 45c537f

File tree

3 files changed

+101
-2
lines changed

3 files changed

+101
-2
lines changed

features/doctrine/order_filter.feature

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,67 @@ Feature: Order filter on collections
300300
}
301301
"""
302302

303+
Scenario: Get collection ordered collection on several property keep the order
304+
# Adding 30 more data with the same name
305+
Given there are 30 dummy objects
306+
When I send a "GET" request to "/dummies?order[name]=desc&order[id]=desc"
307+
Then the response status code should be 200
308+
Then print last JSON response
309+
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
310+
And the JSON should be valid according to this schema:
311+
"""
312+
{
313+
"type": "object",
314+
"properties": {
315+
"@context": {"pattern": "^/contexts/Dummy$"},
316+
"@id": {"pattern": "^/dummies$"},
317+
"@type": {"pattern": "^hydra:Collection$"},
318+
"hydra:member": {
319+
"type": "array",
320+
"items": [
321+
{
322+
"type": "object",
323+
"properties": {
324+
"@id": {
325+
"type": "string",
326+
"pattern": "^/dummies/39$"
327+
}
328+
}
329+
},
330+
{
331+
"type": "object",
332+
"properties": {
333+
"@id": {
334+
"type": "string",
335+
"pattern": "^/dummies/9$"
336+
}
337+
}
338+
},
339+
{
340+
"type": "object",
341+
"properties": {
342+
"@id": {
343+
"type": "string",
344+
"pattern": "^/dummies/38$"
345+
}
346+
}
347+
}
348+
],
349+
"additionalItems": false,
350+
"maxItems": 3,
351+
"minItems": 3
352+
},
353+
"hydra:view": {
354+
"type": "object",
355+
"properties": {
356+
"@id": {"pattern": "^/dummies\\?order%5Bname%5D=desc"},
357+
"@type": {"pattern": "^hydra:PartialCollectionView$"}
358+
}
359+
}
360+
}
361+
}
362+
"""
363+
303364
Scenario: Get collection ordered in ascending order on an association and on which order filter has been enabled in whitelist mode
304365
Given there are 30 dummy objects with relatedDummy
305366
When I send a "GET" request to "/dummies?order[relatedDummy]=asc"

src/EventListener/ReadListener.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ public function onKernelRequest(GetResponseEvent $event)
6363
}
6464

6565
if (null === $filters = $request->attributes->get('_api_filters')) {
66-
$queryString = $request->getQueryString();
66+
$queryString = RequestParser::getQueryString($request);
6767
$filters = $queryString ? RequestParser::parseRequestParams($queryString) : null;
6868
}
6969

src/Util/RequestParser.php

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ private function __construct()
3838
*/
3939
public static function parseAndDuplicateRequest(Request $request): Request
4040
{
41-
$query = self::parseRequestParams($request->getQueryString() ?? '');
41+
$query = self::parseRequestParams(self::getQueryString($request) ?? '');
4242
$body = self::parseRequestParams($request->getContent());
4343

4444
return $request->duplicate($query, $body);
@@ -74,4 +74,42 @@ function ($key) {
7474

7575
return array_combine(array_map('hex2bin', array_keys($params)), $params);
7676
}
77+
78+
/**
79+
* Generates the normalized query string for the Request.
80+
*
81+
* It builds a normalized query string, where keys/value pairs are alphabetized
82+
* and have consistent escaping.
83+
*
84+
* @return string|null A normalized query string for the Request
85+
*/
86+
public static function getQueryString(Request $request)
87+
{
88+
$qs = $request->server->get('QUERY_STRING', '');
89+
if ('' === $qs) {
90+
return null;
91+
}
92+
93+
$parts = [];
94+
95+
foreach (explode('&', $qs) as $param) {
96+
if ('' === $param || '=' === $param[0]) {
97+
// Ignore useless delimiters, e.g. "x=y&".
98+
// Also ignore pairs with empty key, even if there was a value, e.g. "=value", as such nameless values cannot be retrieved anyway.
99+
// PHP also does not include them when building _GET.
100+
continue;
101+
}
102+
103+
$keyValuePair = explode('=', $param, 2);
104+
105+
// GET parameters, that are submitted from a HTML form, encode spaces as "+" by default (as defined in enctype application/x-www-form-urlencoded).
106+
// PHP also converts "+" to spaces when filling the global _GET or when using the function parse_str. This is why we use urldecode and then normalize to
107+
// RFC 3986 with rawurlencode.
108+
$parts[] = isset($keyValuePair[1]) ?
109+
rawurlencode(urldecode($keyValuePair[0])).'='.rawurlencode(urldecode($keyValuePair[1])) :
110+
rawurlencode(urldecode($keyValuePair[0]));
111+
}
112+
113+
return implode('&', $parts);
114+
}
77115
}

0 commit comments

Comments
 (0)