trim($row) !== ''); if (empty($validRows)) return ''; // 计算最长行的字符长度(核心:按实际字符数计算) $maxContentLen = max(array_map('mb_strlen', $validRows)); // 格式化每行:左侧固定前缀,右侧补空格至最长长度 $formattedRows = array_map(function ($row) use ($maxContentLen) { $currentLen = mb_strlen($row); $padding = $maxContentLen - $currentLen; // 精准计算需要补充的空格数 return '| ' . $row . str_repeat(' ', max(0, $padding)) . ' |'; }, $validRows); // 计算边框总宽度(格式化后最长行的长度) $borderLen = max(array_map('strlen', $formattedRows)); $borderLine = str_repeat($borderChar, $borderLen); // 空行(和内容行宽度一致) $emptyLine = '| ' . str_repeat(' ', $maxContentLen) . ' |'; // 拼接文本框 return "\n{$borderLine}\n{$emptyLine}\n" . implode("\n", $formattedRows) . "\n{$emptyLine}\n{$borderLine}"; } /** * 静态信息级别日志 * @param string $message 日志消息 * @param array|Throwable $context 占位符参数或异常对象 * @param string $name 日志器名称 */ public static function info(string $message, array|Throwable $context = [], string $name = '__default'): void { self::new($name)->info($message, $context); } /** * 静态调试级别日志 * @param string $message 日志消息 * @param array|Throwable $context 占位符参数或异常对象 * @param string $name 日志器名称 */ public static function debug(string $message, array|Throwable $context = [], string $name = '__default'): void { self::new($name)->debug($message, $context); } /** * 静态warn方法(warning别名) * @param string $message 日志消息 * @param array|Throwable $context 占位符参数或异常对象 * @param string $name 日志器名称 */ public static function warn(string $message, array|Throwable $context = [], string $name = '__default'): void { self::new($name)->warn($message, $context); } /** * 静态warn方法(warning别名) * @param string $message 日志消息 * @param array|Throwable $context 占位符参数或异常对象 * @param string $name 日志器名称 */ public static function warning(string $message, array|Throwable $context = [], string $name = '__default'): void { self::new($name)->warn($message, $context); } /** * 静态错误级别日志 * @param string $message 日志消息 * @param array|Throwable $context 占位符参数或异常对象 * @param string $name 日志器名称 */ public static function error(string $message, array|Throwable $context = [], string $name = '__default'): void { self::new($name)->error($message, $context); } /** * 静态通用日志方法 * @param string $level 日志级别 * @param string $message 日志消息 * @param array|Throwable $context 占位符参数或异常对象 * @param string $name 日志器名称 */ public static function log(string $level, string $message, array|Throwable $context = [], string $name = '__default'): void { self::new($name)->log($level, $message, $context); } } /** * 日志实例类(仅提供实例方法) */ class LoggerIns { public $name = ''; /** * 构造函数:初始化日志器名称 * @param string $name 日志器名称,默认 '__default' */ public function __construct(string $name = '__default') { $this->name = $name; } /** * 信息级别日志 * @param string $message 日志消息(支持 {} 占位符) * @param array|Throwable $context 占位符替换参数或异常对象 */ public function info(string $message, array|Throwable $context = []): void { $processedMessage = $this->buildMessage($message, $context); if (empty($processedMessage)) return; Log::info($processedMessage); } /** * 调试级别日志 * @param string $message 日志消息(支持 {} 占位符) * @param array|Throwable $context 占位符替换参数或异常对象 */ public function debug(string $message, array|Throwable $context = []): void { $processedMessage = $this->buildMessage($message, $context); if (empty($processedMessage)) return; Log::debug($processedMessage); } /** * 警告级别日志 * @param string $message 日志消息(支持 {} 占位符) * @param array|Throwable $context 占位符替换参数或异常对象 */ public function warning(string $message, array|Throwable $context = []): void { $processedMessage = $this->buildMessage($message, $context); if (empty($processedMessage)) return; Log::warning($processedMessage); } /** * warn 方法 - warning 的别名 * @param string $message 日志消息(支持 {} 占位符) * @param array|Throwable $context 占位符替换参数或异常对象 */ public function warn(string $message, array|Throwable $context = []): void { $this->warning($message, $context); } /** * 错误级别日志 * @param string $message 日志消息(支持 {} 占位符) * @param array|Throwable $context 占位符替换参数或异常对象 */ public function error(string $message, array|Throwable $context = []): void { $processedMessage = $this->buildMessage($message, $context); if (empty($processedMessage)) return; Log::error($processedMessage); } /** * 通用日志方法 * @param string $level 日志级别 (info/debug/warning/warn/error/fatal) * @param string $message 日志消息(支持 {} 占位符) * @param array|Throwable $context 占位符替换参数或异常对象 */ public function log(string $level, string $message, array|Throwable $context = []): void { // 验证并处理日志级别(兼容warn别名) $validLevels = ['info', 'debug', 'warning', 'warn', 'error', 'fatal']; $level = strtolower($level); // 将warn映射为warning if ($level === 'warn') { $level = 'warning'; } if (!in_array($level, $validLevels)) { $level = 'info'; // 默认使用info级别 } $processedMessage = $this->buildMessage($message, $context); if (empty($processedMessage)) return; Log::{$level}($processedMessage); } /** * 处理日志消息:替换占位符、处理异常、添加名称前缀 * @param string $message 原始日志消息 * @param array|Throwable $context 占位符参数或异常对象 * @return string 处理后的日志消息 */ private function buildMessage(string $message, array|Throwable $context = []): string { // 1. 先处理占位符替换(无论是否有异常,先处理消息本身) $processedMessage = $this->replacePlaceholders($message, is_array($context) ? $context : []); // 2. 处理异常对象:换行输出完整异常内容,优化堆栈缩进 if ($context instanceof Throwable) { // 转换异常为字符串并优化堆栈缩进(制表符) $exceptionStr = $this->formatExceptionStackTrace((string)$context); // 使用 PHP_EOL 保证跨系统换行 $processedMessage .= PHP_EOL . $exceptionStr; } // 3. 添加名称前缀 [name] $namePrefix = "name}>"; $result = $namePrefix . $processedMessage; if ($this->isFilter()) return ""; return $result; } /** * 判断当前日志是否需要过滤(核心过滤逻辑) * @return bool true=需要过滤,false=保留日志 */ private function isFilter(): bool { $logFilter = Config::getInstance()->logFilter; // 无过滤规则时直接返回false(不过滤) if (empty($logFilter)) { return false; } // 1. 解析调用堆栈,获取业务代码的类名/方法名 $stackInfo = $this->parseBusinessStackInfo(); $className = $stackInfo['class'] ?? $this->name ?? ''; $methodName = $stackInfo['method'] ?? ''; // 2. 通配符匹配函数 $matchWildcard = function (string $pattern, string $value): bool { // 空规则特殊处理:如:debug 拆分后类规则为空,代表匹配所有类 if ($pattern === '') { return true; } // 将通配符*转换为正则的.*,并转义其他特殊字符 $regexPattern = preg_quote($pattern, '/'); $regexPattern = str_replace('\*', '.*', $regexPattern); // 正则全程匹配 return preg_match('/^' . $regexPattern . '$/', $value) === 1; }; // 3. 遍历过滤规则,匹配则返回true(需要过滤) foreach ($logFilter as $filter) { // 跳过非法过滤规则 if (!is_string($filter)) { continue; } // 拆分规则为类规则和方法规则(最多拆2部分,避免方法名含:) [$filterClassRule, $filterMethodRule] = array_pad(explode(':', $filter, 2), 2, '*'); // 匹配类名规则(支持通配符*) $isClassMatch = $matchWildcard($filterClassRule, $className); // 匹配方法名规则(支持通配符*) $isMethodMatch = $matchWildcard($filterMethodRule, $methodName); // 类和方法都匹配时,返回true(过滤该日志) if ($isClassMatch && $isMethodMatch) { return true; } } // 无匹配规则,返回false(保留日志) return false; } /** * 解析调用堆栈,获取实际业务代码的类名和方法名 * @return array ['class' => 业务类名, 'method' => 业务方法名] */ private function parseBusinessStackInfo(): array { $stackInfo = ['class' => '', 'method' => '']; // 获取调用栈(忽略参数,避免性能损耗,取前10层足够) $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 10); // 跳过的日志相关类/方法(根据实际项目调整) $skipPatterns = [ 'class' => [ 'Monolog\\', // 跳过Monolog核心类 __CLASS__, // 跳过当前类自身 'app\\logger\\', // 跳过大日志封装类(根据你的项目调整) ], 'method' => [ 'buildMessage', // 跳过当前方法 'isFilter', // 跳过过滤方法 'parseBusinessStackInfo', // 跳过堆栈解析方法 'replacePlaceholders',// 跳過占位符替换方法 'formatExceptionStackTrace', // 跳过异常格式化方法 'log', 'error', 'info', 'warning', 'debug' // 跳过日志级别方法 ] ]; // 遍历调用栈,找到第一个业务代码层 foreach ($trace as $step) { // 跳过无文件/无方法的栈帧 if (!isset($step['file']) || !isset($step['function'])) { continue; } // 跳过日志相关类 $currentClass = $step['class'] ?? ''; $isSkipClass = false; foreach ($skipPatterns['class'] as $pattern) { if ($pattern && str_contains($currentClass, $pattern)) { $isSkipClass = true; break; } } if ($isSkipClass) { continue; } // 跳过日志相关方法 $currentMethod = $step['function']; if (in_array($currentMethod, $skipPatterns['method'])) { continue; } // 解析业务类名(简化为项目内相对路径,和原逻辑一致) $projectRoot = defined('BASE_PATH') ? BASE_PATH : (function_exists('base_path') ? base_path() : ''); $file = $step['file']; if ($projectRoot && str_starts_with($file, $projectRoot)) { $file = substr($file, strlen($projectRoot) + 1); // 转换为类名格式(app/controller/Index.php → app.controller.Index) $class = str_replace(['.php', '\\', '/'], ['', '.', '.'], $file); } else { $class = $currentClass ?: basename($file, '.php'); } // 赋值并终止遍历(找到第一个业务层即可) $stackInfo['class'] = trim($class); $stackInfo['method'] = $currentMethod; break; } return $stackInfo; } /** * 格式化异常堆栈:让Stack trace下的每一行添加制表符缩进 * @param string $exceptionStr 原始异常字符串 * @return string 格式化后的异常字符串 */ private function formatExceptionStackTrace(string $exceptionStr): string { // 正则匹配 Stack trace: 后的所有行,并在每行前添加制表符 $pattern = '/(Stack trace:\s*)(.*)$/s'; return preg_replace_callback($pattern, function ($matches) { // $matches[1] 是 "Stack trace: " // $matches[2] 是堆栈内容 $stackTrace = $matches[2]; // 将堆栈的每一行(#开头)前添加 制表符(\t) $formattedStack = preg_replace('/(#\d+)/', "\t$1", $stackTrace); return $matches[1] . $formattedStack; }, $exceptionStr); } /** * 内部方法:替换 {} 占位符 * @param string $message 原始消息 * @param array $context 替换参数 * @return string 替换后的消息 */ private function replacePlaceholders(string $message, array $context = []): string { if (empty($context)) { return $message; } // 将上下文参数转换为安全的字符串 $replacements = array_map(function ($value) { if ($value === null) { return 'null'; } if (is_bool($value)) { return $value ? 'true' : 'false'; } if (is_array($value) || is_object($value)) { return json_encode($value, JSON_UNESCAPED_UNICODE | JSON_PARTIAL_OUTPUT_ON_ERROR); } return (string)$value; }, $context); // 按顺序替换 {} 占位符 $index = 0; $result = preg_replace_callback('/\{\}/', function () use (&$index, $replacements) { return isset($replacements[$index]) ? $replacements[$index++] : '{}'; }, $message); // 替换失败时返回原始消息 return $result ?: $message; } }