This commit is contained in:
zimoyin
2026-04-01 15:07:15 +08:00
commit ac95dbb1c8
55 changed files with 11634 additions and 0 deletions
+379
View File
@@ -0,0 +1,379 @@
<?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;
}
}
+337
View File
@@ -0,0 +1,337 @@
<?php
namespace app\utils;
use support\Db;
use RuntimeException;
class ModelAutoGenerator
{
/**
* 生成单个数据表对应的模型文件
*
* @param string $tableName 数据库表名(如:user
* @param string $modelName 模型类名(如:User
* @param bool $force 是否强制覆盖已存在的模型文件
* @return array 生成结果 ['status' => bool, 'msg' => string]
*/
public static function generate(string $tableName, string $modelName, bool $force = false): array
{
// 1. 定义模型文件路径(兼容webman的app_path
$modelPath = app_path() . '/model/' . ucfirst($modelName) . '.php';
$modelClassName = ucfirst($modelName); // 确保类名首字母大写
// 2. 检查文件是否已存在,存在则返回不覆盖提示
if (file_exists($modelPath) && !$force) {
return [
'status' => false,
'msg' => "模型文件 {$modelClassName}.php 已存在,不执行覆盖操作"
];
}
try {
// 3. 读取数据表基本信息(包含表注释)- 修复参数绑定问题
$tableComment = self::getTableComment($tableName);
// 4. 读取数据表结构(包含字段注释、类型等)
$tableStruct = self::getTableStruct($tableName);
if (empty($tableStruct)) {
return [
'status' => false,
'msg' => "数据表 {$tableName} 无字段信息,生成失败"
];
}
// 5. 构建模型文件内容(传入表注释)
$modelContent = self::buildModelContent($tableName, $modelClassName, $tableStruct, $tableComment);
// 6. 确保model目录存在
if (!is_dir(dirname($modelPath))) {
mkdir(dirname($modelPath), 0755, true);
}
// 7. 写入模型文件
$writeResult = file_put_contents($modelPath, $modelContent);
if ($writeResult === false) {
throw new RuntimeException("模型文件写入失败,检查目录权限");
}
return [
'status' => true,
'msg' => "模型 {$modelClassName}.php 已成功生成至 app/model 目录"
];
} catch (\Exception $e) {
return [
'status' => false,
'msg' => "生成失败:{$e->getMessage()}"
];
}
}
/**
* 批量生成所有数据表的模型文件
*
* @param bool $force 是否强制覆盖已存在的模型文件
* @param array $excludeTables 排除的数据表(如:['migrations', 'logs']
* @return array 批量生成结果 ['success' => 成功数量, 'fail' => 失败列表]
*/
public static function generate_all(bool $force = false, array $excludeTables = []): array
{
$result = [
'success' => 0,
'fail' => []
];
try {
// 1. 获取数据库中所有数据表
$tables = Db::select("SHOW TABLES");
$tableNameKey = 'Tables_in_' . config('database.connections.mysql.database');
foreach ($tables as $table) {
$tableName = $table->$tableNameKey;
// 跳过排除的表
if (in_array($tableName, $excludeTables)) {
continue;
}
// 跳过视图(以 v_ 开头的表,避免视图生成模型)
if (str_starts_with($tableName, 'v_')) {
continue;
}
// 2. 表名转模型名(下划线转驼峰,如 user_info → UserInfo
$modelName = self::tableNameToModelName($tableName);
// 3. 调用单个生成方法
$generateResult = self::generate($tableName, $modelName, $force);
if ($generateResult['status']) {
$result['success']++;
} else {
$result['fail'][] = [
'table' => $tableName,
'model' => $modelName,
'reason' => $generateResult['msg']
];
}
}
return $result;
} catch (\Exception $e) {
$result['fail'][] = [
'table' => 'all',
'model' => 'all',
'reason' => "批量生成异常:{$e->getMessage()}"
];
return $result;
}
}
/**
* 获取数据表注释(修复参数绑定问题)
*
* @param string $tableName 数据表名
* @return string 表注释(无注释则返回空字符串)
*/
private static function getTableComment(string $tableName): string
{
// 修复核心:不使用参数绑定,直接拼接表名(先过滤表名防止注入)
$safeTableName = preg_replace('/[^a-zA-Z0-9_]/', '', $tableName);
$sql = "SHOW TABLE STATUS LIKE '{$safeTableName}'";
try {
$tableInfo = Db::select($sql);
} catch (\Exception $e) {
// 读取表注释失败时返回空字符串,不影响模型生成
return '';
}
if (empty($tableInfo)) {
return '';
}
// 兼容不同数据库驱动的字段名(Comment/comment
$comment = $tableInfo[0]->Comment ?? $tableInfo[0]->comment ?? '';
return trim($comment);
}
/**
* 获取数据表结构(修复关键字冲突+兼容字段注释读取)
*
* @param string $tableName 数据表名
* @return array 表结构数组
*/
private static function getTableStruct(string $tableName): array
{
// 表名安全过滤
$safeTableName = preg_replace('/[^a-zA-Z0-9_]/', '', $tableName);
// 修复关键字冲突:给别名加反引号,避免与MySQL保留字冲突
$sql = "SELECT
COLUMN_NAME AS `Field`,
DATA_TYPE AS `TypeSimple`,
COLUMN_TYPE AS `Type`,
IS_NULLABLE AS `IsNull`,
COLUMN_KEY AS `ColumnKey`,
COLUMN_DEFAULT AS `ColumnDefault`,
COLUMN_COMMENT AS `Comment`
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = ?
AND TABLE_NAME = ?
ORDER BY ORDINAL_POSITION";
// 使用参数绑定读取字段信息(INFORMATION_SCHEMA 支持参数绑定)
$tableStruct = Db::select($sql, [
config('database.connections.mysql.database'),
$safeTableName
]);
// 格式化字段信息,确保注释字段统一
return array_map(function ($field) {
return (object)[
'Field' => $field->Field ?? '',
'Type' => $field->Type ?? $field->TypeSimple ?? '',
'Key' => $field->ColumnKey ?? '', // 对应修改后的别名
// 优先读取 COMMENT,兼容大小写,确保去空格
'Comment' => trim($field->Comment ?? $field->comment ?? ''),
'Null' => $field->IsNull ?? '', // 对应修改后的别名
'Default' => $field->ColumnDefault ?? '' // 对应修改后的别名
];
}, $tableStruct);
}
/**
* 构建模型文件内容(确保字段注释正确展示)
*
* @param string $tableName 数据表名
* @param string $modelClassName 模型类名
* @param array $tableStruct 表结构信息
* @param string $tableComment 表注释
* @return string 模型文件内容
*/
private static function buildModelContent(string $tableName, string $modelClassName, array $tableStruct, string $tableComment): string
{
// 提取主键
$primaryKey = 'id';
foreach ($tableStruct as $field) {
if ($field->Key === 'PRI') {
$primaryKey = $field->Field;
break;
}
}
// 构建字段注释(格式:字段名 数据库注释)
$fieldComments = [];
foreach ($tableStruct as $field) {
$fieldName = $field->Field; // 原始字段名(如 dt
$dbComment = $field->Comment; // 数据库注释(如 基准时间)
// 拼接注释:字段名 + (有数据库注释则加)数据库注释
$fullComment = $fieldName;
if (!empty($dbComment)) {
$fullComment .= " {$dbComment}";
}
// 补充字段属性说明(非空、主键、默认值)
$extComment = [];
if ($field->Null === 'NO') {
$extComment[] = '非空';
}
if ($field->Key === 'PRI') {
$extComment[] = '主键';
}
if ($field->Default !== '') {
$extComment[] = "默认值:{$field->Default}";
}
// 如有属性说明,追加到注释末尾
if (!empty($extComment)) {
$fullComment .= '' . implode('', $extComment) . '';
}
$fieldType = self::dbTypeToPhpType($field->Type);
$fieldComments[] = " * @property {$fieldType} \${$fieldName} {$fullComment}";
}
$fieldCommentsStr = implode("\n", $fieldComments);
// 构建表注释(无注释则使用默认描述)
$tableCommentStr = !empty($tableComment) ? $tableComment : "{$tableName} 数据表模型";
// 模型模板(适配webman的think-orm
return <<<PHP
<?php
namespace app\model;
use support\Model;
/**
* {$modelClassName} 模型
* 表说明:{$tableCommentStr}
* 对应数据表:{$tableName}
{$fieldCommentsStr}
*/
class {$modelClassName} extends Model
{
/**
* 数据表名
* @var string
*/
protected \$table = '{$tableName}';
/**
* 主键字段名
* @var string
*/
protected \$pk = '{$primaryKey}';
/**
* 关闭自动时间戳(如需开启请改为 true,需确保表有 create_time/update_time 字段)
* @var bool
*/
protected \$autoWriteTimestamp = false;
/**
* 字段严格检查(false=允许操作未定义字段)
* @var bool
*/
protected \$strict = false;
}
PHP;
}
/**
* 数据表名转模型名(下划线转驼峰)
*
* @param string $tableName 数据表名(如:user_info
* @return string 模型名(如:UserInfo
*/
private static function tableNameToModelName(string $tableName): string
{
// 移除表前缀(如需处理前缀,可在此添加逻辑)
// $prefix = config('database.connections.mysql.prefix');
// $tableName = str_replace($prefix, '', $tableName);
// 下划线转驼峰并首字母大写
return str_replace(' ', '', ucwords(str_replace('_', ' ', $tableName)));
}
/**
* 数据库字段类型转PHP类型注释
*
* @param string $dbType 数据库类型(如:int(11), varchar(255)
* @return string PHP类型(如:int, string
*/
private static function dbTypeToPhpType(string $dbType): string
{
$type = strtolower(explode('(', $dbType)[0]);
return match ($type) {
'int', 'tinyint', 'smallint', 'mediumint', 'bigint' => 'int',
'float', 'double', 'decimal' => 'float',
'date', 'time', 'datetime', 'timestamp' => 'string',
'bool', 'boolean' => 'bool',
default => 'string'
};
}
}