workflow/publish: update to php-8.1
[mdref/mdref] / mdref / ExceptionHandler.php
1 <?php
2
3 namespace mdref;
4
5 use http\Env;
6 use function debug_print_backtrace;
7 use function error_get_last;
8 use function headers_sent;
9 use function implode;
10 use function ob_end_clean;
11 use function ob_get_clean;
12 use function ob_get_level;
13 use function ob_start;
14 use function register_shutdown_function;
15 use function set_error_handler;
16 use function set_exception_handler;
17 use function sprintf;
18 use const E_COMPILE_ERROR;
19 use const E_CORE_ERROR;
20 use const E_ERROR;
21 use const E_PARSE;
22 use const E_USER_ERROR;
23
24 /**
25 * Exception and error handler
26 */
27 class ExceptionHandler
28 {
29 /**
30 * @var \http\Env\Response
31 */
32 private $response;
33
34 /**
35 * Set up error/exception/shutdown handler
36 * @param Env\Response $r
37 */
38 public function __construct(Env\Response $r) {
39 $this->response = $r;
40 set_exception_handler($this);
41 set_error_handler($this);
42 register_shutdown_function($this);
43 }
44
45 /**
46 * Clean output buffers
47 */
48 private static function cleanBuffers() : void {
49 while (ob_get_level()) {
50 if (!@ob_end_clean()) {
51 break;
52 }
53 }
54 }
55
56 /**
57 * The exception/error/shutdown handler callback
58 *
59 * @param \Throwable|string $e
60 * @param ?string $msg
61 * @throws \Exception
62 */
63 public function __invoke($e = null, ?string $msg = null) : void {
64 if ($e instanceof \Throwable) {
65 try {
66 self::cleanBuffers();
67 echo static::htmlException($e);
68 } catch (\Exception $ignore) {
69 headers_sent() or Env::setResponseCode(500);
70 die("FATAL ERROR:\n$e\n$ignore");
71 }
72 } elseif (isset($e, $msg)) {
73 throw new \Exception($msg, $e);
74 } elseif (($error = error_get_last())) {
75 switch ($error["type"]) {
76 case E_PARSE:
77 case E_ERROR:
78 case E_USER_ERROR:
79 case E_CORE_ERROR:
80 case E_COMPILE_ERROR:
81 self::cleanBuffers();
82 $message = sprintf("%s in %s at line %d",
83 $error["message"], $error["file"], $error["line"]);
84 echo static::htmlError("Application Error", $message, 500, "");
85 break;
86 }
87 }
88 }
89
90 /**
91 * Format an exception as HTML and send appropriate exception info as HTTP headers
92 * @param \Throwable $e
93 * @param array $title_tag
94 * @param array $message_tag
95 * @param array $trace_tag
96 * @return string
97 */
98 public static function htmlException(\Throwable $e, array $title_tag = ["h1"], array $message_tag = ["p"],
99 array $trace_tag = ["pre", "style='font-size:smaller;overflow-x:scroll'"]) : string {
100 if ($e instanceof Exception) {
101 $code = $e->getCode() ?: 500;
102 } else {
103 $code = 500;
104 }
105
106 for ($html = ""; $e; $e = $e->getPrevious()) {
107 $html .= static::htmlError(Env::getResponseStatusForCode($code),
108 $e->getMessage(), $code, $e->getTraceAsString(),
109 $title_tag, $message_tag, $trace_tag);
110 }
111 return $html;
112 }
113
114 /**
115 * Format an error as HTML
116 * @param string $title
117 * @param string $message
118 * @param int $code
119 * @param string $trace
120 * @param array $title_tag
121 * @param array $message_tag
122 * @param array $trace_tag
123 * @return string
124 */
125 public static function htmlError($title, $message, $code, $trace = null, array $title_tag = ["h1"],
126 array $message_tag = ["p"], array $trace_tag = ["pre", "style='font-size:smaller;overflow-x:scroll'"]) : string {
127 Env::setResponseCode($code);
128
129 $html = sprintf("<%s>%s</%s>\n<%s>%s</%s>\n",
130 implode(" ", $title_tag), $title, $title_tag[0],
131 implode(" ", $message_tag), $message, $message_tag[0]);
132 if ($trace_tag) {
133 if (!isset($trace)) {
134 ob_start();
135 debug_print_backtrace();
136 $trace = ob_get_clean();
137 }
138 if (!empty($trace)) {
139 $html .= sprintf("<%s>%s</%s>\n",
140 implode(" ", $trace_tag), $trace, $trace_tag[0]);
141 }
142 }
143
144 return $html;
145 }
146 }