PageRenderTime 62ms CodeModel.GetById 28ms RepoModel.GetById 1ms app.codeStats 0ms

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