Passed
Pull Request — master (#18)
by Anatoly
11:04
created

Message::addHeader()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 15
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 3

Importance

Changes 0
Metric Value
cc 3
eloc 9
nc 3
nop 3
dl 0
loc 15
ccs 10
cts 10
cp 1
crap 3
rs 9.9666
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 Fenric <[email protected]>
7
 * @copyright Copyright (c) 2018, Anatoly Fenric
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\Header\HeaderInterface;
20
use Sunrise\Stream\StreamFactory;
21
use InvalidArgumentException;
22
23
/**
24
 * Import functions
25
 */
26
use function is_string;
27
use function join;
28
use function preg_match;
29
use function sprintf;
30
use function strtolower;
31
use function ucwords;
32
33
/**
34
 * Hypertext Transfer Protocol Message
35
 *
36
 * @link https://tools.ietf.org/html/rfc7230
37
 * @link https://www.php-fig.org/psr/psr-7/
38
 */
39
class Message implements MessageInterface
40
{
41
42
    /**
43
     * The message protocol version
44
     *
45
     * @var string
46
     */
47
    protected $protocolVersion = '1.1';
48
49
    /**
50
     * The message headers
51
     *
52
     * @var array<string, array<string>>
53
     */
54
    protected $headers = [];
55
56
    /**
57
     * The message body
58
     *
59
     * @var StreamInterface|null
60
     */
61
    protected $body = null;
62
63
    /**
64
     * Constructor of the class
65
     *
66
     * @param array<string, string|array<string>>|null $headers
67
     * @param StreamInterface|null $body
68
     * @param string|null $protocolVersion
69
     *
70
     * @throws InvalidArgumentException
71
     */
72 269
    public function __construct(
73
        ?array $headers = null,
74
        ?StreamInterface $body = null,
75
        ?string $protocolVersion = null
76
    ) {
77 269
        if (isset($protocolVersion)) {
78 2
            $this->setProtocolVersion($protocolVersion);
79
        }
80
81 269
        if (isset($headers)) {
82 5
            foreach ($headers as $name => $value) {
83 5
                $this->addHeader($name, $value);
84
            }
85
        }
86
87 269
        if (isset($body)) {
88 2
            $this->body = $body;
89
        }
90 269
    }
91
92
    /**
93
     * {@inheritdoc}
94
     */
95 3
    public function getProtocolVersion() : string
96
    {
97 3
        return $this->protocolVersion;
98
    }
99
100
    /**
101
     * {@inheritdoc}
102
     *
103
     * @throws InvalidArgumentException
104
     */
105 22
    public function withProtocolVersion($version) : MessageInterface
106
    {
107 22
        $clone = clone $this;
108 22
        $clone->setProtocolVersion($version);
109
110 1
        return $clone;
111
    }
112
113
    /**
114
     * {@inheritdoc}
115
     */
116 14
    public function getHeaders() : array
117
    {
118 14
        return $this->headers;
119
    }
120
121
    /**
122
     * {@inheritdoc}
123
     */
124 4
    public function hasHeader($name) : bool
125
    {
126 4
        $name = $this->normalizeHeaderName($name);
127
128 4
        return ! empty($this->headers[$name]);
129
    }
130
131
    /**
132
     * {@inheritdoc}
133
     */
134 3
    public function getHeader($name) : array
135
    {
136 3
        $name = $this->normalizeHeaderName($name);
137
138 3
        return $this->headers[$name] ?? [];
139
    }
140
141
    /**
142
     * {@inheritdoc}
143
     */
144 10
    public function getHeaderLine($name) : string
145
    {
146 10
        $name = $this->normalizeHeaderName($name);
147
148 10
        if (empty($this->headers[$name])) {
149 1
            return '';
150
        }
151
152 10
        return join(', ', $this->headers[$name]);
153
    }
154
155
    /**
156
     * {@inheritdoc}
157
     *
158
     * @throws InvalidArgumentException
159
     */
160 86
    public function withHeader($name, $value) : MessageInterface
161
    {
162 86
        $clone = clone $this;
163 86
        $clone->addHeader($name, $value);
164
165 22
        return $clone;
166
    }
167
168
    /**
169
     * {@inheritdoc}
170
     *
171
     * @throws InvalidArgumentException
172
     */
173 68
    public function withAddedHeader($name, $value) : MessageInterface
174
    {
175 68
        $clone = clone $this;
176 68
        $clone->addHeader($name, $value, false);
177
178 4
        return $clone;
179
    }
180
181
    /**
182
     * {@inheritdoc}
183
     */
184 2
    public function withoutHeader($name) : MessageInterface
185
    {
186 2
        $name = $this->normalizeHeaderName($name);
187
188 2
        $clone = clone $this;
189
190 2
        unset($clone->headers[$name]);
191
192 2
        return $clone;
193
    }
194
195
    /**
196
     * {@inheritdoc}
197
     */
198 7
    public function getBody() : StreamInterface
199
    {
200 7
        if (null === $this->body) {
201 5
            $this->body = (new StreamFactory)->createStream();
202
        }
203
204 7
        return $this->body;
205
    }
206
207
    /**
208
     * {@inheritdoc}
209
     */
210 1
    public function withBody(StreamInterface $body) : MessageInterface
211
    {
212 1
        $clone = clone $this;
213 1
        $clone->body = $body;
214
215 1
        return $clone;
216
    }
217
218
    /**
219
     * Sets the given protocol version to the message
220
     *
221
     * @param string $version
222
     *
223
     * @return void
224
     *
225
     * @throws InvalidArgumentException
226
     */
227 24
    protected function setProtocolVersion($version) : void
228
    {
229 24
        $this->validateProtocolVersion($version);
230
231 3
        $this->protocolVersion = $version;
232 3
    }
233
234
    /**
235
     * Adds the given header field to the message
236
     *
237
     * @param string $name
238
     * @param string|array<string> $value
239
     * @param bool $replace
240
     *
241
     * @return void
242
     *
243
     * @throws InvalidArgumentException
244
     */
245 160
    protected function addHeader($name, $value, bool $replace = true) : void
246
    {
247 160
        $this->validateHeaderName($name);
248 128
        $this->validateHeaderValue($value);
249
250 32
        $name = $this->normalizeHeaderName($name);
251 32
        $value = (array) $value;
252
253 32
        if ($replace) {
254 32
            $this->headers[$name] = $value;
255 32
            return;
256
        }
257
258 4
        foreach ($value as $item) {
259 4
            $this->headers[$name][] = $item;
260
        }
261 4
    }
262
263
    /**
264
     * Validates the given protocol version
265
     *
266
     * @param mixed $version
267
     *
268
     * @return void
269
     *
270
     * @throws InvalidArgumentException
271
     *
272
     * @link https://tools.ietf.org/html/rfc7230#section-2.6
273
     * @link https://tools.ietf.org/html/rfc7540
274
     */
275 24
    protected function validateProtocolVersion($version) : void
276
    {
277
        // allowed protocol versions:
278 24
        static $allowed = [
279
            '1.0' => 1,
280
            '1.1' => 1,
281
            '2.0' => 1,
282
            '2'   => 1,
283
        ];
284
285 24
        if (!is_string($version)) {
286 9
            throw new InvalidArgumentException('Protocol version must be a string');
287
        }
288
289 15
        if (!isset($allowed[$version])) {
290 12
            throw new InvalidArgumentException(sprintf(
291
                'The protocol version "%s" is not valid. ' .
292 12
                'Allowed only: 1.0, 1.1 or 2{.0}',
293 12
                $version
294
            ));
295
        }
296 3
    }
297
298
    /**
299
     * Validates the given header name
300
     *
301
     * @param mixed $name
302
     *
303
     * @return void
304
     *
305
     * @throws InvalidArgumentException
306
     *
307
     * @link https://tools.ietf.org/html/rfc7230#section-3.2
308
     */
309 160
    protected function validateHeaderName($name) : void
310
    {
311 160
        if (!is_string($name)) {
312 18
            throw new InvalidArgumentException('Header name must be a string');
313
        }
314
315 142
        if (!preg_match(HeaderInterface::RFC7230_TOKEN, $name)) {
316 14
            throw new InvalidArgumentException(sprintf('The header name "%s" is not valid', $name));
317
        }
318 128
    }
319
320
    /**
321
     * Validates the given header value
322
     *
323
     * @param mixed $value
324
     *
325
     * @return void
326
     *
327
     * @throws InvalidArgumentException
328
     *
329
     * @link https://tools.ietf.org/html/rfc7230#section-3.2
330
     */
331 128
    protected function validateHeaderValue($value) : void
332
    {
333 128
        $items = (array) $value;
334
335 128
        if ([] === $items) {
336 6
            throw new InvalidArgumentException('Header value must be a string or a non-empty array');
337
        }
338
339 122
        foreach ($items as $item) {
340 122
            if (!is_string($item)) {
341 72
                throw new InvalidArgumentException('Header value must be a string or an array with strings only');
342
            }
343
344 92
            if (!preg_match(HeaderInterface::RFC7230_FIELD_VALUE, $item)) {
345 18
                throw new InvalidArgumentException(sprintf('The header value "%s" is not valid', $item));
346
            }
347
        }
348 32
    }
349
350
    /**
351
     * Normalizes the given header name
352
     *
353
     * @param string $name
354
     *
355
     * @return string
356
     *
357
     * @link https://tools.ietf.org/html/rfc7230#section-3.2
358
     */
359 32
    protected function normalizeHeaderName($name) : string
360
    {
361 32
        return ucwords(strtolower($name), '-');
362
    }
363
}
364