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

RestRequest::getEndpointPrefix()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 12
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

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