1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace As3\Modlr\Rest; |
4
|
|
|
|
5
|
|
|
/** |
6
|
|
|
* The REST Request object. |
7
|
|
|
* Is created/parsed from a core Request object. |
8
|
|
|
* |
9
|
|
|
* @author Jacob Bare <[email protected]> |
10
|
|
|
*/ |
11
|
|
|
class RestRequest |
12
|
|
|
{ |
13
|
|
|
/** |
14
|
|
|
* Request parameter (query string) constants. |
15
|
|
|
*/ |
16
|
|
|
const PARAM_INCLUSIONS = 'include'; |
17
|
|
|
const PARAM_FIELDSETS = 'fields'; |
18
|
|
|
const PARAM_SORTING = 'sort'; |
19
|
|
|
const PARAM_PAGINATION = 'page'; |
20
|
|
|
const PARAM_FILTERING = 'filter'; |
21
|
|
|
|
22
|
|
|
/** |
23
|
|
|
* Filter parameters. |
24
|
|
|
*/ |
25
|
|
|
const FILTER_AUTOCOMPLETE = 'autocomplete'; |
26
|
|
|
const FILTER_AUTOCOMPLETE_KEY = 'key'; |
27
|
|
|
const FILTER_AUTOCOMPLETE_VALUE = 'value'; |
28
|
|
|
const FILTER_QUERY = 'query'; |
29
|
|
|
const FILTER_QUERY_CRITERIA = 'criteria'; |
30
|
|
|
|
31
|
|
|
/** |
32
|
|
|
* The request method, such as GET, POST, PATCH, etc. |
33
|
|
|
* |
34
|
|
|
* @var string |
35
|
|
|
*/ |
36
|
|
|
private $requestMethod; |
37
|
|
|
|
38
|
|
|
/** |
39
|
|
|
* The parsed URL/URI, via PHP's parse_url(). |
40
|
|
|
* |
41
|
|
|
* @var array |
42
|
|
|
*/ |
43
|
|
|
private $parsedUri = []; |
44
|
|
|
|
45
|
|
|
/** |
46
|
|
|
* The entity type requested. |
47
|
|
|
* |
48
|
|
|
* @var string |
49
|
|
|
*/ |
50
|
|
|
private $entityType; |
51
|
|
|
|
52
|
|
|
/** |
53
|
|
|
* The entity identifier (id) value, if sent. |
54
|
|
|
* |
55
|
|
|
* @var string|null |
56
|
|
|
*/ |
57
|
|
|
private $identifier; |
58
|
|
|
|
59
|
|
|
/** |
60
|
|
|
* The entity relationship properties, if sent. |
61
|
|
|
* |
62
|
|
|
* @var array |
63
|
|
|
*/ |
64
|
|
|
private $relationship = []; |
65
|
|
|
|
66
|
|
|
/** |
67
|
|
|
* Relationship fields to include with the response. |
68
|
|
|
* AKA: sideloading the entities of relationships. |
69
|
|
|
* Either a associative array of relationshipKeys => true to specifically include. |
70
|
|
|
* Or a single associative key of '*' => true if all should be included. |
71
|
|
|
* |
72
|
|
|
* @var array |
73
|
|
|
*/ |
74
|
|
|
private $inclusions = []; |
75
|
|
|
|
76
|
|
|
/** |
77
|
|
|
* Sorting criteria. |
78
|
|
|
* |
79
|
|
|
* @var array |
80
|
|
|
*/ |
81
|
|
|
private $sorting = []; |
82
|
|
|
|
83
|
|
|
/** |
84
|
|
|
* Fields to only include with the response. |
85
|
|
|
* |
86
|
|
|
* @var array |
87
|
|
|
*/ |
88
|
|
|
private $fields = []; |
89
|
|
|
|
90
|
|
|
/** |
91
|
|
|
* Pagination (limit/skip) criteria. |
92
|
|
|
* |
93
|
|
|
* @var array |
94
|
|
|
*/ |
95
|
|
|
private $pagination = []; |
96
|
|
|
|
97
|
|
|
/** |
98
|
|
|
* Any request filters, such as quering, search, autocomplete, etc. |
99
|
|
|
* Must ultimately be handled by the Adapter to function. |
100
|
|
|
* |
101
|
|
|
* @var array |
102
|
|
|
*/ |
103
|
|
|
private $filters = []; |
104
|
|
|
|
105
|
|
|
/** |
106
|
|
|
* The request payload, if sent. |
107
|
|
|
* Used for updating/creating entities. |
108
|
|
|
* |
109
|
|
|
* @var RestPayload|null |
110
|
|
|
*/ |
111
|
|
|
private $payload; |
112
|
|
|
|
113
|
|
|
/** |
114
|
|
|
* The REST configuration. |
115
|
|
|
* |
116
|
|
|
* @var RestConfiguration |
117
|
|
|
*/ |
118
|
|
|
private $config; |
119
|
|
|
|
120
|
|
|
/** |
121
|
|
|
* Constructor. |
122
|
|
|
* |
123
|
|
|
* @param RestConfiguration $config The REST configuration. |
124
|
|
|
* @param string $method The request method. |
125
|
|
|
* @param string $uri The complete URI (URL) of the request, included scheme, host, path, and query string. |
126
|
|
|
* @param string|null $payload The request payload (body). |
127
|
|
|
*/ |
128
|
|
|
public function __construct(RestConfiguration $config, $method, $uri, $payload = null) |
129
|
|
|
{ |
130
|
|
|
$this->config = $config; |
131
|
|
|
$this->requestMethod = strtoupper($method); |
132
|
|
|
|
133
|
|
|
$this->sorting = $config->getDefaultSorting(); |
134
|
|
|
$this->pagination = $config->getDefaultPagination(); |
135
|
|
|
|
136
|
|
|
$this->parse($uri); |
137
|
|
|
$this->payload = empty($payload) ? null : new RestPayload($payload); |
138
|
|
|
|
139
|
|
|
// Re-configure the config based on the actually request. |
140
|
|
|
$this->config->setHost($this->getHost()); |
141
|
|
|
$this->config->setScheme($this->getScheme()); |
142
|
|
|
} |
143
|
|
|
|
144
|
|
|
/** |
145
|
|
|
* Generates the request URL based on its current object state. |
146
|
|
|
* |
147
|
|
|
* @todo Add support for inclusions and other items. |
148
|
|
|
* @return string |
149
|
|
|
*/ |
150
|
|
|
public function getUrl() |
151
|
|
|
{ |
152
|
|
|
$query = $this->getQueryString(); |
153
|
|
|
return sprintf('%s://%s/%s/%s%s', |
154
|
|
|
$this->getScheme(), |
155
|
|
|
trim($this->getHost(), '/'), |
156
|
|
|
trim($this->config->getRootEndpoint(), '/'), |
157
|
|
|
$this->getEntityType(), |
158
|
|
|
empty($query) ? '' : sprintf('?%s', $query) |
159
|
|
|
); |
160
|
|
|
} |
161
|
|
|
|
162
|
|
|
/** |
163
|
|
|
* Gets the scheme, such as http or https. |
164
|
|
|
* |
165
|
|
|
* @return string |
166
|
|
|
*/ |
167
|
|
|
public function getScheme() |
168
|
|
|
{ |
169
|
|
|
return $this->parsedUri['scheme']; |
170
|
|
|
} |
171
|
|
|
|
172
|
|
|
/** |
173
|
|
|
* Gets the hostname. |
174
|
|
|
* |
175
|
|
|
* @return string |
176
|
|
|
*/ |
177
|
|
|
public function getHost() |
178
|
|
|
{ |
179
|
|
|
return $this->parsedUri['host']; |
180
|
|
|
} |
181
|
|
|
|
182
|
|
|
/** |
183
|
|
|
* Gets the request method, such as GET, POST, PATCH, etc. |
184
|
|
|
* |
185
|
|
|
* @return string |
186
|
|
|
*/ |
187
|
|
|
public function getMethod() |
188
|
|
|
{ |
189
|
|
|
return $this->requestMethod; |
190
|
|
|
} |
191
|
|
|
|
192
|
|
|
/** |
193
|
|
|
* Gets the requested entity type. |
194
|
|
|
* |
195
|
|
|
* @return string |
196
|
|
|
*/ |
197
|
|
|
public function getEntityType() |
198
|
|
|
{ |
199
|
|
|
return $this->entityType; |
200
|
|
|
} |
201
|
|
|
|
202
|
|
|
/** |
203
|
|
|
* Gets the requested entity identifier (id), if sent. |
204
|
|
|
* |
205
|
|
|
* @return string|null |
206
|
|
|
*/ |
207
|
|
|
public function getIdentifier() |
208
|
|
|
{ |
209
|
|
|
return $this->identifier; |
210
|
|
|
} |
211
|
|
|
|
212
|
|
|
/** |
213
|
|
|
* Gets the query string based on the current object properties. |
214
|
|
|
* |
215
|
|
|
* @return string |
216
|
|
|
*/ |
217
|
|
|
public function getQueryString() |
218
|
|
|
{ |
219
|
|
|
$query = []; |
220
|
|
|
if (!empty($this->pagination)) { |
221
|
|
|
$query[self::PARAM_PAGINATION] = $this->pagination; |
222
|
|
|
} |
223
|
|
|
if (!empty($this->filters)) { |
224
|
|
|
$query[self::PARAM_FILTERING] = $this->filters; |
225
|
|
|
} |
226
|
|
|
foreach ($this->fields as $modelType => $fields) { |
227
|
|
|
$query[self::PARAM_FIELDSETS][$modelType] = implode(',', $fields); |
228
|
|
|
} |
229
|
|
|
$sort = []; |
230
|
|
|
foreach ($this->sorting as $key => $direction) { |
231
|
|
|
$sort[] = (1 === $direction) ? $key : sprintf('-%s', $key); |
232
|
|
|
} |
233
|
|
|
if (!empty($sort)) { |
234
|
|
|
$query[self::PARAM_SORTING] = implode(',', $sort); |
235
|
|
|
} |
236
|
|
|
return http_build_query($query); |
237
|
|
|
} |
238
|
|
|
|
239
|
|
|
/** |
240
|
|
|
* Determines if an entity identifier (id) was sent with the request. |
241
|
|
|
* |
242
|
|
|
* @return bool |
243
|
|
|
*/ |
244
|
|
|
public function hasIdentifier() |
245
|
|
|
{ |
246
|
|
|
return null !== $this->getIdentifier(); |
247
|
|
|
} |
248
|
|
|
|
249
|
|
|
/** |
250
|
|
|
* Determines if this is an entity relationship request. |
251
|
|
|
* |
252
|
|
|
* @return bool |
253
|
|
|
*/ |
254
|
|
|
public function isRelationship() |
255
|
|
|
{ |
256
|
|
|
return !empty($this->relationship); |
257
|
|
|
} |
258
|
|
|
|
259
|
|
|
/** |
260
|
|
|
* Gets the entity relationship request. |
261
|
|
|
* |
262
|
|
|
* @return array |
263
|
|
|
*/ |
264
|
|
|
public function getRelationship() |
265
|
|
|
{ |
266
|
|
|
return $this->relationship; |
267
|
|
|
} |
268
|
|
|
|
269
|
|
|
/** |
270
|
|
|
* Gets the entity relationship field key. |
271
|
|
|
* |
272
|
|
|
* @return string|null |
273
|
|
|
*/ |
274
|
|
|
public function getRelationshipFieldKey() |
275
|
|
|
{ |
276
|
|
|
if (false === $this->isRelationship()) { |
277
|
|
|
return null; |
278
|
|
|
} |
279
|
|
|
return $this->getRelationship()['field']; |
280
|
|
|
} |
281
|
|
|
|
282
|
|
|
/** |
283
|
|
|
* Determines if this is an entity relationship retrieve request. |
284
|
|
|
* |
285
|
|
|
* @return bool |
286
|
|
|
*/ |
287
|
|
View Code Duplication |
public function isRelationshipRetrieve() |
|
|
|
|
288
|
|
|
{ |
289
|
|
|
if (false === $this->isRelationship()) { |
290
|
|
|
return false; |
291
|
|
|
} |
292
|
|
|
return 'self' === $this->getRelationship()['type']; |
293
|
|
|
} |
294
|
|
|
|
295
|
|
|
/** |
296
|
|
|
* Determines if this is an entity relationship modify (create/update/delete) request. |
297
|
|
|
* |
298
|
|
|
* @return bool |
299
|
|
|
*/ |
300
|
|
View Code Duplication |
public function isRelationshipModify() |
|
|
|
|
301
|
|
|
{ |
302
|
|
|
if (false === $this->isRelationship()) { |
303
|
|
|
return false; |
304
|
|
|
} |
305
|
|
|
return 'related' === $this->getRelationship()['type']; |
306
|
|
|
} |
307
|
|
|
|
308
|
|
|
/** |
309
|
|
|
* Determines if this has an autocomplete filter enabled. |
310
|
|
|
* |
311
|
|
|
* @return bool |
312
|
|
|
*/ |
313
|
|
View Code Duplication |
public function isAutocomplete() |
|
|
|
|
314
|
|
|
{ |
315
|
|
|
if (false === $this->hasFilter(self::FILTER_AUTOCOMPLETE)) { |
316
|
|
|
return false; |
317
|
|
|
} |
318
|
|
|
$autocomplete = $this->getFilter(self::FILTER_AUTOCOMPLETE); |
319
|
|
|
return isset($autocomplete[self::FILTER_AUTOCOMPLETE_KEY]) && isset($autocomplete[self::FILTER_AUTOCOMPLETE_VALUE]); |
320
|
|
|
} |
321
|
|
|
|
322
|
|
|
/** |
323
|
|
|
* Gets the autocomplete attribute key. |
324
|
|
|
* |
325
|
|
|
* @return string|null |
326
|
|
|
*/ |
327
|
|
View Code Duplication |
public function getAutocompleteKey() |
|
|
|
|
328
|
|
|
{ |
329
|
|
|
if (false === $this->isAutocomplete()) { |
330
|
|
|
return null; |
331
|
|
|
} |
332
|
|
|
return $this->getFilter(self::FILTER_AUTOCOMPLETE)[self::FILTER_AUTOCOMPLETE_KEY]; |
333
|
|
|
} |
334
|
|
|
|
335
|
|
|
/** |
336
|
|
|
* Gets the autocomplete search value. |
337
|
|
|
* |
338
|
|
|
* @return string|null |
339
|
|
|
*/ |
340
|
|
View Code Duplication |
public function getAutocompleteValue() |
|
|
|
|
341
|
|
|
{ |
342
|
|
|
if (false === $this->isAutocomplete()) { |
343
|
|
|
return null; |
344
|
|
|
} |
345
|
|
|
return $this->getFilter(self::FILTER_AUTOCOMPLETE)[self::FILTER_AUTOCOMPLETE_VALUE]; |
346
|
|
|
} |
347
|
|
|
|
348
|
|
|
/** |
349
|
|
|
* Determines if this has the database query filter enabled. |
350
|
|
|
* |
351
|
|
|
* @return bool |
352
|
|
|
*/ |
353
|
|
View Code Duplication |
public function isQuery() |
|
|
|
|
354
|
|
|
{ |
355
|
|
|
if (false === $this->hasFilter(self::FILTER_QUERY)) { |
356
|
|
|
return false; |
357
|
|
|
} |
358
|
|
|
$query = $this->getFilter(self::FILTER_QUERY); |
359
|
|
|
return isset($query[self::FILTER_QUERY_CRITERIA]); |
360
|
|
|
} |
361
|
|
|
|
362
|
|
|
/** |
363
|
|
|
* Gets the query criteria value. |
364
|
|
|
* |
365
|
|
|
* @return array |
366
|
|
|
*/ |
367
|
|
|
public function getQueryCriteria() |
368
|
|
|
{ |
369
|
|
|
if (false === $this->isQuery()) { |
370
|
|
|
return []; |
371
|
|
|
} |
372
|
|
|
|
373
|
|
|
$queryKey = self::FILTER_QUERY; |
374
|
|
|
$criteriaKey = self::FILTER_QUERY_CRITERIA; |
375
|
|
|
|
376
|
|
|
$decoded = @json_decode($this->getFilter($queryKey)[$criteriaKey], true); |
377
|
|
|
if (!is_array($decoded)) { |
378
|
|
|
$param = sprintf('%s[%s][%s]', self::PARAM_FILTERING, $queryKey, $criteriaKey); |
379
|
|
|
throw RestException::invalidQueryParam($param, 'Was the value sent as valid JSON?'); |
380
|
|
|
} |
381
|
|
|
return $decoded; |
382
|
|
|
} |
383
|
|
|
|
384
|
|
|
/** |
385
|
|
|
* Determines if specific sideloaded include fields were requested. |
386
|
|
|
* |
387
|
|
|
* @return bool |
388
|
|
|
*/ |
389
|
|
|
public function hasInclusions() |
390
|
|
|
{ |
391
|
|
|
$value = $this->getInclusions(); |
392
|
|
|
return !empty($value); |
393
|
|
|
} |
394
|
|
|
|
395
|
|
|
/** |
396
|
|
|
* Gets specific sideloaded relationship fields to include. |
397
|
|
|
* |
398
|
|
|
* @return array |
399
|
|
|
*/ |
400
|
|
|
public function getInclusions() |
401
|
|
|
{ |
402
|
|
|
return $this->inclusions; |
403
|
|
|
} |
404
|
|
|
|
405
|
|
|
/** |
406
|
|
|
* Determines if a specific return fieldset has been specified. |
407
|
|
|
* |
408
|
|
|
* @return bool |
409
|
|
|
*/ |
410
|
|
|
public function hasFieldset() |
411
|
|
|
{ |
412
|
|
|
$value = $this->getFieldset(); |
413
|
|
|
return !empty($value); |
414
|
|
|
} |
415
|
|
|
|
416
|
|
|
/** |
417
|
|
|
* Gets the return fieldset to use. |
418
|
|
|
* |
419
|
|
|
* @return array |
420
|
|
|
*/ |
421
|
|
|
public function getFieldset() |
422
|
|
|
{ |
423
|
|
|
return $this->fields; |
424
|
|
|
} |
425
|
|
|
|
426
|
|
|
/** |
427
|
|
|
* Determines if the request has specified sorting criteria. |
428
|
|
|
* |
429
|
|
|
* @return bool |
430
|
|
|
*/ |
431
|
|
|
public function hasSorting() |
432
|
|
|
{ |
433
|
|
|
$value = $this->getSorting(); |
434
|
|
|
return !empty($value); |
435
|
|
|
} |
436
|
|
|
|
437
|
|
|
/** |
438
|
|
|
* Gets the sorting criteria. |
439
|
|
|
* |
440
|
|
|
* @return array |
441
|
|
|
*/ |
442
|
|
|
public function getSorting() |
443
|
|
|
{ |
444
|
|
|
return $this->sorting; |
445
|
|
|
} |
446
|
|
|
|
447
|
|
|
/** |
448
|
|
|
* Determines if the request has specified pagination (limit/offset) criteria. |
449
|
|
|
* |
450
|
|
|
* @return bool |
451
|
|
|
*/ |
452
|
|
|
public function hasPagination() |
453
|
|
|
{ |
454
|
|
|
$value = $this->getPagination(); |
455
|
|
|
return !empty($value); |
456
|
|
|
} |
457
|
|
|
|
458
|
|
|
/** |
459
|
|
|
* Gets the pagination (limit/offset) criteria. |
460
|
|
|
* |
461
|
|
|
* @return array |
462
|
|
|
*/ |
463
|
|
|
public function getPagination() |
464
|
|
|
{ |
465
|
|
|
return $this->pagination; |
466
|
|
|
} |
467
|
|
|
|
468
|
|
|
/** |
469
|
|
|
* Sets the pagination (limit/offset) criteria. |
470
|
|
|
* |
471
|
|
|
* @param int $offset |
472
|
|
|
* @param int $limit |
473
|
|
|
* @return self |
474
|
|
|
*/ |
475
|
|
|
public function setPagination($offset, $limit) |
476
|
|
|
{ |
477
|
|
|
$this->pagination['offset'] = (Integer) $offset; |
478
|
|
|
$this->pagination['limit'] = (Integer) $limit; |
479
|
|
|
return $this; |
480
|
|
|
} |
481
|
|
|
|
482
|
|
|
/** |
483
|
|
|
* Determines if the request has any filtering criteria. |
484
|
|
|
* |
485
|
|
|
* @return bool |
486
|
|
|
*/ |
487
|
|
|
public function hasFilters() |
488
|
|
|
{ |
489
|
|
|
return !empty($this->filters); |
490
|
|
|
} |
491
|
|
|
|
492
|
|
|
/** |
493
|
|
|
* Determines if a specific filter exists, by key |
494
|
|
|
* |
495
|
|
|
* @param string $key |
496
|
|
|
* @return bool |
497
|
|
|
*/ |
498
|
|
|
public function hasFilter($key) |
499
|
|
|
{ |
500
|
|
|
return null !== $this->getFilter($key); |
501
|
|
|
} |
502
|
|
|
|
503
|
|
|
/** |
504
|
|
|
* Gets a specific filter, by key. |
505
|
|
|
* |
506
|
|
|
* @param string $key |
507
|
|
|
* @return mixed|null |
508
|
|
|
*/ |
509
|
|
|
public function getFilter($key) |
510
|
|
|
{ |
511
|
|
|
if (!isset($this->filters[$key])) { |
512
|
|
|
return null; |
513
|
|
|
} |
514
|
|
|
return $this->filters[$key]; |
515
|
|
|
} |
516
|
|
|
|
517
|
|
|
/** |
518
|
|
|
* Gets the request payload. |
519
|
|
|
* |
520
|
|
|
* @return RestPayload|null |
521
|
|
|
*/ |
522
|
|
|
public function getPayload() |
523
|
|
|
{ |
524
|
|
|
return $this->payload; |
525
|
|
|
} |
526
|
|
|
|
527
|
|
|
/** |
528
|
|
|
* Determines if a request payload is present. |
529
|
|
|
* |
530
|
|
|
* @return bool |
531
|
|
|
*/ |
532
|
|
|
public function hasPayload() |
533
|
|
|
{ |
534
|
|
|
return $this->getPayload() instanceof RestPayload; |
535
|
|
|
} |
536
|
|
|
|
537
|
|
|
/** |
538
|
|
|
* Parses the incoming request URI/URL and sets the appropriate properties on this RestRequest object. |
539
|
|
|
* |
540
|
|
|
* @param string $uri |
541
|
|
|
* @return self |
542
|
|
|
* @throws RestException |
543
|
|
|
*/ |
544
|
|
|
private function parse($uri) |
545
|
|
|
{ |
546
|
|
|
$this->parsedUri = parse_url($uri); |
|
|
|
|
547
|
|
|
|
548
|
|
|
if (false === strstr($this->parsedUri['path'], $this->config->getRootEndpoint())) { |
549
|
|
|
throw RestException::invalidEndpoint($this->parsedUri['path']); |
550
|
|
|
} |
551
|
|
|
|
552
|
|
|
$this->parsedUri['path'] = str_replace($this->config->getRootEndpoint(), '', $this->parsedUri['path']); |
553
|
|
|
$this->parsePath($this->parsedUri['path']); |
554
|
|
|
|
555
|
|
|
$this->parsedUri['query'] = isset($this->parsedUri['query']) ? $this->parsedUri['query'] : ''; |
556
|
|
|
$this->parseQueryString($this->parsedUri['query']); |
557
|
|
|
|
558
|
|
|
return $this; |
559
|
|
|
} |
560
|
|
|
|
561
|
|
|
/** |
562
|
|
|
* Parses the incoming request path and sets appropriate properties on this RestRequest object. |
563
|
|
|
* |
564
|
|
|
* @param string $path |
565
|
|
|
* @return self |
566
|
|
|
* @throws RestException |
567
|
|
|
*/ |
568
|
|
|
private function parsePath($path) |
569
|
|
|
{ |
570
|
|
|
$parts = explode('/', trim($path, '/')); |
571
|
|
|
for ($i = 0; $i < 1; $i++) { |
572
|
|
|
// All paths must contain /{workspace_entityType} |
573
|
|
|
if (false === $this->issetNotEmpty($i, $parts)) { |
574
|
|
|
throw RestException::invalidEndpoint($path); |
575
|
|
|
} |
576
|
|
|
} |
577
|
|
|
$this->extractEntityType($parts); |
578
|
|
|
$this->extractIdentifier($parts); |
579
|
|
|
$this->extractRelationship($parts); |
580
|
|
|
return $this; |
581
|
|
|
} |
582
|
|
|
|
583
|
|
|
/** |
584
|
|
|
* Extracts the entity type from an array of path parts. |
585
|
|
|
* |
586
|
|
|
* @param array $parts |
587
|
|
|
* @return self |
588
|
|
|
*/ |
589
|
|
|
private function extractEntityType(array $parts) |
590
|
|
|
{ |
591
|
|
|
$this->entityType = $parts[0]; |
592
|
|
|
return $this; |
593
|
|
|
} |
594
|
|
|
|
595
|
|
|
/** |
596
|
|
|
* Extracts the entity identifier (id) from an array of path parts. |
597
|
|
|
* |
598
|
|
|
* @param array $parts |
599
|
|
|
* @return self |
600
|
|
|
*/ |
601
|
|
|
private function extractIdentifier(array $parts) |
602
|
|
|
{ |
603
|
|
|
if (isset($parts[1])) { |
604
|
|
|
$this->identifier = $parts[1]; |
605
|
|
|
} |
606
|
|
|
return $this; |
607
|
|
|
} |
608
|
|
|
|
609
|
|
|
/** |
610
|
|
|
* Extracts the entity relationship properties from an array of path parts. |
611
|
|
|
* |
612
|
|
|
* @param array $parts |
613
|
|
|
* @return self |
614
|
|
|
*/ |
615
|
|
|
private function extractRelationship(array $parts) |
616
|
|
|
{ |
617
|
|
|
if (isset($parts[2])) { |
618
|
|
|
if ('relationships' === $parts[2]) { |
619
|
|
|
if (!isset($parts[3])) { |
620
|
|
|
throw RestException::invalidRelationshipEndpoint($this->parsedUri['path']); |
621
|
|
|
} |
622
|
|
|
$this->relationship = [ |
623
|
|
|
'type' => 'self', |
624
|
|
|
'field' => $parts[3], |
625
|
|
|
]; |
626
|
|
|
} else { |
627
|
|
|
$this->relationship = [ |
628
|
|
|
'type' => 'related', |
629
|
|
|
'field' => $parts[2], |
630
|
|
|
]; |
631
|
|
|
} |
632
|
|
|
} |
633
|
|
|
return $this; |
634
|
|
|
} |
635
|
|
|
|
636
|
|
|
/** |
637
|
|
|
* Parses the incoming request query string and sets appropriate properties on this RestRequest object. |
638
|
|
|
* |
639
|
|
|
* @param string $queryString |
640
|
|
|
* @return self |
641
|
|
|
* @throws RestException |
642
|
|
|
*/ |
643
|
|
|
private function parseQueryString($queryString) |
644
|
|
|
{ |
645
|
|
|
parse_str($queryString, $parsed); |
646
|
|
|
|
647
|
|
|
$supported = $this->getSupportedParams(); |
648
|
|
|
foreach ($parsed as $param => $value) { |
|
|
|
|
649
|
|
|
if (!isset($supported[$param])) { |
650
|
|
|
throw RestException::unsupportedQueryParam($param, array_keys($supported)); |
651
|
|
|
} |
652
|
|
|
} |
653
|
|
|
|
654
|
|
|
$this->extractInclusions($parsed); |
|
|
|
|
655
|
|
|
$this->extractSorting($parsed); |
|
|
|
|
656
|
|
|
$this->extractFields($parsed); |
|
|
|
|
657
|
|
|
$this->extractPagination($parsed); |
|
|
|
|
658
|
|
|
$this->extractFilters($parsed); |
|
|
|
|
659
|
|
|
return $this; |
660
|
|
|
} |
661
|
|
|
|
662
|
|
|
/** |
663
|
|
|
* Extracts relationship inclusions from an array of query params. |
664
|
|
|
* |
665
|
|
|
* @param array $params |
666
|
|
|
* @return self |
667
|
|
|
*/ |
668
|
|
|
private function extractInclusions(array $params) |
669
|
|
|
{ |
670
|
|
|
if (false === $this->issetNotEmpty(self::PARAM_INCLUSIONS, $params)) { |
671
|
|
|
if (true === $this->config->includeAllByDefault()) { |
672
|
|
|
$this->inclusions = ['*' => true]; |
673
|
|
|
} |
674
|
|
|
return $this; |
675
|
|
|
} |
676
|
|
|
$inclusions = explode(',', $params[self::PARAM_INCLUSIONS]); |
677
|
|
|
foreach ($inclusions as $inclusion) { |
678
|
|
|
if (false !== stristr($inclusion, '.')) { |
679
|
|
|
throw RestException::invalidParamValue(self::PARAM_INCLUSIONS, sprintf('Inclusion via a relationship path, e.g. "%s" is currently not supported.', $inclusion)); |
680
|
|
|
} |
681
|
|
|
$this->inclusions[$inclusion] = true; |
682
|
|
|
} |
683
|
|
|
return $this; |
684
|
|
|
} |
685
|
|
|
|
686
|
|
|
/** |
687
|
|
|
* Extracts sorting criteria from an array of query params. |
688
|
|
|
* |
689
|
|
|
* @param array $params |
690
|
|
|
* @return self |
691
|
|
|
*/ |
692
|
|
|
private function extractSorting(array $params) |
693
|
|
|
{ |
694
|
|
|
if (false === $this->issetNotEmpty(self::PARAM_SORTING, $params)) { |
695
|
|
|
return $this; |
696
|
|
|
} |
697
|
|
|
$sort = explode(',', $params[self::PARAM_SORTING]); |
698
|
|
|
$this->sorting = []; |
699
|
|
|
foreach ($sort as $field) { |
700
|
|
|
$direction = 1; |
701
|
|
|
if (0 === strpos($field, '-')) { |
702
|
|
|
$direction = -1; |
703
|
|
|
$field = str_replace('-', '', $field); |
704
|
|
|
} |
705
|
|
|
$this->sorting[$field] = $direction; |
706
|
|
|
} |
707
|
|
|
return $this; |
708
|
|
|
} |
709
|
|
|
|
710
|
|
|
/** |
711
|
|
|
* Extracts fields to return from an array of query params. |
712
|
|
|
* |
713
|
|
|
* @param array $params |
714
|
|
|
* @return self |
715
|
|
|
*/ |
716
|
|
View Code Duplication |
private function extractFields(array $params) |
|
|
|
|
717
|
|
|
{ |
718
|
|
|
if (false === $this->issetNotEmpty(self::PARAM_FIELDSETS, $params)) { |
719
|
|
|
return $this; |
720
|
|
|
} |
721
|
|
|
$fields = $params[self::PARAM_FIELDSETS]; |
722
|
|
|
if (!is_array($fields)) { |
723
|
|
|
throw RestException::invalidQueryParam(self::PARAM_FIELDSETS, 'The field parameter must be an array of entity type keys to fields.'); |
724
|
|
|
} |
725
|
|
|
foreach ($fields as $entityType => $string) { |
726
|
|
|
$this->fields[$entityType] = explode(',', $string); |
727
|
|
|
} |
728
|
|
|
return $this; |
729
|
|
|
} |
730
|
|
|
|
731
|
|
|
/** |
732
|
|
|
* Extracts pagination criteria from an array of query params. |
733
|
|
|
* |
734
|
|
|
* @param array $params |
735
|
|
|
* @return self |
736
|
|
|
*/ |
737
|
|
|
private function extractPagination(array $params) |
738
|
|
|
{ |
739
|
|
|
if (false === $this->issetNotEmpty(self::PARAM_PAGINATION, $params)) { |
740
|
|
|
return $this; |
741
|
|
|
} |
742
|
|
|
$page = $params[self::PARAM_PAGINATION]; |
743
|
|
|
if (!is_array($page) || !isset($page['limit'])) { |
744
|
|
|
throw RestException::invalidQueryParam(self::PARAM_PAGINATION, 'The page parameter must be an array containing at least a limit.'); |
745
|
|
|
} |
746
|
|
|
$this->pagination = [ |
747
|
|
|
'offset' => isset($page['offset']) ? (Integer) $page['offset'] : 0, |
748
|
|
|
'limit' => (Integer) $page['limit'], |
749
|
|
|
]; |
750
|
|
|
return $this; |
751
|
|
|
} |
752
|
|
|
|
753
|
|
|
/** |
754
|
|
|
* Extracts filtering criteria from an array of query params. |
755
|
|
|
* |
756
|
|
|
* @param array $params |
757
|
|
|
* @return self |
758
|
|
|
*/ |
759
|
|
View Code Duplication |
private function extractFilters(array $params) |
|
|
|
|
760
|
|
|
{ |
761
|
|
|
if (false === $this->issetNotEmpty(self::PARAM_FILTERING, $params)) { |
762
|
|
|
return $this; |
763
|
|
|
} |
764
|
|
|
$filters = $params[self::PARAM_FILTERING]; |
765
|
|
|
if (!is_array($filters)) { |
766
|
|
|
throw RestException::invalidQueryParam(self::PARAM_FILTERING, 'The filter parameter must be an array keyed by filter name and value.'); |
767
|
|
|
} |
768
|
|
|
foreach ($filters as $key => $value) { |
769
|
|
|
$this->filters[$key] = $value; |
770
|
|
|
} |
771
|
|
|
return $this; |
772
|
|
|
} |
773
|
|
|
|
774
|
|
|
/** |
775
|
|
|
* Gets query string parameters that this request supports. |
776
|
|
|
* |
777
|
|
|
* @return array |
778
|
|
|
*/ |
779
|
|
|
public function getSupportedParams() |
780
|
|
|
{ |
781
|
|
|
return [ |
782
|
|
|
self::PARAM_INCLUSIONS => true, |
783
|
|
|
self::PARAM_FIELDSETS => true, |
784
|
|
|
self::PARAM_SORTING => true, |
785
|
|
|
self::PARAM_PAGINATION => true, |
786
|
|
|
self::PARAM_FILTERING => true, |
787
|
|
|
]; |
788
|
|
|
} |
789
|
|
|
|
790
|
|
|
/** |
791
|
|
|
* Helper that determines if a key and value is set and is not empty. |
792
|
|
|
* |
793
|
|
|
* @param string $key |
794
|
|
|
* @param mixed $value |
795
|
|
|
* @return bool |
796
|
|
|
*/ |
797
|
|
|
private function issetNotEmpty($key, $value) |
798
|
|
|
{ |
799
|
|
|
return isset($value[$key]) && !empty($value[$key]); |
800
|
|
|
} |
801
|
|
|
} |
802
|
|
|
|
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.