Source Code: SmartFactory/ErrorHandler.php

  1: <?php
  2: /**
  3:  * This file contains the implementation of the interface IErrorHandler
  4:  * in the class ErrorHandler for error handling.
  5:  *
  6:  * @package System
  7:  *
  8:  * @author Oleg Schildt
  9:  */
 10: 
 11: namespace SmartFactory;
 12: 
 13: use \SmartFactory\Interfaces\IErrorHandler;
 14: 
 15: /**
 16:  * Class for error handling.
 17:  *
 18:  * @author Oleg Schildt
 19:  */
 20: class ErrorHandler implements IErrorHandler
 21: {
 22:     /**
 23:      * Internal variable for storing the log path.
 24:      *
 25:      * @var ?string
 26:      *
 27:      * @author Oleg Schildt
 28:      */
 29:     protected ?string $log_path = null;
 30:     
 31:     /**
 32:      * Internal variable for storing the last error.
 33:      *
 34:      * @var string
 35:      *
 36:      * @see ErrorHandler::getLastError()
 37:      * @see ErrorHandler::setLastError()
 38:      *
 39:      * @author Oleg Schildt
 40:      */
 41:     protected static string $last_error = "";
 42:     
 43:     /**
 44:      * Internal variable for storing the state of tracing - active or not.
 45:      *
 46:      * @var bool
 47:      *
 48:      * @see ErrorHandler::traceActive()
 49:      * @see ErrorHandler::enableTrace()
 50:      * @see ErrorHandler::disableTrace()
 51:      *
 52:      * @author Oleg Schildt
 53:      */
 54:     protected static bool $trace_disabled = false;
 55:     
 56:     /**
 57:      * Initializes the error handler with parameters.
 58:      *
 59:      * @param array $parameters
 60:      * Settings for logging as an associative array in the form key => value:
 61:      *
 62:      * - $parameters["log_path"] - the target file path where the logs should be stored.
 63:      *
 64:      * @return void
 65:      *
 66:      * @throws \Exception
 67:      * It might throw an exception in the case of any errors:
 68:      *
 69:      * - if the log path is not specified.
 70:      * - if the trace file is not writable.
 71:      *
 72:      * @author Oleg Schildt
 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:      * Formats the standard PHP backtrace (debug_backtrace).
 95:      *
 96:      * @param array $btrace
 97:      * The backtrace.
 98:      *
 99:      * @return string
100:      * Returns the formatted backtrace.
101:      *
102:      * @author Oleg Schildt
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:     } // format_backtrace
130:     
131:     /**
132:      * This is an auxiliary function that cuts off the common part of the path.
133:      *
134:      * @param string $path
135:      * The path.
136:      *
137:      * @return string
138:      * Returns the cut path.
139:      *
140:      * @author Oleg Schildt
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:     } // trim_path
148:     
149:     /**
150:      * Extracts the call stack from the standard PHP backtrace (debug_backtrace).
151:      *
152:      * @param array $btrace
153:      * The backtrace.
154:      *
155:      * @return string
156:      * Returns the extracted call stack.
157:      *
158:      * @author Oleg Schildt
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:     } // extract_call_stack
219:     
220:     /**
221:      * This is an auxiliary function for truncating long arguments.
222:      *
223:      * @param ?string $str
224:      * The string to be truncated.
225:      *
226:      * @param int $limit
227:      * The limit of the length after which the string is truncated.
228:      *
229:      * @return string
230:      * Returns the truncated string.
231:      *
232:      * @author Oleg Schildt
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:      * This is an auxiliary function for generation of the detailed string from the
249:      * function arguments from the standard PHP backtrace (debug_backtrace).
250:      *
251:      * @param array &$arr
252:      * The array of the function arguments.
253:      *
254:      * @return string
255:      * Returns the detailed string from the function arguments.
256:      *
257:      * @used_by make_arg_list()
258:      *
259:      * @author Oleg Schildt
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:     } // deep_implode
286: 
287:     /**
288:      * Generates the detailed string from the function arguments from the
289:      * standard PHP backtrace (debug_backtrace).
290:      *
291:      * @param array $args
292:      * The array of the function arguments.
293:      *
294:      * @return string
295:      * Returns the detailed string from the function arguments.
296:      *
297:      * @author Oleg Schildt
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:     } // make_arg_list
315:     
316:     /**
317:      * Traces an error to the standard trace file (logs/trace.log).
318:      *
319:      * @param string $etype
320:      * The type of the message to be traced.
321:      *
322:      * @param string $message
323:      * The message to be traced.
324:      *
325:      * @return bool
326:      * Returns true if the message has been successfully trace, otherwise false.
327:      *
328:      * @throws \Exception
329:      * It might throw an exception in the case of any errors:
330:      *
331:      * - if the trace file is not writable.
332:      *
333:      * @author Oleg Schildt
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:     } // trace_message
375: 
376:     /**
377:      * This is the function for handling of the PHP errors. It is set in the
378:      * error handler.
379:      *
380:      * @param int $errno
381:      * Error code.
382:      *
383:      * @param string $errstr
384:      * Error text.
385:      *
386:      * @param string $errfile
387:      * Source file where the error occurred.
388:      *
389:      * @param int $errline
390:      * Line number where the error occurred.
391:      *
392:      * @return void
393:      *
394:      * @throws \Exception
395:      * It might throw an exception in the case of any errors.
396:      *
397:      * @author Oleg Schildt
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:         // E_USER_ERROR means that the error has been explicitly triggered by the programmer for tracing.
427:         // The extra programming warning is not necessary because the message will be shown as a program error.
428:         if ($errno != E_USER_ERROR && error_reporting() !== null) {
429:             event()->fireEvent("php_error", ["etype" => $etype, "errstr" => $errstr, "errfile" => $errfile, "errline" => $errline]);
430:         }
431:     } // handleError
432: 
433:     /**
434:      * This is the function for handling of the PHP exceptions. It should
435:      * be called in the catch block to trace detailed information
436:      * if an exception is thrown.
437:      *
438:      * @param \Throwable $ex
439:      * Thrown exception.
440:      *
441:      * @param int $errno
442:      * Error code.
443:      *
444:      * @return void
445:      *
446:      * @throws \Exception
447:      * It might throw an exception in the case of any errors.
448:      *
449:      * @author Oleg Schildt
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:      * Returns the last error.
485:      *
486:      * @return string
487:      * Returns the last error or an empty string if no error occurred so far.
488:      *
489:      * @author Oleg Schildt
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:     } // getLastError
511:     
512:     /**
513:      * Stores the last error.
514:      *
515:      * @param string $error
516:      * The error text to be stored.
517:      *
518:      * @return void
519:      *
520:      * @author Oleg Schildt
521:      */
522:     public function setLastError(string $error): void
523:     {
524:         self::$last_error = $error;
525:     } // setLastError
526:     
527:     /**
528:      * Returns the state whether the trace is active or not.
529:      *
530:      * If the trace is active, any error, warning or notice is traced to
531:      * the standard file.
532:      *
533:      * The trace is generally managed over the setting tracing_enabled.
534:      * But you can also temporarily disable tracing, e.g. to keep the
535:      * trace log clear when you make a check that can produce a trace
536:      * entry, but it is a controlled noticed and should not clutter the trace.
537:      *
538:      * @return bool
539:      * Returns the state whether the trace is active or not.
540:      *
541:      * @see ErrorHandler::enableTrace()
542:      * @see ErrorHandler::disableTrace()
543:      *
544:      * @author Oleg Schildt
545:      */
546:     public function traceActive(): bool
547:     {
548:         return empty(self::$trace_disabled);
549:     } // traceActive
550:     
551:     /**
552:      * Enables the trace.
553:      *
554:      * If the trace is active, any error, warning or notice is traced to
555:      * the standard file.
556:      *
557:      * @return void
558:      *
559:      * @see ErrorHandler::traceActive()
560:      * @see ErrorHandler::disableTrace()
561:      *
562:      * @author Oleg Schildt
563:      */
564:     public function enableTrace(): void
565:     {
566:         self::$trace_disabled = false;
567:     } // enableTrace
568:     
569:     /**
570:      * Disables the trace.
571:      *
572:      * If the trace is active, any error, warning or notice is traced to
573:      * the standard file.
574:      *
575:      * @return void
576:      *
577:      * @see ErrorHandler::traceActive()
578:      * @see ErrorHandler::enableTrace()
579:      *
580:      * @author Oleg Schildt
581:      */
582:     public function disableTrace(): void
583:     {
584:         self::$trace_disabled = true;
585:     } // disableTrace
586: } // ErrorHandler