Completed
Push — master ( 9e1c2f...16e6fb )
by Bohuslav
03:00
created

Message::validateHeaderName()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2

Importance

Changes 2
Bugs 1 Features 0
Metric Value
c 2
b 1
f 0
dl 0
loc 6
ccs 4
cts 4
cp 1
rs 9.4285
cc 2
eloc 3
nc 2
nop 1
crap 2
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 HeadersInterface            $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();
0 ignored issues
show
Documentation Bug introduced by
It seems like $headers ? $headers : ne...o\HttpMessage\Headers() can also be of type object<Kambo\HttpMessage\HeadersInterface>. However, the property $headers is declared as type object<Kambo\HttpMessage\Headers>. 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...
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
    // ------------ PRIVATE METHODS
303
304
    /**
305
     * Normalize provided body and ensure that the result object is stream
306
     *
307
     * @param StreamInterface|string|null $body The request body object
308
     *
309
     * @return Stream Provided body
310
     *
311
     * @throws \InvalidArgumentException If an unsupported argument type is provided.
312
     */
313 54
    private function normalizeBody($body = null)
314
    {
315 54
        $body = $body ? $body : new Stream(fopen('php://temp', 'r+'));
316 54
        if (is_string($body)) {
317 1
            $memoryStream = fopen('php://temp', 'r+');
318 1
            fwrite($memoryStream, $body);
319 1
            rewind($memoryStream);
320 1
            $body = new Stream($memoryStream);
321 54
        } elseif (!($body instanceof StreamInterface)) {
322 1
            throw new InvalidArgumentException(
323
                'Body must be a string, null or implement Psr\Http\Message\StreamInterface'
324 1
            );
325
        }
326
327 53
        return $body;
328
    }
329
330
    /**
331
     * Validate name of the header - it must not be array.
332
     *
333
     * @param string $headerName Name of the header.
334
     *
335
     * @throws \InvalidArgumentException When the header name is not valid.
336
     */
337 4
    private function validateHeaderName($headerName)
338
    {
339 4
        if (is_array($headerName)) {
340 2
            throw new InvalidArgumentException('Invalid HTTP header name');
341
        }
342 2
    }
343
344
    /**
345
     * Validate version of the HTTP protocol.
346
     *
347
     * @param string $version Version of the HTTP protocol.
348
     *
349
     * @throws \InvalidArgumentException When the protocol version is not valid.
350
     */
351 2
    private function validateProtocol($version)
352
    {
353
        $valid = [
354 2
            '1.0' => true,
355 2
            '1.1' => true,
356 2
            '2.0' => true,
357 2
        ];
358
359 2
        if (!isset($valid[$version])) {
360 1
            throw new InvalidArgumentException('Invalid HTTP version. Must be one of: 1.0, 1.1, 2.0');
361
        }
362 1
    }
363
}
364