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()); } } }