diff --git a/app/admin/scheme/TestGoods.php b/app/admin/scheme/TestGoods.php new file mode 100644 index 0000000..57422bc --- /dev/null +++ b/app/admin/scheme/TestGoods.php @@ -0,0 +1,121 @@ + 'mall_cate', 'relationBindSelect' => 'title'])] + public $cate_id; + + #[Field(type: 'char', length: 20, precision: 20, scale: 0, nullable: false, default: '', comment: '商品名称', unsigned: false, autoIncrement: false, primary: false)] + public $title; + + #[Field(type: 'char', length: 255, precision: 255, scale: 0, nullable: false, default: null, comment: '商品logo', unsigned: false, autoIncrement: false, primary: false)] + #[Component(type: 'image', options: [])] + public $logo; + + #[Field(type: 'text', length: null, precision: 0, scale: 0, nullable: false, default: null, comment: '商品图片', unsigned: false, autoIncrement: false, primary: false)] + #[Component(type: 'images', options: [])] + public $images; + + #[Field(type: 'text', length: null, precision: 0, scale: 0, nullable: false, default: null, comment: '商品描述', unsigned: false, autoIncrement: false, primary: false)] + #[Component(type: 'editor', options: [])] + public $describe; + + #[Field(type: 'int', length: 11, precision: 0, scale: 0, nullable: false, default: '0', comment: '总库存', unsigned: true, autoIncrement: false, primary: false)] + public $total_stock; + + #[Field(type: 'int', length: 11, precision: 0, scale: 0, nullable: false, default: '0', comment: '排序', unsigned: true, autoIncrement: false, primary: false)] + public $sort; + + #[Field(type: 'int', length: 11, precision: 0, scale: 0, nullable: false, default: '0', comment: '状态', unsigned: true, autoIncrement: false, primary: false)] + #[Component(type: 'radio', options: ['正常', '禁用'])] + public $status; + + #[Field(type: 'varchar', length: 100, precision: 100, scale: 0, nullable: false, default: null, comment: '合格证', unsigned: false, autoIncrement: false, primary: false)] + #[Component(type: 'file', options: [])] + public $cert_file; + + #[Field(type: 'text', length: null, precision: 0, scale: 0, nullable: false, default: null, comment: '检测报告', unsigned: false, autoIncrement: false, primary: false)] + #[Component(type: 'files', options: [])] + public $verfiy_file; + + #[Field(type: 'char', length: 255, precision: 255, scale: 0, nullable: false, default: '', comment: '备注说明', unsigned: false, autoIncrement: false, primary: false)] + public $remark; + + #[Field(type: 'int', length: 11, precision: 0, scale: 0, nullable: false, default: '0', comment: '', unsigned: true, autoIncrement: false, primary: false)] + public $create_time; + + #[Field(type: 'int', length: 11, precision: 0, scale: 0, nullable: false, default: '0', comment: '', unsigned: true, autoIncrement: false, primary: false)] + public $update_time; + + #[Field(type: 'int', length: 11, precision: 0, scale: 0, nullable: false, default: '0', comment: '', unsigned: true, autoIncrement: false, primary: false)] + public $delete_time; + + #[Field(type: 'datetime', length: null, precision: 0, scale: 0, nullable: false, default: null, comment: '发布日期', unsigned: false, autoIncrement: false, primary: false)] + #[Component(type: 'date', options: ['date'])] + public $publish_time; + + #[Field(type: 'date', length: null, precision: 0, scale: 0, nullable: false, default: null, comment: '售卖日期', unsigned: false, autoIncrement: false, primary: false)] + #[Component(type: 'date', options: ['datetime'])] + public $sale_time; + + #[Field(type: 'varchar', length: 100, precision: 100, scale: 0, nullable: false, default: null, comment: '简介', unsigned: false, autoIncrement: false, primary: false)] + #[Component(type: 'textarea', options: [])] + public $intro; + + #[Field(type: 'int', length: 11, precision: 0, scale: 0, nullable: false, default: null, comment: '秒杀状态', unsigned: true, autoIncrement: false, primary: false)] + #[Component(type: 'select', options: [0 => '未参加', 1 => '已开始', 3 => '已结束'])] + public $time_status; + + #[Field(type: 'int', length: 11, precision: 0, scale: 0, nullable: false, default: '0', comment: '是否推荐', unsigned: false, autoIncrement: false, primary: false)] + #[Component(type: 'switch', options: ['不推荐', '推荐'])] + public $is_recommend; + + #[Field(type: 'varchar', length: 100, precision: 100, scale: 0, nullable: false, default: '0', comment: '商品类型', unsigned: false, autoIncrement: false, primary: false)] + #[Component(type: 'checkbox', options: ['taobao' => '淘宝', 'jd' => '京东'])] + public $shop_type; + + #[Field(type: 'varchar', length: 100, precision: 100, scale: 0, nullable: false, default: null, comment: '商品标签', unsigned: false, autoIncrement: false, primary: false)] + #[Component(type: 'table', options: ['table' => 'mall_tag', 'type' => 'checkbox', 'valueField' => 'id', 'fieldName' => 'title'])] + public $tag; + + #[Field(type: 'varchar', length: 100, precision: 100, scale: 0, nullable: true, default: null, comment: '商品标签(单选)', unsigned: false, autoIncrement: false, primary: false)] + #[Component(type: 'table', options: ['table' => 'mall_tag', 'type' => 'radio', 'valueField' => 'id', 'fieldName' => 'title'])] + public $tag_backup; + + #[Field(type: 'varchar', length: 100, precision: 100, scale: 0, nullable: false, default: null, comment: '产地', unsigned: false, autoIncrement: false, primary: false)] + #[Component(type: 'city', options: ['name-province' => '0', 'code' => '0'])] + public $from_area; + + #[Field(type: 'varchar', length: 100, precision: 100, scale: 0, nullable: false, default: '山东省/临沂市', comment: '仓库', unsigned: false, autoIncrement: false, primary: false)] + #[Component(type: 'city', options: ['level' => 'city'])] + public $store_city; + + #[Field(type: 'varchar', length: 100, precision: 100, scale: 0, nullable: false, default: null, comment: '商品标签 (输入)', unsigned: false, autoIncrement: false, primary: false)] + #[Component(type: 'tag', options: [])] + public $tag_input; + + #[Field(type: 'varchar', length: 100, precision: 100, scale: 0, nullable: false, default: null, comment: '唯一id', unsigned: false, autoIncrement: false, primary: false)] + public $uid; + + #[Field(type: 'decimal', length: 10, precision: 10, scale: 0, nullable: true, default: null, comment: '价格', unsigned: false, autoIncrement: false, primary: false)] + public $price; + + #[Field(type: 'text', length: null, precision: 0, scale: 0, nullable: true, default: null, comment: '详情', unsigned: false, autoIncrement: false, primary: false)] + public $detail; +} \ No newline at end of file diff --git a/app/common/command/scheme/Backup.php b/app/common/command/scheme/Backup.php new file mode 100644 index 0000000..1796cc1 --- /dev/null +++ b/app/common/command/scheme/Backup.php @@ -0,0 +1,9 @@ + Env::get('database.type', 'sqlite'), + 'default' => Env::get('database.main', 'sqlite'), // 自定义时间查询规则 'time_query_rule' => [], @@ -21,7 +21,7 @@ return [ // 数据库连接配置信息 'connections' => [ - 'mysql' => [ + 'main' => [ // 数据库类型 'type' => Env::get('database.type', 'mysql'), // 服务器地址 diff --git a/config/scheme.php b/config/scheme.php new file mode 100644 index 0000000..83d4d06 --- /dev/null +++ b/config/scheme.php @@ -0,0 +1,11 @@ + [ + 'migrations', + 'phinxlog', + ], + // 备份中间前缀 + 'backup_prefix' => 'backup', +]; diff --git a/extend/base/common/command/scheme/Backup.php b/extend/base/common/command/scheme/Backup.php new file mode 100644 index 0000000..7e55c22 --- /dev/null +++ b/extend/base/common/command/scheme/Backup.php @@ -0,0 +1,25 @@ +setName('scheme:backups') + ->setDescription('List all backup tables'); + } + + protected function execute(Input $input, Output $output) + { + $tables = Db::query("SHOW TABLES LIKE 'ul_backup%'"); + foreach ($tables as $t) { + $output->writeln(array_values($t)[0]); + } + } +} diff --git a/extend/base/common/command/scheme/Make.php b/extend/base/common/command/scheme/Make.php new file mode 100644 index 0000000..3bb6a26 --- /dev/null +++ b/extend/base/common/command/scheme/Make.php @@ -0,0 +1,73 @@ +setName('scheme:make') + ->addArgument('table', Argument::OPTIONAL, "The table name (without prefix)") + ->setDescription('Generate Scheme class from Database table'); + } + + protected function execute(Input $input, Output $output) + { + $table = $input->getArgument('table'); + $service = new DbToSchemeService(); + + $tables = []; + if ($table) { + $tables[] = $table; + } else { + // 获取所有表 + $allTables = Db::getTables(); + // 过滤掉忽略的表 + $config = Config::get('scheme.ignore_tables', []); + $prefix = Config::get('database.connections.mysql.prefix'); + + foreach ($allTables as $t) { + // 如果有前缀,去除前缀后再判断 + $shortName = $t; + if ($prefix && str_starts_with($t, $prefix)) { + $shortName = substr($t, strlen($prefix)); + } + + if (!in_array($shortName, $config) && !in_array($t, $config)) { + $tables[] = $shortName; + } + } + } + + foreach ($tables as $t) { + $output->writeln("Processing table: $t"); + try { + $code = $service->generate($t); + + // 提取类名以确定文件名 + if (preg_match('/class\s+(\w+)/', $code, $matches)) { + $className = $matches[1]; + $path = app()->getAppPath() . 'admin/scheme/' . $className . '.php'; + + // 确保目录存在 + if (!is_dir(dirname($path))) { + mkdir(dirname($path), 0755, true); + } + + file_put_contents($path, $code); + $output->writeln("Generated: $path"); + } + } catch (\Exception $e) { + $output->writeln("Error processing $t: " . $e->getMessage() . ""); + } + } + } +} diff --git a/extend/base/common/command/scheme/Sync.php b/extend/base/common/command/scheme/Sync.php new file mode 100644 index 0000000..1baa143 --- /dev/null +++ b/extend/base/common/command/scheme/Sync.php @@ -0,0 +1,53 @@ +setName('scheme:sync') + ->addOption('skip-data', null, Option::VALUE_NONE, 'Skip data migration') + ->addOption('force', null, Option::VALUE_NONE, 'Force execution without confirmation') + ->setDescription('Synchronize Scheme classes to Database'); + } + + protected function execute(Input $input, Output $output) + { + $skipData = $input->getOption('skip-data'); + + $service = new SchemeToDbService(); + $schemeDir = app()->getAppPath() . 'admin/scheme/'; + + if (!is_dir($schemeDir)) { + $output->writeln("Scheme directory not found: $schemeDir"); + return; + } + + $files = glob($schemeDir . '*.php'); + foreach ($files as $file) { + require_once $file; + $className = 'app\\admin\\scheme\\' . basename($file, '.php'); + + if (class_exists($className)) { + $output->writeln("Syncing $className..."); + try { + $backup = $service->sync($className, $skipData); + $output->writeln("Success!"); + if ($backup) { + $output->writeln("Backup created: $backup"); + } + } catch (\Exception $e) { + $output->writeln("Failed: " . $e->getMessage() . ""); + } + } + } + } +} diff --git a/extend/base/common/scheme/BaseScheme.php b/extend/base/common/scheme/BaseScheme.php new file mode 100644 index 0000000..2a79b71 --- /dev/null +++ b/extend/base/common/scheme/BaseScheme.php @@ -0,0 +1,8 @@ +'男', '2'=>'女']) + * @param array $extra 额外参数 + */ + public function __construct( + public string $type, + public array $options = [], + public array $extra = [] + ) { + } +} diff --git a/extend/base/common/scheme/attribute/Field.php b/extend/base/common/scheme/attribute/Field.php new file mode 100644 index 0000000..ed35610 --- /dev/null +++ b/extend/base/common/scheme/attribute/Field.php @@ -0,0 +1,23 @@ +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[] = <<exportArray($idx['columns']); + $indexAttrs .= "\n#[Index(columns: $cols, name: '{$idx['name']}', type: '{$idx['type']}')]"; + } + + return <<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) . ']'; + } +} diff --git a/extend/base/common/service/scheme/SchemeToDbService.php b/extend/base/common/service/scheme/SchemeToDbService.php new file mode 100644 index 0000000..40960ce --- /dev/null +++ b/extend/base/common/service/scheme/SchemeToDbService.php @@ -0,0 +1,243 @@ +connection = Config::get('database.default', 'mysql'); + } + + public function sync(string $className, bool $skipData = false) + { + if (!class_exists($className)) { + throw new \Exception("Class $className not found"); + } + + $ref = new ReflectionClass($className); + + // 获取 Table 注解 + $tableAttrs = $ref->getAttributes(Table::class); + if (empty($tableAttrs)) { + throw new \Exception("Class $className missing #[Table] attribute"); + } + /** @var Table $tableAttr */ + $tableAttr = $tableAttrs[0]->newInstance(); + + $tableName = $tableAttr->name; + $prefix = Config::get('database.connections.' . $this->connection . '.prefix', ''); + + // 确保表名带前缀 + $fullTableName = $tableName; + if ($prefix && !str_starts_with($tableName, $prefix)) { + $fullTableName = $prefix . $tableName; + } + + // 检查表是否存在 + $tableExists = $this->checkTableExists($fullTableName); + $backupTableName = null; + + // 1. 备份 + if ($tableExists) { + $backupPrefix = Config::get('scheme.backup_prefix', 'backup'); + $backupTableName = $prefix . $backupPrefix . '_' . date('YmdHis') . '_' . $fullTableName; + + try { + // 确保没有残留的同名备份表(极端情况) + Db::connect($this->connection)->execute("DROP TABLE IF EXISTS `$backupTableName`"); + Db::connect($this->connection)->execute("RENAME TABLE `$fullTableName` TO `$backupTableName`"); + } catch (\Exception $e) { + // 如果备份失败,可能是权限问题或其他,抛出异常 + throw new \Exception("Backup failed: " . $e->getMessage()); + } + } + + // 2. 建表 + $sql = $this->buildCreateTableSql($fullTableName, $tableAttr, $ref); + Db::connect($this->connection)->execute($sql); + + // 3. 恢复数据 + if ($tableExists && !$skipData && $backupTableName) { + try { + $this->restoreData($fullTableName, $backupTableName, $ref->getProperties()); + } catch (\Exception $e) { + // 如果数据恢复失败,尝试回滚(这里只是简单的删除新表,重命名回旧表) + // 实际生产环境可能需要更复杂的恢复机制 + // Db::connect($this->connection)->execute("DROP TABLE IF EXISTS `$fullTableName`"); + // Db::connect($this->connection)->execute("RENAME TABLE `$backupTableName` TO `$fullTableName`"); + throw $e; + } + } + + return $backupTableName; + } + + protected function checkTableExists($tableName) + { + $tables = Db::connect($this->connection)->query("SHOW TABLES LIKE '$tableName'"); + return !empty($tables); + } + + protected function buildCreateTableSql($tableName, Table $tableAttr, ReflectionClass $ref) + { + $properties = $ref->getProperties(); + $lines = []; + $primaryKeys = []; + + foreach ($properties as $prop) { + $fieldAttrs = $prop->getAttributes(Field::class); + if (empty($fieldAttrs)) { + continue; + } + /** @var Field $field */ + $field = $fieldAttrs[0]->newInstance(); + $fieldName = $prop->getName(); + + $line = "`$fieldName` {$field->type}"; + + // Length/Precision + if ($field->length !== null) { + $line .= "({$field->length})"; + } elseif ($field->precision > 0) { + $line .= "({$field->precision},{$field->scale})"; + } + + // Unsigned + if ($field->unsigned) { + $line .= " UNSIGNED"; + } + + // Nullable + if (!$field->nullable) { + $line .= " NOT NULL"; + } else { + $line .= " DEFAULT NULL"; + } + + // AutoIncrement + if ($field->autoIncrement) { + $line .= " AUTO_INCREMENT"; + } + + // Default + if (!is_null($field->default)) { + $def = $field->default; + if (is_string($def)) { + $line .= " DEFAULT '$def'"; + } elseif (is_bool($def)) { + $line .= " DEFAULT " . ($def ? 1 : 0); + } else { + $line .= " DEFAULT $def"; + } + } + + // Comment + Component Restore + $comment = $field->comment; + + // 检查是否有 Component 注解 + $compAttrs = $prop->getAttributes(Component::class); + if (!empty($compAttrs)) { + /** @var Component $comp */ + $comp = $compAttrs[0]->newInstance(); + $typeStr = "{{$comp->type}}"; + $optionsStr = ''; + if (!empty($comp->options)) { + $parts = []; + foreach ($comp->options as $k => $v) { + // 如果是数字索引,且连续,可能就是简单的值列表? + // 但 Ulthon 格式通常是 k:v。如果 k 是数字,也要输出。 + $parts[] = "$k:$v"; + } + $optionsStr = ' (' . implode(',', $parts) . ')'; + } + // 拼接回注释:原注释 {type} (options) + $comment = trim($comment . " $typeStr$optionsStr"); + } + + if ($comment) { + $line .= " COMMENT '$comment'"; + } + + $lines[] = $line; + + if ($field->primary) { + $primaryKeys[] = "`$fieldName`"; + } + } + + if (!empty($primaryKeys)) { + $lines[] = "PRIMARY KEY (" . implode(',', $primaryKeys) . ")"; + } + + // 处理 Index 注解 + $indexAttrs = $ref->getAttributes(Index::class); + foreach ($indexAttrs as $attr) { + /** @var Index $idx */ + $idx = $attr->newInstance(); + $cols = is_array($idx->columns) ? $idx->columns : [$idx->columns]; + $colStr = "`" . implode("`,`", $cols) . "`"; + $keyName = $idx->name ?: $cols[0]; // 默认使用第一列名 + + if ($idx->type === 'UNIQUE') { + $lines[] = "UNIQUE KEY `$keyName` ($colStr)"; + } elseif ($idx->type === 'FULLTEXT') { + $lines[] = "FULLTEXT KEY `$keyName` ($colStr)"; + } else { + $lines[] = "KEY `$keyName` ($colStr)"; + } + } + + $body = implode(",\n ", $lines); + $comment = $tableAttr->comment ? " COMMENT='{$tableAttr->comment}'" : ""; + + return "CREATE TABLE `$tableName` (\n $body\n) ENGINE={$tableAttr->engine} DEFAULT CHARSET={$tableAttr->charset}$comment"; + } + + protected function restoreData($newTable, $oldTable, array $properties) + { + $fields = []; + foreach ($properties as $prop) { + $fieldAttrs = $prop->getAttributes(Field::class); + if (!empty($fieldAttrs)) { + $fields[] = "`" . $prop->getName() . "`"; + } + } + + if (empty($fields)) return; + + try { + $oldFieldsRaw = Db::connect($this->connection)->getFields($oldTable); + $oldFields = array_keys($oldFieldsRaw); + + $commonFields = []; + foreach ($properties as $prop) { + $name = $prop->getName(); + if (in_array($name, $oldFields)) { + $commonFields[] = "`$name`"; + } + } + + if (empty($commonFields)) return; + + $commonStr = implode(',', $commonFields); + + $sql = "INSERT INTO `$newTable` ($commonStr) SELECT $commonStr FROM `$oldTable`"; + Db::connect($this->connection)->execute($sql); + + } catch (\Exception $e) { + throw new \Exception("Data migration failed: " . $e->getMessage()); + } + } +}