Message::withAddedHeader()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 3
nc 1
nop 2
dl 0
loc 6
ccs 4
cts 4
cp 1
crap 1
rs 10
c 1
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
use Psr\Http\Message\MessageInterface;
15
use Psr\Http\Message\StreamInterface;
16
use Sunrise\Http\Message\Exception\InvalidArgumentException;
17
use Sunrise\Http\Message\Stream\PhpTempStream;
18
19
use function implode;
20
use function is_array;
21
use function is_string;
22
use function preg_match;
23
use function strtolower;
24
25
abstract class Message implements MessageInterface
26
{
27
    /**
28
     * @deprecated 3.2.0
29
     */
30
    public const ALLOWED_HTTP_VERSIONS = ['1.0', '1.1', '2.0', '2'];
31
32
    public const HTTP_VERSION_REGEX = '/^[0-9](?:[.][0-9])?$/';
33
    public const DEFAULT_HTTP_VERSION = '1.1';
34
35
    private string $protocolVersion = self::DEFAULT_HTTP_VERSION;
36
37
    /**
38
     * @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...
39
     */
40
    private array $headers = [];
41
42
    /**
43
     * @var array<string, string>
44
     */
45
    private array $headerNames = [];
46
47
    private ?StreamInterface $body = null;
48
49
    /**
50
     * @inheritDoc
51
     */
52 34
    public function getProtocolVersion(): string
53
    {
54 34
        return $this->protocolVersion;
55
    }
56
57
    /**
58
     * @inheritDoc
59
     *
60
     * @throws InvalidArgumentException
61
     */
62 66
    public function withProtocolVersion($version): MessageInterface
63
    {
64 66
        $clone = clone $this;
65 66
        $clone->setProtocolVersion($version);
66
67 15
        return $clone;
68
    }
69
70
    /**
71
     * @inheritDoc
72
     */
73 104
    public function getHeaders(): array
74
    {
75 104
        return $this->headers;
76
    }
77
78
    /**
79
     * @inheritDoc
80
     */
81 22
    public function hasHeader($name): bool
82
    {
83 22
        $key = strtolower($name);
84
85 22
        return isset($this->headerNames[$key]);
86
    }
87
88
    /**
89
     * @inheritDoc
90
     */
91 15
    public function getHeader($name): array
92
    {
93 15
        $key = strtolower($name);
94
95 15
        if (!isset($this->headerNames[$key])) {
96 6
            return [];
97
        }
98
99 15
        return $this->headers[$this->headerNames[$key]];
100
    }
101
102
    /**
103
     * @inheritDoc
104
     */
105 55
    public function getHeaderLine($name): string
106
    {
107 55
        $key = strtolower($name);
108
109 55
        if (!isset($this->headerNames[$key])) {
110 8
            return '';
111
        }
112
113 53
        return implode(',', $this->headers[$this->headerNames[$key]]);
114
    }
115
116
    /**
117
     * @inheritDoc
118
     */
119 148
    public function withHeader($name, $value): MessageInterface
120
    {
121 148
        $clone = clone $this;
122 148
        $clone->setHeader($name, $value, true);
123
124 94
        return $clone;
125
    }
126
127
    /**
128
     * @inheritDoc
129
     */
130 120
    public function withAddedHeader($name, $value): MessageInterface
131
    {
132 120
        $clone = clone $this;
133 120
        $clone->setHeader($name, $value, false);
134
135 66
        return $clone;
136
    }
137
138
    /**
139
     * @inheritDoc
140
     */
141 9
    public function withoutHeader($name): MessageInterface
142
    {
143 9
        $clone = clone $this;
144 9
        $clone->deleteHeader($name);
145
146 9
        return $clone;
147
    }
148
149
    /**
150
     * @inheritDoc
151
     */
152 26
    public function getBody(): StreamInterface
153
    {
154 26
        return $this->body ??= new PhpTempStream();
155
    }
156
157
    /**
158
     * @inheritDoc
159
     */
160 6
    public function withBody(StreamInterface $body): MessageInterface
161
    {
162 6
        $clone = clone $this;
163 6
        $clone->setBody($body);
164
165 6
        return $clone;
166
    }
167
168
    /**
169
     * Sets the given HTTP version to the message
170
     *
171
     * @param string $protocolVersion
172
     *
173
     * @throws InvalidArgumentException
174
     */
175 135
    final protected function setProtocolVersion($protocolVersion): void
176
    {
177 135
        $this->validateProtocolVersion($protocolVersion);
178
179 70
        $this->protocolVersion = $protocolVersion;
180
    }
181
182
    /**
183
     * Sets a new header to the message with the given name and value(s)
184
     *
185
     * @param string $name
186
     * @param string|string[] $value
187
     *
188
     * @throws InvalidArgumentException
189
     */
190 364
    final protected function setHeader($name, $value, bool $replace = true): void
191
    {
192 364
        if (!is_array($value)) {
193 295
            $value = [$value];
194
        }
195
196 364
        $this->validateHeaderName($name);
197 307
        $this->validateHeaderValue($name, $value);
198
199 225
        $replace and $this->deleteHeader($name);
200
201 225
        $key = strtolower($name);
202
203 225
        $this->headerNames[$key] ??= $name;
204 225
        $this->headers[$this->headerNames[$key]] ??= [];
205
206 225
        foreach ($value as $item) {
207 225
            $this->headers[$this->headerNames[$key]][] = $item;
208
        }
209
    }
210
211
    /**
212
     * Sets the given headers to the message
213
     *
214
     * @param array<string, string|string[]> $headers
215
     *
216
     * @throws InvalidArgumentException
217
     */
218 101
    final protected function setHeaders(array $headers): void
219
    {
220 101
        foreach ($headers as $name => $value) {
221 58
            $this->setHeader($name, $value, false);
222
        }
223
    }
224
225
    /**
226
     * Deletes a header from the message by the given name
227
     *
228
     * @param string $name
229
     */
230 168
    final protected function deleteHeader($name): void
231
    {
232 168
        $key = strtolower($name);
233
234 168
        if (isset($this->headerNames[$key])) {
235 21
            unset($this->headers[$this->headerNames[$key]]);
236 21
            unset($this->headerNames[$key]);
237
        }
238
    }
239
240
    /**
241
     * Sets the given body to the message
242
     */
243 59
    final protected function setBody(StreamInterface $body): void
244
    {
245 59
        $this->body = $body;
246
    }
247
248
    /**
249
     * Validates the given HTTP version
250
     *
251
     * @param mixed $protocolVersion
252
     *
253
     * @throws InvalidArgumentException
254
     */
255 135
    private function validateProtocolVersion($protocolVersion): void
256
    {
257 135
        if ($protocolVersion === '') {
258 3
            throw new InvalidArgumentException('HTTP version cannot be an empty');
259
        }
260
261 132
        if (!is_string($protocolVersion)) {
262 6
            throw new InvalidArgumentException('HTTP version must be a string');
263
        }
264
265 126
        if (!preg_match(self::HTTP_VERSION_REGEX, $protocolVersion)) {
266 56
            throw new InvalidArgumentException('HTTP version is invalid');
267
        }
268
    }
269
270
    /**
271
     * Validates the given header name
272
     *
273
     * @param mixed $name
274
     *
275
     * @throws InvalidArgumentException
276
     */
277 364
    private function validateHeaderName($name): void
278
    {
279 364
        if ($name === '') {
280 15
            throw new InvalidArgumentException('HTTP header name cannot be an empty');
281
        }
282
283 349
        if (!is_string($name)) {
284 33
            throw new InvalidArgumentException('HTTP header name must be a string');
285
        }
286
287 316
        if (!preg_match(HeaderInterface::RFC7230_TOKEN_REGEX, $name)) {
288 9
            throw new InvalidArgumentException('HTTP header name is invalid');
289
        }
290
    }
291
292
    /**
293
     * Validates the given header value
294
     *
295
     * @param array<array-key, mixed> $value
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<array-key, mixed> at position 2 could not be parsed: Unknown type name 'array-key' at position 2 in array<array-key, mixed>.
Loading history...
296
     *
297
     * @throws InvalidArgumentException
298
     */
299 307
    private function validateHeaderValue(string $name, array $value): void
300
    {
301 307
        if ($value === []) {
302 15
            throw new InvalidArgumentException("The value of the HTTP header $name cannot be an empty array");
303
        }
304
305 292
        foreach ($value as $key => $item) {
306 292
            if ($item === '') {
307 18
                continue;
308
            }
309
310 280
            if (!is_string($item)) {
311 48
                throw new InvalidArgumentException("The value of the HTTP header $name:$key must be a string");
312
            }
313
314 250
            if (!preg_match(HeaderInterface::RFC7230_FIELD_VALUE_REGEX, $item)) {
315 20
                throw new InvalidArgumentException("The value of the HTTP header $name:$key is invalid");
316
            }
317
        }
318
    }
319
}
320