Files
ulthon_admin/extend/base/common/service/scheme/DbToSchemeService.php
2026-01-09 21:08:51 +08:00

220 lines
8.1 KiB
PHP
Raw 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 base\common\service\scheme;
use think\facade\Db;
use think\facade\Config;
use think\helper\Str;
class DbToSchemeService
{
protected string $module = 'admin';
protected string $connection;
public function __construct()
{
$this->connection = Config::get('database.default', 'mysql');
}
public function generate(string $tableName, string $className = null): string
{
$prefix = Config::get('database.connections.' . $this->connection . '.prefix', '');
$fullTableName = $tableName;
// 如果表名不包含前缀,且配置了前缀,则添加
if ($prefix && !str_starts_with($tableName, $prefix)) {
$fullTableName = $prefix . $tableName;
}
// 尝试获取表信息
try {
// 获取列信息TP通用方法
$columns = Db::connect($this->connection)->getFields($fullTableName);
} catch (\Exception $e) {
// 尝试直接使用tableName
$fullTableName = $tableName;
$columns = Db::connect($this->connection)->getFields($fullTableName);
}
// 获取表注释
$tableComment = $this->getTableComment($fullTableName);
// 获取索引信息
$indices = $this->getTableIndices($fullTableName);
// 生成类名
if (empty($className)) {
// 去除前缀
$shortName = $tableName;
if ($prefix && str_starts_with($tableName, $prefix)) {
$shortName = substr($tableName, strlen($prefix));
}
$className = Str::studly($shortName);
}
// 构建类内容
return $this->buildClass($className, $fullTableName, $tableComment, $columns, $indices);
}
protected function getTableComment($tableName): string
{
// 简单适配 MySQL
try {
$config = Config::get('database.connections.' . $this->connection);
$database = $config['database'];
// 暂时只支持MySQL的表注释获取其他返回空
$sql = "SELECT table_comment FROM information_schema.TABLES WHERE table_schema = ? AND table_name = ?";
$res = Db::connect($this->connection)->query($sql, [$database, $tableName]);
return $res[0]['table_comment'] ?? '';
} catch (\Exception $e) {
return '';
}
}
protected function getTableIndices($tableName): array
{
try {
// SHOW KEYS FROM table
$keys = Db::connect($this->connection)->query("SHOW KEYS FROM `$tableName`");
$indices = [];
foreach ($keys as $key) {
$name = $key['Key_name'];
if ($name === 'PRIMARY') continue; // 主键在 Field 中处理
if (!isset($indices[$name])) {
$indices[$name] = [
'name' => $name,
'columns' => [],
'type' => $key['Non_unique'] == 0 ? 'UNIQUE' : ($key['Index_type'] == 'FULLTEXT' ? 'FULLTEXT' : 'NORMAL')
];
}
$indices[$name]['columns'][] = $key['Column_name'];
}
return array_values($indices);
} catch (\Exception $e) {
return [];
}
}
protected function buildClass($className, $tableName, $tableComment, $columns, $indices): string
{
$fieldsCode = [];
foreach ($columns as $field) {
$fieldName = $field['name'];
$type = $field['type']; // varchar(255)
$comment = $field['comment'] ?? '';
$default = $field['default'] ?? null;
$notNull = $field['notnull'] ?? false;
$primary = $field['primary'] ?? false;
$autoinc = $field['autoinc'] ?? false;
// 解析类型和长度
preg_match('/(\w+)(?:\((\d+)(?:,(\d+))?\))?/', $type, $matches);
$dbType = $matches[1] ?? 'varchar';
$length = isset($matches[2]) ? (int)$matches[2] : null;
$precision = isset($matches[2]) ? (int)$matches[2] : 0; // For decimal
$scale = isset($matches[3]) ? (int)$matches[3] : 0;
// 解析 Ulthon 组件语法
$componentAttr = '';
$cleanComment = $comment;
// 匹配 {type} (options)
// 改进:允许 options 中含有冒号,且 key:value 之间可能有空格,或者没有括号
// 例子:{radio} (0:禁用,1:启用) 或 {image} 或 {relation} (table:mall_cate,relationBindSelect:title)
if (preg_match('/\{(.*?)\}\s*(\((.*?)\))?/', $comment, $cmatch)) {
$compType = $cmatch[1];
$compOptionsStr = $cmatch[3] ?? '';
$cleanComment = trim(str_replace($cmatch[0], '', $comment));
// 解析选项 (1:A, 2:B) 或 (table:mall_cate,relationBindSelect:title)
$optionsCode = '[]';
if ($compOptionsStr) {
$options = [];
// 简单的 explode 可能有问题,如果值里面有逗号。但目前假设选项格式比较简单。
$parts = explode(',', $compOptionsStr);
foreach ($parts as $p) {
// 尝试分割 key:value
$kv = explode(':', trim($p), 2); // Limit to 2 parts
if (count($kv) >= 2) {
$k = trim($kv[0]);
$v = trim($kv[1]);
$options[$k] = $v;
} else {
$options[] = trim($p);
}
}
$optionsCode = $this->exportArray($options);
}
$componentAttr = "\n #[Component(type: '$compType', options: $optionsCode)]";
}
// 构建 Field 注解
$defaultStr = is_null($default) ? 'null' : (is_string($default) ? "'$default'" : $default);
$nullable = $notNull ? 'false' : 'true';
$unsigned = str_contains($type, 'unsigned') ? 'true' : 'false';
$primaryStr = $primary ? 'true' : 'false';
$autoincStr = $autoinc ? 'true' : 'false';
// 如果是整型且未指定长度默认11 (兼容旧习俗)
if (str_contains($dbType, 'int') && !$length) {
$length = 11;
}
$lengthStr = is_null($length) ? 'null' : $length;
$fieldsCode[] = <<<PHP
#[Field(type: '$dbType', length: $lengthStr, precision: $precision, scale: $scale, nullable: $nullable, default: $defaultStr, comment: '$cleanComment', unsigned: $unsigned, autoIncrement: $autoincStr, primary: $primaryStr)]$componentAttr
public \$$fieldName;
PHP;
}
$fieldsBody = implode("\n\n", $fieldsCode);
// 构建 Index 注解
$indexAttrs = '';
foreach ($indices as $idx) {
$cols = $this->exportArray($idx['columns']);
$indexAttrs .= "\n#[Index(columns: $cols, name: '{$idx['name']}', type: '{$idx['type']}')]";
}
return <<<PHP
<?php
namespace app\\{$this->module}\scheme;
use app\common\scheme\BaseScheme;
use app\common\scheme\attribute\Table;
use app\common\scheme\attribute\Field;
use app\common\scheme\attribute\Component;
use app\common\scheme\attribute\Index;
#[Table(name: '$tableName', comment: '$tableComment')]$indexAttrs
class $className extends BaseScheme
{
$fieldsBody
}
PHP;
}
protected function exportArray(array $array): string
{
// 简易数组导出
$items = [];
// 判断是否是关联数组
$isAssoc = array_keys($array) !== range(0, count($array) - 1);
foreach ($array as $k => $v) {
$val = is_string($v) ? "'$v'" : $v;
if ($isAssoc) {
$key = is_int($k) ? $k : "'$k'";
$items[] = "$key => $val";
} else {
$items[] = $val;
}
}
return '[' . implode(', ', $items) . ']';
}
}