Code Coverage
 
Lines
Branches
Paths
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
106 / 106
98.61% covered (success)
98.61%
71 / 72
76.56% covered (warning)
76.56%
49 / 64
96.55% covered (success)
96.55%
28 / 29
CRAP
0.00% covered (danger)
0.00%
0 / 1
Response
100.00% covered (success)
100.00%
106 / 106
98.61% covered (success)
98.61%
71 / 72
76.56% covered (warning)
76.56%
49 / 64
100.00% covered (success)
100.00%
29 / 29
89.16
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 create
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 unwrap
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 wrap
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 status
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 getStatusCode
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getReasonPhrase
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getProtocolVersion
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 protocolVersion
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 header
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 removeHeader
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 headers
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getHeader
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasHeader
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 body
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
9 / 9
71.43% covered (warning)
71.43%
5 / 7
100.00% covered (success)
100.00%
1 / 1
5.58
 setStringBody
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 setStreamBody
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setResourceBody
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 getBody
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 write
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 redirect
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 withContentType
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
3 / 3
50.00% covered (danger)
50.00%
1 / 2
100.00% covered (success)
100.00%
1 / 1
2.50
 html
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 text
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 json
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 file
100.00% covered (success)
100.00%
26 / 26
100.00% covered (success)
100.00%
14 / 14
50.00% covered (danger)
50.00%
10 / 20
100.00% covered (success)
100.00%
1 / 1
30.00
 download
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 sendfile
100.00% covered (success)
100.00%
7 / 7
85.71% covered (warning)
85.71%
6 / 7
50.00% covered (danger)
50.00%
2 / 4
100.00% covered (success)
100.00%
1 / 1
2.50
 validateFile
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2
3declare(strict_types=1);
4
5namespace Celemas\Core;
6
7use Celemas\Core\Exception\FileNotFoundException;
8use Celemas\Core\Exception\RuntimeException;
9use Celemas\Core\Factory\Factory;
10use Celemas\Router\ResponseWrapper;
11use finfo;
12use Override;
13use Psr\Http\Message\ResponseInterface as PsrResponse;
14use Psr\Http\Message\StreamFactoryInterface as PsrStreamFactory;
15use Psr\Http\Message\StreamInterface as PsrStream;
16use Stringable;
17use Traversable;
18
19/** @api */
20class Response implements ResponseWrapper
21{
22    public function __construct(
23        protected PsrResponse $psrResponse,
24        protected readonly ?PsrStreamFactory $streamFactory = null,
25    ) {}
26
27    public static function create(Factory $factory): self
28    {
29        return new self($factory->response(), $factory->streamFactory());
30    }
31
32    #[Override]
33    public function unwrap(): PsrResponse
34    {
35        return $this->psrResponse;
36    }
37
38    public function wrap(PsrResponse $response): static
39    {
40        $this->psrResponse = $response;
41
42        return $this;
43    }
44
45    public function status(int $statusCode, string $reasonPhrase = ''): static
46    {
47        if ($reasonPhrase === '') {
48            $this->psrResponse = $this->psrResponse->withStatus($statusCode);
49        } else {
50            $this->psrResponse = $this->psrResponse->withStatus($statusCode, $reasonPhrase);
51        }
52
53        return $this;
54    }
55
56    public function getStatusCode(): int
57    {
58        return $this->psrResponse->getStatusCode();
59    }
60
61    public function getReasonPhrase(): string
62    {
63        return $this->psrResponse->getReasonPhrase();
64    }
65
66    public function getProtocolVersion(): string
67    {
68        return $this->psrResponse->getProtocolVersion();
69    }
70
71    public function protocolVersion(string $protocol): static
72    {
73        $this->psrResponse = $this->psrResponse->withProtocolVersion($protocol);
74
75        return $this;
76    }
77
78    public function header(string $name, string $value): static
79    {
80        $this->psrResponse = $this->psrResponse->withAddedHeader($name, $value);
81
82        return $this;
83    }
84
85    public function removeHeader(string $name): static
86    {
87        $this->psrResponse = $this->psrResponse->withoutHeader($name);
88
89        return $this;
90    }
91
92    public function headers(): array
93    {
94        return $this->psrResponse->getHeaders();
95    }
96
97    public function getHeader(string $name): array
98    {
99        return $this->psrResponse->getHeader($name);
100    }
101
102    public function hasHeader(string $name): bool
103    {
104        return $this->psrResponse->hasHeader($name);
105    }
106
107    public function body(mixed $body): static
108    {
109        if ($body instanceof PsrStream) {
110            return $this->setStreamBody($body);
111        }
112
113        if (is_string($body) || $body instanceof Stringable) {
114            return $this->setStringBody((string) $body);
115        }
116
117        if (is_resource($body)) {
118            return $this->setResourceBody($body);
119        }
120
121        throw new RuntimeException(
122            'Only strings, Stringable or resources are allowed to create streams!',
123        );
124    }
125
126    protected function setStringBody(string $body): static
127    {
128        if ($this->streamFactory) {
129            $this->psrResponse = $this->psrResponse->withBody($this->streamFactory->createStream($body));
130
131            return $this;
132        }
133
134        $stream = $this->psrResponse->getBody();
135
136        if ($stream->isWritable()) {
137            $stream->rewind();
138            $stream->write($body);
139
140            return $this;
141        }
142
143        throw new RuntimeException('The response body is not writable!');
144    }
145
146    protected function setStreamBody(PsrStream $body): static
147    {
148        $this->psrResponse = $this->psrResponse->withBody($body);
149
150        return $this;
151    }
152
153    /**
154     * @param resource $body
155     */
156    protected function setResourceBody(mixed $body): static
157    {
158        if ($this->streamFactory) {
159            $this->psrResponse = $this->psrResponse->withBody($this->streamFactory->createStreamFromResource(
160                $body,
161            ));
162
163            return $this;
164        }
165
166        throw new RuntimeException('No factory available to create stream from resource!');
167    }
168
169    public function getBody(): PsrStream
170    {
171        return $this->psrResponse->getBody();
172    }
173
174    public function write(string $content): static
175    {
176        $this->psrResponse->getBody()->write($content);
177
178        return $this;
179    }
180
181    public function redirect(string $url, int $code = 302): static
182    {
183        $this->header('Location', $url);
184        $this->status($code);
185
186        return $this;
187    }
188
189    public function withContentType(
190        string $contentType,
191        mixed $body = null,
192        int $code = 200,
193        string $reasonPhrase = '',
194    ): static {
195        $this->psrResponse = $this->psrResponse
196            ->withStatus($code, $reasonPhrase)
197            ->withAddedHeader('Content-Type', $contentType);
198
199        if ($body !== null) {
200            $this->body($body);
201        }
202
203        return $this;
204    }
205
206    /**
207     * @param null|PsrStream|resource|string $body
208     */
209    public function html(mixed $body = null, int $code = 200, string $reasonPhrase = ''): static
210    {
211        return $this->withContentType('text/html', $body, $code, $reasonPhrase);
212    }
213
214    /**
215     * @param null|PsrStream|resource|string $body
216     */
217    public function text(mixed $body = null, int $code = 200, string $reasonPhrase = ''): static
218    {
219        return $this->withContentType('text/plain', $body, $code, $reasonPhrase);
220    }
221
222    public function json(
223        mixed $data,
224        int $code = 200,
225        string $reasonPhrase = '',
226        int $flags = JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR,
227    ): static {
228        if ($data instanceof Traversable) {
229            $body = json_encode(iterator_to_array($data), $flags);
230        } else {
231            $body = json_encode($data, $flags);
232        }
233
234        return $this->withContentType('application/json', $body, $code, $reasonPhrase);
235    }
236
237    public function file(
238        string $file,
239        int $code = 200,
240        string $reasonPhrase = '',
241    ): static {
242        $this->validateFile($file);
243
244        $finfo = new finfo(FILEINFO_MIME_TYPE);
245        $contentType = match (strtolower(pathinfo($file, PATHINFO_EXTENSION))) {
246            'js' => 'text/javascript',
247            'css' => 'text/css',
248            'json' => 'application/json',
249            'html' => 'text/html',
250            'md' => 'text/markdown',
251            'markdown' => 'text/markdown',
252            'csv' => 'text/csv',
253            'xml' => 'application/xml',
254            'xhtml' => 'application/xhtml+xml',
255            default => $finfo->file($file),
256        };
257
258        $finfo = new finfo(FILEINFO_MIME_ENCODING);
259        $encoding = $finfo->file($file);
260        assert(isset($this->streamFactory), 'Stream factory must be set before creating a file stream');
261        $stream = $this->streamFactory->createStreamFromFile($file, 'rb');
262
263        $this->psrResponse = $this->psrResponse
264            ->withStatus($code, $reasonPhrase)
265            ->withAddedHeader('Content-Type', $contentType)
266            ->withAddedHeader('Content-Transfer-Encoding', $encoding)
267            ->withBody($stream);
268
269        $size = $stream->getSize();
270
271        if (!($size === null)) {
272            $this->psrResponse = $this->psrResponse->withAddedHeader('Content-Length', (string) $size);
273        }
274
275        return $this;
276    }
277
278    public function download(
279        string $file,
280        string $newName = '',
281        int $code = 200,
282        string $reasonPhrase = '',
283    ): static {
284        $response = $this->file($file, $code, $reasonPhrase);
285        $response->header(
286            'Content-Disposition',
287            'attachment; filename="' . ($newName ?: basename($file)) . '"',
288        );
289
290        return $response;
291    }
292
293    public function sendfile(
294        string $file,
295        int $code = 200,
296        string $reasonPhrase = '',
297    ): static {
298        $this->validateFile($file);
299        $server = strtolower($_SERVER['SERVER_SOFTWARE'] ?? '');
300        $this->psrResponse = $this->psrResponse->withStatus($code, $reasonPhrase);
301
302        if (str_contains($server, 'nginx')) {
303            $this->psrResponse = $this->psrResponse->withAddedHeader('X-Accel-Redirect', $file);
304        } else {
305            $this->psrResponse = $this->psrResponse->withAddedHeader('X-Sendfile', $file);
306        }
307
308        return $this;
309    }
310
311    protected function validateFile(string $file): void
312    {
313        if (!is_file($file)) {
314            throw new FileNotFoundException('File not found: ' . $file);
315        }
316    }
317}

Branches

Below are the source code lines that represent each code branch as identified by Xdebug. Please note a branch is not necessarily coterminous with a line, a line may contain multiple branches and therefore show up more than once. Please also be aware that some branches may be implicit rather than explicit, e.g. an if statement always has an else as part of its logical flow even if you didn't write one.

Response->__construct
23        protected PsrResponse $psrResponse,
24        protected readonly ?PsrStreamFactory $streamFactory = null,
25    ) {}
Response->body
107    public function body(mixed $body): static
108    {
109        if ($body instanceof PsrStream) {
110            return $this->setStreamBody($body);
113        if (is_string($body) || $body instanceof Stringable) {
113        if (is_string($body) || $body instanceof Stringable) {
113        if (is_string($body) || $body instanceof Stringable) {
114            return $this->setStringBody((string) $body);
117        if (is_resource($body)) {
118            return $this->setResourceBody($body);
121        throw new RuntimeException(
122            'Only strings, Stringable or resources are allowed to create streams!',
123        );
124    }
Response->create
27    public static function create(Factory $factory): self
28    {
29        return new self($factory->response(), $factory->streamFactory());
30    }
Response->download
279        string $file,
280        string $newName = '',
281        int $code = 200,
282        string $reasonPhrase = '',
283    ): static {
284        $response = $this->file($file, $code, $reasonPhrase);
285        $response->header(
286            'Content-Disposition',
287            'attachment; filename="' . ($newName ?: basename($file)) . '"',
288        );
289
290        return $response;
291    }
Response->file
238        string $file,
239        int $code = 200,
240        string $reasonPhrase = '',
241    ): static {
242        $this->validateFile($file);
243
244        $finfo = new finfo(FILEINFO_MIME_TYPE);
245        $contentType = match (strtolower(pathinfo($file, PATHINFO_EXTENSION))) {
246            'js' => 'text/javascript',
247            'css' => 'text/css',
248            'json' => 'application/json',
249            'html' => 'text/html',
250            'md' => 'text/markdown',
251            'markdown' => 'text/markdown',
252            'csv' => 'text/csv',
253            'xml' => 'application/xml',
254            'xhtml' => 'application/xhtml+xml',
255            default => $finfo->file($file),
255            default => $finfo->file($file),
256        };
257
258        $finfo = new finfo(FILEINFO_MIME_ENCODING);
259        $encoding = $finfo->file($file);
260        assert(isset($this->streamFactory), 'Stream factory must be set before creating a file stream');
261        $stream = $this->streamFactory->createStreamFromFile($file, 'rb');
262
263        $this->psrResponse = $this->psrResponse
264            ->withStatus($code, $reasonPhrase)
265            ->withAddedHeader('Content-Type', $contentType)
266            ->withAddedHeader('Content-Transfer-Encoding', $encoding)
267            ->withBody($stream);
268
269        $size = $stream->getSize();
270
271        if (!($size === null)) {
272            $this->psrResponse = $this->psrResponse->withAddedHeader('Content-Length', (string) $size);
273        }
274
275        return $this;
275        return $this;
276    }
Response->getBody
171        return $this->psrResponse->getBody();
172    }
Response->getHeader
97    public function getHeader(string $name): array
98    {
99        return $this->psrResponse->getHeader($name);
100    }
Response->getProtocolVersion
68        return $this->psrResponse->getProtocolVersion();
69    }
Response->getReasonPhrase
63        return $this->psrResponse->getReasonPhrase();
64    }
Response->getStatusCode
58        return $this->psrResponse->getStatusCode();
59    }
Response->hasHeader
102    public function hasHeader(string $name): bool
103    {
104        return $this->psrResponse->hasHeader($name);
105    }
Response->header
78    public function header(string $name, string $value): static
79    {
80        $this->psrResponse = $this->psrResponse->withAddedHeader($name, $value);
81
82        return $this;
83    }
Response->headers
94        return $this->psrResponse->getHeaders();
95    }
Response->html
209    public function html(mixed $body = null, int $code = 200, string $reasonPhrase = ''): static
210    {
211        return $this->withContentType('text/html', $body, $code, $reasonPhrase);
212    }
Response->json
223        mixed $data,
224        int $code = 200,
225        string $reasonPhrase = '',
226        int $flags = JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR,
227    ): static {
228        if ($data instanceof Traversable) {
228        if ($data instanceof Traversable) {
229            $body = json_encode(iterator_to_array($data), $flags);
231            $body = json_encode($data, $flags);
232        }
233
234        return $this->withContentType('application/json', $body, $code, $reasonPhrase);
234        return $this->withContentType('application/json', $body, $code, $reasonPhrase);
235    }
Response->protocolVersion
71    public function protocolVersion(string $protocol): static
72    {
73        $this->psrResponse = $this->psrResponse->withProtocolVersion($protocol);
74
75        return $this;
76    }
Response->redirect
181    public function redirect(string $url, int $code = 302): static
182    {
183        $this->header('Location', $url);
184        $this->status($code);
185
186        return $this;
187    }
Response->removeHeader
85    public function removeHeader(string $name): static
86    {
87        $this->psrResponse = $this->psrResponse->withoutHeader($name);
88
89        return $this;
90    }
Response->sendfile
294        string $file,
295        int $code = 200,
296        string $reasonPhrase = '',
297    ): static {
298        $this->validateFile($file);
299        $server = strtolower($_SERVER['SERVER_SOFTWARE'] ?? '');
300        $this->psrResponse = $this->psrResponse->withStatus($code, $reasonPhrase);
301
302        if (str_contains($server, 'nginx')) {
302        if (str_contains($server, 'nginx')) {
302        if (str_contains($server, 'nginx')) {
302        if (str_contains($server, 'nginx')) {
302        if (str_contains($server, 'nginx')) {
303            $this->psrResponse = $this->psrResponse->withAddedHeader('X-Accel-Redirect', $file);
305            $this->psrResponse = $this->psrResponse->withAddedHeader('X-Sendfile', $file);
306        }
307
308        return $this;
308        return $this;
309    }
Response->setResourceBody
156    protected function setResourceBody(mixed $body): static
157    {
158        if ($this->streamFactory) {
159            $this->psrResponse = $this->psrResponse->withBody($this->streamFactory->createStreamFromResource(
160                $body,
161            ));
162
163            return $this;
166        throw new RuntimeException('No factory available to create stream from resource!');
167    }
Response->setStreamBody
146    protected function setStreamBody(PsrStream $body): static
147    {
148        $this->psrResponse = $this->psrResponse->withBody($body);
149
150        return $this;
151    }
Response->setStringBody
126    protected function setStringBody(string $body): static
127    {
128        if ($this->streamFactory) {
129            $this->psrResponse = $this->psrResponse->withBody($this->streamFactory->createStream($body));
130
131            return $this;
134        $stream = $this->psrResponse->getBody();
135
136        if ($stream->isWritable()) {
137            $stream->rewind();
138            $stream->write($body);
139
140            return $this;
143        throw new RuntimeException('The response body is not writable!');
144    }
Response->status
45    public function status(int $statusCode, string $reasonPhrase = ''): static
46    {
47        if ($reasonPhrase === '') {
47        if ($reasonPhrase === '') {
48            $this->psrResponse = $this->psrResponse->withStatus($statusCode);
50            $this->psrResponse = $this->psrResponse->withStatus($statusCode, $reasonPhrase);
51        }
52
53        return $this;
53        return $this;
54    }
Response->text
217    public function text(mixed $body = null, int $code = 200, string $reasonPhrase = ''): static
218    {
219        return $this->withContentType('text/plain', $body, $code, $reasonPhrase);
220    }
Response->unwrap
35        return $this->psrResponse;
36    }
Response->validateFile
311    protected function validateFile(string $file): void
312    {
313        if (!is_file($file)) {
314            throw new FileNotFoundException('File not found: ' . $file);
316    }
Response->withContentType
190        string $contentType,
191        mixed $body = null,
192        int $code = 200,
193        string $reasonPhrase = '',
194    ): static {
195        $this->psrResponse = $this->psrResponse
196            ->withStatus($code, $reasonPhrase)
197            ->withAddedHeader('Content-Type', $contentType);
198
199        if ($body !== null) {
200            $this->body($body);
201        }
202
203        return $this;
203        return $this;
204    }
Response->wrap
38    public function wrap(PsrResponse $response): static
39    {
40        $this->psrResponse = $response;
41
42        return $this;
43    }
Response->write
174    public function write(string $content): static
175    {
176        $this->psrResponse->getBody()->write($content);
177
178        return $this;
179    }