Passed
Push — master ( 62cb71...c30d3b )
by Evgeniy
02:30 queued 52s
created

MessageTrait::getHeaderLine()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2

Importance

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