Passed
Push — master ( 90372d...e80252 )
by Nikolay
25:24
created

MessageTrait::assertHeader()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 1
dl 0
loc 3
c 0
b 0
f 0
rs 10
cc 1
nc 1
nop 1
1
<?php
2
/**
3
 * @see       https://github.com/zendframework/zend-diactoros for the canonical source repository
4
 * @copyright Copyright (c) 2015-2018 Zend Technologies USA Inc. (http://www.zend.com)
5
 * @license   https://github.com/zendframework/zend-diactoros/blob/master/LICENSE.md New BSD License
6
 */
7
8
declare(strict_types=1);
9
10
namespace Zend\Diactoros;
11
12
use Psr\Http\Message\MessageInterface;
13
use Psr\Http\Message\StreamInterface;
14
15
use function array_map;
16
use function array_merge;
17
use function get_class;
18
use function gettype;
19
use function implode;
20
use function is_array;
21
use function is_object;
22
use function is_resource;
23
use function is_string;
24
use function preg_match;
25
use function sprintf;
26
use function strtolower;
27
28
/**
29
 * Trait implementing the various methods defined in MessageInterface.
30
 *
31
 * @see https://github.com/php-fig/http-message/tree/master/src/MessageInterface.php
32
 */
33
trait MessageTrait
34
{
35
    /**
36
     * List of all registered headers, as key => array of values.
37
     *
38
     * @var array
39
     */
40
    protected $headers = [];
41
42
    /**
43
     * Map of normalized header name to original name used to register header.
44
     *
45
     * @var array
46
     */
47
    protected $headerNames = [];
48
49
    /**
50
     * @var string
51
     */
52
    private $protocol = '1.1';
53
54
    /**
55
     * @var StreamInterface
56
     */
57
    private $stream;
58
59
    /**
60
     * Retrieves the HTTP protocol version as a string.
61
     *
62
     * The string MUST contain only the HTTP version number (e.g., "1.1", "1.0").
63
     *
64
     * @return string HTTP protocol version.
65
     */
66
    public function getProtocolVersion() : string
67
    {
68
        return $this->protocol;
69
    }
70
71
    /**
72
     * Return an instance with the specified HTTP protocol version.
73
     *
74
     * The version string MUST contain only the HTTP version number (e.g.,
75
     * "1.1", "1.0").
76
     *
77
     * This method MUST be implemented in such a way as to retain the
78
     * immutability of the message, and MUST return an instance that has the
79
     * new protocol version.
80
     *
81
     * @param string $version HTTP protocol version
82
     * @return static
83
     */
84
    public function withProtocolVersion($version) : MessageInterface
85
    {
86
        $this->validateProtocolVersion($version);
87
        $new = clone $this;
88
        $new->protocol = $version;
89
        return $new;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $new returns the type Zend\Diactoros\MessageTrait which is incompatible with the type-hinted return Psr\Http\Message\MessageInterface.
Loading history...
90
    }
91
92
    /**
93
     * Retrieves all message headers.
94
     *
95
     * The keys represent the header name as it will be sent over the wire, and
96
     * each value is an array of strings associated with the header.
97
     *
98
     *     // Represent the headers as a string
99
     *     foreach ($message->getHeaders() as $name => $values) {
100
     *         echo $name . ": " . implode(", ", $values);
101
     *     }
102
     *
103
     *     // Emit headers iteratively:
104
     *     foreach ($message->getHeaders() as $name => $values) {
105
     *         foreach ($values as $value) {
106
     *             header(sprintf('%s: %s', $name, $value), false);
107
     *         }
108
     *     }
109
     *
110
     * @return array Returns an associative array of the message's headers. Each
111
     *     key MUST be a header name, and each value MUST be an array of strings.
112
     */
113
    public function getHeaders() : array
114
    {
115
        return $this->headers;
116
    }
117
118
    /**
119
     * Checks if a header exists by the given case-insensitive name.
120
     *
121
     * @param string $header Case-insensitive header name.
122
     * @return bool Returns true if any header names match the given header
123
     *     name using a case-insensitive string comparison. Returns false if
124
     *     no matching header name is found in the message.
125
     */
126
    public function hasHeader($header) : bool
127
    {
128
        return isset($this->headerNames[strtolower($header)]);
129
    }
130
131
    /**
132
     * Retrieves a message header value by the given case-insensitive name.
133
     *
134
     * This method returns an array of all the header values of the given
135
     * case-insensitive header name.
136
     *
137
     * If the header does not appear in the message, this method MUST return an
138
     * empty array.
139
     *
140
     * @param string $header Case-insensitive header field name.
141
     * @return string[] An array of string values as provided for the given
142
     *    header. If the header does not appear in the message, this method MUST
143
     *    return an empty array.
144
     */
145
    public function getHeader($header) : array
146
    {
147
        if (! $this->hasHeader($header)) {
148
            return [];
149
        }
150
151
        $header = $this->headerNames[strtolower($header)];
152
153
        return $this->headers[$header];
154
    }
155
156
    /**
157
     * Retrieves a comma-separated string of the values for a single header.
158
     *
159
     * This method returns all of the header values of the given
160
     * case-insensitive header name as a string concatenated together using
161
     * a comma.
162
     *
163
     * NOTE: Not all header values may be appropriately represented using
164
     * comma concatenation. For such headers, use getHeader() instead
165
     * and supply your own delimiter when concatenating.
166
     *
167
     * If the header does not appear in the message, this method MUST return
168
     * an empty string.
169
     *
170
     * @param string $name Case-insensitive header field name.
171
     * @return string A string of values as provided for the given header
172
     *    concatenated together using a comma. If the header does not appear in
173
     *    the message, this method MUST return an empty string.
174
     */
175
    public function getHeaderLine($name) : string
176
    {
177
        $value = $this->getHeader($name);
178
        if (empty($value)) {
179
            return '';
180
        }
181
182
        return implode(',', $value);
183
    }
184
185
    /**
186
     * Return an instance with the provided header, replacing any existing
187
     * values of any headers with the same case-insensitive name.
188
     *
189
     * While header names are case-insensitive, the casing of the header will
190
     * be preserved by this function, and returned from getHeaders().
191
     *
192
     * This method MUST be implemented in such a way as to retain the
193
     * immutability of the message, and MUST return an instance that has the
194
     * new and/or updated header and value.
195
     *
196
     * @param string $header Case-insensitive header field name.
197
     * @param string|string[] $value Header value(s).
198
     * @return static
199
     * @throws Exception\InvalidArgumentException for invalid header names or values.
200
     */
201
    public function withHeader($header, $value) : MessageInterface
202
    {
203
        $this->assertHeader($header);
204
205
        $normalized = strtolower($header);
206
207
        $new = clone $this;
208
        if ($new->hasHeader($header)) {
209
            unset($new->headers[$new->headerNames[$normalized]]);
210
        }
211
212
        $value = $this->filterHeaderValue($value);
213
214
        $new->headerNames[$normalized] = $header;
215
        $new->headers[$header]         = $value;
216
217
        return $new;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $new returns the type Zend\Diactoros\MessageTrait which is incompatible with the type-hinted return Psr\Http\Message\MessageInterface.
Loading history...
218
    }
219
220
    /**
221
     * Return an instance with the specified header appended with the
222
     * given value.
223
     *
224
     * Existing values for the specified header will be maintained. The new
225
     * value(s) will be appended to the existing list. If the header did not
226
     * exist previously, it will be added.
227
     *
228
     * This method MUST be implemented in such a way as to retain the
229
     * immutability of the message, and MUST return an instance that has the
230
     * new header and/or value.
231
     *
232
     * @param string $header Case-insensitive header field name to add.
233
     * @param string|string[] $value Header value(s).
234
     * @return static
235
     * @throws Exception\InvalidArgumentException for invalid header names or values.
236
     */
237
    public function withAddedHeader($header, $value) : MessageInterface
238
    {
239
        $this->assertHeader($header);
240
241
        if (! $this->hasHeader($header)) {
242
            return $this->withHeader($header, $value);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->withHeader($header, $value) returns the type Psr\Http\Message\MessageInterface which is incompatible with the documented return type Zend\Diactoros\MessageTrait.
Loading history...
243
        }
244
245
        $header = $this->headerNames[strtolower($header)];
246
247
        $new = clone $this;
248
        $value = $this->filterHeaderValue($value);
249
        $new->headers[$header] = array_merge($this->headers[$header], $value);
250
        return $new;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $new returns the type Zend\Diactoros\MessageTrait which is incompatible with the type-hinted return Psr\Http\Message\MessageInterface.
Loading history...
251
    }
252
253
    /**
254
     * Return an instance without the specified header.
255
     *
256
     * Header resolution MUST be done without case-sensitivity.
257
     *
258
     * This method MUST be implemented in such a way as to retain the
259
     * immutability of the message, and MUST return an instance that removes
260
     * the named header.
261
     *
262
     * @param string $header Case-insensitive header field name to remove.
263
     * @return static
264
     */
265
    public function withoutHeader($header) : MessageInterface
266
    {
267
        if (! $this->hasHeader($header)) {
268
            return clone $this;
0 ignored issues
show
Bug Best Practice introduced by
The expression return clone $this returns the type Zend\Diactoros\MessageTrait which is incompatible with the type-hinted return Psr\Http\Message\MessageInterface.
Loading history...
269
        }
270
271
        $normalized = strtolower($header);
272
        $original   = $this->headerNames[$normalized];
273
274
        $new = clone $this;
275
        unset($new->headers[$original], $new->headerNames[$normalized]);
276
        return $new;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $new returns the type Zend\Diactoros\MessageTrait which is incompatible with the type-hinted return Psr\Http\Message\MessageInterface.
Loading history...
277
    }
278
279
    /**
280
     * Gets the body of the message.
281
     *
282
     * @return StreamInterface Returns the body as a stream.
283
     */
284
    public function getBody() : StreamInterface
285
    {
286
        return $this->stream;
287
    }
288
289
    /**
290
     * Return an instance with the specified message body.
291
     *
292
     * The body MUST be a StreamInterface object.
293
     *
294
     * This method MUST be implemented in such a way as to retain the
295
     * immutability of the message, and MUST return a new instance that has the
296
     * new body stream.
297
     *
298
     * @param StreamInterface $body Body.
299
     * @return static
300
     * @throws Exception\InvalidArgumentException When the body is not valid.
301
     */
302
    public function withBody(StreamInterface $body) : MessageInterface
303
    {
304
        $new = clone $this;
305
        $new->stream = $body;
306
        return $new;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $new returns the type Zend\Diactoros\MessageTrait which is incompatible with the type-hinted return Psr\Http\Message\MessageInterface.
Loading history...
307
    }
308
309
    private function getStream($stream, string $modeIfNotInstance) : StreamInterface
310
    {
311
        if ($stream instanceof StreamInterface) {
312
            return $stream;
313
        }
314
315
        if (! is_string($stream) && ! is_resource($stream)) {
316
            throw new Exception\InvalidArgumentException(
317
                'Stream must be a string stream resource identifier, '
318
                . 'an actual stream resource, '
319
                . 'or a Psr\Http\Message\StreamInterface implementation'
320
            );
321
        }
322
323
        return new Stream($stream, $modeIfNotInstance);
324
    }
325
326
    /**
327
     * Filter a set of headers to ensure they are in the correct internal format.
328
     *
329
     * Used by message constructors to allow setting all initial headers at once.
330
     *
331
     * @param array $originalHeaders Headers to filter.
332
     */
333
    private function setHeaders(array $originalHeaders) : void
334
    {
335
        $headerNames = $headers = [];
336
337
        foreach ($originalHeaders as $header => $value) {
338
            $value = $this->filterHeaderValue($value);
339
340
            $this->assertHeader($header);
341
342
            $headerNames[strtolower($header)] = $header;
343
            $headers[$header] = $value;
344
        }
345
346
        $this->headerNames = $headerNames;
347
        $this->headers = $headers;
348
    }
349
350
    /**
351
     * Validate the HTTP protocol version
352
     *
353
     * @param string $version
354
     * @throws Exception\InvalidArgumentException on invalid HTTP protocol version
355
     */
356
    private function validateProtocolVersion($version) : void
357
    {
358
        if (empty($version)) {
359
            throw new Exception\InvalidArgumentException(
360
                'HTTP protocol version can not be empty'
361
            );
362
        }
363
        if (! is_string($version)) {
0 ignored issues
show
introduced by
The condition is_string($version) is always true.
Loading history...
364
            throw new Exception\InvalidArgumentException(sprintf(
365
                'Unsupported HTTP protocol version; must be a string, received %s',
366
                (is_object($version) ? get_class($version) : gettype($version))
367
            ));
368
        }
369
370
        // HTTP/1 uses a "<major>.<minor>" numbering scheme to indicate
371
        // versions of the protocol, while HTTP/2 does not.
372
        if (! preg_match('#^(1\.[01]|2)$#', $version)) {
373
            throw new Exception\InvalidArgumentException(sprintf(
374
                'Unsupported HTTP protocol version "%s" provided',
375
                $version
376
            ));
377
        }
378
    }
379
380
    /**
381
     * @param mixed $values
382
     * @return string[]
383
     */
384
    private function filterHeaderValue($values) : array
385
    {
386
        if (! is_array($values)) {
387
            $values = [$values];
388
        }
389
390
        if ([] === $values) {
391
            throw new Exception\InvalidArgumentException(
392
                'Invalid header value: must be a string or array of strings; '
393
                . 'cannot be an empty array'
394
            );
395
        }
396
397
        return array_map(function ($value) {
398
            HeaderSecurity::assertValid($value);
399
400
            return (string) $value;
401
        }, array_values($values));
402
    }
403
404
    /**
405
     * Ensure header name and values are valid.
406
     *
407
     * @param string $name
408
     *
409
     * @throws Exception\InvalidArgumentException
410
     */
411
    private function assertHeader($name) : void
412
    {
413
        HeaderSecurity::assertValidName($name);
414
    }
415
}
416