Completed
Pull Request — master (#106)
by Joshua
06:23
created

RestRequest::setPagination()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 6
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 4
nc 1
nop 2
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->uri = $uri;
0 ignored issues
show
Bug introduced by
The property uri does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
132
        $this->requestMethod = strtoupper($method);
133
134
        $this->sorting      = $config->getDefaultSorting();
135
        $this->pagination   = $config->getDefaultPagination();
136
137
        $this->parse($uri);
138
        $this->payload = empty($payload) ? null : new RestPayload($payload);
139
140
        // Re-configure the config based on the actually request.
141
        $this->config->setHost($this->getHost());
142
        $this->config->setScheme($this->getScheme());
143
    }
144
145
    /**
146
     * Generates the request URL based on its current object state.
147
     *
148
     * @todo    Add support for inclusions and other items.
149
     * @return  string
150
     */
151
    public function getUrl()
152
    {
153
        $query = $this->getQueryString();
154
        return sprintf('%s://%s/%s/%s%s',
155
            $this->getScheme(),
156
            trim($this->getHost(), '/'),
157
            trim($this->config->getRootEndpoint(), '/'),
158
            $this->getEntityType(),
159
            empty($query) ? '' : sprintf('?%s', $query)
160
        );
161
    }
162
163
    protected function adjustRootEndpoint($path)
164
    {
165
        $root = $this->config->getRootEndpoint();
166
        if (0 !== strpos($path, $root)) {
167
            $end = strrpos($path, $root) + strlen($root);
168
            $endpoint = substr($path, 0, $end);
169
            $this->config->setRootEndpoint($endpoint);
170
        }
171
    }
172
173
    /**
174
     * Gets the scheme, such as http or https.
175
     *
176
     * @return  string
177
     */
178
    public function getScheme()
179
    {
180
        return $this->parsedUri['scheme'];
181
    }
182
183
    /**
184
     * Gets the hostname.
185
     *
186
     * @return  string
187
     */
188
    public function getHost()
189
    {
190
        return $this->parsedUri['host'];
191
    }
192
193
    /**
194
     * Gets the request method, such as GET, POST, PATCH, etc.
195
     *
196
     * @return  string
197
     */
198
    public function getMethod()
199
    {
200
        return $this->requestMethod;
201
    }
202
203
    /**
204
     * Gets the requested entity type.
205
     *
206
     * @return  string
207
     */
208
    public function getEntityType()
209
    {
210
        return $this->entityType;
211
    }
212
213
    /**
214
     * Gets the requested entity identifier (id), if sent.
215
     *
216
     * @return  string|null
217
     */
218
    public function getIdentifier()
219
    {
220
        return $this->identifier;
221
    }
222
223
    /**
224
     * Gets the query string based on the current object properties.
225
     *
226
     * @return  string
227
     */
228
    public function getQueryString()
229
    {
230
        $query = [];
231
        if (!empty($this->pagination)) {
232
            $query[self::PARAM_PAGINATION] = $this->pagination;
233
        }
234
        if (!empty($this->filters)) {
235
            $query[self::PARAM_FILTERING] = $this->filters;
236
        }
237
        foreach ($this->fields as $modelType => $fields) {
238
            $query[self::PARAM_FIELDSETS][$modelType] = implode(',', $fields);
239
        }
240
        $sort = [];
241
        foreach ($this->sorting as $key => $direction) {
242
            $sort[] = (1 === $direction) ? $key : sprintf('-%s', $key);
243
        }
244
        if (!empty($sort)) {
245
            $query[self::PARAM_SORTING] = implode(',', $sort);
246
        }
247
        return http_build_query($query);
248
    }
249
250
    /**
251
     * Determines if an entity identifier (id) was sent with the request.
252
     *
253
     * @return  bool
254
     */
255
    public function hasIdentifier()
256
    {
257
        return null !== $this->getIdentifier();
258
    }
259
260
    /**
261
     * Determines if this is an entity relationship request.
262
     *
263
     * @return  bool
264
     */
265
    public function isRelationship()
266
    {
267
        return !empty($this->relationship);
268
    }
269
270
    /**
271
     * Gets the entity relationship request.
272
     *
273
     * @return  array
274
     */
275
    public function getRelationship()
276
    {
277
        return $this->relationship;
278
    }
279
280
    /**
281
     * Gets the entity relationship field key.
282
     *
283
     * @return  string|null
284
     */
285
    public function getRelationshipFieldKey()
286
    {
287
        if (false === $this->isRelationship()) {
288
            return null;
289
        }
290
        return $this->getRelationship()['field'];
291
    }
292
293
    /**
294
     * Determines if this is an entity relationship retrieve request.
295
     *
296
     * @return  bool
297
     */
298 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...
299
    {
300
        if (false === $this->isRelationship()) {
301
            return false;
302
        }
303
        return 'self' === $this->getRelationship()['type'];
304
    }
305
306
    /**
307
     * Determines if this is an entity relationship modify (create/update/delete) request.
308
     *
309
     * @return  bool
310
     */
311 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...
312
    {
313
        if (false === $this->isRelationship()) {
314
            return false;
315
        }
316
        return 'related' === $this->getRelationship()['type'];
317
    }
318
319
    /**
320
     * Determines if this has an autocomplete filter enabled.
321
     *
322
     * @return  bool
323
     */
324 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...
325
    {
326
        if (false === $this->hasFilter(self::FILTER_AUTOCOMPLETE)) {
327
            return false;
328
        }
329
        $autocomplete = $this->getFilter(self::FILTER_AUTOCOMPLETE);
330
        return isset($autocomplete[self::FILTER_AUTOCOMPLETE_KEY]) && isset($autocomplete[self::FILTER_AUTOCOMPLETE_VALUE]);
331
    }
332
333
    /**
334
     * Gets the autocomplete attribute key.
335
     *
336
     * @return  string|null
337
     */
338 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...
339
    {
340
        if (false === $this->isAutocomplete()) {
341
            return null;
342
        }
343
        return $this->getFilter(self::FILTER_AUTOCOMPLETE)[self::FILTER_AUTOCOMPLETE_KEY];
344
    }
345
346
    /**
347
     * Gets the autocomplete search value.
348
     *
349
     * @return  string|null
350
     */
351 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...
352
    {
353
        if (false === $this->isAutocomplete()) {
354
            return null;
355
        }
356
        return $this->getFilter(self::FILTER_AUTOCOMPLETE)[self::FILTER_AUTOCOMPLETE_VALUE];
357
    }
358
359
    /**
360
     * Determines if this has the database query filter enabled.
361
     *
362
     * @return  bool
363
     */
364 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...
365
    {
366
        if (false === $this->hasFilter(self::FILTER_QUERY)) {
367
            return false;
368
        }
369
        $query = $this->getFilter(self::FILTER_QUERY);
370
        return isset($query[self::FILTER_QUERY_CRITERIA]);
371
    }
372
373
    /**
374
     * Gets the query criteria value.
375
     *
376
     * @return  array
377
     */
378
    public function getQueryCriteria()
379
    {
380
        if (false === $this->isQuery()) {
381
            return [];
382
        }
383
384
        $queryKey = self::FILTER_QUERY;
385
        $criteriaKey = self::FILTER_QUERY_CRITERIA;
386
387
        $decoded = @json_decode($this->getFilter($queryKey)[$criteriaKey], true);
388
        if (!is_array($decoded)) {
389
            $param = sprintf('%s[%s][%s]', self::PARAM_FILTERING, $queryKey, $criteriaKey);
390
            throw RestException::invalidQueryParam($param, 'Was the value sent as valid JSON?');
391
        }
392
        return $decoded;
393
    }
394
395
    /**
396
     * Determines if specific sideloaded include fields were requested.
397
     *
398
     * @return  bool
399
     */
400
    public function hasInclusions()
401
    {
402
        $value = $this->getInclusions();
403
        return !empty($value);
404
    }
405
406
    /**
407
     * Gets specific sideloaded relationship fields to include.
408
     *
409
     * @return  array
410
     */
411
    public function getInclusions()
412
    {
413
        return $this->inclusions;
414
    }
415
416
    /**
417
     * Determines if a specific return fieldset has been specified.
418
     *
419
     * @return  bool
420
     */
421
    public function hasFieldset()
422
    {
423
        $value = $this->getFieldset();
424
        return !empty($value);
425
    }
426
427
    /**
428
     * Gets the return fieldset to use.
429
     *
430
     * @return  array
431
     */
432
    public function getFieldset()
433
    {
434
        return $this->fields;
435
    }
436
437
    /**
438
     * Determines if the request has specified sorting criteria.
439
     *
440
     * @return  bool
441
     */
442
    public function hasSorting()
443
    {
444
        $value = $this->getSorting();
445
        return !empty($value);
446
    }
447
448
    /**
449
     * Gets the sorting criteria.
450
     *
451
     * @return  array
452
     */
453
    public function getSorting()
454
    {
455
        return $this->sorting;
456
    }
457
458
    /**
459
     * Determines if the request has specified pagination (limit/offset) criteria.
460
     *
461
     * @return  bool
462
     */
463
    public function hasPagination()
464
    {
465
        $value = $this->getPagination();
466
        return !empty($value);
467
    }
468
469
    /**
470
     * Gets the pagination (limit/offset) criteria.
471
     *
472
     * @return  array
473
     */
474
    public function getPagination()
475
    {
476
        return $this->pagination;
477
    }
478
479
    /**
480
     * Sets the pagination (limit/offset) criteria.
481
     *
482
     * @param   int     $offset
483
     * @param   int     $limit
484
     * @return  self
485
     */
486
    public function setPagination($offset, $limit)
487
    {
488
        $this->pagination['offset'] = (Integer) $offset;
489
        $this->pagination['limit'] = (Integer) $limit;
490
        return $this;
491
    }
492
493
    /**
494
     * Determines if the request has any filtering criteria.
495
     *
496
     * @return  bool
497
     */
498
    public function hasFilters()
499
    {
500
        return !empty($this->filters);
501
    }
502
503
    /**
504
     * Determines if a specific filter exists, by key
505
     *
506
     * @param   string  $key
507
     * @return  bool
508
     */
509
    public function hasFilter($key)
510
    {
511
        return null !== $this->getFilter($key);
512
    }
513
514
    /**
515
     * Gets a specific filter, by key.
516
     *
517
     * @param   string  $key
518
     * @return  mixed|null
519
     */
520
    public function getFilter($key)
521
    {
522
        if (!isset($this->filters[$key])) {
523
            return null;
524
        }
525
        return $this->filters[$key];
526
    }
527
528
    /**
529
     * Gets the request payload.
530
     *
531
     * @return  RestPayload|null
532
     */
533
    public function getPayload()
534
    {
535
        return $this->payload;
536
    }
537
538
    /**
539
     * Determines if a request payload is present.
540
     *
541
     * @return  bool
542
     */
543
    public function hasPayload()
544
    {
545
        return $this->getPayload() instanceof RestPayload;
546
    }
547
548
    /**
549
     * Parses the incoming request URI/URL and sets the appropriate properties on this RestRequest object.
550
     *
551
     * @param   string  $uri
552
     * @return  self
553
     * @throws  RestException
554
     */
555
    private function parse($uri)
556
    {
557
        $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...
558
559
        if (false === strstr($this->parsedUri['path'], $this->config->getRootEndpoint())) {
560
            throw RestException::invalidEndpoint($this->parsedUri['path']);
561
        }
562
563
        $this->adjustRootEndpoint($this->parsedUri['path']);
564
565
        $this->parsedUri['path'] = str_replace($this->config->getRootEndpoint(), '', $this->parsedUri['path']);
566
        $this->parsePath($this->parsedUri['path']);
567
568
        $this->parsedUri['query'] = isset($this->parsedUri['query']) ? $this->parsedUri['query'] : '';
569
        $this->parseQueryString($this->parsedUri['query']);
570
571
        return $this;
572
    }
573
574
    /**
575
     * Parses the incoming request path and sets appropriate properties on this RestRequest object.
576
     *
577
     * @param   string  $path
578
     * @return  self
579
     * @throws  RestException
580
     */
581
    private function parsePath($path)
582
    {
583
        $parts = explode('/', trim($path, '/'));
584
        for ($i = 0; $i < 1; $i++) {
585
            // All paths must contain /{workspace_entityType}
586
            if (false === $this->issetNotEmpty($i, $parts)) {
587
                throw RestException::invalidEndpoint($path);
588
            }
589
        }
590
        $this->extractEntityType($parts);
591
        $this->extractIdentifier($parts);
592
        $this->extractRelationship($parts);
593
        return $this;
594
    }
595
596
    /**
597
     * Extracts the entity type from an array of path parts.
598
     *
599
     * @param   array   $parts
600
     * @return  self
601
     */
602
    private function extractEntityType(array $parts)
603
    {
604
        $this->entityType = $parts[0];
605
        return $this;
606
    }
607
608
    /**
609
     * Extracts the entity identifier (id) from an array of path parts.
610
     *
611
     * @param   array   $parts
612
     * @return  self
613
     */
614
    private function extractIdentifier(array $parts)
615
    {
616
        if (isset($parts[1])) {
617
            $this->identifier = $parts[1];
618
        }
619
        return $this;
620
    }
621
622
    /**
623
     * Extracts the entity relationship properties from an array of path parts.
624
     *
625
     * @param   array   $parts
626
     * @return  self
627
     */
628
    private function extractRelationship(array $parts)
629
    {
630
        if (isset($parts[2])) {
631
            if ('relationships' === $parts[2]) {
632
                if (!isset($parts[3])) {
633
                    throw RestException::invalidRelationshipEndpoint($this->parsedUri['path']);
634
                }
635
                $this->relationship = [
636
                    'type'  => 'self',
637
                    'field' => $parts[3],
638
                ];
639
            } else {
640
                $this->relationship = [
641
                    'type'  => 'related',
642
                    'field' => $parts[2],
643
                ];
644
            }
645
        }
646
        return $this;
647
    }
648
649
    /**
650
     * Parses the incoming request query string and sets appropriate properties on this RestRequest object.
651
     *
652
     * @param   string  $queryString
653
     * @return  self
654
     * @throws  RestException
655
     */
656
    private function parseQueryString($queryString)
657
    {
658
        parse_str($queryString, $parsed);
659
660
        $supported = $this->getSupportedParams();
661
        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...
662
            if (!isset($supported[$param])) {
663
                throw RestException::unsupportedQueryParam($param, array_keys($supported));
664
            }
665
        }
666
667
        $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...
668
        $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...
669
        $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...
670
        $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...
671
        $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...
672
        return $this;
673
    }
674
675
    /**
676
     * Extracts relationship inclusions from an array of query params.
677
     *
678
     * @param   array   $params
679
     * @return  self
680
     */
681
    private function extractInclusions(array $params)
682
    {
683
        if (false === $this->issetNotEmpty(self::PARAM_INCLUSIONS, $params)) {
684
            if (true === $this->config->includeAllByDefault()) {
685
                $this->inclusions = ['*' => true];
686
            }
687
            return $this;
688
        }
689
        $inclusions = explode(',', $params[self::PARAM_INCLUSIONS]);
690
        foreach ($inclusions as $inclusion) {
691
            if (false !== stristr($inclusion, '.')) {
692
                throw RestException::invalidParamValue(self::PARAM_INCLUSIONS, sprintf('Inclusion via a relationship path, e.g. "%s" is currently not supported.', $inclusion));
693
            }
694
            $this->inclusions[$inclusion] = true;
695
        }
696
        return $this;
697
    }
698
699
    /**
700
     * Extracts sorting criteria from an array of query params.
701
     *
702
     * @param   array   $params
703
     * @return  self
704
     */
705
    private function extractSorting(array $params)
706
    {
707
        if (false === $this->issetNotEmpty(self::PARAM_SORTING, $params)) {
708
            return $this;
709
        }
710
        $sort = explode(',', $params[self::PARAM_SORTING]);
711
        $this->sorting = [];
712
        foreach ($sort as $field) {
713
            $direction = 1;
714
            if (0 === strpos($field, '-')) {
715
                $direction = -1;
716
                $field = str_replace('-', '', $field);
717
            }
718
            $this->sorting[$field] = $direction;
719
        }
720
        return $this;
721
    }
722
723
    /**
724
     * Extracts fields to return from an array of query params.
725
     *
726
     * @param   array   $params
727
     * @return  self
728
     */
729 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...
730
    {
731
        if (false === $this->issetNotEmpty(self::PARAM_FIELDSETS, $params)) {
732
            return $this;
733
        }
734
        $fields = $params[self::PARAM_FIELDSETS];
735
        if (!is_array($fields)) {
736
            throw RestException::invalidQueryParam(self::PARAM_FIELDSETS, 'The field parameter must be an array of entity type keys to fields.');
737
        }
738
        foreach ($fields as $entityType => $string) {
739
            $this->fields[$entityType] = explode(',', $string);
740
        }
741
        return $this;
742
    }
743
744
    /**
745
     * Extracts pagination criteria from an array of query params.
746
     *
747
     * @param   array   $params
748
     * @return  self
749
     */
750
    private function extractPagination(array $params)
751
    {
752
        if (false === $this->issetNotEmpty(self::PARAM_PAGINATION, $params)) {
753
            return $this;
754
        }
755
        $page = $params[self::PARAM_PAGINATION];
756
        if (!is_array($page) || !isset($page['limit'])) {
757
            throw RestException::invalidQueryParam(self::PARAM_PAGINATION, 'The page parameter must be an array containing at least a limit.');
758
        }
759
        $this->pagination = [
760
            'offset'    => isset($page['offset']) ? (Integer) $page['offset'] : 0,
761
            'limit'     => (Integer) $page['limit'],
762
        ];
763
        return $this;
764
    }
765
766
    /**
767
     * Extracts filtering criteria from an array of query params.
768
     *
769
     * @param   array   $params
770
     * @return  self
771
     */
772 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...
773
    {
774
        if (false === $this->issetNotEmpty(self::PARAM_FILTERING, $params)) {
775
            return $this;
776
        }
777
        $filters = $params[self::PARAM_FILTERING];
778
        if (!is_array($filters)) {
779
            throw RestException::invalidQueryParam(self::PARAM_FILTERING, 'The filter parameter must be an array keyed by filter name and value.');
780
        }
781
        foreach ($filters as $key => $value) {
782
            $this->filters[$key] = $value;
783
        }
784
        return $this;
785
    }
786
787
    /**
788
     * Gets query string parameters that this request supports.
789
     *
790
     * @return  array
791
     */
792
    public function getSupportedParams()
793
    {
794
        return [
795
            self::PARAM_INCLUSIONS  => true,
796
            self::PARAM_FIELDSETS   => true,
797
            self::PARAM_SORTING     => true,
798
            self::PARAM_PAGINATION  => true,
799
            self::PARAM_FILTERING   => true,
800
        ];
801
    }
802
803
    /**
804
     * Helper that determines if a key and value is set and is not empty.
805
     *
806
     * @param   string  $key
807
     * @param   mixed   $value
808
     * @return  bool
809
     */
810
    private function issetNotEmpty($key, $value)
811
    {
812
        return isset($value[$key]) && !empty($value[$key]);
813
    }
814
}
815