/src/mako/error/ErrorHandler.php

https://github.com/mako-framework/framework · PHP · 399 lines · 207 code · 57 blank · 135 comment · 20 complexity · 30ada8896575ff8b9615e69ab7eecebe MD5 · raw file

  1. <?php
  2. /**
  3. * @copyright Frederic G. Østby
  4. * @license http://www.makoframework.com/license
  5. */
  6. namespace mako\error;
  7. use Closure;
  8. use ErrorException;
  9. use mako\error\handlers\HandlerInterface;
  10. use mako\http\exceptions\HttpStatusException;
  11. use mako\syringe\Container;
  12. use Psr\Log\LoggerInterface;
  13. use Throwable;
  14. use function array_unique;
  15. use function array_unshift;
  16. use function error_get_last;
  17. use function error_log;
  18. use function error_reporting;
  19. use function filter_var;
  20. use function fwrite;
  21. use function headers_sent;
  22. use function http_response_code;
  23. use function in_array;
  24. use function ini_get;
  25. use function ob_end_clean;
  26. use function ob_get_level;
  27. use function register_shutdown_function;
  28. use function set_exception_handler;
  29. use function sprintf;
  30. /**
  31. * Error handler.
  32. */
  33. class ErrorHandler
  34. {
  35. /**
  36. * Container.
  37. *
  38. * @var \mako\syringe\Container
  39. */
  40. protected $container;
  41. /**
  42. * Is the shutdown handler disabled?
  43. *
  44. * @var bool
  45. */
  46. protected $disableShutdownHandler = false;
  47. /**
  48. * Exception handlers.
  49. *
  50. * @var array
  51. */
  52. protected $handlers = [];
  53. /**
  54. * Logger instance.
  55. *
  56. * @var \Closure|\Psr\Log\LoggerInterface|null
  57. */
  58. protected $logger;
  59. /**
  60. * Exception types that shouldn't be logged.
  61. *
  62. * @var array
  63. */
  64. protected $dontLog = [];
  65. /**
  66. * Constructor.
  67. *
  68. * @param \mako\syringe\Container|null $container Container
  69. */
  70. public function __construct(?Container $container = null)
  71. {
  72. $this->container = $container ?? new Container;
  73. // Add a basic exception handler to the stack
  74. $this->handle(Throwable::class, $this->getFallbackHandler());
  75. // Registers the exception handler
  76. $this->register();
  77. }
  78. /**
  79. * Should errors be displayed?
  80. *
  81. * @return bool
  82. */
  83. protected function displayErrors(): bool
  84. {
  85. $displayErrors = ini_get('display_errors');
  86. if(in_array($displayErrors, ['stderr', 'stdout']) || filter_var($displayErrors, FILTER_VALIDATE_BOOLEAN))
  87. {
  88. return true;
  89. }
  90. return false;
  91. }
  92. /**
  93. * Write to output.
  94. *
  95. * @param string $output String to write to output
  96. */
  97. protected function write(string $output): void
  98. {
  99. if(PHP_SAPI === 'cli' && ini_get('display_errors') === 'stderr')
  100. {
  101. fwrite(STDERR, $output);
  102. return;
  103. }
  104. echo $output;
  105. }
  106. /**
  107. * Returns the fallback handler.
  108. *
  109. * @return \Closure
  110. */
  111. protected function getFallbackHandler(): Closure
  112. {
  113. return function (Throwable $e): void
  114. {
  115. if($this->displayErrors())
  116. {
  117. $this->write('[ ' . $e::class . "] {$e->getMessage()} on line [ {$e->getLine()} ] in [ {$e->getFile()} ]" . PHP_EOL);
  118. $this->write($e->getTraceAsString() . PHP_EOL);
  119. }
  120. };
  121. }
  122. /**
  123. * Registers the exception handler.
  124. */
  125. protected function register(): void
  126. {
  127. // Allows us to handle "fatal" errors
  128. register_shutdown_function(function (): void
  129. {
  130. $e = error_get_last();
  131. if($e !== null && (error_reporting() & $e['type']) !== 0 && !$this->disableShutdownHandler)
  132. {
  133. $this->handler(new ErrorException($e['message'], code: $e['type'], filename: $e['file'], line: $e['line']));
  134. }
  135. });
  136. // Set the exception handler
  137. set_exception_handler([$this, 'handler']);
  138. }
  139. /**
  140. * Set logger instance or closure that returns a logger instance.
  141. *
  142. * @param \Closure|\Psr\Log\LoggerInterface $logger Logger
  143. */
  144. public function setLogger(Closure|LoggerInterface $logger): void
  145. {
  146. $this->logger = $logger;
  147. }
  148. /**
  149. * Return logger instance.
  150. *
  151. * @return \Psr\Log\LoggerInterface|null
  152. */
  153. public function getLogger(): ?LoggerInterface
  154. {
  155. if($this->logger instanceof Closure)
  156. {
  157. return ($this->logger)();
  158. }
  159. return $this->logger;
  160. }
  161. /**
  162. * Disables logging for an exception type.
  163. *
  164. * @param array|string $exceptionType Exception type or array of exception types
  165. */
  166. public function dontLog(array|string $exceptionType): void
  167. {
  168. $this->dontLog = array_unique([...$this->dontLog, ...(array) $exceptionType]);
  169. }
  170. /**
  171. * Disables the shutdown handler.
  172. */
  173. public function disableShutdownHandler(): void
  174. {
  175. $this->disableShutdownHandler = true;
  176. }
  177. /**
  178. * Prepends an exception handler to the stack.
  179. *
  180. * @param string $exceptionType Exception type
  181. * @param \Closure|string $handler Exception handler
  182. * @param array $parameters Constructor parameters
  183. */
  184. public function handle(string $exceptionType, Closure|string $handler, array $parameters = []): void
  185. {
  186. array_unshift($this->handlers, ['exceptionType' => $exceptionType, 'handler' => $handler, 'parameters' => $parameters]);
  187. }
  188. /**
  189. * Clears all error handlers for an exception type.
  190. *
  191. * @param string $exceptionType Exception type
  192. */
  193. public function clearHandlers(string $exceptionType): void
  194. {
  195. foreach($this->handlers as $key => $handler)
  196. {
  197. if($handler['exceptionType'] === $exceptionType)
  198. {
  199. unset($this->handlers[$key]);
  200. }
  201. }
  202. }
  203. /**
  204. * Replaces all error handlers for an exception type with a new one.
  205. *
  206. * @param string $exceptionType Exception type
  207. * @param \Closure $handler Exception handler
  208. */
  209. public function replaceHandlers(string $exceptionType, Closure $handler): void
  210. {
  211. $this->clearHandlers($exceptionType);
  212. $this->handle($exceptionType, $handler);
  213. }
  214. /**
  215. * Clear output buffers.
  216. */
  217. protected function clearOutputBuffers(): void
  218. {
  219. while(ob_get_level() > 0) ob_end_clean();
  220. }
  221. /**
  222. * Should the exception be logged?
  223. *
  224. * @param \Throwable $exception An exception object
  225. * @return bool
  226. */
  227. protected function shouldExceptionBeLogged(Throwable $exception): bool
  228. {
  229. if($this->logger === null)
  230. {
  231. return false;
  232. }
  233. foreach($this->dontLog as $exceptionType)
  234. {
  235. if($exception instanceof $exceptionType)
  236. {
  237. return false;
  238. }
  239. }
  240. return true;
  241. }
  242. /**
  243. * Creates and returns an error handler instance.
  244. *
  245. * @param string $handler Handler class name
  246. * @param array $parameters Constructor parameters
  247. * @return \mako\error\handlers\HandlerInterface
  248. */
  249. protected function handlerFactory(string $handler, array $parameters): HandlerInterface
  250. {
  251. return $this->container->get($handler, $parameters);
  252. }
  253. /**
  254. * Handle the exception.
  255. *
  256. * @param \Throwable $exception Exceotion
  257. * @param \Closure|string $handler Exception handler
  258. * @param array $parameters Constructor parameters
  259. * @return mixed
  260. */
  261. protected function handleException(Throwable $exception, Closure|string $handler, array $parameters): mixed
  262. {
  263. if($handler instanceof Closure)
  264. {
  265. return $handler($exception);
  266. }
  267. return $this->handlerFactory($handler, $parameters)->handle($exception);
  268. }
  269. /**
  270. * Logs the exception.
  271. *
  272. * @param \Throwable $exception An exception object
  273. */
  274. protected function logException(Throwable $exception): void
  275. {
  276. if($this->shouldExceptionBeLogged($exception))
  277. {
  278. try
  279. {
  280. $this->getLogger()->error($exception->getMessage(), ['exception' => $exception]);
  281. }
  282. catch(Throwable $e)
  283. {
  284. error_log(sprintf('%s on line %s in %s.', $e->getMessage(), $e->getLine(), $e->getLine()));
  285. }
  286. }
  287. }
  288. /**
  289. * Handles uncaught exceptions.
  290. *
  291. * @param \Throwable $exception An exception object
  292. */
  293. public function handler(Throwable $exception): void
  294. {
  295. try
  296. {
  297. // Empty output buffers
  298. $this->clearOutputBuffers();
  299. // Loop through the exception handlers
  300. foreach($this->handlers as $handler)
  301. {
  302. if($exception instanceof $handler['exceptionType'])
  303. {
  304. if($this->handleException($exception, $handler['handler'], $handler['parameters']) !== null)
  305. {
  306. break;
  307. }
  308. }
  309. }
  310. }
  311. catch(Throwable $e)
  312. {
  313. if((PHP_SAPI === 'cli' || headers_sent() === false) && $this->displayErrors())
  314. {
  315. if(PHP_SAPI !== 'cli')
  316. {
  317. http_response_code($exception instanceof HttpStatusException ? $exception->getCode() : 500);
  318. }
  319. // Empty output buffers
  320. $this->clearOutputBuffers();
  321. // One of the exception handlers failed so we'll just show the user a generic error screen
  322. $this->getFallbackHandler()($exception);
  323. // We'll also show some information about how the exception handler failed
  324. $this->write('Additionally, the error handler failed with the following error:' . PHP_EOL);
  325. $this->getFallbackHandler()($e);
  326. // And finally we'll log the additional exception
  327. $this->logException($e);
  328. }
  329. }
  330. finally
  331. {
  332. try
  333. {
  334. $this->logException($exception);
  335. }
  336. finally
  337. {
  338. exit(1);
  339. }
  340. }
  341. }
  342. }