"debug", self::LOG_LEVEL_INFO => "info", self::LOG_LEVEL_WARNING => "warning", self::LOG_LEVEL_ERROR => "error", self::LOG_LEVEL_SEVERE => "severe" ]; public static Logger $INSTANCE; private ?SQL $sql; private string $module; // unit tests private bool $unitTestMode; private ?string $lastMessage; private ?string $lastLevel; public function __construct(string $module = "Unknown", ?SQL $sql = null) { $this->module = $module; $this->sql = $sql; $this->unitTestMode = false; $this->lastMessage = null; $this->lastLevel = null; } protected function getStackTrace(int $pop = 2, ?array $debugTrace = null): string { if ($debugTrace === null) { $debugTrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); } if ($pop > 0) { array_splice($debugTrace, 0, $pop); } return implode("\n", array_map(function ($trace) { if (isset($trace["file"])) { return $trace["file"] . "#" . $trace["line"] . ": " . $trace["function"] . "()"; } else { return $trace["function"] . "()"; } }, $debugTrace)); } public function log(string|\Throwable $logEntry, string $severity, bool $appendStackTrace = true): void { $debugTrace = null; $message = $logEntry; if ($message instanceof \Throwable) { $message = $logEntry->getMessage(); $debugTrace = $logEntry->getTrace(); } if ($appendStackTrace) { $message .= "\n" . $this->getStackTrace(2, $debugTrace); } $this->lastMessage = $message; $this->lastLevel = $severity; if ($this->unitTestMode) { return; } if ($severity >= self::LOG_LEVEL_WARNING) { error_log($message); } if ($this->sql !== null && $this->sql->isConnected()) { $success = $this->sql->insert("SystemLog", ["module", "message", "severity"]) ->addRow($this->module, $message, $severity) ->execute(); if ($success !== false) { return; } } // database logging failed, try to log to file $module = preg_replace("/[^a-zA-Z0-9-]/", "-", $this->module); $date = (\DateTime::createFromFormat('U.u', microtime(true)))->format(self::LOG_FILE_DATE_FORMAT); $logFile = implode("_", [$module, $severity, $date]) . ".log"; $logPath = implode(DIRECTORY_SEPARATOR, [WEBROOT, "Site", "Logs", $logFile]); @file_put_contents($logPath, $message); } public function error(string|\Throwable $message): string { $this->log($message, "error"); return $message; } public function severe(string|\Throwable $message): string { $this->log($message, "severe"); return $message; } public function warning(string|\Throwable $message): string { $this->log($message, "warning", false); return $message; } public function info(string|\Throwable $message): string { $this->log($message, "info", false); return $message; } public function debug(string|\Throwable $message, bool $appendStackTrace = false): string { $this->log($message, "debug", $appendStackTrace); return $message; } public static function instance(): Logger { if (self::$INSTANCE === null) { self::$INSTANCE = new Logger("Global"); } return self::$INSTANCE; } public function getLastMessage(): ?string { return $this->lastMessage; } public function getLastLevel(): ?string { return $this->lastLevel; } /** * Calling this method will prevent the logger from persisting log messages (writing to database/file), */ public function unitTestMode(): void { $this->unitTestMode = true; } }