Response   B
last analyzed

Complexity

Total Complexity 43

Size/Duplication

Total Lines 279
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 43
eloc 94
c 1
b 0
f 0
dl 0
loc 279
ccs 121
cts 121
cp 1
rs 8.96

29 Methods

Rating   Name   Duplication   Size   Complexity  
A getStatusCode() 0 3 1
A header() 0 5 1
A hasHeader() 0 3 1
A fromFactory() 0 3 1
A removeHeader() 0 5 1
A getHeader() 0 3 1
A getReasonPhrase() 0 3 1
A status() 0 9 2
A getProtocolVersion() 0 3 1
A wrap() 0 5 1
A unwrap() 0 3 1
A __construct() 0 4 1
A protocolVersion() 0 5 1
A headers() 0 3 1
A html() 0 3 1
A sendfile() 0 16 2
A getBody() 0 3 1
A write() 0 5 1
A setStringBody() 0 18 3
A redirect() 0 6 1
A json() 0 13 2
A validateFile() 0 4 2
A body() 0 15 5
A file() 0 27 2
A download() 0 13 2
A text() 0 3 1
A withContentType() 0 15 2
A setStreamBody() 0 5 1
A setResourceBody() 0 9 2

How to fix   Complexity   

Complex Class

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

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Conia\Http;
6
7
use Conia\Http\Exception\FileNotFoundException;
8
use Conia\Http\Exception\RuntimeException;
9
use finfo;
10
use Psr\Http\Message\ResponseFactoryInterface as PsrResponseFactory;
11
use Psr\Http\Message\ResponseInterface as PsrResponse;
12
use Psr\Http\Message\StreamFactoryInterface as PsrStreamFactory;
13
use Psr\Http\Message\StreamInterface as PsrStream;
14
use Stringable;
15
use Traversable;
16
17
/** @psalm-api */
18
class Response
19
{
20 31
    public function __construct(
21
        protected PsrResponse $psrResponse,
22
        protected readonly PsrStreamFactory|null $streamFactory = null,
23
    ) {
24 31
    }
25
26 12
    public static function fromFactory(PsrResponseFactory $responseFactory, PsrStreamFactory $streamFactory): self
27
    {
28 12
        return new self($responseFactory->createResponse(), $streamFactory);
29
    }
30
31 2
    public function unwrap(): PsrResponse
32
    {
33 2
        return $this->psrResponse;
34
    }
35
36 1
    public function wrap(PsrResponse $response): static
37
    {
38 1
        $this->psrResponse = $response;
39
40 1
        return $this;
41
    }
42
43 4
    public function status(int $statusCode, ?string $reasonPhrase = null): static
44
    {
45 4
        if (empty($reasonPhrase)) {
46 3
            $this->psrResponse = $this->psrResponse->withStatus($statusCode);
47
        } else {
48 1
            $this->psrResponse = $this->psrResponse->withStatus($statusCode, $reasonPhrase);
49
        }
50
51 4
        return $this;
52
    }
53
54 5
    public function getStatusCode(): int
55
    {
56 5
        return $this->psrResponse->getStatusCode();
57
    }
58
59 3
    public function getReasonPhrase(): string
60
    {
61 3
        return $this->psrResponse->getReasonPhrase();
62
    }
63
64 1
    public function getProtocolVersion(): string
65
    {
66 1
        return $this->psrResponse->getProtocolVersion();
67
    }
68
69 1
    public function protocolVersion(string $protocol): static
70
    {
71 1
        $this->psrResponse = $this->psrResponse->withProtocolVersion($protocol);
72
73 1
        return $this;
74
    }
75
76 7
    public function header(string $name, string $value): static
77
    {
78 7
        $this->psrResponse = $this->psrResponse->withAddedHeader($name, $value);
79
80 7
        return $this;
81
    }
82
83 1
    public function removeHeader(string $name): static
84
    {
85 1
        $this->psrResponse = $this->psrResponse->withoutHeader($name);
86
87 1
        return $this;
88
    }
89
90 1
    public function headers(): array
91
    {
92 1
        return $this->psrResponse->getHeaders();
93
    }
94
95 15
    public function getHeader(string $name): array
96
    {
97 15
        return $this->psrResponse->getHeader($name);
98
    }
99
100 2
    public function hasHeader(string $name): bool
101
    {
102 2
        return $this->psrResponse->hasHeader($name);
103
    }
104
105 15
    public function body(mixed $body): static
106
    {
107 15
        if ($body instanceof PsrStream) {
108 4
            return $this->setStreamBody($body);
109
        }
110
111 12
        if (is_string($body) || $body instanceof Stringable) {
112 9
            return $this->setStringBody((string)$body);
113
        }
114
115 3
        if (is_resource($body)) {
116 2
            return $this->setResourceBody($body);
117
        }
118
119 1
        throw new RuntimeException('Only strings, Stringable or resources are allowed to create streams!');
120
    }
121
122 9
    protected function setStringBody(string $body): static
123
    {
124 9
        if ($this->streamFactory) {
125 6
            $this->psrResponse = $this->psrResponse->withBody($this->streamFactory->createStream($body));
126
127 6
            return $this;
128
        }
129
130 3
        $stream = $this->psrResponse->getBody();
131
132 3
        if ($stream->isWritable()) {
133 2
            $stream->rewind();
134 2
            $stream->write($body);
135
136 2
            return $this;
137
        }
138
139 1
        throw new RuntimeException('The response body is not writable!');
140
    }
141
142 4
    protected function setStreamBody(PsrStream $body): static
143
    {
144 4
        $this->psrResponse = $this->psrResponse->withBody($body);
145
146 4
        return $this;
147
    }
148
149
    /**
150
     * @param resource $body
151
     */
152 2
    protected function setResourceBody(mixed $body): static
153
    {
154 2
        if ($this->streamFactory) {
155 1
            $this->psrResponse = $this->psrResponse->withBody($this->streamFactory->createStreamFromResource($body));
156
157 1
            return $this;
158
        }
159
160 1
        throw new RuntimeException('No factory available to create stream from resource!');
161
    }
162
163 13
    public function getBody(): PsrStream
164
    {
165 13
        return $this->psrResponse->getBody();
166
    }
167
168 1
    public function write(string $content): static
169
    {
170 1
        $this->psrResponse->getBody()->write($content);
171
172 1
        return $this;
173
    }
174
175 2
    public function redirect(string $url, int $code = 302): static
176
    {
177 2
        $this->header('Location', $url);
178 2
        $this->status($code);
179
180 2
        return $this;
181
    }
182
183 10
    public function withContentType(
184
        string $contentType,
185
        mixed $body = null,
186
        int $code = 200,
187
        string $reasonPhrase = ''
188
    ): static {
189 10
        $this->psrResponse = $this->psrResponse
190 10
            ->withStatus($code, $reasonPhrase)
191 10
            ->withAddedHeader('Content-Type', $contentType);
192
193 10
        if ($body) {
194 10
            $this->body($body);
195
        }
196
197 8
        return $this;
198
    }
199
200
    /**
201
     * @param null|PsrStream|resource|string $body
202
     */
203 2
    public function html(mixed $body = null, int $code = 200, string $reasonPhrase = ''): static
204
    {
205 2
        return $this->withContentType('text/html', $body, $code, $reasonPhrase);
206
    }
207
208
    /**
209
     * @param null|PsrStream|resource|string $body
210
     */
211 1
    public function text(mixed $body = null, int $code = 200, string $reasonPhrase = ''): static
212
    {
213 1
        return $this->withContentType('text/plain', $body, $code, $reasonPhrase);
214
    }
215
216 2
    public function json(
217
        mixed $data,
218
        int $code = 200,
219
        string $reasonPhrase = '',
220
        int $flags = JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR,
221
    ): static {
222 2
        if ($data instanceof Traversable) {
223 1
            $body = json_encode(iterator_to_array($data), $flags);
224
        } else {
225 1
            $body = json_encode($data, $flags);
226
        }
227
228 2
        return $this->withContentType('application/json', $body, $code, $reasonPhrase);
229
    }
230
231 4
    public function file(
232
        string $file,
233
        int $code = 200,
234
        string $reasonPhrase = '',
235
    ): static {
236 4
        $this->validateFile($file);
237
238 3
        $finfo = new finfo(FILEINFO_MIME_TYPE);
239 3
        $contentType = $finfo->file($file);
240 3
        $finfo = new finfo(FILEINFO_MIME_ENCODING);
241 3
        $encoding = $finfo->file($file);
242 3
        assert(isset($this->streamFactory));
243 3
        $stream = $this->streamFactory->createStreamFromFile($file, 'rb');
0 ignored issues
show
Bug introduced by
The method createStreamFromFile() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

243
        /** @scrutinizer ignore-call */ 
244
        $stream = $this->streamFactory->createStreamFromFile($file, 'rb');

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
244
245 3
        $this->psrResponse = $this->psrResponse
246 3
            ->withStatus($code, $reasonPhrase)
247 3
            ->withAddedHeader('Content-Type', $contentType)
248 3
            ->withAddedHeader('Content-Transfer-Encoding', $encoding)
249 3
            ->withBody($stream);
250
251 3
        $size = $stream->getSize();
252
253 3
        if (!is_null($size)) {
254 3
            $this->psrResponse = $this->psrResponse->withAddedHeader('Content-Length', (string)$size);
255
        }
256
257 3
        return $this;
258
    }
259
260 2
    public function download(
261
        string $file,
262
        string $newName = '',
263
        int $code = 200,
264
        string $reasonPhrase = '',
265
    ): static {
266 2
        $response = $this->file($file, $code, $reasonPhrase);
267 2
        $response->header(
268 2
            'Content-Disposition',
269 2
            'attachment; filename="' . ($newName ?: basename($file)) . '"'
270 2
        );
271
272 2
        return $response;
273
    }
274
275 1
    public function sendfile(
276
        string $file,
277
        int $code = 200,
278
        string $reasonPhrase = '',
279
    ): static {
280 1
        $this->validateFile($file);
281 1
        $server = strtolower($_SERVER['SERVER_SOFTWARE'] ?? '');
282 1
        $this->psrResponse = $this->psrResponse->withStatus($code, $reasonPhrase);
283
284 1
        if (strpos($server, 'nginx') !== false) {
285 1
            $this->psrResponse = $this->psrResponse->withAddedHeader('X-Accel-Redirect', $file);
286
        } else {
287 1
            $this->psrResponse = $this->psrResponse->withAddedHeader('X-Sendfile', $file);
288
        }
289
290 1
        return $this;
291
    }
292
293 5
    protected function validateFile(string $file): void
294
    {
295 5
        if (!is_file($file)) {
296 1
            throw new FileNotFoundException('File not found: ' . $file);
297
        }
298
    }
299
}
300