Files
dsserver/app/utils/ModelAutoGenerator.php
zimoyin ac95dbb1c8 init
2026-04-01 15:07:15 +08:00

337 lines
12 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 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'
};
}
}