Passed
Push — master ( 2a966a...079cce )
by y
01:17
created

Response   F

Complexity

Total Complexity 61

Size/Duplication

Total Lines 343
Duplicated Lines 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 133
dl 0
loc 343
rs 3.52
c 2
b 0
f 0
wmc 61

20 Methods

Rating   Name   Duplication   Size   Complexity  
A getId() 0 2 1
A setTimestamp() 0 9 2
A getTimestamp() 0 2 1
A _onShutdown() 0 10 4
A touch() 0 5 2
A __construct() 0 11 2
A offsetUnset() 0 2 1
A view() 0 4 1
A redirect() 0 5 1
A _onRender() 0 16 5
A error() 0 19 4
A mixed() 0 10 5
A setCode() 0 3 1
F file() 0 50 20
A isModified() 0 13 5
A getCode() 0 2 1
A offsetSet() 0 2 1
A isEmpty() 0 2 2
A offsetGet() 0 2 1
A offsetExists() 0 2 1

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
namespace Helix\Site;
4
5
use ArrayAccess;
6
use Helix\Site;
7
use Throwable;
8
9
/**
10
 * The response.
11
 */
12
class Response implements ArrayAccess {
13
14
    /**
15
     * @var int
16
     */
17
    protected $code = 404;
18
19
    /**
20
     * Associative values and/or enumerated literal headers.
21
     *
22
     * @var array
23
     */
24
    protected $headers = [
25
        'Accept-Ranges' => 'none',
26
        'Cache-Control' => 'no-cache'
27
    ];
28
29
    /**
30
     * `X-Response-Id`, also used in logging errors.
31
     *
32
     * @var string
33
     */
34
    protected $id;
35
36
    /**
37
     * @var Request
38
     */
39
    protected $request;
40
41
    /**
42
     * @var Site
43
     */
44
    protected $site;
45
46
    /**
47
     * Last modification time. Ignored when zero.
48
     *
49
     * @var int
50
     */
51
    protected $timestamp = 0;
52
53
    /**
54
     * @param Site $site
55
     */
56
    public function __construct (Site $site) {
57
        $this->site = $site;
58
        $this->request = $site->getRequest();
59
        $this->id = uniqid();
60
        if (isset($this->request['X-Request-Id'])) {
61
            $this['X-Request-Id'] = $this->request['X-Request-Id'];
62
        }
63
        ob_end_clean();
64
        ob_implicit_flush(); // flush() non-empty output.
65
        header_register_callback([$this, '_onRender']);
66
        register_shutdown_function([$this, '_onShutdown']);
67
    }
68
69
    /**
70
     * Injects headers before content is put in PHP's SAPI write buffer.
71
     *
72
     * @return void
73
     */
74
    public function _onRender (): void {
75
        header_remove('X-Powered-By');
76
        $this['X-Response-Id'] = $this->id;
77
        ksort($this->headers);
78
        foreach ($this->headers as $key => $value) {
79
            if (is_string($key)) {
80
                $value = "{$key}: {$value}";
81
            }
82
            header($value);
83
        }
84
        if (!$this->isModified()) {
85
            $this->setCode(304);
86
        }
87
        http_response_code($this->code);
88
        if ($this->isEmpty()) {
89
            exit;
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
90
        }
91
    }
92
93
    /**
94
     * Renders an error if nothing was written to the SAPI yet.
95
     * Logs the response to the SAPI handler if the site is in dev mode.
96
     *
97
     * @return void
98
     */
99
    public function _onShutdown (): void {
100
        if ($this->site->isDev()) {
101
            $ip = $this->request->getClient();
102
            $method = $this->request->getMethod();
103
            $path = $this->request->getPath();
104
            $line = "{$ip} [{$this->code}]: {$method} {$path}";
105
            error_log($line, 4);
106
        }
107
        if (!headers_sent()) {
108
            $this->error() and exit;
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
109
        }
110
    }
111
112
    /**
113
     * Renders an error and exits.
114
     *
115
     * @param Throwable $error Defaults to an `Error` with the current response code.
116
     */
117
    public function error (Throwable $error = null): void {
118
        $error = $error ?? new Error($this->code);
119
        $code = 500;
120
        if ($error instanceof Error) {
121
            $code = $error->getCode();
122
        }
123
        $this->setCode($code);
124
        $template = __DIR__ . '/error.phtml';
125
        if (file_exists("view/{$code}.phtml")) {
126
            $template = "view/{$code}.phtml";
127
        }
128
        elseif (file_exists('view/error.phtml')) {
129
            $template = 'view/error.phtml';
130
        }
131
        $this->view(new View($template, [
132
            'site' => $this->site,
133
            'error' => $error
134
        ]));
135
        exit;
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
136
    }
137
138
    /**
139
     * Outputs a file (or requested range) and exits.
140
     *
141
     * @param string $path
142
     * @param bool $download
143
     */
144
    public function file (string $path, bool $download = false): void {
145
        clearstatcache(true, $path);
146
        if (!file_exists($path)) {
147
            $this->setCode(404)->error() and exit;
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
148
        }
149
        if (!is_file($path) or !is_readable($path)) {
150
            $this->setCode(403)->error() and exit;
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
151
        }
152
        $fh = fopen($path, 'rb');
153
        flock($fh, LOCK_SH);
0 ignored issues
show
Bug introduced by
It seems like $fh can also be of type false; however, parameter $handle of flock() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

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

153
        flock(/** @scrutinizer ignore-type */ $fh, LOCK_SH);
Loading history...
154
        $size = filesize($path);
155
        $this->setTimestamp(filemtime($path));
156
        $this['ETag'] = $eTag = filemtime($path);
157
        $this['Accept-Ranges'] = 'bytes';
158
        if ($download) {
159
            $this['Content-Disposition'] = sprintf('attachment; filename="%s"', basename($path));
160
        }
161
        $range = $this->request['Range'];
162
        $ifRange = trim($this->request['If-Range'], '"');
163
        if (!$range or ($ifRange and $ifRange !== $eTag and strtotime($ifRange) !== $this->timestamp)) {
164
            $this->setCode(200);
165
            $this['Content-Length'] = $size;
166
            $this['Content-Type'] = mime_content_type($path);
167
            fpassthru($fh);
0 ignored issues
show
Bug introduced by
It seems like $fh can also be of type false; however, parameter $handle of fpassthru() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

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

167
            fpassthru(/** @scrutinizer ignore-type */ $fh);
Loading history...
168
            flush();
169
            exit;
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
170
        }
171
        if (preg_match('/^bytes=(\d+)?-(\d+)?$/', $range, $range, PREG_UNMATCHED_AS_NULL)) {
0 ignored issues
show
Bug introduced by
$range of type string is incompatible with the type array|null expected by parameter $matches of preg_match(). ( Ignorable by Annotation )

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

171
        if (preg_match('/^bytes=(\d+)?-(\d+)?$/', $range, /** @scrutinizer ignore-type */ $range, PREG_UNMATCHED_AS_NULL)) {
Loading history...
172
            $max = $size - 1;
173
            $start = $range[1] ?? (isset($range[2]) ? $size - $range[2] : 0);
174
            $stop = isset($range[1]) ? $range[2] ?? $max : $max;
175
            if (0 <= $start and $start <= $stop and $stop <= $max) {
176
                $this->setCode(206);
177
                $this['Content-Length'] = $length = $stop - $start + 1;
0 ignored issues
show
Unused Code introduced by
The assignment to $length is dead and can be removed.
Loading history...
178
                $this['Content-Range'] = "bytes {$start}-{$stop}/{$size}";
179
                fseek($fh, $start);
0 ignored issues
show
Bug introduced by
It seems like $fh can also be of type false; however, parameter $handle of fseek() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

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

179
                fseek(/** @scrutinizer ignore-type */ $fh, $start);
Loading history...
180
                if ($stop === $max) {
181
                    fpassthru($fh);
182
                    flush();
183
                    exit;
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
184
                }
185
                while (!feof($fh)) {
0 ignored issues
show
Bug introduced by
It seems like $fh can also be of type false; however, parameter $handle of feof() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

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

185
                while (!feof(/** @scrutinizer ignore-type */ $fh)) {
Loading history...
186
                    echo fread($fh, 8192);
0 ignored issues
show
Bug introduced by
It seems like $fh can also be of type false; however, parameter $handle of fread() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

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

186
                    echo fread(/** @scrutinizer ignore-type */ $fh, 8192);
Loading history...
187
                }
188
                flush();
189
                exit;
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
190
            }
191
        }
192
        $this['Content-Range'] = "bytes */{$size}";
193
        $this->setCode(416)->error() and exit;
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
194
    }
195
196
    /**
197
     * @return int
198
     */
199
    final public function getCode (): int {
200
        return $this->code;
201
    }
202
203
    /**
204
     * @return string
205
     */
206
    final public function getId (): string {
207
        return $this->id;
208
    }
209
210
    /**
211
     * @return int
212
     */
213
    public function getTimestamp (): int {
214
        return $this->timestamp;
215
    }
216
217
    /**
218
     * Whether the response will only consist of headers.
219
     *
220
     * @return bool
221
     */
222
    public function isEmpty (): bool {
223
        return $this->request->isHead() or in_array($this->code, [204, 205, 304]);
224
    }
225
226
    /**
227
     * Whether the response body would be considered fresh.
228
     *
229
     * @return bool
230
     */
231
    public function isModified (): bool {
232
        // Explicit 304 takes precedence over all.
233
        if ($this->code === 304) {
234
            return false;
235
        }
236
        // If-None-Match takes precedence over If-Modified-Since
237
        if ($this->request['If-None-Match']) {
238
            return !in_array($this['ETag'], str_getcsv($this->request['If-None-Match']), true);
239
        }
240
        if ($this->timestamp and $this->request['If-Modified-Since']) {
241
            return $this->timestamp > strtotime($this->request['If-Modified-Since']);
242
        }
243
        return true;
244
    }
245
246
    /**
247
     * Renders mixed content and exits.
248
     *
249
     * @param mixed $content
250
     */
251
    public function mixed ($content): void {
252
        if ($content instanceof ViewableInterface) {
253
            $this->view($content) and exit;
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
254
        }
255
        if ($content instanceof Throwable) {
256
            $this->error($content) and exit;
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
257
        }
258
        echo $content;
259
        flush();
260
        exit;
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
261
    }
262
263
    /**
264
     * @param mixed $key
265
     * @return bool
266
     */
267
    public function offsetExists ($key) {
268
        return isset($this->headers[$key]);
269
    }
270
271
    /**
272
     * @param mixed $key
273
     * @return null|string
274
     */
275
    public function offsetGet ($key) {
276
        return $this->headers[$key] ?? null;
277
    }
278
279
    /**
280
     * @param mixed $key
281
     * @param string $value
282
     */
283
    public function offsetSet ($key, $value) {
284
        $this->headers[$key] = $value;
285
    }
286
287
    /**
288
     * @param mixed $key
289
     */
290
    public function offsetUnset ($key) {
291
        unset($this->headers[$key]);
292
    }
293
294
    /**
295
     * Issues a redirect and exits.
296
     *
297
     * @param string $location
298
     * @param int $code
299
     */
300
    public function redirect (string $location, $code = 302) {
301
        $this->setCode($code);
302
        $this['Location'] = $location;
303
        flush();
304
        exit;
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
305
    }
306
307
    /**
308
     * @param int $code
309
     * @return $this
310
     */
311
    public function setCode (int $code) {
312
        $this->code = $code;
313
        return $this;
314
    }
315
316
    /**
317
     * Sets or unsets the timestamp and `Last-Modified` header.
318
     *
319
     * @param int $timestamp
320
     * @return $this
321
     */
322
    public function setTimestamp (int $timestamp) {
323
        if ($timestamp) {
324
            $this['Last-Modified'] = gmdate('D, d M Y H:i:s T', $timestamp);
325
        }
326
        else {
327
            unset($this['Last-Modified']);
328
        }
329
        $this->timestamp = $timestamp;
330
        return $this;
331
    }
332
333
    /**
334
     * Updates the modification time if the given one is greater.
335
     *
336
     * @param int $timestamp
337
     * @return $this
338
     */
339
    public function touch (int $timestamp) {
340
        if ($timestamp > $this->timestamp) {
341
            $this->setTimestamp($timestamp);
342
        }
343
        return $this;
344
    }
345
346
    /**
347
     * Renders a view and exits.
348
     *
349
     * @param ViewableInterface $view
350
     */
351
    public function view (ViewableInterface $view): void {
352
        $view->render();
353
        flush();
354
        exit;
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
355
    }
356
}