Files
zimoyin defe163190 test
2026-03-18 13:27:48 +08:00

379 lines
14 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace app\utils;
use app\config\Config;
use support\Log;
use Throwable;
/**
* 日志工具类(仅提供静态方法)
*/
class Logger
{
/**
* 获取日志实例(核心入口)
* @param string $name 日志器名称,默认 '__default'
* @return LoggerIns
*/
public static function new(string $name = '__default'): LoggerIns
{
return new LoggerIns($name);
}
/**
* 生成自适应宽度的文本框(精准空格版,绝对对齐)
* @param array $rows 文本框内的行内容数组
* @param string $borderChar 边框字符(默认横线)
* @return string 格式化后的文本框字符串
*/
public static function generateTextBox(array $rows, string $borderChar = '-'): string
{
// 过滤空行
$validRows = array_filter($rows, fn($row) => 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 = "<RP_NAME:{$this->name}>";
$result = $namePrefix . $processedMessage;
// if ($this->isFilter()) return "";
return $result;
}
/**
* 解析调用堆栈,获取实际业务代码的类名和方法名
* @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;
}
}