1: <?php
2: 3: 4: 5: 6: 7: 8: 9:
10:
11: namespace SmartFactory;
12:
13: use \SmartFactory\Interfaces\IErrorHandler;
14:
15: 16: 17: 18: 19:
20: class ErrorHandler implements IErrorHandler
21: {
22: 23: 24: 25: 26: 27: 28:
29: protected ?string $log_path = null;
30:
31: 32: 33: 34: 35: 36: 37: 38: 39: 40:
41: protected static string $last_error = "";
42:
43: 44: 45: 46: 47: 48: 49: 50: 51: 52: 53:
54: protected static bool $trace_disabled = false;
55:
56: 57: 58: 59: 60: 61: 62: 63: 64: 65: 66: 67: 68: 69: 70: 71: 72: 73:
74: public function init(array $parameters): void
75: {
76: if (empty($parameters["log_path"])) {
77: throw new \Exception("Log path is not specified!");
78: }
79:
80: if ($parameters["log_path"] == "stdout") {
81: $this->log_path = $parameters["log_path"];
82: return;
83: }
84:
85: $this->log_path = rtrim(str_replace("\\", "/", $parameters["log_path"]), "/") . "/";
86:
87: $file = $this->log_path . "trace.log";
88: if (!file_exists($this->log_path) || !is_writable($this->log_path) || (file_exists($file) && !is_writable($file))) {
89: throw new \Exception(sprintf("The trace file '%s' is not writable!", $file));
90: }
91: }
92:
93: 94: 95: 96: 97: 98: 99: 100: 101: 102: 103:
104: function format_backtrace(array $btrace): string
105: {
106: if (empty($btrace) || count($btrace) == 0) {
107: return "backtrace empty";
108: }
109:
110: $trace = "";
111:
112: foreach ($btrace as $nr => $btrace_entry) {
113: if ($nr == 0) {
114: $trace .= $btrace_entry["args"][1];
115:
116: $trace .= "\r\n\r\n";
117:
118: $trace .= "#URI: " . ($_SERVER["REQUEST_URI"] ?? "") . "\r\n";
119: $trace .= "#Source: " . $this->trim_path(str_replace("\\", "/", $btrace_entry["args"][2])) . ", " . $btrace_entry["args"][3] . "\r\n";
120:
121: $cstack = $this->extract_call_stack($btrace);
122: if (!empty($cstack)) {
123: $trace .= "#Callstack:\r\n" . $cstack . "\r\n";
124: }
125: }
126: }
127:
128: return $trace;
129: }
130:
131: 132: 133: 134: 135: 136: 137: 138: 139: 140: 141:
142: protected function trim_path(string $path): string
143: {
144: $common_prefix = common_prefix(str_replace("\\", "/", __DIR__), $path);
145:
146: return str_replace($common_prefix, "", $path);
147: }
148:
149: 150: 151: 152: 153: 154: 155: 156: 157: 158: 159:
160: protected function extract_call_stack(array $btrace): string
161: {
162: if (empty($btrace)) {
163: return "";
164: }
165:
166: $trace = "";
167:
168: $indent = "";
169: foreach ($btrace as $btrace_entry) {
170:
171: if (!empty($btrace_entry["function"]) && ($btrace_entry["function"] == "handle_error" || str_contains($btrace_entry["function"], "{closure}") || $btrace_entry["function"] == "handleError" || $btrace_entry["function"] == "trigger_error")) {
172: continue;
173: }
174:
175: if (empty($btrace_entry["file"])) {
176: continue;
177: }
178:
179: if (!empty($btrace_entry["function"])) {
180: $function = $btrace_entry["function"];
181:
182: if (preg_match("/{closure:([^}{]+)}/", $function, $matches)) {
183: $trace .= $indent . "{closure:" . $this->trim_path(str_replace("\\", "/", $matches[1])) . "}" . "(";
184: } else {
185: $trace .= $indent . str_replace("SmartFactory\\", "", $function) . "(";
186: }
187:
188: $args = (isset($btrace_entry["args"])) ? $btrace_entry["args"] : [];
189: $args_str = $this->make_arg_list($args);
190:
191: if (!empty($args_str)) {
192: $trace .= $args_str;
193: }
194:
195: $trace .= ")";
196: }
197:
198: $trace .= " [";
199:
200: $trace .= $this->trim_path(str_replace("\\", "/", $btrace_entry["file"]));
201:
202: $trace .= ", ";
203:
204: if (empty($btrace_entry["line"])) {
205: $trace .= "line number undefined";
206: } else {
207: $trace .= $btrace_entry["line"];
208: }
209:
210: $trace .= "]";
211:
212: $trace .= "\r\n";
213:
214: $indent .= " ";
215: }
216:
217: return trim($trace);
218: }
219:
220: 221: 222: 223: 224: 225: 226: 227: 228: 229: 230: 231: 232: 233:
234: protected function truncate_argument(?string $str, int $limit): string
235: {
236: if (empty($str)) {
237: return "";
238: }
239:
240: $str = preg_replace("/[\r\n\t]+/", " ", $str);
241:
242: return mb_strlen($str) > $limit
243: ? mb_substr($str, 0, $limit) . "..."
244: : $str;
245: }
246:
247: 248: 249: 250: 251: 252: 253: 254: 255: 256: 257: 258: 259: 260:
261: protected function deep_implode(array &$arr): string
262: {
263: $list = "";
264:
265: $count = 1;
266:
267: foreach ($arr as $nm => &$val) {
268: if ($count >= 4) {
269: $list .= "...";
270: break;
271: }
272:
273: if (is_array($val)) {
274: $list .= $this->deep_implode($val) . ", ";
275: } elseif (is_object($val)) {
276: $list .= str_replace("SmartFactory\\", "", get_class($val)) . ", ";
277: } else {
278: $list .= $nm . "=" . $this->truncate_argument($val, 15) . ", ";
279: }
280:
281: $count++;
282: }
283:
284: return "[" . trim($list, ", ") . "]";
285: }
286:
287: 288: 289: 290: 291: 292: 293: 294: 295: 296: 297: 298:
299: protected function make_arg_list(array $args): string
300: {
301: $list = "";
302:
303: foreach ($args as $arg) {
304: if (is_array($arg)) {
305: $list .= $this->deep_implode($arg) . ", ";
306: } elseif (is_object($arg)) {
307: $list .= str_replace("SmartFactory\\", "", get_class($arg)) . ", ";
308: } else {
309: $list .= $this->truncate_argument($arg, 15) . ", ";
310: }
311: }
312:
313: return trim($list, ", ");
314: }
315:
316: 317: 318: 319: 320: 321: 322: 323: 324: 325: 326: 327: 328: 329: 330: 331: 332: 333: 334:
335: protected function trace_message(string $etype, string $message): bool
336: {
337: if (!$this->traceActive()) {
338: return true;
339: }
340:
341: $message = date("Y-m-d H:i:s") . "\r\n" .
342: "------------------------------------\r\n" .
343: "### " . $etype . " ###\r\n\r\n" .
344: $message . "\r\n" .
345: "------------------------------------\r\n" .
346: "\r\n\r\n";
347:
348: $trace_file_writable = true;
349:
350: $file = $this->log_path . "trace.log";
351: if (!empty($this->log_path) && $this->log_path != "stdout" &&
352: (!file_exists($this->log_path) || !is_writable($this->log_path) || (file_exists($file) && !is_writable($file)))) {
353: $trace_file_writable = false;
354:
355: $message = sprintf("The trace file '%s' is not writable!", $file) . "\n" .
356: "Tracing to the stdout.\n\n" . $message;
357: }
358:
359: if(empty($this->log_path)) {
360: $message = "The trace file is not specified!" . "\n" .
361: "Tracing to the stdout.\n\n" . $message;
362: }
363:
364: if (empty($this->log_path) || $this->log_path == "stdout" || !$trace_file_writable) {
365: if (is_web()) {
366: $message = "<pre>" . escape_html($message) . "</pre>";
367: }
368:
369: echo $message;
370: return true;
371: }
372:
373: return file_put_contents($file, $message, FILE_APPEND) !== false;
374: }
375:
376: 377: 378: 379: 380: 381: 382: 383: 384: 385: 386: 387: 388: 389: 390: 391: 392: 393: 394: 395: 396: 397: 398:
399: public function handleError(int $errno, string $errstr, string $errfile, int $errline): void
400: {
401: $this->setLastError($errstr);
402:
403: $errortype = [
404: E_ERROR => "Error",
405: E_WARNING => "Warning",
406: E_PARSE => "Parsing Error",
407: E_NOTICE => "Notice",
408: E_CORE_ERROR => "Core Error",
409: E_CORE_WARNING => "Core Warning",
410: E_COMPILE_ERROR => "Compile Error",
411: E_COMPILE_WARNING => "Compile Warning",
412: E_USER_ERROR => "User Error",
413: E_USER_WARNING => "User Warning",
414: E_USER_NOTICE => "User Notice",
415: E_DEPRECATED => "Deprecated Notice"
416: ];
417:
418: if (empty($errortype[$errno])) {
419: $etype = $errno;
420: } else {
421: $etype = $errortype[$errno];
422: }
423:
424: $this->trace_message($etype, $this->format_backtrace(debug_backtrace()));
425:
426:
427:
428: if ($errno != E_USER_ERROR && error_reporting() !== null) {
429: event()->fireEvent("php_error", ["etype" => $etype, "errstr" => $errstr, "errfile" => $errfile, "errline" => $errline]);
430: }
431: }
432:
433: 434: 435: 436: 437: 438: 439: 440: 441: 442: 443: 444: 445: 446: 447: 448: 449: 450:
451: public function handleException(\Throwable $ex, int $errno): void
452: {
453: $this->setLastError($ex->getMessage());
454:
455: $errortype = [
456: E_ERROR => "Error",
457: E_WARNING => "Warning",
458: E_PARSE => "Parsing Error",
459: E_NOTICE => "Notice",
460: E_CORE_ERROR => "Core Error",
461: E_CORE_WARNING => "Core Warning",
462: E_COMPILE_ERROR => "Compile Error",
463: E_COMPILE_WARNING => "Compile Warning",
464: E_USER_ERROR => "User Error",
465: E_USER_WARNING => "User Warning",
466: E_USER_NOTICE => "User Notice",
467: E_DEPRECATED => "Deprecated Notice"
468: ];
469:
470: if (empty($errortype[$errno])) {
471: $etype = $errno;
472: } else {
473: $etype = $errortype[$errno];
474: }
475:
476: $trace = $ex->getTrace();
477: $trace_entry["args"] = ["", $ex->getMessage(), $ex->getFile(), $ex->getLine()];
478: array_unshift($trace, $trace_entry);
479:
480: $this->trace_message($etype, $this->format_backtrace($trace));
481: }
482:
483: 484: 485: 486: 487: 488: 489: 490:
491: public function getLastError(): string
492: {
493: if (empty(self::$last_error)) {
494: $errors = error_get_last();
495:
496: if (empty($errors)) {
497: return "";
498: }
499:
500: $error = array_pop($errors);
501:
502: if (empty($error)) {
503: return "";
504: }
505:
506: return $error["message"] ?? "";
507: }
508:
509: return self::$last_error;
510: }
511:
512: 513: 514: 515: 516: 517: 518: 519: 520: 521:
522: public function setLastError(string $error): void
523: {
524: self::$last_error = $error;
525: }
526:
527: 528: 529: 530: 531: 532: 533: 534: 535: 536: 537: 538: 539: 540: 541: 542: 543: 544: 545:
546: public function traceActive(): bool
547: {
548: return empty(self::$trace_disabled);
549: }
550:
551: 552: 553: 554: 555: 556: 557: 558: 559: 560: 561: 562: 563:
564: public function enableTrace(): void
565: {
566: self::$trace_disabled = false;
567: }
568:
569: 570: 571: 572: 573: 574: 575: 576: 577: 578: 579: 580: 581:
582: public function disableTrace(): void
583: {
584: self::$trace_disabled = true;
585: }
586: }