/
RequestHandler.php
287 lines (258 loc) · 8.99 KB
/
RequestHandler.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
<?php
declare(strict_types = 1);
/**
* /src/Rest/RequestHandler.php
*
* @author TLe, Tarmo Leppänen <tarmo.leppanen@protacon.com>
*/
namespace App\Rest;
use App\Utils\JSON;
use Closure;
use JsonException;
use LogicException;
use Symfony\Component\HttpFoundation\Request as HttpFoundationRequest;
use Symfony\Component\HttpFoundation\Response as HttpFoundationResponse;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Throwable;
use function abs;
use function array_filter;
use function array_key_exists;
use function array_unique;
use function array_values;
use function array_walk;
use function explode;
use function in_array;
use function is_array;
use function is_string;
use function mb_strtoupper;
use function mb_substr;
use function strncmp;
/**
* Class RequestHandler
*
* @package App\Rest
* @author TLe, Tarmo Leppänen <tarmo.leppanen@protacon.com>
*/
final class RequestHandler
{
/**
* Method to get used criteria array for 'find' and 'count' methods. Some examples below.
*
* Basic usage:
* ?where={"foo": "bar"} => WHERE entity.foo = 'bar'
* ?where={"bar.foo": "foobar"} => WHERE bar.foo = 'foobar'
* ?where={"id": [1,2,3]} => WHERE entity.id IN (1,2,3)
* ?where={"bar.foo": [1,2,3]} => WHERE bar.foo IN (1,2,3)
*
* Advanced usage:
* By default you cannot make anything else that described above, but you can easily manage special cases within
* your controller 'processCriteria' method, where you can modify this generated 'criteria' array as you like.
*
* Note that with advanced usage you can easily use everything that App\Repository\Base::getExpression method
* supports - and that is basically 99% that you need on advanced search criteria.
*
* @param HttpFoundationRequest $request
*
* @return array|array<string, string|array<string|int, string|int>>
*
* @throws HttpException
*/
public static function getCriteria(HttpFoundationRequest $request): array
{
try {
$where = array_filter(
(array)JSON::decode((string)$request->get('where', '{}'), true),
fn ($value): bool => $value !== null
);
} catch (JsonException $error) {
throw new HttpException(
HttpFoundationResponse::HTTP_BAD_REQUEST,
'Current \'where\' parameter is not valid JSON.',
$error
);
}
return $where;
}
/**
* Getter method for used order by option within 'find' method. Some examples below.
*
* Basic usage:
* ?order=column1 => ORDER BY entity.column1 ASC
* ?order=-column1 => ORDER BY entity.column2 DESC
* ?order=foo.column1 => ORDER BY foo.column1 ASC
* ?order=-foo.column1 => ORDER BY foo.column2 DESC
*
* Array parameter usage:
* ?order[column1]=ASC => ORDER BY entity.column1 ASC
* ?order[column1]=DESC => ORDER BY entity.column1 DESC
* ?order[column1]=foobar => ORDER BY entity.column1 ASC
* ?order[column1]=DESC&order[column2]=DESC => ORDER BY entity.column1 DESC, entity.column2 DESC
* ?order[foo.column1]=ASC => ORDER BY foo.column1 ASC
* ?order[foo.column1]=DESC => ORDER BY foo.column1 DESC
* ?order[foo.column1]=foobar => ORDER BY foo.column1 ASC
* ?order[foo.column1]=DESC&order[column2]=DESC => ORDER BY foo.column1 DESC, entity.column2 DESC
*
* @param HttpFoundationRequest $request
*
* @return mixed[]
*/
public static function getOrderBy(HttpFoundationRequest $request): array
{
// Normalize parameter value
$input = array_filter((array)$request->get('order', []));
// Initialize output
$output = [];
// Process user input
array_walk($input, self::getIterator($output));
return $output;
}
/**
* Getter method for used limit option within 'find' method.
*
* Usage:
* ?limit=10
*
* @param HttpFoundationRequest $request
*
* @return int|null
*/
public static function getLimit(HttpFoundationRequest $request): ?int
{
$limit = $request->get('limit');
return $limit !== null ? (int)abs($limit) : null;
}
/**
* Getter method for used offset option within 'find' method.
*
* Usage:
* ?offset=10
*
* @param HttpFoundationRequest $request
*
* @return int|null
*/
public static function getOffset(HttpFoundationRequest $request): ?int
{
$offset = $request->get('offset');
return $offset !== null ? (int)abs($offset) : null;
}
/**
* Getter method for used search terms within 'find' and 'count' methods. Note that these will affect to columns /
* properties that you have specified to your resource service repository class.
*
* Usage examples:
* ?search=term
* ?search=term1+term2
* ?search={"and": ["term1", "term2"]}
* ?search={"or": ["term1", "term2"]}
* ?search={"and": ["term1", "term2"], "or": ["term3", "term4"]}
*
* @param HttpFoundationRequest $request
*
* @return mixed[]
*
* @throws HttpException
*/
public static function getSearchTerms(HttpFoundationRequest $request): array
{
$search = $request->get('search');
return $search !== null ? self::getSearchTermCriteria($search) : [];
}
/**
* Method to return search term criteria as an array that repositories can easily use.
*
* @param string $search
*
* @return mixed[]
*
* @throws HttpException
*/
private static function getSearchTermCriteria(string $search): array
{
$searchTerms = self::determineSearchTerms($search);
// By default we want to use 'OR' operand with given search words.
$output = [
'or' => array_unique(array_values(array_filter(explode(' ', $search)))),
];
if ($searchTerms !== null) {
$output = self::normalizeSearchTerms($searchTerms);
}
return $output;
}
/**
* Method to determine used search terms. Note that this will first try to JSON decode given search term. This is
* for cases that 'search' request parameter contains 'and' or 'or' terms.
*
* @param string $search
*
* @return mixed[]|null
*
* @throws HttpException
*/
private static function determineSearchTerms(string $search): ?array
{
try {
$searchTerms = JSON::decode($search, true);
self::checkSearchTerms($searchTerms);
} catch (JsonException | LogicException $error) {
(fn (Throwable $error) => null)($error);
$searchTerms = null;
}
return $searchTerms;
}
/**
* @param mixed $searchTerms
*
* @throws LogicException
* @throws HttpException
*/
private static function checkSearchTerms($searchTerms): void
{
if (!is_array($searchTerms)) {
throw new LogicException('Search term is not an array, fallback to string handling');
}
if (!array_key_exists('and', $searchTerms) && !array_key_exists('or', $searchTerms)) {
throw new HttpException(
HttpFoundationResponse::HTTP_BAD_REQUEST,
'Given search parameter is not valid, within JSON provide \'and\' and/or \'or\' property.'
);
}
}
/**
* Method to normalize specified search terms. Within this we will just filter out any "empty" values and return
* unique terms after that.
*
* @param string[] $searchTerms
*
* @return string[]
*/
private static function normalizeSearchTerms(array $searchTerms): array
{
// Normalize user input, note that this support array and string formats on value
array_walk($searchTerms, fn (array $terms): array => array_unique(array_values(array_filter($terms))));
return $searchTerms;
}
/**
* @param mixed[] $output
*
* @return Closure
*/
private static function getIterator(array &$output): Closure
{
/**
* @psalm-suppress MissingClosureParamType
*
* @param string $value
* @param int|string $key
*/
return static function (string &$value, $key) use (&$output): void {
$order = in_array(mb_strtoupper($value), ['ASC', 'DESC'], true) ? mb_strtoupper($value) : 'ASC';
$column = is_string($key) ? $key : $value;
if (strncmp($column, '-', 1) === 0) {
$column = mb_substr($column, 1);
$order = 'DESC';
}
$output[$column] = $order;
};
}
}