Completed
Push — master ( 3f5fb7...f6575e )
by Bohuslav
02:50
created

Message   A

Complexity

Total Complexity 22

Size/Duplication

Total Lines 345
Duplicated Lines 5.8 %

Coupling/Cohesion

Components 3
Dependencies 2

Test Coverage

Coverage 100%

Importance

Changes 6
Bugs 4 Features 0
Metric Value
wmc 22
c 6
b 4
f 0
lcom 3
cbo 2
dl 20
loc 345
ccs 68
cts 68
cp 1
rs 10

16 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 9 2
A getProtocolVersion() 0 4 1
A withProtocolVersion() 0 9 1
A getHeaders() 0 4 1
A hasHeader() 0 4 1
A getHeader() 0 4 1
A getHeaderLine() 0 4 1
A withHeader() 10 10 1
A withAddedHeader() 10 10 1
A withoutHeader() 0 8 1
A getBody() 0 4 1
A withBody() 0 7 1
A provideHeaders() 0 4 1
A normalizeBody() 0 16 4
A validateHeaderName() 0 6 2
A validateProtocol() 0 12 2

How to fix   Duplicated Code   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

1
<?php
2
namespace Kambo\HttpMessage;
3
4
// \Spl
5
use InvalidArgumentException;
6
7
// \HttpMessage
8
use Kambo\HttpMessage\Stream;
9
use Kambo\HttpMessage\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\HttpMessage
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                     $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 54
    public function __construct(
64
        $headers = null,
65
        $body = null,
66
        $protocol = '1.1'
67
    ) {
68 54
        $this->headers         = $headers ? $headers : new Headers();
69 54
        $this->body            = $this->normalizeBody($body);
70 53
        $this->protocolVersion = $protocol;
71 53
    }
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 14
    public function hasHeader($name)
148
    {
149 14
        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)
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)
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 Provided body
322
     *
323
     * @throws \InvalidArgumentException If an unsupported argument type is provided.
324
     */
325 54
    private function normalizeBody($body = null)
326
    {
327 54
        $body = $body ? $body : new Stream(fopen('php://temp', 'r+'));
328 54
        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 54
        } 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 53
        return $body;
340
    }
341
342
    /**
343
     * Validate name of the header - it must not be array.
344
     *
345
     * @param string $headerName Name of the header.
346
     *
347
     * @throws \InvalidArgumentException When the header name is not valid.
348
     */
349 4
    private function validateHeaderName($headerName)
350
    {
351 4
        if (is_array($headerName)) {
352 2
            throw new InvalidArgumentException('Invalid HTTP header name');
353
        }
354 2
    }
355
356
    /**
357
     * Validate version of the HTTP protocol.
358
     *
359
     * @param string $version Version of the HTTP protocol.
360
     *
361
     * @throws \InvalidArgumentException When the protocol version is not valid.
362
     */
363 2
    private function validateProtocol($version)
364
    {
365
        $valid = [
366 2
            '1.0' => true,
367 2
            '1.1' => true,
368 2
            '2.0' => true,
369 2
        ];
370
371 2
        if (!isset($valid[$version])) {
372 1
            throw new InvalidArgumentException('Invalid HTTP version. Must be one of: 1.0, 1.1, 2.0');
373
        }
374 1
    }
375
}
376