|
1
|
|
|
<?php |
|
2
|
|
|
declare(strict_types=1); |
|
3
|
|
|
/** |
|
4
|
|
|
* Caridea |
|
5
|
|
|
* |
|
6
|
|
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not |
|
7
|
|
|
* use this file except in compliance with the License. You may obtain a copy of |
|
8
|
|
|
* the License at |
|
9
|
|
|
* |
|
10
|
|
|
* http://www.apache.org/licenses/LICENSE-2.0 |
|
11
|
|
|
* |
|
12
|
|
|
* Unless required by applicable law or agreed to in writing, software |
|
13
|
|
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
|
14
|
|
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
|
15
|
|
|
* License for the specific language governing permissions and limitations under |
|
16
|
|
|
* the License. |
|
17
|
|
|
* |
|
18
|
|
|
* @copyright 2015-2018 LibreWorks contributors |
|
19
|
|
|
* @license Apache-2.0 |
|
20
|
|
|
*/ |
|
21
|
|
|
namespace Caridea\Http; |
|
22
|
|
|
|
|
23
|
|
|
/** |
|
24
|
|
|
* Produces Pagination objects based on request parameters. |
|
25
|
|
|
* |
|
26
|
|
|
* @copyright 2015-2018 LibreWorks contributors |
|
27
|
|
|
* @license Apache-2.0 |
|
28
|
|
|
*/ |
|
29
|
|
|
class PaginationFactory |
|
30
|
|
|
{ |
|
31
|
|
|
const DESC = "desc"; |
|
32
|
|
|
const SORT = "sort"; |
|
33
|
|
|
const ORDER = "order"; |
|
34
|
|
|
const PAGE = "page"; |
|
35
|
|
|
const START_PAGE = "startPage"; |
|
36
|
|
|
const START_INDEX = "startIndex"; |
|
37
|
|
|
const START = "start"; |
|
38
|
|
|
const COUNT = "count"; |
|
39
|
|
|
const MAX = "max"; |
|
40
|
|
|
const LIMIT = "limit"; |
|
41
|
|
|
const OFFSET = "offset"; |
|
42
|
|
|
const RANGE = "Range"; |
|
43
|
|
|
const REGEX_RANGE = '/^items=(\\d+)-(\\d+)$/'; |
|
44
|
|
|
const REGEX_DOJO_SORT = '/^sort\\(.*\\)$/'; |
|
45
|
|
|
|
|
46
|
|
|
private static $maxAlias = [self::MAX => null, self::LIMIT => null, self::COUNT => null]; |
|
47
|
|
|
private static $offsetAlias = [self::START => null, self::OFFSET => null]; |
|
48
|
|
|
private static $pageAlias = [self::PAGE => null, self::START_PAGE => null]; |
|
49
|
|
|
|
|
50
|
|
|
/** |
|
51
|
|
|
* Checks the request query parameters and headers for pagination info. |
|
52
|
|
|
* |
|
53
|
|
|
* This class supports a good size sampling of different ways to provide |
|
54
|
|
|
* pagination info. |
|
55
|
|
|
* |
|
56
|
|
|
* #### Range |
|
57
|
|
|
* - `max` + `offset` (e.g. Grails): `&max=25&offset=0` |
|
58
|
|
|
* - `count` + `start` (e.g. `dojox.data.QueryReadStore`): `&count=25&start=0` |
|
59
|
|
|
* - `Range` header (e.g. `dojo.store.JsonRest`): `Range: items=0-24` |
|
60
|
|
|
* - `count` + `startIndex` (e.g. OpenSearch): `&count=25&startIndex=1` |
|
61
|
|
|
* - `count` + `startPage` (e.g. OpenSearch): `&count=25&startPage=1` |
|
62
|
|
|
* - `limit` + `page` (e.g. Spring Data REST): `&limit=25&page=1` |
|
63
|
|
|
* - `limit` + `start` (e.g. ExtJS): `&limit=25&start=0` |
|
64
|
|
|
* |
|
65
|
|
|
* Dojo, Grails, and ExtJS are all zero-based. OpenSearch and Spring Data |
|
66
|
|
|
* are one-based. |
|
67
|
|
|
* |
|
68
|
|
|
* #### Order |
|
69
|
|
|
* - OpenSearchServer: `&sort=foo&sort=-bar` |
|
70
|
|
|
* - OpenSearch extension: `&sort=foo:ascending&sort=bar:descending` |
|
71
|
|
|
* - Grails: `&sort=foo&order=asc` |
|
72
|
|
|
* - Spring Data REST: `&sort=foo,asc&sort=bar,desc` |
|
73
|
|
|
* - Dojo: `&sort(+foo,-bar)` |
|
74
|
|
|
* - Dojo w/field: `&[field]=+foo,-bar` |
|
75
|
|
|
* - ExtJS JSON: `&sort=[{"property":"foo","direction":"asc"},{"property":"bar","direction":"desc"}]` |
|
76
|
|
|
* |
|
77
|
|
|
* Because of the fact that many order syntaxes use multiple query string |
|
78
|
|
|
* parameters with the same name, it is absolutely *vital* that you do not |
|
79
|
|
|
* use a `ServerRequestInterface` that has been constructed with the `$_GET` |
|
80
|
|
|
* superglobal. |
|
81
|
|
|
* |
|
82
|
|
|
* The problem here is that PHP will overwrite entries in the `$_GET` |
|
83
|
|
|
* superglobal if they share the same name. With PHP, a request to |
|
84
|
|
|
* `file.php?foobar=foo&foobar=bar` will result in `$_GET` set to |
|
85
|
|
|
* `['foobar' => 'bar']`. |
|
86
|
|
|
* |
|
87
|
|
|
* Other platforms, like the Java Servlet specification, allow list-like |
|
88
|
|
|
* access to these parameters. Make sure the object you pass for `$request` |
|
89
|
|
|
* has a `queryParams` property that has been created to account for |
|
90
|
|
|
* multiple query parameters with the same name. The `QueryParams` class |
|
91
|
|
|
* will produce an array that accounts for this case. |
|
92
|
|
|
* |
|
93
|
|
|
* @param \Psr\Http\Message\ServerRequestInterface $request The server request. Please read important docs above. |
|
94
|
|
|
* @param string $sortParameter The name of the sort parameter |
|
95
|
|
|
* @param array<string,bool> $defaultSort The default sort if request lacks it |
|
96
|
|
|
* @return \Caridea\Http\Pagination The pagination details |
|
97
|
|
|
*/ |
|
98
|
8 |
|
public function create(\Psr\Http\Message\ServerRequestInterface $request, string $sortParameter = self::SORT, array $defaultSort = []): Pagination |
|
99
|
|
|
{ |
|
100
|
8 |
|
$offset = 0; |
|
101
|
8 |
|
$max = PHP_INT_MAX; |
|
|
|
|
|
|
102
|
|
|
|
|
103
|
8 |
|
$params = $request->getQueryParams(); |
|
104
|
8 |
|
$range = $request->getHeaderLine(self::RANGE); |
|
105
|
|
|
|
|
106
|
8 |
|
if ($range !== null && preg_match(self::REGEX_RANGE, $range, $rm)) { |
|
107
|
|
|
// dojo.store.JsonRest style |
|
108
|
1 |
|
$offset = (int)$rm[1]; |
|
109
|
1 |
|
$max = (int)$rm[2] - $offset + 1; |
|
110
|
|
|
} else { |
|
111
|
8 |
|
$max = $this->parse(self::$maxAlias, $params, PHP_INT_MAX); |
|
112
|
|
|
// Grails, ExtJS, dojox.data.QueryReadStore, all zero-based |
|
113
|
8 |
|
$offVal = $this->parse(self::$offsetAlias, $params, 0); |
|
114
|
8 |
|
if ($offVal > 0) { |
|
115
|
4 |
|
$offset = $offVal; |
|
116
|
4 |
|
} elseif (isset($params[self::START_INDEX])) { |
|
117
|
|
|
// OpenSearch style, 1-based |
|
118
|
2 |
|
$startIdx = isset($params[self::START_INDEX]) ? (int)$params[self::START_INDEX] : 0; |
|
119
|
2 |
|
if ($startIdx > 0) { |
|
120
|
2 |
|
$offset = $startIdx - 1; |
|
121
|
|
|
} |
|
122
|
4 |
|
} elseif (isset($params[self::START_PAGE]) || isset($params[self::PAGE])) { |
|
123
|
|
|
// OpenSearch or Spring Data style, 1-based |
|
124
|
3 |
|
$startPage = $this->parse(self::$pageAlias, $params, 0); |
|
125
|
3 |
|
if ($startPage > 0) { |
|
126
|
3 |
|
$offset = ($max * ($startPage - 1)); |
|
127
|
|
|
} |
|
128
|
|
|
} |
|
129
|
|
|
} |
|
130
|
|
|
|
|
131
|
8 |
|
return new Pagination($max, $offset, $this->getOrder($request, $sortParameter, $defaultSort)); |
|
132
|
|
|
} |
|
133
|
|
|
|
|
134
|
|
|
/** |
|
135
|
|
|
* Parses the order array. |
|
136
|
|
|
* |
|
137
|
|
|
* Because of the fact that many order syntaxes use multiple query string |
|
138
|
|
|
* parameters with the same name, it is absolutely *vital* that you do not |
|
139
|
|
|
* use a `ServerRequestInterface` that has been constructed with the `$_GET` |
|
140
|
|
|
* superglobal. |
|
141
|
|
|
* |
|
142
|
|
|
* The problem here is that PHP will overwrite entries in the `$_GET` |
|
143
|
|
|
* superglobal if they share the same name. With PHP, a request to |
|
144
|
|
|
* `file.php?foobar=foo&foobar=bar` will result in `$_GET` set to |
|
145
|
|
|
* `['foobar' => 'bar']`. |
|
146
|
|
|
* |
|
147
|
|
|
* Other platforms, like the Java Servlet specification, allow list-like |
|
148
|
|
|
* access to these parameters. Make sure the object you pass for `$request` |
|
149
|
|
|
* has a `queryParams` property that has been created to account for |
|
150
|
|
|
* multiple query parameters with the same name. The `QueryParams` class |
|
151
|
|
|
* will produce an array that accounts for this case. |
|
152
|
|
|
* |
|
153
|
|
|
* @param \Psr\Http\Message\ServerRequestInterface $request The request |
|
154
|
|
|
* @param string $sortParameter The sort parameter |
|
155
|
|
|
* @param array<string,bool> $default The default sort order |
|
156
|
|
|
* @return array<string,bool> String keys to boolean values |
|
157
|
|
|
*/ |
|
158
|
8 |
|
protected function getOrder(\Psr\Http\Message\ServerRequestInterface $request, string $sortParameter, array $default = []): array |
|
159
|
|
|
{ |
|
160
|
8 |
|
$order = []; |
|
161
|
8 |
|
$params = $request->getQueryParams(); |
|
162
|
8 |
|
if (isset($params[$sortParameter])) { |
|
163
|
8 |
|
if (isset($params[self::ORDER])) { |
|
164
|
|
|
// stupid Grails ordering |
|
165
|
1 |
|
$order[$params[$sortParameter]] = strcasecmp(self::DESC, $params[self::ORDER]) !== 0; |
|
166
|
|
|
} else { |
|
167
|
7 |
|
$param = $params[$sortParameter]; |
|
168
|
7 |
|
foreach ((is_array($param) ? $param : [$param]) as $s) { |
|
169
|
8 |
|
$this->parseSort($s, $order); |
|
170
|
|
|
} |
|
171
|
|
|
} |
|
172
|
|
|
} else { |
|
173
|
1 |
|
foreach (preg_grep(self::REGEX_DOJO_SORT, array_keys($params)) as $s) { |
|
174
|
|
|
// sort(+foo,-bar) |
|
175
|
1 |
|
$this->parseSort(substr($s, 5, -1), $order); |
|
176
|
|
|
} |
|
177
|
|
|
} |
|
178
|
8 |
|
return empty($order) ? $default : $order; |
|
179
|
|
|
} |
|
180
|
|
|
|
|
181
|
|
|
/** |
|
182
|
|
|
* Attempts to parse a single sort value. |
|
183
|
|
|
* |
|
184
|
|
|
* @param string $sort The sort value |
|
185
|
|
|
* @param array<string,bool> $sorts String keys to boolean values |
|
186
|
|
|
*/ |
|
187
|
7 |
|
protected function parseSort(string $sort, array &$sorts) |
|
188
|
|
|
{ |
|
189
|
7 |
|
if (strlen(trim($sort)) === 0) { |
|
190
|
1 |
|
return; |
|
191
|
|
|
} |
|
192
|
7 |
|
if (substr($sort, 0, 1) == "[") { |
|
193
|
|
|
// it might be the ridiculous JSON ExtJS sort format |
|
194
|
2 |
|
$json = json_decode($sort); |
|
195
|
2 |
|
if (is_array($json)) { |
|
196
|
1 |
|
foreach ($json as $s) { |
|
197
|
1 |
|
if (is_object($s)) { |
|
198
|
1 |
|
$sorts[$s->property] = strcasecmp(self::DESC, $s->direction) !== 0; |
|
199
|
|
|
} |
|
200
|
|
|
} |
|
201
|
1 |
|
return; |
|
202
|
|
|
} |
|
203
|
|
|
} |
|
204
|
6 |
|
if (substr($sort, -4) == ",asc") { |
|
205
|
|
|
// foo,asc |
|
206
|
1 |
|
$sorts[substr($sort, 0, strlen($sort) - 4)] = true; |
|
207
|
6 |
|
} elseif (substr($sort, -5) == ",desc") { |
|
208
|
|
|
// foo,desc |
|
209
|
1 |
|
$sorts[substr($sort, 0, strlen($sort) - 5)] = false; |
|
210
|
5 |
|
} elseif (substr($sort, -10) == ":ascending") { |
|
211
|
|
|
// foo:ascending |
|
212
|
1 |
|
$sorts[substr($sort, 0, strlen($sort) - 10)] = true; |
|
213
|
5 |
|
} elseif (substr($sort, -11) == ":descending") { |
|
214
|
|
|
// foo:descending |
|
215
|
1 |
|
$sorts[substr($sort, 0, strlen($sort) - 11)] = false; |
|
216
|
|
|
} else { |
|
217
|
4 |
|
foreach (explode(',', $sort) as $s) { |
|
218
|
4 |
|
if (substr($s, 0, 1) === '-') { |
|
219
|
|
|
// -foo |
|
220
|
3 |
|
$sorts[substr($s, 1)] = false; |
|
221
|
4 |
|
} elseif (substr($s, 0, 1) === '+') { |
|
222
|
|
|
// +foo |
|
223
|
3 |
|
$sorts[substr($s, 1)] = true; |
|
224
|
|
|
} else { |
|
225
|
|
|
// foo |
|
226
|
4 |
|
$sorts[$s] = true; |
|
227
|
|
|
} |
|
228
|
|
|
} |
|
229
|
|
|
} |
|
230
|
6 |
|
} |
|
231
|
|
|
|
|
232
|
|
|
/** |
|
233
|
|
|
* Gets the first numeric value from `$params`, otherwise `$defaultValue`. |
|
234
|
|
|
* |
|
235
|
|
|
* @param array $names |
|
236
|
|
|
* @param array $params |
|
237
|
|
|
* @param int $defaultValue |
|
238
|
|
|
* @return int |
|
239
|
|
|
*/ |
|
240
|
|
|
protected function parse(array &$names, array &$params, int $defaultValue) : int |
|
241
|
|
|
{ |
|
242
|
8 |
|
$value = array_reduce(array_intersect_key($params, $names), function ($carry, $item) { |
|
243
|
7 |
|
return $carry !== null ? $carry : (is_numeric($item) ? (int)$item : null); |
|
244
|
8 |
|
}); |
|
245
|
8 |
|
return $value === null ? $defaultValue : $value; |
|
246
|
|
|
} |
|
247
|
|
|
} |
|
248
|
|
|
|
This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.
Both the
$myVarassignment in line 1 and the$higherassignment in line 2 are dead. The first because$myVaris never used and the second because$higheris always overwritten for every possible time line.