PageRenderTime 45ms CodeModel.GetById 33ms app.highlight 7ms RepoModel.GetById 1ms app.codeStats 1ms

/lib/Widget/Error.php

https://github.com/putersham/widget
PHP | 349 lines | 173 code | 47 blank | 129 comment | 31 complexity | 23ebecbc78c5d30b8fc24ba8fefb61a5 MD5 | raw file
  1<?php
  2/**
  3 * Widget Framework
  4 *
  5 * @copyright   Twin Huang
  6 * @license     http://opensource.org/licenses/mit-license.php MIT License
  7 */
  8
  9namespace Widget;
 10
 11/**
 12 * A widget that handles exception and display pretty exception message
 13 *
 14 * @property    Request $request The HTTP request widget
 15 * @property    Logger $logger The logger widget
 16 * @property    Response $response The HTTP response widget
 17 */
 18class Error extends AbstractWidget
 19{
 20    /**
 21     * The default error message display when debug is not enable
 22     *
 23     * @var string
 24     */
 25    protected $message = 'Error';
 26
 27    /**
 28     * The detail error message display when debug is not enable
 29     *
 30     * @var string
 31     */
 32    protected $detail = 'Unfortunately, an error occurred. Please try again later.';
 33
 34    /**
 35     * The detail error message display when thrown 404 exception
 36     *
 37     * @var string
 38     */
 39    protected $notFoundDetail = 'Sorry, the page you requested was not found. Please check the URL and try again.';
 40
 41    /**
 42     * Whether ignore the previous exception handler or attach it again to the
 43     * exception event
 44     *
 45     * @var bool
 46     */
 47    protected $ignorePrevHandler = false;
 48
 49    /**
 50     * The previous exception handler
 51     *
 52     * @var null|callback
 53     */
 54    protected $prevExceptionHandler;
 55
 56    /**
 57     * The custom error handlers
 58     *
 59     * @var array
 60     */
 61    protected $handlers = array(
 62        'error'     => array(),
 63        'fatal'     => array(),
 64        'notFound'  => array()
 65    );
 66
 67    /**
 68     * Constructor
 69     *
 70     * @param array $options
 71     */
 72    public function __construct($options = array())
 73    {
 74        parent::__construct($options);
 75
 76        $this->registerErrorHandler();
 77        $this->registerExceptionHandler();
 78        $this->registerFatalHandler();
 79    }
 80
 81    /**
 82     * Attach a handler to exception error
 83     *
 84     * @param callback $fn The error handler
 85     * @return Error
 86     */
 87    public function __invoke($fn)
 88    {
 89        $this->handlers['error'][] = $fn;
 90
 91        return $this;
 92    }
 93
 94    /**
 95     * Attach a handler to not found error
 96     *
 97     * @param callback $fn The error handler
 98     * @return Error
 99     */
100    public function notFound($fn)
101    {
102        $this->handlers['notFound'][] = $fn;
103
104        return $this;
105    }
106
107    /**
108     * Attach a handler to fatal error
109     *
110     * @param callback $fn The error handler
111     * @return Error
112     */
113    public function fatal($fn)
114    {
115        $this->handlers['fatal'][] = $fn;
116
117        return $this;
118    }
119
120    /**
121     * Register exception Handler
122     */
123    protected function registerExceptionHandler()
124    {
125        $this->prevExceptionHandler = set_exception_handler(array($this, 'handleException'));
126    }
127
128    /**
129     * Register error Handler
130     */
131    protected function registerErrorHandler()
132    {
133        set_error_handler(array($this, 'handleError'));
134    }
135
136    /**
137     * Detect fatal error and register fatal handler
138     */
139    protected function registerFatalHandler()
140    {
141        $error = $this;
142
143        // When shutdown, the current working directory will be set to the web
144        // server directory, store it for later use
145        $cwd = getcwd();
146
147        register_shutdown_function(function() use($error, $cwd) {
148            $e = error_get_last();
149            if (!$e || !in_array($e['type'], array(E_ERROR, E_CORE_ERROR, E_COMPILE_ERROR, E_PARSE))) {
150                // No error or not fatal error
151                return;
152            }
153
154            ob_get_length() && ob_end_clean();
155
156            // Reset the current working directory to make sure everything work as usual
157            chdir($cwd);
158
159            $exception = new \ErrorException($e['message'], $e['type'], 0, $e['file'], $e['line']);
160
161            if ($error->triggerHandler('fatal', $exception)) {
162                // Handled!
163                return;
164            }
165
166            // Fallback to error handlers
167            if ($error->triggerHandler('error', $exception)) {
168                // Handled!
169                return;
170            }
171
172            // Fallback to internal error Handlers
173            $error->internalHandleException($exception);
174        });
175    }
176
177    /**
178     * Trigger a error handler
179     *
180     * @param string $type The type of error handlers
181     * @param \Exception $exception
182     * @return bool
183     * @internal description
184     */
185    public function triggerHandler($type, \Exception $exception)
186    {
187        foreach ($this->handlers[$type] as $handler) {
188            $result = call_user_func_array($handler, array($exception, $this->widget));
189            if (true === $result) {
190                return true;
191            }
192        }
193        return false;
194    }
195
196    /**
197     * The exception handler to render pretty message
198     *
199     * @param \Exception $exception
200     */
201    public function handleException(\Exception $exception)
202    {
203        if (!$this->ignorePrevHandler && $this->prevExceptionHandler) {
204            call_user_func($this->prevExceptionHandler, $exception);
205        }
206
207        if (404 == $exception->getCode()) {
208            if ($this->triggerHandler('notFound', $exception)) {
209                return;
210            }
211        }
212
213        if (!$this->triggerHandler('error', $exception)) {
214            $this->internalHandleException($exception);
215        }
216
217        restore_exception_handler();
218    }
219
220    public function internalHandleException(\Exception $exception)
221    {
222        $debug = $this->widget->inDebug();
223        $ajax = $this->request->inAjax();
224        $code = $exception->getCode();
225
226        // HTTP status code
227        if ($code < 100 || $code > 600) {
228            $code = 500;
229        }
230
231        try {
232            // This widgets may show exception too
233            $this->response->setStatusCode($code)->send();
234            $this->logger->critical((string)$exception);
235
236            $this->renderException($exception, $debug, $ajax);
237        } catch (\Exception $e) {
238            $this->renderException($e, $debug, $ajax);
239        }
240    }
241
242    /**
243     * Render exception message
244     *
245     * @param \Exception $exception
246     * @param bool $debug Whether show debug trace
247     * @param bool $ajax Whether return json instead html string
248     */
249    public function renderException(\Exception $exception, $debug, $ajax)
250    {
251        $code       = $exception->getCode();
252        $file       = $exception->getFile();
253        $line       = $exception->getLine();
254        $class      = get_class($exception);
255        $trace      = htmlspecialchars($exception->getTraceAsString(), ENT_QUOTES);
256
257        // Prepare message
258        if ($debug || 404 == $code) {
259            $message = $exception->getMessage();
260        } else {
261            $message = $this->message;
262        }
263        $message = htmlspecialchars($message, ENT_QUOTES);
264
265        // Prepare detail message
266        if ($debug) {
267            $detail = sprintf('Threw by %s in %s on line %s', $class, $file, $line);
268        } elseif (404 == $code) {
269            $detail = $this->notFoundDetail;
270        } else {
271            $detail = $this->detail;
272        }
273
274        if ($ajax) {
275            $json = array(
276                'code'      => -($code ? abs($code) : 500),
277                'message'   => $message
278            );
279            $debug && $json += array(
280                'detail'    => $detail,
281                'trace'     => $trace
282            );
283            echo json_encode($json);
284        } else {
285            // File Information
286            $mtime = date('Y-m-d H:i:s', filemtime($file));
287            $fileInfo = $this->getFileCode($file, $line);
288
289            // Display view file
290            require __DIR__ . '/Resource/views/error.php';
291        }
292    }
293
294    /**
295     * The error handler convert PHP error to exception
296     *
297     * @param int $code The level of the error raised
298     * @param string $message The error message
299     * @param string $file The filename that the error was raised in
300     * @param int $line The line number the error was raised at
301     * @throws \ErrorException convert PHP error to exception
302     * @internal use for set_error_handler only
303     */
304    public function handleError($code, $message, $file, $line)
305    {
306        if (!(error_reporting() & $code)) {
307            // This error code is not included in error_reporting
308            return;
309        }
310        restore_error_handler();
311        throw new \ErrorException($message, $code, 500, $file, $line);
312    }
313
314    /**
315     * Get file code in specified range
316     *
317     * @param  string $file  The file name
318     * @param  int    $line  The file line
319     * @param  int    $range The line range
320     * @return string
321     */
322    public function getFileCode($file, $line, $range = 20)
323    {
324        $code = file($file);
325        $half = (int) ($range / 2);
326
327        $start = $line - $half;
328        0 > $start && $start = 0;
329
330        $total = count($code);
331        $end = $line + $half;
332        $total < $end && $end = $total;
333
334        $len = strlen($end);
335
336        array_unshift($code, null);
337        $content = '';
338        for ($i = $start; $i < $end; $i++) {
339            $temp = str_pad($i, $len, 0, STR_PAD_LEFT) . ':  ' . $code[$i];
340            if ($line != $i) {
341                $content .= htmlspecialchars($temp, ENT_QUOTES);
342            } else {
343                $content .= '<strong>' . htmlspecialchars($temp, ENT_QUOTES) . '</strong>';
344            }
345        }
346
347        return $content;
348    }
349}