Passed
Push — master ( 157485...b7615a )
by Nikolaos
09:23
created

AbstractMessage   B

Complexity

Total Complexity 46

Size/Duplication

Total Lines 539
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 46
eloc 110
c 0
b 0
f 0
dl 0
loc 539
rs 8.72

21 Methods

Rating   Name   Duplication   Size   Complexity  
A getUriHost() 0 9 2
A getHeaders() 0 3 1
A getUri() 0 3 1
A checkHeaderName() 0 8 3
A checkHeaderValue() 0 13 5
A getProtocolVersion() 0 3 1
A processBody() 0 15 5
A withHeader() 0 10 1
A getHeaderLine() 0 5 1
A checkHeaderHost() 0 25 5
A hasHeader() 0 3 1
A getHeaderValue() 0 22 4
A getBody() 0 3 1
A processProtocol() 0 20 4
A withAddedHeader() 0 12 1
A getHeader() 0 5 1
A processHeaders() 0 17 4
A withBody() 0 5 1
A withProtocolVersion() 0 5 1
A populateHeaderCollection() 0 13 2
A withoutHeader() 0 7 1

How to fix   Complexity   

Complex Class

Complex classes like AbstractMessage often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use AbstractMessage, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/**
4
 * This file is part of the Phalcon Framework.
5
 *
6
 * (c) Phalcon Team <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE.txt
9
 * file that was distributed with this source code.
10
 *
11
 * Implementation of this file has been influenced by Zend Diactoros
12
 * @link    https://github.com/zendframework/zend-diactoros
13
 * @license https://github.com/zendframework/zend-diactoros/blob/master/LICENSE.md
14
 */
15
16
declare(strict_types=1);
17
18
namespace Phalcon\Http\Message;
19
20
use Phalcon\Collection;
21
use Phalcon\Http\Message\Exception\InvalidArgumentException;
22
use Psr\Http\Message\StreamInterface;
23
use Psr\Http\Message\UriInterface;
24
25
use function array_merge;
26
use function implode;
27
use function is_array;
28
use function is_numeric;
29
use function is_object;
30
use function is_resource;
31
use function is_string;
32
use function preg_match;
33
34
/**
35
 * Class AbstractMessage
36
 *
37
 * @property StreamInterface $body
38
 * @property Collection      $headers
39
 * @property string          $protocolVersion
40
 * @property UriInterface    $uri
41
 */
42
abstract class AbstractMessage extends AbstractCommon
43
{
44
    /**
45
     * Gets the body of the message.
46
     *
47
     * @var StreamInterface
48
     */
49
    protected $body;
50
51
    /**
52
     * @var Collection
53
     */
54
    protected $headers;
55
56
    /**
57
     * Retrieves the HTTP protocol version as a string.
58
     *
59
     * The string MUST contain only the HTTP version number (e.g., '1.1',
60
     * '1.0').
61
     *
62
     * @return string HTTP protocol version.
63
     *
64
     * @var string
65
     */
66
    protected $protocolVersion = "1.1";
67
68
    /**
69
     * Retrieves the URI instance.
70
     *
71
     * This method MUST return a UriInterface instance.
72
     *
73
     * @see http://tools.ietf.org/html/rfc3986#section-4.3
74
     *
75
     * @var UriInterface
76
     */
77
    protected $uri;
78
79
    /**
80
     * @return StreamInterface
81
     */
82
    public function getBody(): StreamInterface
83
    {
84
        return $this->body;
85
    }
86
87
    /**
88
     * Retrieves a message header value by the given case-insensitive name.
89
     *
90
     * This method returns an array of all the header values of the given
91
     * case-insensitive header name.
92
     *
93
     * If the header does not appear in the message, this method MUST return an
94
     * empty array.
95
     *
96
     * @param string $name
97
     *
98
     * @return array
99
     */
100
    public function getHeader($name): array
101
    {
102
        $name = (string) $name;
103
104
        return $this->headers->get($name, []);
105
    }
106
107
    /**
108
     * Retrieves a comma-separated string of the values for a single header.
109
     *
110
     * This method returns all of the header values of the given
111
     * case-insensitive header name as a string concatenated together using
112
     * a comma.
113
     *
114
     * NOTE: Not all header values may be appropriately represented using
115
     * comma concatenation. For such headers, use getHeader() instead
116
     * and supply your own delimiter when concatenating.
117
     *
118
     * If the header does not appear in the message, this method MUST return
119
     * an empty string.
120
     *
121
     * @param string $name
122
     *
123
     * @return string
124
     */
125
    public function getHeaderLine($name): string
126
    {
127
        $header = $this->getHeader($name);
128
129
        return implode(",", $header);
130
    }
131
132
    /**
133
     * Retrieves all message header values.
134
     *
135
     * The keys represent the header name as it will be sent over the wire, and
136
     * each value is an array of strings associated with the header.
137
     *
138
     *     // Represent the headers as a string
139
     *     foreach ($message->getHeaders() as $name => $values) {
140
     *         echo $name . ': ' . implode(', ', $values);
141
     *     }
142
     *
143
     *     // Emit headers iteratively:
144
     *     foreach ($message->getHeaders() as $name => $values) {
145
     *         foreach ($values as $value) {
146
     *             header(sprintf('%s: %s', $name, $value), false);
147
     *         }
148
     *     }
149
     *
150
     * While header names are not case-sensitive, getHeaders() will preserve the
151
     * exact case in which headers were originally specified.
152
     *
153
     * @return array
154
     */
155
    public function getHeaders(): array
156
    {
157
        return $this->headers->toArray();
158
    }
159
160
    /**
161
     * @return string
162
     */
163
    public function getProtocolVersion(): string
164
    {
165
        return $this->protocolVersion;
166
    }
167
168
    /**
169
     * @return UriInterface
170
     */
171
    public function getUri(): UriInterface
172
    {
173
        return $this->uri;
174
    }
175
176
    /**
177
     * Checks if a header exists by the given case-insensitive name.
178
     *
179
     * @param string $name
180
     *
181
     * @return bool
182
     */
183
    public function hasHeader($name): bool
184
    {
185
        return $this->headers->has($name);
186
    }
187
188
    /**
189
     * Return an instance with the specified header appended with the given
190
     * value.
191
     *
192
     * Existing values for the specified header will be maintained. The new
193
     * value(s) will be appended to the existing list. If the header did not
194
     * exist previously, it will be added.
195
     *
196
     * This method MUST be implemented in such a way as to retain the
197
     * immutability of the message, and MUST return an instance that has the
198
     * new header and/or value.
199
     *
200
     * @param string          $name
201
     * @param string|string[] $value
202
     *
203
     * @return self
204
     */
205
    public function withAddedHeader($name, $value): object
206
    {
207
        $this->checkHeaderName($name);
208
209
        $headers  = clone $this->headers;
210
        $existing = $headers->get($name, []);
211
        $value    = $this->getHeaderValue($value);
212
        $value    = array_merge($existing, $value);
213
214
        $headers->set($name, $value);
215
216
        return $this->cloneInstance($headers, "headers");
217
    }
218
219
    /**
220
     * Return an instance with the specified message body.
221
     *
222
     * The body MUST be a StreamInterface object.
223
     *
224
     * This method MUST be implemented in such a way as to retain the
225
     * immutability of the message, and MUST return a new instance that has the
226
     * new body stream.
227
     *
228
     * @param StreamInterface $body
229
     *
230
     * @return self
231
     * @throws InvalidArgumentException When the body is not valid.
232
     *
233
     */
234
    public function withBody(StreamInterface $body): object
235
    {
236
        $newBody = $this->processBody($body, "w+b");
237
238
        return $this->cloneInstance($newBody, "body");
239
    }
240
241
    /**
242
     * Return an instance with the provided value replacing the specified
243
     * header.
244
     *
245
     * While header names are case-insensitive, the casing of the header will
246
     * be preserved by this function, and returned from getHeaders().
247
     *
248
     * This method MUST be implemented in such a way as to retain the
249
     * immutability of the message, and MUST return an instance that has the
250
     * new and/or updated header and value.
251
     *
252
     * @param string          $name
253
     * @param string|string[] $value
254
     *
255
     * @return self
256
     * @throws InvalidArgumentException for invalid header names or values.
257
     *
258
     */
259
    public function withHeader($name, $value): object
260
    {
261
        $this->checkHeaderName($name);
262
263
        $headers = clone $this->headers;
264
        $value   = $this->getHeaderValue($value);
265
266
        $headers->set($name, $value);
267
268
        return $this->cloneInstance($headers, "headers");
269
    }
270
271
    /**
272
     * Return an instance with the specified HTTP protocol version.
273
     *
274
     * The version string MUST contain only the HTTP version number (e.g.,
275
     * '1.1', '1.0').
276
     *
277
     * This method MUST be implemented in such a way as to retain the
278
     * immutability of the message, and MUST return an instance that has the
279
     * new protocol version.
280
     *
281
     * @param string $version
282
     *
283
     * @return self
284
     */
285
    public function withProtocolVersion($version): object
286
    {
287
        $this->processProtocol($version);
288
289
        return $this->cloneInstance($version, "protocolVersion");
290
    }
291
292
    /**
293
     * Return an instance without the specified header.
294
     *
295
     * Header resolution MUST be done without case-sensitivity.
296
     *
297
     * This method MUST be implemented in such a way as to retain the
298
     * immutability of the message, and MUST return an instance that removes
299
     * the named header.
300
     *
301
     * @param string $name
302
     *
303
     * @return self
304
     */
305
    public function withoutHeader($name): object
306
    {
307
        $headers = clone $this->headers;
308
309
        $headers->remove($name);
310
311
        return $this->cloneInstance($headers, "headers");
312
    }
313
314
    /**
315
     * Ensure Host is the first header.
316
     *
317
     * @see: http://tools.ietf.org/html/rfc7230#section-5.4
318
     *
319
     * @param Collection $collection
320
     *
321
     * @return Collection
322
     */
323
    final protected function checkHeaderHost(Collection $collection): Collection
324
    {
325
        if (
326
            $collection->has("host") &&
327
            !empty($this->uri) &&
328
            "" !== $this->uri->getHost()
329
        ) {
330
            $host      = $this->getUriHost($this->uri);
331
            $hostArray = $host;
332
            if (!is_array($host)) {
0 ignored issues
show
introduced by
The condition is_array($host) is always false.
Loading history...
333
                $hostArray = [$host];
334
            }
335
336
            $collection->remove("host");
337
338
            $data           = $collection->toArray();
339
            $header         = [];
340
            $header["Host"] = $hostArray;
341
            $header         = $header + (array) $data;
342
343
            $collection->clear();
344
            $collection->init($header);
345
        }
346
347
        return $collection;
348
    }
349
350
    /**
351
     * Check the name of the header. Throw exception if not valid
352
     *
353
     * @see http://tools.ietf.org/html/rfc7230#section-3.2
354
     *
355
     * @param string $name
356
     */
357
    final protected function checkHeaderName($name): void
358
    {
359
        if (
360
            !is_string($name)  ||
0 ignored issues
show
introduced by
The condition is_string($name) is always true.
Loading history...
361
            !preg_match("/^[a-zA-Z0-9'`#$%&*+.^_|~!-]+$/", $name)
362
        ) {
363
            throw new InvalidArgumentException(
364
                "Invalid header name " . $name
365
            );
366
        }
367
    }
368
369
    /**
370
     * Validates a header value
371
     *
372
     * Most HTTP header field values are defined using common syntax
373
     * components (token, quoted-string, and comment) separated by
374
     * whitespace or specific delimiting characters.  Delimiters are chosen
375
     * from the set of US-ASCII visual characters not allowed in a token
376
     * (DQUOTE and '(),/:;<=>?@[\]{}').
377
     *
378
     *     token          = 1*tchar
379
     *
380
     *     tchar          = '!' / '#' / '$' / '%' / '&' / ''' / '*'
381
     *                    / '+' / '-' / '.' / '^' / '_' / '`' / '|' / '~'
382
     *                    / DIGIT / ALPHA
383
     *                    ; any VCHAR, except delimiters
384
     *
385
     * A string of text is parsed as a single value if it is quoted using
386
     * double-quote marks.
387
     *
388
     *     quoted-string  = DQUOTE *( qdtext / quoted-pair ) DQUOTE
389
     *     qdtext         = HTAB / SP /%x21 / %x23-5B / %x5D-7E / obs-text
390
     *     obs-text       = %x80-FF
391
     *
392
     * Comments can be included in some HTTP header fields by surrounding
393
     * the comment text with parentheses.  Comments are only allowed in
394
     * fields containing 'comment' as part of their field value definition.
395
     *
396
     *     comment        = '(' *( ctext / quoted-pair / comment ) ')'
397
     *     ctext          = HTAB / SP / %x21-27 / %x2A-5B / %x5D-7E / obs-text
398
     *
399
     * The backslash octet ('\') can be used as a single-octet quoting
400
     * mechanism within quoted-string and comment constructs.  Recipients
401
     * that process the value of a quoted-string MUST handle a quoted-pair
402
     * as if it were replaced by the octet following the backslash.
403
     *
404
     *     quoted-pair    = '\' ( HTAB / SP / VCHAR / obs-text )
405
     *
406
     * A sender SHOULD NOT generate a quoted-pair in a quoted-string except
407
     * where necessary to quote DQUOTE and backslash octets occurring within
408
     * that string.  A sender SHOULD NOT generate a quoted-pair in a comment
409
     * except where necessary to quote parentheses ['(' and ')'] and
410
     * backslash octets occurring within that comment.
411
     *
412
     * @see https://tools.ietf.org/html/rfc7230#section-3.2.6
413
     *
414
     * @param mixed $value
415
     */
416
    final protected function checkHeaderValue($value): void
417
    {
418
        if (!is_string($value) && !is_numeric($value)) {
419
            throw new InvalidArgumentException("Invalid header value");
420
        }
421
422
        $value = (string) $value;
423
424
        if (
425
            preg_match("#(?:(?:(?<!\r)\n)|(?:\r(?!\n))|(?:\r\n(?![ \t])))#", $value) ||
426
            preg_match("/[^\x09\x0a\x0d\x20-\x7E\x80-\xFE]/", $value)
427
        ) {
428
            throw new InvalidArgumentException("Invalid header value");
429
        }
430
    }
431
432
    /**
433
     * Returns the header values checked for validity
434
     *
435
     * @param string|array $values
436
     *
437
     * @return array
438
     */
439
    final protected function getHeaderValue($values): array
440
    {
441
        $valueArray = $values;
442
        if (!is_array($values)) {
443
            $valueArray = [$values];
444
        }
445
446
        if (empty($valueArray)) {
447
            throw new InvalidArgumentException(
448
                "Invalid header value: must be a string or " .
449
                "array of strings; cannot be an empty array"
450
            );
451
        }
452
453
        $valueData = [];
454
        foreach ($valueArray as $value) {
455
            $this->checkHeaderValue($value);
456
457
            $valueData[] = (string) $value;
458
        }
459
460
        return $valueData;
461
    }
462
463
    /**
464
     * Return the host and if applicable the port
465
     *
466
     * @param UriInterface $uri
467
     *
468
     * @return string
469
     */
470
    final protected function getUriHost(UriInterface $uri): string
471
    {
472
        $host = $uri->getHost();
473
474
        if (null !== $uri->getPort()) {
475
            $host .= ":" . $uri->getPort();
476
        }
477
478
        return $host;
479
    }
480
481
    /**
482
     * Populates the header collection
483
     *
484
     * @param array $headers
485
     *
486
     * @return Collection
487
     */
488
    final protected function populateHeaderCollection(array $headers): Collection
489
    {
490
        $collection = new Collection();
491
        foreach ($headers as $name => $value) {
492
            $this->checkHeaderName($name);
493
494
            $name  = (string) $name;
495
            $value = $this->getHeaderValue($value);
496
497
            $collection->set($name, $value);
498
        }
499
500
        return $collection;
501
    }
502
503
    /**
504
     * Set a valid stream
505
     *
506
     * @param StreamInterface|resource|string $body
507
     * @param string                          $mode
508
     *
509
     * @return StreamInterface
510
     */
511
    final protected function processBody(
512
        $body = "php://memory",
513
        string $mode = "r+b"
514
    ): StreamInterface {
515
        if (is_object($body) && $body instanceof StreamInterface) {
516
            return $body;
517
        }
518
519
        if (!is_string($body) && !is_resource($body)) {
520
            throw new InvalidArgumentException(
521
                "Invalid stream passed as a parameter"
522
            );
523
        }
524
525
        return new Stream($body, $mode);
526
    }
527
528
    /**
529
     * Sets the headers
530
     *
531
     * @param array|object|Collection $headers
532
     *
533
     * @return Collection
534
     */
535
    final protected function processHeaders($headers): Collection
536
    {
537
        if (is_array($headers)) {
538
            $collection = $this->populateHeaderCollection($headers);
539
            $collection = $this->checkHeaderHost($collection);
540
        } else {
541
            if (!(is_object($headers) && $headers instanceof Collection)) {
542
                throw new InvalidArgumentException(
543
                    "Headers needs to be either an array " .
544
                    "or instance of Phalcon\\Collection"
545
                );
546
            }
547
548
            $collection = $headers;
549
        }
550
551
        return $collection;
552
    }
553
554
    /**
555
     * Checks the protocol
556
     *
557
     * @param string $protocol
558
     *
559
     * @return string
560
     */
561
    final protected function processProtocol($protocol = ""): string
562
    {
563
        $protocols = [
564
            "1.0" => 1,
565
            "1.1" => 1,
566
            "2.0" => 1,
567
            "3.0" => 1
568
        ];
569
570
        if (empty($protocol) || !is_string($protocol)) {
571
            throw new InvalidArgumentException("Invalid protocol value");
572
        }
573
574
        if (!isset($protocols[$protocol])) {
575
            throw new InvalidArgumentException(
576
                "Unsupported protocol " . $protocol
577
            );
578
        }
579
580
        return $protocol;
581
    }
582
}
583