Completed
Push — master ( d604f1...f5591f )
by Oscar
03:00
created

ErrorHandler::statusCode()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
c 1
b 0
f 1
dl 0
loc 6
rs 9.4285
cc 1
eloc 3
nc 1
nop 1
1
<?php
2
3
namespace Psr7Middlewares\Middleware;
4
5
use Psr7Middlewares\Utils;
6
use Psr\Http\Message\ServerRequestInterface;
7
use Psr\Http\Message\ResponseInterface;
8
9
/**
10
 * Middleware to handle php errors and exceptions.
11
 */
12
class ErrorHandler
13
{
14
    const KEY = 'EXCEPTION';
15
16
    use Utils\CallableTrait;
17
    use Utils\AttributeTrait;
18
    use Utils\StreamTrait;
19
20
    /**
21
     * @var callable|string The handler used
22
     */
23
    private $handler;
24
25
    /**
26
     * @var callable|null The status code validator
27
     */
28
    private $statusCodeValidator;
29
30
    /**
31
     * @var bool Whether or not catch exceptions
32
     */
33
    private $catchExceptions = false;
34
35
    /**
36
     * Returns the exception throwed.
37
     *
38
     * @param ServerRequestInterface $request
39
     *
40
     * @return \Exception|null
41
     */
42
    public static function getException(ServerRequestInterface $request)
43
    {
44
        return self::getAttribute($request, self::KEY);
45
    }
46
47
    /**
48
     * Constructor.
49
     *
50
     * @param callable|string|null $handler
51
     */
52
    public function __construct($handler = null)
53
    {
54
        $this->handler = $handler;
55
    }
56
57
    /**
58
     * Configure the catchExceptions.
59
     *
60
     * @param bool $catch
61
     *
62
     * @return self
63
     */
64
    public function catchExceptions($catch = true)
65
    {
66
        $this->catchExceptions = (boolean) $catch;
67
68
        return $this;
69
    }
70
71
    /**
72
     * Configure the status code validator.
73
     *
74
     * @param callable $statusCodeValidator
75
     *
76
     * @return self
77
     */
78
    public function statusCode(callable $statusCodeValidator)
79
    {
80
        $this->statusCodeValidator = $statusCodeValidator;
81
82
        return $this;
83
    }
84
85
    /**
86
     * Execute the middleware.
87
     *
88
     * @param ServerRequestInterface $request
89
     * @param ResponseInterface      $response
90
     * @param callable               $next
91
     *
92
     * @return ResponseInterface
93
     */
94
    public function __invoke(ServerRequestInterface $request, ResponseInterface $response, callable $next)
95
    {
96
        ob_start();
97
        $level = ob_get_level();
98
99
        try {
100
            $response = $next($request, $response);
101
        } catch (\Throwable $exception) {
0 ignored issues
show
Bug introduced by
The class Throwable does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
102
            if (!$this->catchExceptions) {
103
                throw $exception;
104
            }
105
106
            $request = self::setAttribute($request, self::KEY, $exception);
107
            $response = $response->withStatus(500);
108
        } catch (\Exception $exception) {
109
            if (!$this->catchExceptions) {
110
                throw $exception;
111
            }
112
113
            $request = self::setAttribute($request, self::KEY, $exception);
114
            $response = $response->withStatus(500);
115
        } finally {
116
            Utils\Helpers::getOutput($level);
117
        }
118
119
        if ($this->isError($response->getStatusCode())) {
120
            $callable = $this->handler ?: [$this, 'defaultHandler'];
121
122
            return $this->executeCallable($callable, $request, $response->withBody(self::createStream()));
123
        }
124
125
        return $response;
126
    }
127
128
    /**
129
     * Check whether the status code represents an error or not
130
     * 
131
     * @param int $statusCode
132
     * 
133
     * @return bool
134
     */
135
    private function isError($statusCode)
136
    {
137
        if ($this->statusCodeValidator) {
138
            return call_user_func($this->statusCodeValidator, $statusCode);
139
        }
140
141
        return $statusCode >= 400 && $statusCode < 600;
142
    }
143
144
    /**
145
     * Default handler.
146
     * 
147
     * @param ServerRequestInterface $request
148
     * @param ResponseInterface      $response
149
     * 
150
     * @return string
151
     */
152
    private function defaultHandler(ServerRequestInterface $request, ResponseInterface $response)
153
    {
154
        $statusCode = $response->getStatusCode();
155
        $exception = self::getException($request);
156
        $message = $exception ? $exception->getMessage() : '';
157
158
        switch (Utils\Helpers::getMimeType($response)) {
159
            case 'text/plain':
160
            case 'text/css':
161
            case 'text/javascript':
162
                return self::errorText($statusCode, $message);
163
164
            case 'image/jpeg':
165
                return self::errorImage($statusCode, $message, 'imagejpeg');
166
167
            case 'image/gif':
168
                return self::errorImage($statusCode, $message, 'imagegif');
169
170
            case 'image/png':
171
                return self::errorImage($statusCode, $message, 'imagepng');
172
173
            case 'image/svg+xml':
174
                return self::errorSvg($statusCode, $message);
175
176
            case 'application/json':
177
                return self::errorJson($statusCode, $message);
178
179
            case 'text/xml':
180
                return self::errorXml($statusCode, $message);
181
182
            default:
183
                return self::errorHtml($statusCode, $message);
184
        }
185
    }
186
187
    /**
188
     * Print the error as plain text.
189
     * 
190
     * @param int    $statusCode
191
     * @param string $message
192
     * 
193
     * @return string
194
     */
195
    private static function errorText($statusCode, $message)
196
    {
197
        return sprintf("Error %s\n%s", $statusCode, $message);
198
    }
199
200
    /**
201
     * Print the error as svg image.
202
     * 
203
     * @param int    $statusCode
204
     * @param string $message
205
     * 
206
     * @return string
207
     */
208
    private static function errorSvg($statusCode, $message)
209
    {
210
        return <<<EOT
211
<svg xmlns="http://www.w3.org/2000/svg" width="200" height="50" viewBox="0 0 200 50">
212
    <text x="20" y="30" font-family="sans-serif" title="{$message}">
213
        Error {$statusCode}
214
    </text>
215
</svg>
216
EOT;
217
    }
218
219
    /**
220
     * Print the error as html.
221
     * 
222
     * @param int    $statusCode
223
     * @param string $message
224
     * 
225
     * @return string
226
     */
227
    private static function errorHtml($statusCode, $message)
228
    {
229
        return <<<EOT
230
<!DOCTYPE html>
231
<html>
232
<head>
233
    <meta charset="utf-8">
234
    <title>Error {$statusCode}</title>
235
    <style>html{font-family: sans-serif;}</style>
236
    <meta name="viewport" content="width=device-width, initial-scale=1">
237
</head>
238
<body>
239
    <h1>Error {$statusCode}</h1>
240
    {$message}
241
</body>
242
</html>
243
EOT;
244
    }
245
246
    /**
247
     * Print the error as image.
248
     * 
249
     * @param int    $statusCode
250
     * @param string $message
251
     * @param string $output
252
     * 
253
     * @return string
254
     */
255
    private static function errorImage($statusCode, $message, $output)
256
    {
257
        $size = 200;
258
        $image = imagecreatetruecolor($size, $size);
259
        $textColor = imagecolorallocate($image, 255, 255, 255);
260
        imagestring($image, 5, 10, 10, "Error {$statusCode}", $textColor);
261
262
        foreach (str_split($message, intval($size / 10)) as $line => $text) {
263
            imagestring($image, 5, 10, ($line * 18) + 28, $text, $textColor);
264
        }
265
266
        ob_start();
267
        $output($image);
268
269
        return ob_get_clean();
270
    }
271
272
    /**
273
     * Print the error as json.
274
     * 
275
     * @param int    $statusCode
276
     * @param string $message
277
     * 
278
     * @return string
279
     */
280
    private static function errorJson($statusCode, $message)
281
    {
282
        $output = ['error' => $statusCode];
283
284
        if (!empty($message)) {
285
            $output['message'] = $message;
286
        }
287
288
        return json_encode($output);
289
    }
290
291
    /**
292
     * Print the error as xml.
293
     * 
294
     * @param int    $statusCode
295
     * @param string $message
296
     * 
297
     * @return string
298
     */
299
    private static function errorXml($statusCode, $message)
300
    {
301
        return <<<EOT
302
<?xml version="1.0" encoding="UTF-8"?>
303
<error>
304
    <code>{$statusCode}</code>
305
    <message>{$message}</message>
306
</error>
307
EOT;
308
    }
309
}
310