Passed
Push — master ( f6ef55...62cb71 )
by Evgeniy
14:37
created

MessageTrait::validateHeaderValue()   B

Complexity

Conditions 10
Paths 4

Size

Total Lines 15
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 10

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 10
eloc 10
c 1
b 0
f 0
nc 4
nop 1
dl 0
loc 15
ccs 9
cts 9
cp 1
crap 10
rs 7.6666

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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 7
64
    /**
65 7
     * 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
    public function getProtocolVersion(): string
72
    {
73
        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 11
     * 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 11
     * new protocol version.
85 1
     *
86
     * @param string $version HTTP protocol version
87
     * @return static
88 10
     * @throws InvalidArgumentException for invalid HTTP protocol version.
89 1
     */
90 1
    public function withProtocolVersion($version): MessageInterface
91 1
    {
92
        if ($version === $this->protocol) {
93
            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
        $this->validateProtocolVersion($version);
97
        $new = clone $this;
98
        $new->protocol = $version;
99
        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 10
     *
120
     * While header names are not case-sensitive, getHeaders() will preserve the
121 10
     * 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
    public function getHeaders(): array
128
    {
129
        return $this->headers;
130
    }
131
132
    /**
133 82
     * Checks if a header exists by the given case-insensitive name.
134
     *
135 82
     * @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
    public function hasHeader($name): bool
142
    {
143
        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 6
     * If the header does not appear in the message, this method MUST return an
153
     * empty array.
154 6
     *
155 2
     * @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 4
     *    return an empty array.
159
     */
160
    public function getHeader($name): array
161
    {
162
        if (!$this->hasHeader($name)) {
163
            return [];
164
        }
165
166
        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 2
     * If the header does not appear in the message, this method MUST return
181
     * an empty string.
182 2
     *
183 1
     * @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 1
     *    the message, this method MUST return an empty string.
187
     */
188
    public function getHeaderLine($name): string
189
    {
190
        if (!$value = $this->getHeader($name)) {
191
            return '';
192
        }
193
194
        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 26
     * new and/or updated header and value.
206
     *
207 26
     * @param string $name Case-insensitive header field name.
208 15
     * @param string|string[] $value Header value(s).
209 15
     * @return static
210
     * @throws InvalidArgumentException for invalid header names or values.
211 15
     * @psalm-suppress MixedPropertyTypeCoercion
212
     */
213
    public function withHeader($name, $value): MessageInterface
214
    {
215 15
        $normalized = $this->normalizeHeaderName($name);
216 15
        $value = $this->normalizeHeaderValue($value);
217
        $new = clone $this;
218 6
219 6
        if (isset($new->headerNames[$normalized])) {
220 6
            unset($new->headers[$new->headerNames[$normalized]]);
221
        }
222
223
        $new->headerNames[$normalized] = $name;
224
        $new->headers[$name] = $value;
225
        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 1
     * @param string|string[] $value Header value(s).
241
     * @return static
242 1
     * @throws InvalidArgumentException for invalid header names or values.
243
     * @psalm-suppress MixedPropertyTypeCoercion
244 1
     */
245 1
    public function withAddedHeader($name, $value): MessageInterface
246
    {
247
        if (!$this->hasHeader($name)) {
248 1
            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 1
        }
250 1
251
        $header = $this->headerNames[$this->normalizeHeaderName($name)];
252 1
        $value = $this->normalizeHeaderValue($value);
253 1
254 1
        $new = clone $this;
255
        $new->headers[$header] = array_merge($this->headers[$header], $value);
256
        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 1
     * @return static
270
     */
271 1
    public function withoutHeader($name): MessageInterface
272
    {
273
        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 1
        }
276 1
277 1
        $normalized = $this->normalizeHeaderName($name);
278 1
        $new = clone $this;
279
        unset($new->headers[$this->headerNames[$normalized]], $new->headerNames[$normalized]);
280
        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 8
     * @return StreamInterface Returns the body as a stream.
287
     */
288 8
    public function getBody(): StreamInterface
289
    {
290
        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 1
     * @throws InvalidArgumentException When the body is not valid.
305
     */
306 1
    public function withBody(StreamInterface $body): MessageInterface
307 1
    {
308 1
        $new = clone $this;
309
        $new->stream = $body;
310
        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 143
     * @psalm-suppress RedundantConditionGivenDocblockType
317
     */
318 143
    private function registerStream($stream, string $mode = 'wb+'): void
319 1
    {
320 1
        if ($stream instanceof StreamInterface) {
321
            $this->stream = $stream;
322
            return;
323 143
        }
324 143
325 143
        if (is_string($stream) || is_resource($stream)) {
326
            $this->stream = new Stream($stream, $mode);
327
            return;
328 7
        }
329
330
        throw new InvalidArgumentException(sprintf(
331 7
            'Stream must be a `Psr\Http\Message\StreamInterface` implementation'
332
            . ' or a string stream resource identifier or an actual stream resource; received `%s`.',
333
            (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 143
     * @psalm-suppress MixedPropertyTypeCoercion
341
     */
342 143
    private function registerHeaders(array $originalHeaders = []): void
343 143
    {
344
        $this->headers = [];
345 143
        $this->headerNames = [];
346 1
347 1
        foreach ($originalHeaders as $name => $value) {
348 1
            $this->headerNames[$this->normalizeHeaderName($name)] = $name;
349 1
            $this->headers[$name] = $this->normalizeHeaderValue($value);
350 1
        }
351
    }
352 143
353
    /**
354
     * @param string $protocol
355
     * @throws InvalidArgumentException for invalid HTTP protocol version.
356
     */
357
    private function registerProtocolVersion(string $protocol): void
358 143
    {
359
        if (!empty($protocol) && $protocol !== $this->protocol) {
360 143
            $this->validateProtocolVersion($protocol);
361 1
            $this->protocol = $protocol;
362 1
        }
363
    }
364 143
365
    /**
366
     * @param mixed $name
367
     * @return string
368
     * @throws InvalidArgumentException for invalid header name.
369
     */
370 83
    private function normalizeHeaderName($name): string
371
    {
372 83
        if (!is_string($name) || !preg_match('/^[a-zA-Z0-9\'`#$%&*+.^_|~!-]+$/', $name)) {
373
            throw new InvalidArgumentException(sprintf(
374
                '`%s` is not valid header name.',
375
                (is_object($name) ? get_class($name) : (is_string($name) ? $name : gettype($name)))
376
            ));
377
        }
378
379 16
        return strtolower($name);
380
    }
381 16
382
    /**
383
     * @param mixed $value
384
     * @return array
385
     * @throws InvalidArgumentException for invalid header name.
386
     */
387
    private function normalizeHeaderValue($value): array
388 27
    {
389
        $value = is_array($value) ? array_values($value) : [$value];
390 27
391 11
        if (empty($value)) {
392
            throw new InvalidArgumentException(
393 11
                'Header value must be a string or an array of strings, empty array given.',
394
            );
395
        }
396 16
397
        foreach ($value as $v) {
398
            if ((!is_string($v) && !is_numeric($v)) || !preg_match('/^[ \t\x21-\x7E\x80-\xFF]*$/', (string) $v)) {
399
                throw new InvalidArgumentException(sprintf(
400
                    '"%s" is not valid header value.',
401
                    (is_object($v) ? get_class($v) : (is_string($v) ? $v : gettype($v)))
402
                ));
403 16
            }
404
        }
405 16
406 1
        return $value;
407
    }
408
409 15
    /**
410
     * @param mixed $protocol
411 15
     * @throws InvalidArgumentException for invalid HTTP protocol version.
412 15
     */
413 15
    private function validateProtocolVersion($protocol): void
414
    {
415 8
        if (!in_array($protocol, self::$supportedProtocolVersions, true)) {
416
            throw new InvalidArgumentException(sprintf(
417 8
                'Unsupported HTTP protocol version "%s" provided. The following strings are supported: "%s".',
418
                is_string($protocol) ? $protocol : gettype($protocol),
419
                implode('", "', self::$supportedProtocolVersions),
420
            ));
421 7
        }
422
    }
423
}
424