Completed
Push — master ( 74ac7c...809da3 )
by Damien
02:39
created

Response::__construct()   B

Complexity

Conditions 5
Paths 12

Size

Total Lines 20
Code Lines 10

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 20
rs 8.8571
cc 5
eloc 10
nc 12
nop 4
1
<?php
2
/**
3
 * Veto.
4
 * PHP Microframework.
5
 *
6
 * @author Damien Walsh <[email protected]>
7
 * @copyright Damien Walsh 2013-2014
8
 * @version 0.1
9
 * @package veto
10
 */
11
namespace Veto\Http;
12
13
use Psr\Http\Message\ResponseInterface;
14
use Psr\Http\Message\StreamInterface;
15
use Veto\Collection\Bag;
16
17
/**
18
 * Response
19
 *
20
 * @since 0.1
21
 */
22
class Response implements ResponseInterface
23
{
24
    /**
25
     * The response HTTP protocol version
26
     *
27
     * @var string
28
     */
29
    protected $protocolVersion = '1.1';
30
31
    /**
32
     * The response HTTP status code
33
     *
34
     * @var int
35
     */
36
    protected $status = 200;
37
38
    /**
39
     * The response reason phrase
40
     *
41
     * @var string
42
     */
43
    protected $reasonPhrase;
44
45
    /**
46
     * The response HTTP headers
47
     *
48
     * @var HeaderBag
49
     */
50
    protected $headers;
51
52
    /**
53
     * The response cookies
54
     *
55
     * @var Bag
56
     */
57
    protected $cookies;
58
59
    /**
60
     * The response body
61
     *
62
     * @var StreamInterface
63
     */
64
    protected $body;
65
66
    /**
67
     * Create new HTTP response
68
     *
69
     * @param StreamInterface|string|null $body The response body
70
     * @param int $status The response HTTP status code
71
     * @param Bag|null $headers The response HTTP headers
72
     * @param Bag|null $cookies The response cookies
73
     */
74
    public function __construct($body = null, $status = 200, Bag $headers = null, Bag $cookies = null)
75
    {
76
        // If we're passed a StreamInterface, use it as the response body
77
        if ($body instanceof StreamInterface) {
78
            $this->body = $body;
79
        } else {
80
81
            // Otherwise, create one
82
            $this->body = new MessageBody(fopen('php://temp', 'r+'));
83
84
            // Optionally, writing the provided string to it
85
            if (is_string($body)) {
86
                $this->body->write($body);
87
            }
88
        }
89
90
        $this->status = $status;
91
        $this->headers = $headers ? $headers : new HeaderBag();
0 ignored issues
show
Documentation Bug introduced by
$headers ? $headers : new \Veto\Http\HeaderBag() is of type object<Veto\Collection\Bag>, but the property $headers was declared to be of type object<Veto\Http\HeaderBag>. Are you sure that you always receive this specific sub-class here, or does it make sense to add an instanceof 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 given class or a super-class is assigned to a property that is type hinted more strictly.

Either this assignment is in error or an instanceof check should be added for that assignment.

class Alien {}

class Dalek extends Alien {}

class Plot
{
    /** @var  Dalek */
    public $villain;
}

$alien = new Alien();
$plot = new Plot();
if ($alien instanceof Dalek) {
    $plot->villain = $alien;
}
Loading history...
92
        $this->cookies = $cookies ? $cookies : new Bag();
93
    }
94
95
    /**
96
     * Deep-copy any associated objects when cloning.
97
     */
98
    public function __clone()
99
    {
100
        $this->headers = clone $this->headers;
101
        $this->cookies = clone $this->cookies;
102
        $this->body = clone $this->body;
103
    }
104
105
    /**
106
     * Send both headers and content.
107
     */
108
    public function send()
109
    {
110
        $this->sendHeaders();
111
        $this->sendBody();
112
    }
113
114
    /**
115
     * Send the HTTP headers for this response.
116
     */
117
    public function sendHeaders()
118
    {
119
        if (false === headers_sent()) {
120
121
            // Send the initial line compliant with the current SAPI
122
            if (strpos(PHP_SAPI, 'cgi') === 0) {
123
                header(sprintf(
124
                    'Status: %s %s',
125
                    $this->getStatusCode(),
126
                    $this->getReasonPhrase()
127
                ));
128
            } else {
129
                header(sprintf(
130
                    'HTTP/%s %s %s',
131
                    $this->getProtocolVersion(),
132
                    $this->getStatusCode(),
133
                    $this->getReasonPhrase()
134
                ));
135
            }
136
137
            // Send each header
138
            foreach ($this->getHeaders() as $name => $values) {
139
                foreach ($values as $value) {
140
                    header(sprintf('%s: %s', $name, $value), false);
141
                }
142
            }
143
        }
144
145
        return $this;
146
    }
147
148
    /**
149
     * Send the body for this response.
150
     *
151
     * @param int $bufferSize The size of the chunks to read from the body into the output stream
152
     * @return $this
153
     */
154
    public function sendBody($bufferSize = 1024)
155
    {
156
        $body = $this->getBody();
157
158
        if ($body instanceof MessageBody) {
159
            if ($body->isAttached()) {
160
                $body->rewind();
161
                while (false === $body->eof()) {
162
                    print $body->read($bufferSize);
163
                }
164
            }
165
        }
166
167
        return $this;
168
    }
169
170
    /**
171
     * Retrieves the HTTP protocol version as a string.
172
     *
173
     * The string MUST contain only the HTTP version number (e.g., "1.1", "1.0").
174
     *
175
     * @return string HTTP protocol version.
176
     */
177
    public function getProtocolVersion()
178
    {
179
        return $this->protocolVersion;
180
    }
181
182
    /**
183
     * Create a new instance with the specified HTTP protocol version.
184
     *
185
     * The version string MUST contain only the HTTP version number (e.g.,
186
     * "1.1", "1.0").
187
     *
188
     * This method MUST be implemented in such a way as to retain the
189
     * immutability of the message, and MUST return a new instance that has the
190
     * new protocol version.
191
     *
192
     * @param string $version HTTP protocol version
193
     * @return self
194
     */
195
    public function withProtocolVersion($version)
196
    {
197
        $clone = clone $this;
198
        $clone->protocolVersion = $version;
199
200
        return $clone;
201
    }
202
203
    /**
204
     * Retrieves all message headers.
205
     *
206
     * The keys represent the header name as it will be sent over the wire, and
207
     * each value is an array of strings associated with the header.
208
     *
209
     *     // Represent the headers as a string
210
     *     foreach ($message->getHeaders() as $name => $values) {
211
     *         echo $name . ": " . implode(", ", $values);
212
     *     }
213
     *
214
     *     // Emit headers iteratively:
215
     *     foreach ($message->getHeaders() as $name => $values) {
216
     *         foreach ($values as $value) {
217
     *             header(sprintf('%s: %s', $name, $value), false);
218
     *         }
219
     *     }
220
     *
221
     * While header names are not case-sensitive, getHeaders() will preserve the
222
     * exact case in which headers were originally specified.
223
     *
224
     * @return array Returns an associative array of the message's headers. Each
225
     *     key MUST be a header name, and each value MUST be an array of strings.
226
     */
227
    public function getHeaders()
228
    {
229
        return $this->headers;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this->headers; (Veto\Http\HeaderBag) is incompatible with the return type declared by the interface Psr\Http\Message\MessageInterface::getHeaders of type array.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

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

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
230
    }
231
232
    /**
233
     * Checks if a header exists by the given case-insensitive name.
234
     *
235
     * @param string $name Case-insensitive header field name.
236
     * @return bool Returns true if any header names match the given header
237
     *     name using a case-insensitive string comparison. Returns false if
238
     *     no matching header name is found in the message.
239
     */
240
    public function hasHeader($name)
241
    {
242
        return $this->headers->has($name);
243
    }
244
245
    /**
246
     * Retrieves a message header value by the given case-insensitive name.
247
     *
248
     * This method returns an array of all the header values of the given
249
     * case-insensitive header name.
250
     *
251
     * If the header does not appear in the message, this method MUST return an
252
     * empty array.
253
     *
254
     * @param string $name Case-insensitive header field name.
255
     * @return string[] An array of string values as provided for the given
256
     *    header. If the header does not appear in the message, this method MUST
257
     *    return an empty array.
258
     */
259
    public function getHeader($name)
260
    {
261
        return $this->headers->get($name);
262
    }
263
264
    /**
265
     * Retrieves the line for a single header, with the header values as a
266
     * comma-separated string.
267
     *
268
     * This method returns all of the header values of the given
269
     * case-insensitive header name as a string concatenated together using
270
     * a comma.
271
     *
272
     * NOTE: Not all header values may be appropriately represented using
273
     * comma concatenation. For such headers, use getHeader() instead
274
     * and supply your own delimiter when concatenating.
275
     *
276
     * If the header does not appear in the message, this method MUST return
277
     * a null value.
278
     *
279
     * @param string $name Case-insensitive header field name.
280
     * @return string|null A string of values as provided for the given header
281
     *    concatenated together using a comma. If the header does not appear in
282
     *    the message, this method MUST return a null value.
283
     */
284
    public function getHeaderLine($name)
285
    {
286
        return implode(',', $this->headers->get($name));
287
    }
288
289
    /**
290
     * Create a new instance with the provided header, replacing any existing
291
     * values of any headers with the same case-insensitive name.
292
     *
293
     * While header names are case-insensitive, the casing of the header will
294
     * be preserved by this function, and returned from getHeaders().
295
     *
296
     * This method MUST be implemented in such a way as to retain the
297
     * immutability of the message, and MUST return a new instance that has the
298
     * new and/or updated header and value.
299
     *
300
     * @param string $name Case-insensitive header field name.
301
     * @param string|string[] $value Header value(s).
302
     * @return self
303
     * @throws \InvalidArgumentException for invalid header names or values.
304
     */
305 View Code Duplication
    public function withHeader($name, $value)
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...
306
    {
307
        $clone = clone $this;
308
        $clone->headers = new HeaderBag();
309
        $clone->headers->add($name, $value);
310
311
        return $clone;
312
    }
313
314
    /**
315
     * Creates a new instance, with the specified header appended with the
316
     * given value.
317
     *
318
     * Existing values for the specified header will be maintained. The new
319
     * value(s) will be appended to the existing list. If the header did not
320
     * exist previously, it will be added.
321
     *
322
     * This method MUST be implemented in such a way as to retain the
323
     * immutability of the message, and MUST return a new instance that has the
324
     * new header and/or value.
325
     *
326
     * @param string $name Case-insensitive header field name to add.
327
     * @param string|string[] $value Header value(s).
328
     * @return self
329
     * @throws \InvalidArgumentException for invalid header names or values.
330
     */
331
    public function withAddedHeader($name, $value)
332
    {
333
        $clone = clone $this;
334
        $clone->headers->add($name, $value);
335
336
        return $clone;
337
    }
338
339
    /**
340
     * Creates a new instance, without the specified header.
341
     *
342
     * Header resolution MUST be done without case-sensitivity.
343
     *
344
     * This method MUST be implemented in such a way as to retain the
345
     * immutability of the message, and MUST return a new instance that removes
346
     * the named header.
347
     *
348
     * @param string $name Case-insensitive header field name to remove.
349
     * @return self
350
     */
351
    public function withoutHeader($name)
352
    {
353
        $clone = clone $this;
354
        $clone->headers = new HeaderBag();
355
        $clone->headers->remove($name);
356
357
        return $clone;
358
    }
359
360
    /**
361
     * Gets the body of the message.
362
     *
363
     * @return StreamInterface Returns the body as a stream.
364
     */
365
    public function getBody()
366
    {
367
        return $this->body;
368
    }
369
370
    /**
371
     * Create a new instance, with the specified message body.
372
     *
373
     * The body MUST be a StreamInterface object.
374
     *
375
     * This method MUST be implemented in such a way as to retain the
376
     * immutability of the message, and MUST return a new instance that has the
377
     * new body stream.
378
     *
379
     * @param StreamInterface $body Body.
380
     * @return self
381
     * @throws \InvalidArgumentException When the body is not valid.
382
     */
383
    public function withBody(StreamInterface $body)
384
    {
385
        $clone = clone $this;
386
        $clone->body = $body;
387
388
        return $clone;
389
    }
390
391
    /**
392
     * Gets the response Status-Code.
393
     *
394
     * The Status-Code is a 3-digit integer result code of the server's attempt
395
     * to understand and satisfy the request.
396
     *
397
     * @return integer Status code.
398
     */
399
    public function getStatusCode()
400
    {
401
        return $this->status;
402
    }
403
404
    /**
405
     * Create a new instance with the specified status code, and optionally
406
     * reason phrase, for the response.
407
     *
408
     * If no Reason-Phrase is specified, implementations MAY choose to default
409
     * to the RFC 7231 or IANA recommended reason phrase for the response's
410
     * Status-Code.
411
     *
412
     * This method MUST be implemented in such a way as to retain the
413
     * immutability of the message, and MUST return a new instance that has the
414
     * updated status and reason phrase.
415
     *
416
     * @link http://tools.ietf.org/html/rfc7231#section-6
417
     * @link http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml
418
     * @param integer $code The 3-digit integer result code to set.
419
     * @param null|string $reasonPhrase The reason phrase to use with the
420
     *     provided status code; if none is provided, implementations MAY
421
     *     use the defaults as suggested in the HTTP specification.
422
     * @return self
423
     * @throws \InvalidArgumentException For invalid status code arguments.
424
     */
425
    public function withStatus($code, $reasonPhrase = null)
426
    {
427
        $clone = clone $this;
428
        $clone->status = $code;
429
        $clone->reasonPhrase = $reasonPhrase;
430
431
        return $clone;
432
    }
433
434
    /**
435
     * Gets the response Reason-Phrase, a short textual description of the Status-Code.
436
     *
437
     * Because a Reason-Phrase is not a required element in a response
438
     * Status-Line, the Reason-Phrase value MAY be null. Implementations MAY
439
     * choose to return the default RFC 7231 recommended reason phrase (or those
440
     * listed in the IANA HTTP Status Code Registry) for the response's
441
     * Status-Code.
442
     *
443
     * @link http://tools.ietf.org/html/rfc7231#section-6
444
     * @link http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml
445
     * @return string|null Reason phrase, or null if unknown.
446
     */
447
    public function getReasonPhrase()
448
    {
449
        return $this->reasonPhrase;
450
    }
451
}
452