Completed
Pull Request — master (#106)
by Joshua
10:54 queued 05:38
created

RestRequest::adjustRootEndpoint()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 9
rs 9.6666
c 0
b 0
f 0
cc 2
eloc 6
nc 2
nop 1
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
    protected function adjustRootEndpoint($path)
163
    {
164
        $root = $this->config->getRootEndpoint();
165
        if (0 !== strpos($path, $root)) {
166
            $end = strrpos($path, $root) + strlen($root);
167
            $endpoint = substr($path, 0, $end);
168
            $this->config->setRootEndpoint($endpoint);
169
        }
170
    }
171
172
    /**
173
     * Gets the scheme, such as http or https.
174
     *
175
     * @return  string
176
     */
177
    public function getScheme()
178
    {
179
        return $this->parsedUri['scheme'];
180
    }
181
182
    /**
183
     * Gets the hostname.
184
     *
185
     * @return  string
186
     */
187
    public function getHost()
188
    {
189
        return $this->parsedUri['host'];
190
    }
191
192
    /**
193
     * Gets the request method, such as GET, POST, PATCH, etc.
194
     *
195
     * @return  string
196
     */
197
    public function getMethod()
198
    {
199
        return $this->requestMethod;
200
    }
201
202
    /**
203
     * Gets the requested entity type.
204
     *
205
     * @return  string
206
     */
207
    public function getEntityType()
208
    {
209
        return $this->entityType;
210
    }
211
212
    /**
213
     * Gets the requested entity identifier (id), if sent.
214
     *
215
     * @return  string|null
216
     */
217
    public function getIdentifier()
218
    {
219
        return $this->identifier;
220
    }
221
222
    /**
223
     * Gets the query string based on the current object properties.
224
     *
225
     * @return  string
226
     */
227
    public function getQueryString()
228
    {
229
        $query = [];
230
        if (!empty($this->pagination)) {
231
            $query[self::PARAM_PAGINATION] = $this->pagination;
232
        }
233
        if (!empty($this->filters)) {
234
            $query[self::PARAM_FILTERING] = $this->filters;
235
        }
236
        foreach ($this->fields as $modelType => $fields) {
237
            $query[self::PARAM_FIELDSETS][$modelType] = implode(',', $fields);
238
        }
239
        $sort = [];
240
        foreach ($this->sorting as $key => $direction) {
241
            $sort[] = (1 === $direction) ? $key : sprintf('-%s', $key);
242
        }
243
        if (!empty($sort)) {
244
            $query[self::PARAM_SORTING] = implode(',', $sort);
245
        }
246
        return http_build_query($query);
247
    }
248
249
    /**
250
     * Determines if an entity identifier (id) was sent with the request.
251
     *
252
     * @return  bool
253
     */
254
    public function hasIdentifier()
255
    {
256
        return null !== $this->getIdentifier();
257
    }
258
259
    /**
260
     * Determines if this is an entity relationship request.
261
     *
262
     * @return  bool
263
     */
264
    public function isRelationship()
265
    {
266
        return !empty($this->relationship);
267
    }
268
269
    /**
270
     * Gets the entity relationship request.
271
     *
272
     * @return  array
273
     */
274
    public function getRelationship()
275
    {
276
        return $this->relationship;
277
    }
278
279
    /**
280
     * Gets the entity relationship field key.
281
     *
282
     * @return  string|null
283
     */
284
    public function getRelationshipFieldKey()
285
    {
286
        if (false === $this->isRelationship()) {
287
            return null;
288
        }
289
        return $this->getRelationship()['field'];
290
    }
291
292
    /**
293
     * Determines if this is an entity relationship retrieve request.
294
     *
295
     * @return  bool
296
     */
297 View Code Duplication
    public function isRelationshipRetrieve()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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.

Loading history...
298
    {
299
        if (false === $this->isRelationship()) {
300
            return false;
301
        }
302
        return 'self' === $this->getRelationship()['type'];
303
    }
304
305
    /**
306
     * Determines if this is an entity relationship modify (create/update/delete) request.
307
     *
308
     * @return  bool
309
     */
310 View Code Duplication
    public function isRelationshipModify()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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.

Loading history...
311
    {
312
        if (false === $this->isRelationship()) {
313
            return false;
314
        }
315
        return 'related' === $this->getRelationship()['type'];
316
    }
317
318
    /**
319
     * Determines if this has an autocomplete filter enabled.
320
     *
321
     * @return  bool
322
     */
323 View Code Duplication
    public function isAutocomplete()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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.

Loading history...
324
    {
325
        if (false === $this->hasFilter(self::FILTER_AUTOCOMPLETE)) {
326
            return false;
327
        }
328
        $autocomplete = $this->getFilter(self::FILTER_AUTOCOMPLETE);
329
        return isset($autocomplete[self::FILTER_AUTOCOMPLETE_KEY]) && isset($autocomplete[self::FILTER_AUTOCOMPLETE_VALUE]);
330
    }
331
332
    /**
333
     * Gets the autocomplete attribute key.
334
     *
335
     * @return  string|null
336
     */
337 View Code Duplication
    public function getAutocompleteKey()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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.

Loading history...
338
    {
339
        if (false === $this->isAutocomplete()) {
340
            return null;
341
        }
342
        return $this->getFilter(self::FILTER_AUTOCOMPLETE)[self::FILTER_AUTOCOMPLETE_KEY];
343
    }
344
345
    /**
346
     * Gets the autocomplete search value.
347
     *
348
     * @return  string|null
349
     */
350 View Code Duplication
    public function getAutocompleteValue()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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.

Loading history...
351
    {
352
        if (false === $this->isAutocomplete()) {
353
            return null;
354
        }
355
        return $this->getFilter(self::FILTER_AUTOCOMPLETE)[self::FILTER_AUTOCOMPLETE_VALUE];
356
    }
357
358
    /**
359
     * Determines if this has the database query filter enabled.
360
     *
361
     * @return  bool
362
     */
363 View Code Duplication
    public function isQuery()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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.

Loading history...
364
    {
365
        if (false === $this->hasFilter(self::FILTER_QUERY)) {
366
            return false;
367
        }
368
        $query = $this->getFilter(self::FILTER_QUERY);
369
        return isset($query[self::FILTER_QUERY_CRITERIA]);
370
    }
371
372
    /**
373
     * Gets the query criteria value.
374
     *
375
     * @return  array
376
     */
377
    public function getQueryCriteria()
378
    {
379
        if (false === $this->isQuery()) {
380
            return [];
381
        }
382
383
        $queryKey = self::FILTER_QUERY;
384
        $criteriaKey = self::FILTER_QUERY_CRITERIA;
385
386
        $decoded = @json_decode($this->getFilter($queryKey)[$criteriaKey], true);
387
        if (!is_array($decoded)) {
388
            $param = sprintf('%s[%s][%s]', self::PARAM_FILTERING, $queryKey, $criteriaKey);
389
            throw RestException::invalidQueryParam($param, 'Was the value sent as valid JSON?');
390
        }
391
        return $decoded;
392
    }
393
394
    /**
395
     * Determines if specific sideloaded include fields were requested.
396
     *
397
     * @return  bool
398
     */
399
    public function hasInclusions()
400
    {
401
        $value = $this->getInclusions();
402
        return !empty($value);
403
    }
404
405
    /**
406
     * Gets specific sideloaded relationship fields to include.
407
     *
408
     * @return  array
409
     */
410
    public function getInclusions()
411
    {
412
        return $this->inclusions;
413
    }
414
415
    /**
416
     * Determines if a specific return fieldset has been specified.
417
     *
418
     * @return  bool
419
     */
420
    public function hasFieldset()
421
    {
422
        $value = $this->getFieldset();
423
        return !empty($value);
424
    }
425
426
    /**
427
     * Gets the return fieldset to use.
428
     *
429
     * @return  array
430
     */
431
    public function getFieldset()
432
    {
433
        return $this->fields;
434
    }
435
436
    /**
437
     * Determines if the request has specified sorting criteria.
438
     *
439
     * @return  bool
440
     */
441
    public function hasSorting()
442
    {
443
        $value = $this->getSorting();
444
        return !empty($value);
445
    }
446
447
    /**
448
     * Gets the sorting criteria.
449
     *
450
     * @return  array
451
     */
452
    public function getSorting()
453
    {
454
        return $this->sorting;
455
    }
456
457
    /**
458
     * Determines if the request has specified pagination (limit/offset) criteria.
459
     *
460
     * @return  bool
461
     */
462
    public function hasPagination()
463
    {
464
        $value = $this->getPagination();
465
        return !empty($value);
466
    }
467
468
    /**
469
     * Gets the pagination (limit/offset) criteria.
470
     *
471
     * @return  array
472
     */
473
    public function getPagination()
474
    {
475
        return $this->pagination;
476
    }
477
478
    /**
479
     * Sets the pagination (limit/offset) criteria.
480
     *
481
     * @param   int     $offset
482
     * @param   int     $limit
483
     * @return  self
484
     */
485
    public function setPagination($offset, $limit)
486
    {
487
        $this->pagination['offset'] = (Integer) $offset;
488
        $this->pagination['limit'] = (Integer) $limit;
489
        return $this;
490
    }
491
492
    /**
493
     * Determines if the request has any filtering criteria.
494
     *
495
     * @return  bool
496
     */
497
    public function hasFilters()
498
    {
499
        return !empty($this->filters);
500
    }
501
502
    /**
503
     * Determines if a specific filter exists, by key
504
     *
505
     * @param   string  $key
506
     * @return  bool
507
     */
508
    public function hasFilter($key)
509
    {
510
        return null !== $this->getFilter($key);
511
    }
512
513
    /**
514
     * Gets a specific filter, by key.
515
     *
516
     * @param   string  $key
517
     * @return  mixed|null
518
     */
519
    public function getFilter($key)
520
    {
521
        if (!isset($this->filters[$key])) {
522
            return null;
523
        }
524
        return $this->filters[$key];
525
    }
526
527
    /**
528
     * Gets the request payload.
529
     *
530
     * @return  RestPayload|null
531
     */
532
    public function getPayload()
533
    {
534
        return $this->payload;
535
    }
536
537
    /**
538
     * Determines if a request payload is present.
539
     *
540
     * @return  bool
541
     */
542
    public function hasPayload()
543
    {
544
        return $this->getPayload() instanceof RestPayload;
545
    }
546
547
    /**
548
     * Parses the incoming request URI/URL and sets the appropriate properties on this RestRequest object.
549
     *
550
     * @param   string  $uri
551
     * @return  self
552
     * @throws  RestException
553
     */
554
    private function parse($uri)
555
    {
556
        $this->parsedUri = parse_url($uri);
0 ignored issues
show
Documentation Bug introduced by
It seems like parse_url($uri) can also be of type false. However, the property $parsedUri is declared as type array. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
557
558
        if (false === strstr($this->parsedUri['path'], $this->config->getRootEndpoint())) {
559
            throw RestException::invalidEndpoint($this->parsedUri['path']);
560
        }
561
562
        $this->adjustRootEndpoint($this->parsedUri['path']);
563
564
        $this->parsedUri['path'] = str_replace($this->config->getRootEndpoint(), '', $this->parsedUri['path']);
565
        $this->parsePath($this->parsedUri['path']);
566
567
        $this->parsedUri['query'] = isset($this->parsedUri['query']) ? $this->parsedUri['query'] : '';
568
        $this->parseQueryString($this->parsedUri['query']);
569
570
        return $this;
571
    }
572
573
    /**
574
     * Parses the incoming request path and sets appropriate properties on this RestRequest object.
575
     *
576
     * @param   string  $path
577
     * @return  self
578
     * @throws  RestException
579
     */
580
    private function parsePath($path)
581
    {
582
        $parts = explode('/', trim($path, '/'));
583
        for ($i = 0; $i < 1; $i++) {
584
            // All paths must contain /{workspace_entityType}
585
            if (false === $this->issetNotEmpty($i, $parts)) {
586
                throw RestException::invalidEndpoint($path);
587
            }
588
        }
589
        $this->extractEntityType($parts);
590
        $this->extractIdentifier($parts);
591
        $this->extractRelationship($parts);
592
        return $this;
593
    }
594
595
    /**
596
     * Extracts the entity type from an array of path parts.
597
     *
598
     * @param   array   $parts
599
     * @return  self
600
     */
601
    private function extractEntityType(array $parts)
602
    {
603
        $this->entityType = $parts[0];
604
        return $this;
605
    }
606
607
    /**
608
     * Extracts the entity identifier (id) from an array of path parts.
609
     *
610
     * @param   array   $parts
611
     * @return  self
612
     */
613
    private function extractIdentifier(array $parts)
614
    {
615
        if (isset($parts[1])) {
616
            $this->identifier = $parts[1];
617
        }
618
        return $this;
619
    }
620
621
    /**
622
     * Extracts the entity relationship properties from an array of path parts.
623
     *
624
     * @param   array   $parts
625
     * @return  self
626
     */
627
    private function extractRelationship(array $parts)
628
    {
629
        if (isset($parts[2])) {
630
            if ('relationships' === $parts[2]) {
631
                if (!isset($parts[3])) {
632
                    throw RestException::invalidRelationshipEndpoint($this->parsedUri['path']);
633
                }
634
                $this->relationship = [
635
                    'type'  => 'self',
636
                    'field' => $parts[3],
637
                ];
638
            } else {
639
                $this->relationship = [
640
                    'type'  => 'related',
641
                    'field' => $parts[2],
642
                ];
643
            }
644
        }
645
        return $this;
646
    }
647
648
    /**
649
     * Parses the incoming request query string and sets appropriate properties on this RestRequest object.
650
     *
651
     * @param   string  $queryString
652
     * @return  self
653
     * @throws  RestException
654
     */
655
    private function parseQueryString($queryString)
656
    {
657
        parse_str($queryString, $parsed);
658
659
        $supported = $this->getSupportedParams();
660
        foreach ($parsed as $param => $value) {
0 ignored issues
show
Bug introduced by
The expression $parsed of type null|array is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
661
            if (!isset($supported[$param])) {
662
                throw RestException::unsupportedQueryParam($param, array_keys($supported));
663
            }
664
        }
665
666
        $this->extractInclusions($parsed);
0 ignored issues
show
Bug introduced by
It seems like $parsed can also be of type null; however, As3\Modlr\Rest\RestRequest::extractInclusions() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
667
        $this->extractSorting($parsed);
0 ignored issues
show
Bug introduced by
It seems like $parsed can also be of type null; however, As3\Modlr\Rest\RestRequest::extractSorting() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
668
        $this->extractFields($parsed);
0 ignored issues
show
Bug introduced by
It seems like $parsed can also be of type null; however, As3\Modlr\Rest\RestRequest::extractFields() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
669
        $this->extractPagination($parsed);
0 ignored issues
show
Bug introduced by
It seems like $parsed can also be of type null; however, As3\Modlr\Rest\RestRequest::extractPagination() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
670
        $this->extractFilters($parsed);
0 ignored issues
show
Bug introduced by
It seems like $parsed can also be of type null; however, As3\Modlr\Rest\RestRequest::extractFilters() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
671
        return $this;
672
    }
673
674
    /**
675
     * Extracts relationship inclusions from an array of query params.
676
     *
677
     * @param   array   $params
678
     * @return  self
679
     */
680
    private function extractInclusions(array $params)
681
    {
682
        if (false === $this->issetNotEmpty(self::PARAM_INCLUSIONS, $params)) {
683
            if (true === $this->config->includeAllByDefault()) {
684
                $this->inclusions = ['*' => true];
685
            }
686
            return $this;
687
        }
688
        $inclusions = explode(',', $params[self::PARAM_INCLUSIONS]);
689
        foreach ($inclusions as $inclusion) {
690
            if (false !== stristr($inclusion, '.')) {
691
                throw RestException::invalidParamValue(self::PARAM_INCLUSIONS, sprintf('Inclusion via a relationship path, e.g. "%s" is currently not supported.', $inclusion));
692
            }
693
            $this->inclusions[$inclusion] = true;
694
        }
695
        return $this;
696
    }
697
698
    /**
699
     * Extracts sorting criteria from an array of query params.
700
     *
701
     * @param   array   $params
702
     * @return  self
703
     */
704
    private function extractSorting(array $params)
705
    {
706
        if (false === $this->issetNotEmpty(self::PARAM_SORTING, $params)) {
707
            return $this;
708
        }
709
        $sort = explode(',', $params[self::PARAM_SORTING]);
710
        $this->sorting = [];
711
        foreach ($sort as $field) {
712
            $direction = 1;
713
            if (0 === strpos($field, '-')) {
714
                $direction = -1;
715
                $field = str_replace('-', '', $field);
716
            }
717
            $this->sorting[$field] = $direction;
718
        }
719
        return $this;
720
    }
721
722
    /**
723
     * Extracts fields to return from an array of query params.
724
     *
725
     * @param   array   $params
726
     * @return  self
727
     */
728 View Code Duplication
    private function extractFields(array $params)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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.

Loading history...
729
    {
730
        if (false === $this->issetNotEmpty(self::PARAM_FIELDSETS, $params)) {
731
            return $this;
732
        }
733
        $fields = $params[self::PARAM_FIELDSETS];
734
        if (!is_array($fields)) {
735
            throw RestException::invalidQueryParam(self::PARAM_FIELDSETS, 'The field parameter must be an array of entity type keys to fields.');
736
        }
737
        foreach ($fields as $entityType => $string) {
738
            $this->fields[$entityType] = explode(',', $string);
739
        }
740
        return $this;
741
    }
742
743
    /**
744
     * Extracts pagination criteria from an array of query params.
745
     *
746
     * @param   array   $params
747
     * @return  self
748
     */
749
    private function extractPagination(array $params)
750
    {
751
        if (false === $this->issetNotEmpty(self::PARAM_PAGINATION, $params)) {
752
            return $this;
753
        }
754
        $page = $params[self::PARAM_PAGINATION];
755
        if (!is_array($page) || !isset($page['limit'])) {
756
            throw RestException::invalidQueryParam(self::PARAM_PAGINATION, 'The page parameter must be an array containing at least a limit.');
757
        }
758
        $this->pagination = [
759
            'offset'    => isset($page['offset']) ? (Integer) $page['offset'] : 0,
760
            'limit'     => (Integer) $page['limit'],
761
        ];
762
        return $this;
763
    }
764
765
    /**
766
     * Extracts filtering criteria from an array of query params.
767
     *
768
     * @param   array   $params
769
     * @return  self
770
     */
771 View Code Duplication
    private function extractFilters(array $params)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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.

Loading history...
772
    {
773
        if (false === $this->issetNotEmpty(self::PARAM_FILTERING, $params)) {
774
            return $this;
775
        }
776
        $filters = $params[self::PARAM_FILTERING];
777
        if (!is_array($filters)) {
778
            throw RestException::invalidQueryParam(self::PARAM_FILTERING, 'The filter parameter must be an array keyed by filter name and value.');
779
        }
780
        foreach ($filters as $key => $value) {
781
            $this->filters[$key] = $value;
782
        }
783
        return $this;
784
    }
785
786
    /**
787
     * Gets query string parameters that this request supports.
788
     *
789
     * @return  array
790
     */
791
    public function getSupportedParams()
792
    {
793
        return [
794
            self::PARAM_INCLUSIONS  => true,
795
            self::PARAM_FIELDSETS   => true,
796
            self::PARAM_SORTING     => true,
797
            self::PARAM_PAGINATION  => true,
798
            self::PARAM_FILTERING   => true,
799
        ];
800
    }
801
802
    /**
803
     * Helper that determines if a key and value is set and is not empty.
804
     *
805
     * @param   string  $key
806
     * @param   mixed   $value
807
     * @return  bool
808
     */
809
    private function issetNotEmpty($key, $value)
810
    {
811
        return isset($value[$key]) && !empty($value[$key]);
812
    }
813
}
814