Message::withAddedHeader()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 10
Code Lines 6

Duplication

Lines 10
Ratio 100 %

Code Coverage

Tests 6
CRAP Score 1

Importance

Changes 0
Metric Value
dl 10
loc 10
ccs 6
cts 6
cp 1
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 6
nc 1
nop 2
crap 1
1
<?php
2
namespace Kambo\Http\Message;
3
4
// \Spl
5
use InvalidArgumentException;
6
7
// \Http\Message
8
use Kambo\Http\Message\Stream;
9
use Kambo\Http\Message\Headers;
10
11
// \Psr
12
use Psr\Http\Message\MessageInterface;
13
use Psr\Http\Message\StreamInterface;
14
15
/**
16
 * HTTP messages consist of requests from a client to a server and responses
17
 * from a server to a client. This interface defines the methods common to
18
 * each.
19
 *
20
 * Messages are considered immutable; all methods that change state retain the 
21
 * internal state of the current message and return an instance that contains 
22
 * the changed state.
23
 *
24
 * @link http://www.ietf.org/rfc/rfc7230.txt
25
 * @link http://www.ietf.org/rfc/rfc7231.txt
26
 *
27
 * @package Kambo\Http\Message
28
 * @author  Bohuslav Simek <[email protected]>
29
 * @license MIT
30
 */
31
class Message implements MessageInterface
32
{
33
    /**
34
     * Protocol version
35
     *
36
     * @var string
37
     */
38
    protected $protocolVersion = '1.1';
39
40
    /**
41
     * Headers
42
     *
43
     * @var Headers
44
     */
45
    protected $headers;
46
47
    /**
48
     * Body data
49
     *
50
     * @var StreamInterface
51
     */
52
    protected $body;
53
54
    /**
55
     * Create a new message
56
     *
57
     * @param Headers|array               $headers  The request headers collection
58
     * @param StreamInterface|string|null $body     The request body object
59
     * @param string                      $protocol The request version of the protocol
60
     *
61
     * @throws \InvalidArgumentException If an unsupported argument type is provided for the body.
62
     */
63 62
    public function __construct(
64
        $headers = [],
65
        $body = null,
66
        $protocol = '1.1'
67
    ) {
68 62
        $this->headers         = $this->normalizeHeaders($headers);
69 61
        $this->body            = $this->normalizeBody($body);
70 60
        $this->protocolVersion = $protocol;
71 60
    }
72
73
    /**
74
     * Retrieves the HTTP protocol version as a string.
75
     *
76
     * The string contain only the HTTP version number (e.g., "1.1", "1.0").
77
     *
78
     * @return string HTTP protocol version.
79
     */
80 3
    public function getProtocolVersion()
81
    {
82 3
        return $this->protocolVersion;
83
    }
84
85
    /**
86
     * Return an instance with the specified HTTP protocol version.
87
     *
88
     * The version string MUST contain only the HTTP version number (e.g.,
89
     * "1.1", "1.0").
90
     *
91
     * This method retains the state of the current instance, and return
92
     * an instance that contains the new protocol version.
93
     *
94
     * @param string $version HTTP protocol version
95
     *
96
     * @return self
97
     */
98 2
    public function withProtocolVersion($version)
99
    {
100 2
        $this->validateProtocol($version);
101
102 1
        $clone                  = clone $this;
103 1
        $clone->protocolVersion = $version;
104
105 1
        return $clone;
106
    }
107
108
    /**
109
     * Retrieves all message header values.
110
     *
111
     * The keys represent the header name as it will be sent over the wire, and
112
     * each value is an array of strings associated with the header.
113
     *
114
     *     // Represent the headers as a string
115
     *     foreach ($message->getHeaders() as $name => $values) {
116
     *         echo $name . ": " . implode(", ", $values);
117
     *     }
118
     *
119
     *     // Emit headers iteratively:
120
     *     foreach ($message->getHeaders() as $name => $values) {
121
     *         foreach ($values as $value) {
122
     *             header(sprintf('%s: %s', $name, $value), false);
123
     *         }
124
     *     }
125
     *
126
     * While header names are not case-sensitive, getHeaders() will preserve the
127
     * exact case in which headers were originally specified.
128
     *
129
     * @return array Returns an associative array of the message's headers. Each
130
     *               key is a header name, and each value is an array of strings
131
     *               for that header.
132
     */
133 4
    public function getHeaders()
134
    {
135 4
        return $this->headers->all();
136
    }
137
138
    /**
139
     * Checks if a header exists by the given case-insensitive name.
140
     *
141
     * @param string $name Case-insensitive header field name.
142
     *
143
     * @return bool Returns true if any header names match the given header name using
144
     *              a case-insensitive string comparison. Returns false if no matching
145
     *              header name is found in the message.
146
     */
147 16
    public function hasHeader($name)
148
    {
149 16
        return $this->headers->exists($name);
150
    }
151
152
    /**
153
     * Retrieves a message header value by the given case-insensitive name.
154
     *
155
     * This method returns an array of all the header values of the given
156
     * case-insensitive header name.
157
     *
158
     * If the header does not appear in the message, this method return an
159
     * empty array.
160
     *
161
     * @param string $name Case-insensitive header field name.
162
     *
163
     * @return string[] An array of string values as provided for the given header.
164
     *                  If the header does not appear in the message, an empty
165
     *                  array is returned.
166
     */
167 6
    public function getHeader($name)
168
    {
169 6
        return $this->headers->get($name);
170
    }
171
172
    /**
173
     * Retrieves a comma-separated string of the values for a single header.
174
     *
175
     * This method returns all of the header values of the given
176
     * case-insensitive header name as a string concatenated together using
177
     * a comma.
178
     *
179
     * NOTE: Not all header values are appropriately represented using comma concatenation.
180
     *       If you want values without concatenation, use getHeader() instead and supply
181
     *       your own delimiter when concatenating.
182
     *
183
     * If the header does not appear in the message, an empty string is returned.
184
     *
185
     * @param string $name Case-insensitive header field name.
186
     *
187
     * @return string A string of values as provided for the given header concatenated
188
     *                together using a comma. If the header does not appear in the message,
189
     *                this method return an empty string.
190
     */
191 1
    public function getHeaderLine($name)
192
    {
193 1
        return $this->headers->getLine($name);
194
    }
195
196
    /**
197
     * Return an instance with the provided value replacing the specified header.
198
     *
199
     * While header names are case-insensitive, the casing of the header will
200
     * be preserved by this function, and returned from getHeaders().
201
     *
202
     * This method retains the state of the current instance, and return
203
     * an instance with new and/or updated header and value.
204
     *     
205
     * @param string          $name  Case-insensitive header field name.
206
     * @param string|string[] $value Header value(s).
207
     *
208
     * @return self
209
     *
210
     * @throws \InvalidArgumentException for invalid header names or values.
211
     */
212 2 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...
213
    {
214 2
        $this->validateHeaderName($name);
215
216 1
        $clone          = clone $this;
217 1
        $clone->headers = clone $this->headers;
218 1
        $clone->headers->set($name, $value);
219
220 1
        return $clone;
221
    }
222
223
    /**
224
     * Return an instance with the specified header appended with the given value.
225
     *
226
     * Existing values for the specified header will be maintained. The new
227
     * value(s) will be appended to the existing list. If the header did not
228
     * exist previously, it will be added.
229
     *
230
     * This method retains the state of the current instance, and return
231
     * an instance with new and/or updated header and value.
232
     *
233
     * @param string          $name  Case-insensitive header field name to add.
234
     * @param string|string[] $value Header value(s).
235
     *
236
     * @return self
237
     *
238
     * @throws \InvalidArgumentException for invalid header names or values.
239
     */
240 2 View Code Duplication
    public function withAddedHeader($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...
241
    {
242 2
        $this->validateHeaderName($name);
243
244 1
        $clone          = clone $this;
245 1
        $clone->headers = clone $this->headers;
246 1
        $clone->headers->add($name, $value);
247
248 1
        return $clone;
249
    }
250
251
    /**
252
     * Return an instance without the specified header.
253
     *
254
     * Header resolution is done without case-sensitivity.
255
     *
256
     * This method retains the state of the current instance, and return
257
     * an instance that removes the named header.
258
     *
259
     * @param string $name Case-insensitive header field name to remove.
260
     *
261
     * @return self
262
     */
263 2
    public function withoutHeader($name)
264
    {
265 2
        $clone          = clone $this;
266 2
        $clone->headers = clone $this->headers;
267 2
        $clone->headers->remove($name);
268
269 2
        return $clone;
270
    }
271
272
    /**
273
     * Gets the body of the message.
274
     *
275
     * @return StreamInterface Returns the body as a stream.
276
     */
277 3
    public function getBody()
278
    {
279 3
        return $this->body;
280
    }
281
282
    /**
283
     * Return an instance with the specified message body.
284
     *
285
     * This method retains the state of the current instance, and return
286
     * an instance that has the new body stream.
287
     *     
288
     * @param StreamInterface $body Body.
289
     *
290
     * @return self
291
     *
292
     * @throws \InvalidArgumentException When the body is not valid.
293
     */
294 1
    public function withBody(StreamInterface $body)
295
    {
296 1
        $clone       = clone $this;
297 1
        $clone->body = $body;
298
299 1
        return $clone;
300
    }
301
302
    // ------------ PROTECTED METHODS
303
304
    /**
305
     * Provide message headers
306
     *
307
     * @return Headers Message headers
308
     */
309 3
    protected function provideHeaders()
310
    {
311 3
        return $this->headers;
312
    }
313
314
    // ------------ PRIVATE METHODS
315
316
    /**
317
     * Normalize provided body and ensure that the result object is Stream.
318
     *
319
     * @param StreamInterface|string|null $body The request body object
320
     *
321
     * @return Stream Normalized body
322
     *
323
     * @throws \InvalidArgumentException If an unsupported argument type is provided.
324
     */
325 61
    private function normalizeBody($body = null)
326
    {
327 61
        $body = $body ? $body : new Stream(fopen('php://temp', 'r+'));
328 61
        if (is_string($body)) {
329 1
            $memoryStream = fopen('php://temp', 'r+');
330 1
            fwrite($memoryStream, $body);
331 1
            rewind($memoryStream);
332 1
            $body = new Stream($memoryStream);
333 61
        } elseif (!($body instanceof StreamInterface)) {
334 1
            throw new InvalidArgumentException(
335
                'Body must be a string, null or implement Psr\Http\Message\StreamInterface'
336 1
            );
337
        }
338
339 60
        return $body;
340
    }
341
342
    /**
343
     * Normalize provided headers and ensure that the result object is Headers.
344
     *
345
     * @param Headers|array $headers The request body object
346
     *
347
     * @return Headers Normalized headers
348
     *
349
     * @throws \InvalidArgumentException If an unsupported argument type is provided.
350
     */
351 62
    private function normalizeHeaders($headers)
352
    {
353 62
        if (is_array($headers)) {
354 56
            $headers = new Headers($headers);
355 62
        } elseif (!($headers instanceof Headers)) {
356 1
            throw new InvalidArgumentException(
357
                'Headers must be an array or instance of Headers'
358 1
            );
359
        }
360
361 61
        return $headers;
362
    }
363
364
    /**
365
     * Validate name of the header - it must not be array.
366
     *
367
     * @param string $headerName Name of the header.
368
     *
369
     * @throws \InvalidArgumentException When the header name is not valid.
370
     */
371 4
    private function validateHeaderName($headerName)
372
    {
373 4
        if (is_array($headerName)) {
374 2
            throw new InvalidArgumentException('Invalid HTTP header name');
375
        }
376 2
    }
377
378
    /**
379
     * Validate version of the HTTP protocol.
380
     *
381
     * @param string $version Version of the HTTP protocol.
382
     *
383
     * @throws \InvalidArgumentException When the protocol version is not valid.
384
     */
385 2
    private function validateProtocol($version)
386
    {
387
        $valid = [
388 2
            '1.0' => true,
389 2
            '1.1' => true,
390 2
            '2.0' => true,
391 2
        ];
392
393 2
        if (!isset($valid[$version])) {
394 1
            throw new InvalidArgumentException('Invalid HTTP version. Must be one of: 1.0, 1.1, 2.0');
395
        }
396 1
    }
397
}
398