ServerRequest   A
last analyzed

Complexity

Total Complexity 41

Size/Duplication

Total Lines 202
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 8
Bugs 0 Features 1
Metric Value
eloc 84
c 8
b 0
f 1
dl 0
loc 202
ccs 87
cts 87
cp 1
rs 9.1199
wmc 41

18 Methods

Rating   Name   Duplication   Size   Complexity  
A isXHR() 0 3 1
A withoutAttribute() 0 5 1
A getParsedBody() 0 9 3
A getAttributes() 0 3 1
A withParsedBody() 0 23 6
A withAttribute() 0 5 1
A buildUri() 0 13 4
A withQueryParams() 0 5 1
A getQueryParams() 0 3 1
A getServerParams() 0 3 1
A __construct() 0 6 1
A withAttributes() 0 7 2
A getAttribute() 0 3 1
A extractHttpHeaders() 0 16 5
A useOnlyPost() 0 8 4
A getRawInput() 0 3 2
A parseInput() 0 9 3
A extractServerData() 0 11 3

How to fix   Complexity   

Complex Class

Complex classes like ServerRequest 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 ServerRequest, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/*
4
 * This file is part of the Koded package.
5
 *
6
 * (c) Mihail Binev <[email protected]>
7
 *
8
 * Please view the LICENSE distributed with this source code
9
 * for the full copyright and license information.
10
 *
11
 */
12
13
namespace Koded\Http;
14
15
use Koded\Http\Interfaces\Request;
16
use Psr\Http\Message\ServerRequestInterface;
17
18
class ServerRequest extends ClientRequest implements Request
19
{
20
    use CookieTrait, FilesTrait, ValidatableTrait;
21
22
    protected string $serverSoftware = '';
23
    protected array  $attributes     = [];
24
    protected array  $queryParams    = [];
25
26
    protected object|array|null $parsedBody = null;
27
28
    /**
29
     * ServerRequest constructor.
30
     *
31
     * @param array $attributes
32
     */
33
    public function __construct(array $attributes = [])
34
    {
35
        parent::__construct($_SERVER['REQUEST_METHOD'] ?? Request::GET, $this->buildUri());
36
        $this->attributes = $attributes;
37
        $this->extractHttpHeaders($_SERVER);
38
        $this->extractServerData($_SERVER);
39
    }
40
41 52
    public function getServerParams(): array
42
    {
43 52
        return $_SERVER;
44 52
    }
45 52
46 52
    public function getQueryParams(): array
47 52
    {
48
        return $this->queryParams;
49 3
    }
50
51 3
    public function withQueryParams(array $query): static
52
    {
53
        $instance              = clone $this;
54 3
        $instance->queryParams = \array_merge($instance->queryParams, $query);
55
        return $instance;
56 3
    }
57
58
    public function getParsedBody(): object|array|null
59 2
    {
60
        if ($this->useOnlyPost()) {
61 2
            return $_POST;
62 2
        }
63
        if (false === empty($_POST)) {
64 2
            return $_POST;
65
        }
66
        return $this->parsedBody;
67 17
    }
68
69 17
    public function withParsedBody($data): static
70 2
    {
71
        $instance = clone $this;
72
        if ($this->useOnlyPost()) {
73 15
            $instance->parsedBody = $_POST;
74 5
            return $instance;
75
        }
76
        // If nothing is available for the body
77 10
        if (null === $data) {
78
            $instance->parsedBody = null;
79
            return $instance;
80 13
        }
81
        // Supports array or iterable object
82 13
        if (\is_iterable($data)) {
83
            $instance->parsedBody = \is_array($data) ? $data : \iterator_to_array($data);
84 13
            return $instance;
85 1
        }
86
        if (\is_object($data)) {
87 1
            $instance->parsedBody = $data;
88
            return $instance;
89
        }
90
        throw new \InvalidArgumentException(
91 12
            \sprintf('Unsupported data provided (%s), Expects NULL, array or iterable', \gettype($data))
92 2
        );
93
    }
94 2
95
    public function getAttributes(): array
96
    {
97
        return $this->attributes;
98 10
    }
99 4
100
    public function getAttribute($name, $default = null): mixed
101 4
    {
102
        return $this->attributes[$name] ?? $default;
103
    }
104 6
105 1
    public function withAttribute($name, $value): static
106
    {
107 1
        $instance                    = clone $this;
108
        $instance->attributes[$name] = $value;
109
        return $instance;
110 5
    }
111 5
112
    public function withoutAttribute($name): static
113
    {
114
        $instance = clone $this;
115 3
        unset($instance->attributes[$name]);
116
        return $instance;
117 3
    }
118
119
    public function withAttributes(array $attributes): static
120 3
    {
121
        $instance = clone $this;
122 3
        foreach ($attributes as $name => $value) {
123
            $instance->attributes[$name] = $value;
124
        }
125 4
        return $instance;
126
    }
127 4
128 4
    public function isXHR(): bool
129
    {
130 4
        return 'XMLHTTPREQUEST' === \strtoupper($_SERVER['HTTP_X_REQUESTED_WITH'] ?? '');
131
    }
132
133 2
    protected function buildUri(): Uri
134
    {
135 2
        if (\strpos($_SERVER['REQUEST_URI'] ?? '', '://')) {
136 2
            return new Uri($_SERVER['REQUEST_URI']);
137
        }
138 2
        if ($host = $_SERVER['SERVER_NAME'] ?? $_SERVER['SERVER_ADDR'] ?? '') {
139
            return new Uri('http' . ($_SERVER['HTTPS'] ?? $_SERVER['HTTP_X_FORWARDED_PROTO'] ?? false ? 's' : '')
140
                . '://' . $host
141 1
                . ':' . ($_SERVER['SERVER_PORT'] ?? 80)
142
                . ($_SERVER['REQUEST_URI'] ?? '')
143 1
            );
144
        }
145 1
        return new Uri($_SERVER['REQUEST_URI'] ?? '');
146 1
    }
147
148
    protected function extractHttpHeaders(array $server): void
149 1
    {
150
        foreach ($server as $k => $v) {
151
            // Calisthenics :)
152 2
            \str_starts_with($k, 'HTTP_') && $this->normalizeHeader(\str_replace('HTTP_', '', $k), $v, false);
153
        }
154 2
        if (isset($server['HTTP_IF_NONE_MATCH'])) {
155
            // ETag workaround for various broken Apache2 versions
156
            $this->headers['ETag']    = \str_replace('-gzip', '', $server['HTTP_IF_NONE_MATCH']);
157 52
            $this->headersMap['etag'] = 'ETag';
158
        }
159 52
        if (isset($server['CONTENT_TYPE'])) {
160 1
            $this->headers['Content-Type']    = \strtolower($server['CONTENT_TYPE']);
161
            $this->headersMap['content-type'] = 'Content-Type';
162
        }
163 52
        $this->setHost();
164 50
    }
165 50
166 50
    protected function extractServerData(array $server): void
167 50
    {
168
        $this->protocolVersion = \str_ireplace('HTTP/', '', $server['SERVER_PROTOCOL'] ?? $this->protocolVersion);
169
        $this->serverSoftware  = $server['SERVER_SOFTWARE'] ?? '';
170
        $this->queryParams     = $_GET;
171 5
        $this->cookieParams    = $_COOKIE;
172
        if (false === $this->isSafeMethod()) {
173
            $this->parseInput();
174 52
        }
175
        if ($_FILES) {
176 52
            $this->uploadedFiles = $this->parseUploadedFiles($_FILES);
177
        }
178 52
    }
179
180
    /**
181 52
     * Per recommendation:
182 52
     *
183
     * @return bool If the request Content-Type is either
184 52
     * application/x-www-form-urlencoded or multipart/form-data
185
     * and the request method is POST,
186 22
     * then it MUST return the contents of $_POST
187 22
     * @see ServerRequestInterface::withParsedBody()
188
     *
189
     * @see ServerRequestInterface::getParsedBody()
190 52
     */
191 1
    protected function useOnlyPost(): bool
192 1
    {
193
        if (empty($contentType = $this->getHeaderLine('Content-Type'))) {
194
            return false;
195 52
        }
196 52
        return $this->method === self::POST && (
197
            \str_contains('application/x-www-form-urlencoded', $contentType) ||
198 52
            \str_contains('multipart/form-data', $contentType));
199
    }
200 52
201 52
    /**
202 52
     * Try to unserialize a JSON string or form encoded request body.
203 52
     * Very useful if JavaScript app stringify objects in AJAX requests.
204
     */
205 52
    protected function parseInput(): void
206 22
    {
207
        if (empty($input = $this->getRawInput())) {
208
            return;
209 52
        }
210 4
        // Try JSON deserialization
211
        $this->parsedBody = \json_decode($input, true, 512, JSON_BIGINT_AS_STRING);
212 52
        if (null === $this->parsedBody) {
213
            \parse_str($input, $this->parsedBody);
214
        }
215
    }
216
217
    protected function getRawInput(): string
218
    {
219
        return \file_get_contents('php://input') ?: '';
220
    }
221
}
222