mirror of
https://gitee.com/ulthon/ulthon_admin.git
synced 2026-07-01 23:42:48 +08:00
282 lines
9.5 KiB
PHP
282 lines
9.5 KiB
PHP
<?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
|
||
{
|
||
$type = strtolower((string)Config::get('database.connections.' . $this->connection . '.type', ''));
|
||
if ($type !== 'mysql') {
|
||
return '';
|
||
}
|
||
|
||
try {
|
||
$escaped = addcslashes($tableName, "\\_%");
|
||
$rows = Db::connect($this->connection)->query("SHOW TABLE STATUS LIKE '$escaped'");
|
||
if (!empty($rows)) {
|
||
return isset($rows[0]['Comment']) ? (string)$rows[0]['Comment'] : '';
|
||
}
|
||
} catch (\Exception $e) {
|
||
}
|
||
|
||
try {
|
||
$rows = Db::connect($this->connection)->query("SHOW CREATE TABLE `$tableName`");
|
||
if (!empty($rows)) {
|
||
$create = $rows[0]['Create Table'] ?? '';
|
||
if ($create !== '' && preg_match("/COMMENT='(.*?)'/s", $create, $matches)) {
|
||
return stripcslashes($matches[1]);
|
||
}
|
||
}
|
||
} 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)]";
|
||
}
|
||
|
||
// 如果是整型且未指定长度,默认11 (兼容旧习俗)
|
||
if (str_contains($dbType, 'int') && !$length) {
|
||
$length = 11;
|
||
}
|
||
|
||
// 构建 Field 注解参数
|
||
$fieldParams = [];
|
||
|
||
// type (default: varchar)
|
||
if ($dbType !== 'varchar') {
|
||
$fieldParams[] = "type: '$dbType'";
|
||
}
|
||
|
||
// length (default: null)
|
||
if ($length !== null) {
|
||
$fieldParams[] = "length: $length";
|
||
}
|
||
|
||
// precision (default: 0)
|
||
if ($precision !== 0) {
|
||
$fieldParams[] = "precision: $precision";
|
||
}
|
||
|
||
// scale (default: 0)
|
||
if ($scale !== 0) {
|
||
$fieldParams[] = "scale: $scale";
|
||
}
|
||
|
||
// nullable (default: true)
|
||
if ($notNull) {
|
||
$fieldParams[] = "nullable: false";
|
||
}
|
||
|
||
// default (default: null)
|
||
if (!is_null($default)) {
|
||
$defaultVal = is_string($default) ? "'$default'" : $default;
|
||
$fieldParams[] = "default: $defaultVal";
|
||
}
|
||
|
||
// comment (default: '')
|
||
if ($cleanComment !== '') {
|
||
$fieldParams[] = "comment: '$cleanComment'";
|
||
}
|
||
|
||
// unsigned (default: false)
|
||
if (str_contains($type, 'unsigned')) {
|
||
$fieldParams[] = "unsigned: true";
|
||
}
|
||
|
||
// autoIncrement (default: false)
|
||
if ($autoinc) {
|
||
$fieldParams[] = "autoIncrement: true";
|
||
}
|
||
|
||
// primary (default: false)
|
||
if ($primary) {
|
||
$fieldParams[] = "primary: true";
|
||
}
|
||
|
||
$fieldAttrStr = implode(', ', $fieldParams);
|
||
|
||
$fieldsCode[] = <<<PHP
|
||
#[Field($fieldAttrStr)]$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) . ']';
|
||
}
|
||
}
|