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
$myVar
assignment in line 1 and the$higher
assignment in line 2 are dead. The first because$myVar
is never used and the second because$higher
is always overwritten for every possible time line.