Response::isRedirect()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 2
eloc 2
nc 2
nop 0
1
<?php
2
3
namespace Guzzle\Http\Message;
4
5
use Guzzle\Common\Version;
6
use Guzzle\Common\ToArrayInterface;
7
use Guzzle\Common\Exception\RuntimeException;
8
use Guzzle\Http\EntityBodyInterface;
9
use Guzzle\Http\EntityBody;
10
use Guzzle\Http\Exception\BadResponseException;
11
use Guzzle\Http\RedirectPlugin;
12
use Guzzle\Parser\ParserRegistry;
13
14
/**
15
 * Guzzle HTTP response object
16
 */
17
class Response extends AbstractMessage implements \Serializable
18
{
19
    /**
20
     * @var array Array of reason phrases and their corresponding status codes
21
     */
22
    private static $statusTexts = array(
23
        100 => 'Continue',
24
        101 => 'Switching Protocols',
25
        102 => 'Processing',
26
        200 => 'OK',
27
        201 => 'Created',
28
        202 => 'Accepted',
29
        203 => 'Non-Authoritative Information',
30
        204 => 'No Content',
31
        205 => 'Reset Content',
32
        206 => 'Partial Content',
33
        207 => 'Multi-Status',
34
        208 => 'Already Reported',
35
        226 => 'IM Used',
36
        300 => 'Multiple Choices',
37
        301 => 'Moved Permanently',
38
        302 => 'Found',
39
        303 => 'See Other',
40
        304 => 'Not Modified',
41
        305 => 'Use Proxy',
42
        307 => 'Temporary Redirect',
43
        308 => 'Permanent Redirect',
44
        400 => 'Bad Request',
45
        401 => 'Unauthorized',
46
        402 => 'Payment Required',
47
        403 => 'Forbidden',
48
        404 => 'Not Found',
49
        405 => 'Method Not Allowed',
50
        406 => 'Not Acceptable',
51
        407 => 'Proxy Authentication Required',
52
        408 => 'Request Timeout',
53
        409 => 'Conflict',
54
        410 => 'Gone',
55
        411 => 'Length Required',
56
        412 => 'Precondition Failed',
57
        413 => 'Request Entity Too Large',
58
        414 => 'Request-URI Too Long',
59
        415 => 'Unsupported Media Type',
60
        416 => 'Requested Range Not Satisfiable',
61
        417 => 'Expectation Failed',
62
        422 => 'Unprocessable Entity',
63
        423 => 'Locked',
64
        424 => 'Failed Dependency',
65
        425 => 'Reserved for WebDAV advanced collections expired proposal',
66
        426 => 'Upgrade required',
67
        428 => 'Precondition Required',
68
        429 => 'Too Many Requests',
69
        431 => 'Request Header Fields Too Large',
70
        500 => 'Internal Server Error',
71
        501 => 'Not Implemented',
72
        502 => 'Bad Gateway',
73
        503 => 'Service Unavailable',
74
        504 => 'Gateway Timeout',
75
        505 => 'HTTP Version Not Supported',
76
        506 => 'Variant Also Negotiates (Experimental)',
77
        507 => 'Insufficient Storage',
78
        508 => 'Loop Detected',
79
        510 => 'Not Extended',
80
        511 => 'Network Authentication Required',
81
    );
82
83
    /** @var EntityBodyInterface The response body */
84
    protected $body;
85
86
    /** @var string The reason phrase of the response (human readable code) */
87
    protected $reasonPhrase;
88
89
    /** @var string The status code of the response */
90
    protected $statusCode;
91
92
    /** @var array Information about the request */
93
    protected $info = array();
94
95
    /** @var string The effective URL that returned this response */
96
    protected $effectiveUrl;
97
98
    /** @var array Cacheable response codes (see RFC 2616:13.4) */
99
    protected static $cacheResponseCodes = array(200, 203, 206, 300, 301, 410);
100
101
    /**
102
     * Create a new Response based on a raw response message
103
     *
104
     * @param string $message Response message
105
     *
106
     * @return self|bool Returns false on error
107
     */
108
    public static function fromMessage($message)
109
    {
110
        $data = ParserRegistry::getInstance()->getParser('message')->parseResponse($message);
111
        if (!$data) {
112
            return false;
113
        }
114
115
        $response = new static($data['code'], $data['headers'], $data['body']);
116
        $response->setProtocol($data['protocol'], $data['version'])
117
                 ->setStatus($data['code'], $data['reason_phrase']);
118
119
        // Set the appropriate Content-Length if the one set is inaccurate (e.g. setting to X)
120
        $contentLength = (string) $response->getHeader('Content-Length');
121
        $actualLength = strlen($data['body']);
122
        if (strlen($data['body']) > 0 && $contentLength != $actualLength) {
123
            $response->setHeader('Content-Length', $actualLength);
124
        }
125
126
        return $response;
127
    }
128
129
    /**
130
     * Construct the response
131
     *
132
     * @param string                              $statusCode The response status code (e.g. 200, 404, etc)
133
     * @param ToArrayInterface|array              $headers    The response headers
134
     * @param string|resource|EntityBodyInterface $body       The body of the response
135
     *
136
     * @throws BadResponseException if an invalid response code is given
137
     */
138
    public function __construct($statusCode, $headers = null, $body = null)
139
    {
140
        parent::__construct();
141
        $this->setStatus($statusCode);
142
        $this->body = EntityBody::factory($body !== null ? $body : '');
0 ignored issues
show
Bug introduced by
It seems like $body !== null ? $body : '' can also be of type object<Guzzle\Http\EntityBodyInterface>; however, Guzzle\Http\EntityBody::factory() does only seem to accept string|resource|object<Guzzle\Http\EntityBody>, 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...
143
144
        if ($headers) {
145
            if (is_array($headers)) {
146
                $this->setHeaders($headers);
147
            } elseif ($headers instanceof ToArrayInterface) {
148
                $this->setHeaders($headers->toArray());
149
            } else {
150
                throw new BadResponseException('Invalid headers argument received');
151
            }
152
        }
153
    }
154
155
    /**
156
     * @return string
157
     */
158
    public function __toString()
159
    {
160
        return $this->getMessage();
161
    }
162
163
    public function serialize()
164
    {
165
        return json_encode(array(
166
            'status'  => $this->statusCode,
167
            'body'    => (string) $this->body,
168
            'headers' => $this->headers->toArray()
0 ignored issues
show
Bug introduced by
The method toArray cannot be called on $this->headers (of type array).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
169
        ));
170
    }
171
172
    public function unserialize($serialize)
173
    {
174
        $data = json_decode($serialize, true);
175
        $this->__construct($data['status'], $data['headers'], $data['body']);
176
    }
177
178
    /**
179
     * Get the response entity body
180
     *
181
     * @param bool $asString Set to TRUE to return a string of the body rather than a full body object
182
     *
183
     * @return EntityBodyInterface|string
184
     */
185
    public function getBody($asString = false)
186
    {
187
        return $asString ? (string) $this->body : $this->body;
188
    }
189
190
    /**
191
     * Set the response entity body
192
     *
193
     * @param EntityBodyInterface|string $body Body to set
194
     *
195
     * @return self
196
     */
197
    public function setBody($body)
198
    {
199
        $this->body = EntityBody::factory($body);
0 ignored issues
show
Bug introduced by
It seems like $body defined by parameter $body on line 197 can also be of type object<Guzzle\Http\EntityBodyInterface>; however, Guzzle\Http\EntityBody::factory() does only seem to accept string|resource|object<Guzzle\Http\EntityBody>, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
200
201
        return $this;
202
    }
203
204
    /**
205
     * Set the protocol and protocol version of the response
206
     *
207
     * @param string $protocol Response protocol
208
     * @param string $version  Protocol version
209
     *
210
     * @return self
211
     */
212
    public function setProtocol($protocol, $version)
213
    {
214
        $this->protocol = $protocol;
215
        $this->protocolVersion = $version;
216
217
        return $this;
218
    }
219
220
    /**
221
     * Get the protocol used for the response (e.g. HTTP)
222
     *
223
     * @return string
224
     */
225
    public function getProtocol()
226
    {
227
        return $this->protocol;
228
    }
229
230
    /**
231
     * Get the HTTP protocol version
232
     *
233
     * @return string
234
     */
235
    public function getProtocolVersion()
236
    {
237
        return $this->protocolVersion;
238
    }
239
240
    /**
241
     * Get a cURL transfer information
242
     *
243
     * @param string $key A single statistic to check
244
     *
245
     * @return array|string|null Returns all stats if no key is set, a single stat if a key is set, or null if a key
246
     *                           is set and not found
247
     * @link http://www.php.net/manual/en/function.curl-getinfo.php
248
     */
249
    public function getInfo($key = null)
250
    {
251
        if ($key === null) {
252
            return $this->info;
253
        } elseif (array_key_exists($key, $this->info)) {
254
            return $this->info[$key];
255
        } else {
256
            return null;
257
        }
258
    }
259
260
    /**
261
     * Set the transfer information
262
     *
263
     * @param array $info Array of cURL transfer stats
264
     *
265
     * @return self
266
     */
267
    public function setInfo(array $info)
268
    {
269
        $this->info = $info;
270
271
        return $this;
272
    }
273
274
    /**
275
     * Set the response status
276
     *
277
     * @param int    $statusCode   Response status code to set
278
     * @param string $reasonPhrase Response reason phrase
279
     *
280
     * @return self
281
     * @throws BadResponseException when an invalid response code is received
282
     */
283
    public function setStatus($statusCode, $reasonPhrase = '')
284
    {
285
        $this->statusCode = (int) $statusCode;
0 ignored issues
show
Documentation Bug introduced by
The property $statusCode was declared of type string, but (int) $statusCode is of type integer. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
286
287
        if (!$reasonPhrase && isset(self::$statusTexts[$this->statusCode])) {
288
            $this->reasonPhrase = self::$statusTexts[$this->statusCode];
289
        } else {
290
            $this->reasonPhrase = $reasonPhrase;
291
        }
292
293
        return $this;
294
    }
295
296
    /**
297
     * Get the response status code
298
     *
299
     * @return integer
300
     */
301
    public function getStatusCode()
302
    {
303
        return $this->statusCode;
304
    }
305
306
    /**
307
     * Get the entire response as a string
308
     *
309
     * @return string
310
     */
311
    public function getMessage()
312
    {
313
        $message = $this->getRawHeaders();
314
315
        // Only include the body in the message if the size is < 2MB
316
        $size = $this->body->getSize();
317
        if ($size < 2097152) {
318
            $message .= (string) $this->body;
319
        }
320
321
        return $message;
322
    }
323
324
    /**
325
     * Get the the raw message headers as a string
326
     *
327
     * @return string
328
     */
329
    public function getRawHeaders()
330
    {
331
        $headers = 'HTTP/1.1 ' . $this->statusCode . ' ' . $this->reasonPhrase . "\r\n";
332
        $lines = $this->getHeaderLines();
333
        if (!empty($lines)) {
334
            $headers .= implode("\r\n", $lines) . "\r\n";
335
        }
336
337
        return $headers . "\r\n";
338
    }
339
340
    /**
341
     * Get the response reason phrase- a human readable version of the numeric
342
     * status code
343
     *
344
     * @return string
345
     */
346
    public function getReasonPhrase()
347
    {
348
        return $this->reasonPhrase;
349
    }
350
351
    /**
352
     * Get the Accept-Ranges HTTP header
353
     *
354
     * @return string Returns what partial content range types this server supports.
355
     */
356
    public function getAcceptRanges()
357
    {
358
        return (string) $this->getHeader('Accept-Ranges');
359
    }
360
361
    /**
362
     * Calculate the age of the response
363
     *
364
     * @return integer
365
     */
366
    public function calculateAge()
367
    {
368
        $age = $this->getHeader('Age');
369
370
        if ($age === null && $this->getDate()) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->getDate() of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
371
            $age = time() - strtotime($this->getDate());
372
        }
373
374
        return $age === null ? null : (int) (string) $age;
375
    }
376
377
    /**
378
     * Get the Age HTTP header
379
     *
380
     * @return integer|null Returns the age the object has been in a proxy cache in seconds.
381
     */
382
    public function getAge()
383
    {
384
        return (string) $this->getHeader('Age');
385
    }
386
387
    /**
388
     * Get the Allow HTTP header
389
     *
390
     * @return string|null Returns valid actions for a specified resource. To be used for a 405 Method not allowed.
391
     */
392
    public function getAllow()
393
    {
394
        return (string) $this->getHeader('Allow');
395
    }
396
397
    /**
398
     * Check if an HTTP method is allowed by checking the Allow response header
399
     *
400
     * @param string $method Method to check
401
     *
402
     * @return bool
403
     */
404
    public function isMethodAllowed($method)
405
    {
406
        $allow = $this->getHeader('Allow');
407
        if ($allow) {
408
            foreach (explode(',', $allow) as $allowable) {
409
                if (!strcasecmp(trim($allowable), $method)) {
410
                    return true;
411
                }
412
            }
413
        }
414
415
        return false;
416
    }
417
418
    /**
419
     * Get the Cache-Control HTTP header
420
     *
421
     * @return string
422
     */
423
    public function getCacheControl()
424
    {
425
        return (string) $this->getHeader('Cache-Control');
426
    }
427
428
    /**
429
     * Get the Connection HTTP header
430
     *
431
     * @return string
432
     */
433
    public function getConnection()
434
    {
435
        return (string) $this->getHeader('Connection');
436
    }
437
438
    /**
439
     * Get the Content-Encoding HTTP header
440
     *
441
     * @return string|null
442
     */
443
    public function getContentEncoding()
444
    {
445
        return (string) $this->getHeader('Content-Encoding');
446
    }
447
448
    /**
449
     * Get the Content-Language HTTP header
450
     *
451
     * @return string|null Returns the language the content is in.
452
     */
453
    public function getContentLanguage()
454
    {
455
        return (string) $this->getHeader('Content-Language');
456
    }
457
458
    /**
459
     * Get the Content-Length HTTP header
460
     *
461
     * @return integer Returns the length of the response body in bytes
462
     */
463
    public function getContentLength()
464
    {
465
        return (int) (string) $this->getHeader('Content-Length');
466
    }
467
468
    /**
469
     * Get the Content-Location HTTP header
470
     *
471
     * @return string|null Returns an alternate location for the returned data (e.g /index.htm)
472
     */
473
    public function getContentLocation()
474
    {
475
        return (string) $this->getHeader('Content-Location');
476
    }
477
478
    /**
479
     * Get the Content-Disposition HTTP header
480
     *
481
     * @return string|null Returns the Content-Disposition header
482
     */
483
    public function getContentDisposition()
484
    {
485
        return (string) $this->getHeader('Content-Disposition');
486
    }
487
488
    /**
489
     * Get the Content-MD5 HTTP header
490
     *
491
     * @return string|null Returns a Base64-encoded binary MD5 sum of the content of the response.
492
     */
493
    public function getContentMd5()
494
    {
495
        return (string) $this->getHeader('Content-MD5');
496
    }
497
498
    /**
499
     * Get the Content-Range HTTP header
500
     *
501
     * @return string Returns where in a full body message this partial message belongs (e.g. bytes 21010-47021/47022).
502
     */
503
    public function getContentRange()
504
    {
505
        return (string) $this->getHeader('Content-Range');
506
    }
507
508
    /**
509
     * Get the Content-Type HTTP header
510
     *
511
     * @return string Returns the mime type of this content.
512
     */
513
    public function getContentType()
514
    {
515
        return (string) $this->getHeader('Content-Type');
516
    }
517
518
    /**
519
     * Checks if the Content-Type is of a certain type.  This is useful if the
520
     * Content-Type header contains charset information and you need to know if
521
     * the Content-Type matches a particular type.
522
     *
523
     * @param string $type Content type to check against
524
     *
525
     * @return bool
526
     */
527
    public function isContentType($type)
528
    {
529
        return stripos($this->getHeader('Content-Type'), $type) !== false;
530
    }
531
532
    /**
533
     * Get the Date HTTP header
534
     *
535
     * @return string|null Returns the date and time that the message was sent.
536
     */
537
    public function getDate()
538
    {
539
        return (string) $this->getHeader('Date');
540
    }
541
542
    /**
543
     * Get the ETag HTTP header
544
     *
545
     * @return string|null Returns an identifier for a specific version of a resource, often a Message digest.
546
     */
547
    public function getEtag()
548
    {
549
        return (string) $this->getHeader('ETag');
550
    }
551
552
    /**
553
     * Get the Expires HTTP header
554
     *
555
     * @return string|null Returns the date/time after which the response is considered stale.
556
     */
557
    public function getExpires()
558
    {
559
        return (string) $this->getHeader('Expires');
560
    }
561
562
    /**
563
     * Get the Last-Modified HTTP header
564
     *
565
     * @return string|null Returns the last modified date for the requested object, in RFC 2822 format
566
     *                     (e.g. Tue, 15 Nov 1994 12:45:26 GMT)
567
     */
568
    public function getLastModified()
569
    {
570
        return (string) $this->getHeader('Last-Modified');
571
    }
572
573
    /**
574
     * Get the Location HTTP header
575
     *
576
     * @return string|null Used in redirection, or when a new resource has been created.
577
     */
578
    public function getLocation()
579
    {
580
        return (string) $this->getHeader('Location');
581
    }
582
583
    /**
584
     * Get the Pragma HTTP header
585
     *
586
     * @return Header|null Returns the implementation-specific headers that may have various effects anywhere along
587
     *                     the request-response chain.
588
     */
589
    public function getPragma()
590
    {
591
        return (string) $this->getHeader('Pragma');
592
    }
593
594
    /**
595
     * Get the Proxy-Authenticate HTTP header
596
     *
597
     * @return string|null Authentication to access the proxy (e.g. Basic)
598
     */
599
    public function getProxyAuthenticate()
600
    {
601
        return (string) $this->getHeader('Proxy-Authenticate');
602
    }
603
604
    /**
605
     * Get the Retry-After HTTP header
606
     *
607
     * @return int|null If an entity is temporarily unavailable, this instructs the client to try again after a
608
     *                  specified period of time.
609
     */
610
    public function getRetryAfter()
611
    {
612
        return (string) $this->getHeader('Retry-After');
613
    }
614
615
    /**
616
     * Get the Server HTTP header
617
     *
618
     * @return string|null A name for the server
619
     */
620
    public function getServer()
621
    {
622
        return (string)  $this->getHeader('Server');
623
    }
624
625
    /**
626
     * Get the Set-Cookie HTTP header
627
     *
628
     * @return string|null An HTTP cookie.
629
     */
630
    public function getSetCookie()
631
    {
632
        return (string) $this->getHeader('Set-Cookie');
633
    }
634
635
    /**
636
     * Get the Trailer HTTP header
637
     *
638
     * @return string|null The Trailer general field value indicates that the given set of header fields is present in
639
     *                     the trailer of a message encoded with chunked transfer-coding.
640
     */
641
    public function getTrailer()
642
    {
643
        return (string) $this->getHeader('Trailer');
644
    }
645
646
    /**
647
     * Get the Transfer-Encoding HTTP header
648
     *
649
     * @return string|null The form of encoding used to safely transfer the entity to the user
650
     */
651
    public function getTransferEncoding()
652
    {
653
        return (string) $this->getHeader('Transfer-Encoding');
654
    }
655
656
    /**
657
     * Get the Vary HTTP header
658
     *
659
     * @return string|null Tells downstream proxies how to match future request headers to decide whether the cached
660
     *                     response can be used rather than requesting a fresh one from the origin server.
661
     */
662
    public function getVary()
663
    {
664
        return (string) $this->getHeader('Vary');
665
    }
666
667
    /**
668
     * Get the Via HTTP header
669
     *
670
     * @return string|null Informs the client of proxies through which the response was sent.
671
     */
672
    public function getVia()
673
    {
674
        return (string) $this->getHeader('Via');
675
    }
676
677
    /**
678
     * Get the Warning HTTP header
679
     *
680
     * @return string|null A general warning about possible problems with the entity body
681
     */
682
    public function getWarning()
683
    {
684
        return (string) $this->getHeader('Warning');
685
    }
686
687
    /**
688
     * Get the WWW-Authenticate HTTP header
689
     *
690
     * @return string|null Indicates the authentication scheme that should be used to access the requested entity
691
     */
692
    public function getWwwAuthenticate()
693
    {
694
        return (string) $this->getHeader('WWW-Authenticate');
695
    }
696
697
    /**
698
     * Checks if HTTP Status code is a Client Error (4xx)
699
     *
700
     * @return bool
701
     */
702
    public function isClientError()
703
    {
704
        return $this->statusCode >= 400 && $this->statusCode < 500;
705
    }
706
707
    /**
708
     * Checks if HTTP Status code is Server OR Client Error (4xx or 5xx)
709
     *
710
     * @return boolean
711
     */
712
    public function isError()
713
    {
714
        return $this->isClientError() || $this->isServerError();
715
    }
716
717
    /**
718
     * Checks if HTTP Status code is Information (1xx)
719
     *
720
     * @return bool
721
     */
722
    public function isInformational()
723
    {
724
        return $this->statusCode < 200;
725
    }
726
727
    /**
728
     * Checks if HTTP Status code is a Redirect (3xx)
729
     *
730
     * @return bool
731
     */
732
    public function isRedirect()
733
    {
734
        return $this->statusCode >= 300 && $this->statusCode < 400;
735
    }
736
737
    /**
738
     * Checks if HTTP Status code is Server Error (5xx)
739
     *
740
     * @return bool
741
     */
742
    public function isServerError()
743
    {
744
        return $this->statusCode >= 500 && $this->statusCode < 600;
745
    }
746
747
    /**
748
     * Checks if HTTP Status code is Successful (2xx | 304)
749
     *
750
     * @return bool
751
     */
752
    public function isSuccessful()
753
    {
754
        return ($this->statusCode >= 200 && $this->statusCode < 300) || $this->statusCode == 304;
755
    }
756
757
    /**
758
     * Check if the response can be cached based on the response headers
759
     *
760
     * @return bool Returns TRUE if the response can be cached or false if not
761
     */
762
    public function canCache()
763
    {
764
        // Check if the response is cacheable based on the code
765
        if (!in_array((int) $this->getStatusCode(), self::$cacheResponseCodes)) {
766
            return false;
767
        }
768
769
        // Make sure a valid body was returned and can be cached
770
        if ((!$this->getBody()->isReadable() || !$this->getBody()->isSeekable())
771
            && ($this->getContentLength() > 0 || $this->getTransferEncoding() == 'chunked')) {
772
            return false;
773
        }
774
775
        // Never cache no-store resources (this is a private cache, so private
776
        // can be cached)
777
        if ($this->getHeader('Cache-Control') && $this->getHeader('Cache-Control')->hasDirective('no-store')) {
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class Guzzle\Http\Message\Header as the method hasDirective() does only exist in the following sub-classes of Guzzle\Http\Message\Header: Guzzle\Http\Message\Header\CacheControl. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
778
            return false;
779
        }
780
781
        return $this->isFresh() || $this->getFreshness() === null || $this->canValidate();
782
    }
783
784
    /**
785
     * Gets the number of seconds from the current time in which this response is still considered fresh
786
     *
787
     * @return int|null Returns the number of seconds
788
     */
789
    public function getMaxAge()
790
    {
791
        if ($header = $this->getHeader('Cache-Control')) {
792
            // s-max-age, then max-age, then Expires
793
            if ($age = $header->getDirective('s-maxage')) {
794
                return $age;
795
            }
796
            if ($age = $header->getDirective('max-age')) {
797
                return $age;
798
            }
799
        }
800
801
        if ($this->getHeader('Expires')) {
802
            return strtotime($this->getExpires()) - time();
803
        }
804
805
        return null;
806
    }
807
808
    /**
809
     * Check if the response is considered fresh.
810
     *
811
     * A response is considered fresh when its age is less than or equal to the freshness lifetime (maximum age) of the
812
     * response.
813
     *
814
     * @return bool|null
815
     */
816
    public function isFresh()
817
    {
818
        $fresh = $this->getFreshness();
819
820
        return $fresh === null ? null : $fresh >= 0;
821
    }
822
823
    /**
824
     * Check if the response can be validated against the origin server using a conditional GET request.
825
     *
826
     * @return bool
827
     */
828
    public function canValidate()
829
    {
830
        return $this->getEtag() || $this->getLastModified();
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->getEtag() of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
Bug Best Practice introduced by
The expression $this->getLastModified() of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
831
    }
832
833
    /**
834
     * Get the freshness of the response by returning the difference of the maximum lifetime of the response and the
835
     * age of the response (max-age - age).
836
     *
837
     * Freshness values less than 0 mean that the response is no longer fresh and is ABS(freshness) seconds expired.
838
     * Freshness values of greater than zero is the number of seconds until the response is no longer fresh. A NULL
839
     * result means that no freshness information is available.
840
     *
841
     * @return int
842
     */
843
    public function getFreshness()
844
    {
845
        $maxAge = $this->getMaxAge();
846
        $age = $this->calculateAge();
847
848
        return $maxAge && $age ? ($maxAge - $age) : null;
0 ignored issues
show
Bug Best Practice introduced by
The expression $maxAge of type integer|null is loosely compared to true; this is ambiguous if the integer can be zero. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
849
    }
850
851
    /**
852
     * Parse the JSON response body and return an array
853
     *
854
     * @return array|string|int|bool|float
855
     * @throws RuntimeException if the response body is not in JSON format
856
     */
857
    public function json()
858
    {
859
        $data = json_decode((string) $this->body, true);
860
        if (JSON_ERROR_NONE !== json_last_error()) {
861
            throw new RuntimeException('Unable to parse response body into JSON: ' . json_last_error());
862
        }
863
864
        return $data === null ? array() : $data;
865
    }
866
867
    /**
868
     * Parse the XML response body and return a \SimpleXMLElement.
869
     *
870
     * In order to prevent XXE attacks, this method disables loading external
871
     * entities. If you rely on external entities, then you must parse the
872
     * XML response manually by accessing the response body directly.
873
     *
874
     * @return \SimpleXMLElement
875
     * @throws RuntimeException if the response body is not in XML format
876
     * @link http://websec.io/2012/08/27/Preventing-XXE-in-PHP.html
877
     */
878
    public function xml()
879
    {
880
        $errorMessage = null;
881
        $internalErrors = libxml_use_internal_errors(true);
882
        $disableEntities = libxml_disable_entity_loader(true);
883
        libxml_clear_errors();
884
885
        try {
886
            $xml = new \SimpleXMLElement((string) $this->body ?: '<root />', LIBXML_NONET);
887
            if ($error = libxml_get_last_error()) {
888
                $errorMessage = $error->message;
889
            }
890
        } catch (\Exception $e) {
891
            $errorMessage = $e->getMessage();
892
        }
893
894
        libxml_clear_errors();
895
        libxml_use_internal_errors($internalErrors);
896
        libxml_disable_entity_loader($disableEntities);
897
898
        if ($errorMessage) {
899
            throw new RuntimeException('Unable to parse response body into XML: ' . $errorMessage);
900
        }
901
902
        return $xml;
903
    }
904
905
    /**
906
     * Get the redirect count of this response
907
     *
908
     * @return int
909
     */
910
    public function getRedirectCount()
911
    {
912
        return (int) $this->params->get(RedirectPlugin::REDIRECT_COUNT);
913
    }
914
915
    /**
916
     * Set the effective URL that resulted in this response (e.g. the last redirect URL)
917
     *
918
     * @param string $url The effective URL
919
     *
920
     * @return self
921
     */
922
    public function setEffectiveUrl($url)
923
    {
924
        $this->effectiveUrl = $url;
925
926
        return $this;
927
    }
928
929
    /**
930
     * Get the effective URL that resulted in this response (e.g. the last redirect URL)
931
     *
932
     * @return string
933
     */
934
    public function getEffectiveUrl()
935
    {
936
        return $this->effectiveUrl;
937
    }
938
939
    /**
940
     * @deprecated
941
     * @codeCoverageIgnore
942
     */
943
    public function getPreviousResponse()
944
    {
945
        Version::warn(__METHOD__ . ' is deprecated. Use the HistoryPlugin.');
946
        return null;
947
    }
948
949
    /**
950
     * @deprecated
951
     * @codeCoverageIgnore
952
     */
953
    public function setRequest($request)
0 ignored issues
show
Unused Code introduced by
The parameter $request is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
954
    {
955
        Version::warn(__METHOD__ . ' is deprecated');
956
        return $this;
957
    }
958
959
    /**
960
     * @deprecated
961
     * @codeCoverageIgnore
962
     */
963
    public function getRequest()
964
    {
965
        Version::warn(__METHOD__ . ' is deprecated');
966
        return null;
967
    }
968
}
969