Passed
Pull Request — master (#27)
by Anatoly
04:06
created

Message::normalizeHeaderName()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 1
dl 0
loc 3
ccs 2
cts 2
cp 1
crap 1
rs 10
c 0
b 0
f 0
1
<?php declare(strict_types=1);
2
3
/**
4
 * It's free open-source software released under the MIT License.
5
 *
6
 * @author Anatoly Nekhay <[email protected]>
7
 * @copyright Copyright (c) 2018, Anatoly Nekhay
8
 * @license https://github.com/sunrise-php/http-message/blob/master/LICENSE
9
 * @link https://github.com/sunrise-php/http-message
10
 */
11
12
namespace Sunrise\Http\Message;
13
14
/**
15
 * Import classes
16
 */
17
use Psr\Http\Message\MessageInterface;
18
use Psr\Http\Message\StreamInterface;
19
use Sunrise\Http\Message\Exception\InvalidArgumentException;
20
use Sunrise\Http\Message\Exception\InvalidHeaderException;
21
use Sunrise\Http\Message\Exception\InvalidHeaderNameException;
22
use Sunrise\Http\Message\Exception\InvalidHeaderValueException;
23
use Sunrise\Http\Message\Stream\PhpTempStream;
24
25
/**
26
 * Import functions
27
 */
28
use function implode;
29
use function in_array;
30
use function is_array;
31
use function is_string;
32
use function preg_match;
33
use function sprintf;
34
use function strtolower;
35
36
/**
37
 * Hypertext Transfer Protocol Message
38
 *
39
 * @link https://tools.ietf.org/html/rfc7230
40
 * @link https://www.php-fig.org/psr/psr-7/
41
 */
42
abstract class Message implements MessageInterface
43
{
44
45
    /**
46
     * Default HTTP version
47
     *
48
     * @var string
49
     */
50
    public const DEFAULT_HTTP_VERSION = '1.1';
51
52
    /**
53
     * Supported HTTP versions
54
     *
55
     * @var list<string>
56
     */
57
    public const SUPPORTED_HTTP_VERSIONS = ['1.0', '1.1', '2.0', '2'];
58
59
    /**
60
     * The message HTTP version
61
     *
62
     * @var string
63
     */
64
    private string $protocolVersion = self::DEFAULT_HTTP_VERSION;
65
66
    /**
67
     * The message headers
68
     *
69
     * @var array<string, list<string>>
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<string, list<string>> at position 4 could not be parsed: Expected '>' at position 4, but found 'list'.
Loading history...
70
     */
71
    private array $headers = [];
72
73
    /**
74
     * Original header names (see $headers)
75
     *
76
     * @var array<string, string>
77
     */
78
    private array $headerNames = [];
79
80
    /**
81
     * The message body
82
     *
83
     * @var StreamInterface|null
84
     */
85
    private ?StreamInterface $body = null;
86
87
    /**
88
     * Gets the message HTTP version
89
     *
90
     * @return string
91
     */
92 31
    public function getProtocolVersion(): string
93
    {
94 31
        return $this->protocolVersion;
95
    }
96
97
    /**
98
     * Creates a new instance of the message with the given HTTP version
99
     *
100
     * @param string $version
101
     *
102
     * @return static
103
     *
104
     * @throws InvalidArgumentException
105
     *         If the HTTP version isn't valid.
106
     */
107 71
    public function withProtocolVersion($version): MessageInterface
108
    {
109 71
        $clone = clone $this;
110 71
        $clone->setProtocolVersion($version);
111
112 14
        return $clone;
113
    }
114
115
    /**
116
     * Gets the message headers
117
     *
118
     * @return array<string, list<string>>
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<string, list<string>> at position 4 could not be parsed: Expected '>' at position 4, but found 'list'.
Loading history...
119
     */
120 102
    public function getHeaders(): array
121
    {
122 102
        return $this->headers;
123
    }
124
125
    /**
126
     * Checks if a header exists in the message by the given name
127
     *
128
     * @param string $name
129
     *
130
     * @return bool
131
     */
132 63
    public function hasHeader($name): bool
133
    {
134 63
        $key = strtolower($name);
135
136 63
        return isset($this->headerNames[$key]);
137
    }
138
139
    /**
140
     * Gets a header value from the message by the given name
141
     *
142
     * @param string $name
143
     *
144
     * @return list<string>
0 ignored issues
show
Bug introduced by
The type Sunrise\Http\Message\list was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
145
     */
146 55
    public function getHeader($name): array
147
    {
148 55
        if (!$this->hasHeader($name)) {
149 11
            return [];
0 ignored issues
show
Bug Best Practice introduced by
The expression return array() returns the type array which is incompatible with the documented return type Sunrise\Http\Message\list.
Loading history...
150
        }
151
152 54
        $key = strtolower($name);
153 54
        $originalName = $this->headerNames[$key];
154 54
        $value = $this->headers[$originalName];
155
156 54
        return $value;
157
    }
158
159
    /**
160
     * Gets a header value as a string from the message by the given name
161
     *
162
     * @param string $name
163
     *
164
     * @return string
165
     */
166 44
    public function getHeaderLine($name): string
167
    {
168 44
        $value = $this->getHeader($name);
169 44
        if ([] === $value) {
170 6
            return '';
171
        }
172
173 43
        return implode(',', $value);
174
    }
175
176
    /**
177
     * Creates a new instance of the message with the given header overwriting the old header
178
     *
179
     * @param string $name
180
     * @param string|string[] $value
181
     *
182
     * @return static
183
     *
184
     * @throws InvalidHeaderException
185
     *         If the header isn't valid.
186
     */
187 140
    public function withHeader($name, $value): MessageInterface
188
    {
189 140
        $clone = clone $this;
190 140
        $clone->setHeader($name, $value, true);
191
192 93
        return $clone;
193
    }
194
195
    /**
196
     * Creates a new instance of the message with the given header NOT overwriting the old header
197
     *
198
     * @param string $name
199
     * @param string|string[] $value
200
     *
201
     * @return static
202
     *
203
     * @throws InvalidHeaderException
204
     *         If the header isn't valid.
205
     */
206 105
    public function withAddedHeader($name, $value): MessageInterface
207
    {
208 105
        $clone = clone $this;
209 105
        $clone->setHeader($name, $value, false);
210
211 58
        return $clone;
212
    }
213
214
    /**
215
     * Creates a new instance of the message without a header by the given name
216
     *
217
     * @param string $name
218
     *
219
     * @return static
220
     */
221 8
    public function withoutHeader($name): MessageInterface
222
    {
223 8
        $clone = clone $this;
224 8
        $clone->deleteHeader($name);
225
226 8
        return $clone;
227
    }
228
229
    /**
230
     * Gets the message body
231
     *
232
     * @return StreamInterface
233
     */
234 17
    public function getBody(): StreamInterface
235
    {
236 17
        return $this->body ??= new PhpTempStream();
237
    }
238
239
    /**
240
     * Creates a new instance of the message with the given body
241
     *
242
     * @param StreamInterface $body
243
     *
244
     * @return static
245
     */
246 5
    public function withBody(StreamInterface $body): MessageInterface
247
    {
248 5
        $clone = clone $this;
249 5
        $clone->setBody($body);
250
251 5
        return $clone;
252
    }
253
254
    /**
255
     * Sets the given HTTP version to the message
256
     *
257
     * @param string $protocolVersion
258
     *
259
     * @return void
260
     *
261
     * @throws InvalidArgumentException
262
     *         If the HTTP version isn't valid.
263
     */
264 150
    final protected function setProtocolVersion($protocolVersion): void
265
    {
266 150
        $this->validateProtocolVersion($protocolVersion);
267
268 74
        $this->protocolVersion = $protocolVersion;
269
    }
270
271
    /**
272
     * Sets a new header to the message with the given name and value(s)
273
     *
274
     * @param string $name
275
     * @param string|string[] $value
276
     * @param bool $replace
277
     *
278
     * @return void
279
     *
280
     * @throws InvalidHeaderException
281
     *         If the header isn't valid.
282
     */
283 328
    final protected function setHeader($name, $value, bool $replace = true): void
284
    {
285 328
        if (!is_array($value)) {
286 262
            $value = [$value];
287
        }
288
289 328
        $this->validateHeaderName($name);
290 279
        $this->validateHeaderValue($name, $value);
291
292 203
        if ($replace) {
293 145
            $this->deleteHeader($name);
294
        }
295
296 203
        $key = strtolower($name);
297
298 203
        $this->headerNames[$key] ??= $name;
299 203
        $this->headers[$this->headerNames[$key]] ??= [];
300
301 203
        foreach ($value as $subvalue) {
302 203
            $this->headers[$this->headerNames[$key]][] = $subvalue;
303
        }
304
    }
305
306
    /**
307
     * Sets the given headers to the message
308
     *
309
     * @param array<string, string|string[]> $headers
310
     *
311
     * @return void
312
     *
313
     * @throws InvalidHeaderException
314
     *         If one of the headers isn't valid.
315
     */
316 105
    final protected function setHeaders(array $headers): void
317
    {
318 105
        foreach ($headers as $name => $value) {
319 64
            $this->setHeader($name, $value, false);
320
        }
321
    }
322
323
    /**
324
     * Deletes a header from the message by the given name
325
     *
326
     * @param string $name
327
     *
328
     * @return void
329
     */
330 147
    final protected function deleteHeader($name): void
331
    {
332 147
        $key = strtolower($name);
333
334 147
        if (isset($this->headerNames[$key])) {
335 20
            unset($this->headers[$this->headerNames[$key]]);
336 20
            unset($this->headerNames[$key]);
337
        }
338
    }
339
340
    /**
341
     * Sets the given body to the message
342
     *
343
     * @param StreamInterface $body
344
     *
345
     * @return void
346
     */
347 49
    final protected function setBody(StreamInterface $body): void
348
    {
349 49
        $this->body = $body;
350
    }
351
352
    /**
353
     * Validates the given HTTP version
354
     *
355
     * @param mixed $protocolVersion
356
     *
357
     * @return void
358
     *
359
     * @throws InvalidArgumentException
360
     *         If the HTTP version isn't valid.
361
     */
362 150
    private function validateProtocolVersion($protocolVersion): void
363
    {
364 150
        if (!in_array($protocolVersion, self::SUPPORTED_HTTP_VERSIONS, true)) {
365 76
            throw new InvalidArgumentException('Invalid or unsupported HTTP version');
366
        }
367
    }
368
369
    /**
370
     * Validates the given header name
371
     *
372
     * @param mixed $name
373
     *
374
     * @return void
375
     *
376
     * @throws InvalidHeaderNameException
377
     *         If the header name isn't valid.
378
     */
379 328
    private function validateHeaderName($name): void
380
    {
381 328
        if ($name === '') {
382 13
            throw new InvalidHeaderNameException('HTTP header name cannot be an empty');
383
        }
384
385 315
        if (!is_string($name)) {
386 27
            throw new InvalidHeaderNameException('HTTP header name must be a string');
387
        }
388
389 288
        if (!preg_match(Header::RFC7230_VALID_TOKEN, $name)) {
390 9
            throw new InvalidHeaderNameException('HTTP header name is invalid');
391
        }
392
    }
393
394
    /**
395
     * Validates the given header value
396
     *
397
     * @param string $validName
398
     * @param array $value
399
     *
400
     * @return void
401
     *
402
     * @throws InvalidHeaderValueException
403
     *         If the header value isn't valid.
404
     */
405 279
    private function validateHeaderValue(string $validName, array $value): void
406
    {
407 279
        if ([] === $value) {
408 13
            throw new InvalidHeaderValueException(sprintf(
409 13
                'The "%s" HTTP header value cannot be an empty array',
410 13
                $validName,
411 13
            ));
412
        }
413
414 266
        foreach ($value as $i => $subvalue) {
415 266
            if ('' === $subvalue) {
416 17
                continue;
417
            }
418
419 254
            if (!is_string($subvalue)) {
420 44
                throw new InvalidHeaderValueException(sprintf(
421 44
                    'The "%s[%d]" HTTP header value must be a string',
422 44
                    $validName,
423 44
                    $i
424 44
                ));
425
            }
426
427 228
            if (!preg_match(Header::RFC7230_VALID_FIELD_VALUE, $subvalue)) {
428 20
                throw new InvalidHeaderValueException(sprintf(
429 20
                    'The "%s[%d]" HTTP header value is invalid',
430 20
                    $validName,
431 20
                    $i
432 20
                ));
433
            }
434
        }
435
    }
436
}
437