PHP项目迁移与种子管理完整指南
项目迁移最佳实践
迁移前评估
# 检查PHP版本兼容性 php -v # 检查扩展依赖 php -m | grep -E "pdo|mysql|redis|mbstring" # 代码静态分析 phpstan analyze --level=max src/
数据库迁移策略
Schema迁移 (使用Phinx)

// migrations/20240101000001_create_users.php
use Phinx\Migration\AbstractMigration;
class CreateUsers extends AbstractMigration
{
public function up()
{
$table = $this->table('users');
$table->addColumn('username', 'string', ['limit' => 50])
->addColumn('email', 'string', ['limit' => 100])
->addColumn('created_at', 'datetime')
->create();
}
public function down()
{
$this->dropTable('users');
}
}
数据迁移脚本
// migrations/20240102000001_import_legacy_data.php
class ImportLegacyData extends AbstractMigration
{
public function up()
{
$legacy = new PDO('mysql:host=old_host;dbname=old_db', 'user', 'pass');
$new = $this->getAdapter()->getConnection();
// 分批处理避免内存溢出
$offset = 0;
$limit = 1000;
while ($rows = $legacy->query("SELECT * FROM old_users LIMIT $offset, $limit")) {
foreach ($rows as $row) {
$stmt = $new->prepare("INSERT INTO users (username, email) VALUES (?, ?)");
$stmt->execute([$row['name'], $row['email']]);
}
$offset += $limit;
}
}
}
环境配置管理
使用环境变量
// config/app.php
return [
'db' => [
'host' => $_ENV['DB_HOST'] ?? 'localhost',
'database' => $_ENV['DB_NAME'],
'username' => $_ENV['DB_USER'],
'password' => $_ENV['DB_PASS'],
],
'redis' => [
'host' => $_ENV['REDIS_HOST'] ?? '127.0.0.1',
'port' => $_ENV['REDIS_PORT'] ?? 6379,
]
];
多环境配置文件
# .env.example
APP_ENV=development
DB_HOST=localhost
DB_NAME=myapp_dev
DB_USER=dev_user
DB_PASS=dev_pass
REDIS_HOST=127.0.0.1
# .env.production
APP_ENV=production
DB_HOST=prod-db.example.com
DB_NAME=myapp
DB_USER=prod_user
DB_PASS=${DB_PASSWORD}
种子数据管理
使用Seeder类 (Laravel风格)
// database/seeds/UserSeeder.php
use Illuminate\Database\Seeder;
use App\Models\User;
use Illuminate\Support\Facades\Hash;
use Faker\Factory as Faker;
class UserSeeder extends Seeder
{
public function run()
{
$faker = Faker::create();
// 创建管理员账户
User::create([
'username' => 'admin',
'email' => 'admin@example.com',
'password' => Hash::make('password'),
'role' => 'admin'
]);
// 创建100个测试用户
for ($i = 0; $i < 100; $i++) {
User::create([
'username' => $faker->userName,
'email' => $faker->unique()->email,
'password' => Hash::make('password'),
'role' => 'user'
]);
}
}
}
自定义种子管理工具
// SeedManager.php
class SeedManager
{
private $seeders = [];
public function registerSeeder($name, callable $callback)
{
$this->seeders[$name] = $callback;
return $this;
}
public function run($seederName = null)
{
if ($seederName) {
if (!isset($this->seeders[$seederName])) {
throw new Exception("Seeder '$seederName' not found");
}
$this->executeSeeder($seederName);
} else {
foreach ($this->seeders as $name => $callback) {
$this->executeSeeder($name);
}
}
}
private function executeSeeder($name)
{
echo "Running seeder: $name...\n";
$start = microtime(true);
call_user_func($this->seeders[$name]);
$elapsed = microtime(true) - $start;
echo "Seeder '$name' completed in " . round($elapsed, 2) . "s\n";
}
}
// 使用示例
$manager = new SeedManager();
$manager->registerSeeder('users', function () {
// 用户种子逻辑
});
$manager->registerSeeder('products', function () {
// 产品种子逻辑
});
// 运行所有种子
$manager->run();
// 运行特定种子
$manager->run('users');
数据工厂模式
// UserFactory.php
class UserFactory
{
private $faker;
private $count = 1;
private $overrides = [];
public function __construct()
{
$this->faker = Faker\Factory::create();
}
public function count($n)
{
$this->count = $n;
return $this;
}
public function state(array $override)
{
$this->overrides = $override;
return $this;
}
public function make()
{
$users = [];
for ($i = 0; $i < $this->count; $i++) {
$users[] = array_merge([
'username' => $this->faker->userName,
'email' => $this->faker->unique()->email,
'password' => password_hash('password', PASSWORD_DEFAULT),
'created_at' => new DateTime()
], $this->overrides);
}
return $users;
}
public function create()
{
$users = $this->make();
// 批量插入数据库
$this->batchInsert($users);
return $users;
}
}
// 使用
$factory = new UserFactory();
$admin = $factory->count(1)->state(['role' => 'admin'])->create();
$users = $factory->count(50)->state(['role' => 'user'])->create();
迁移脚本执行系统
// MigrationRunner.php
class MigrationRunner
{
private $db;
private $migrationsDir;
private $tableName = 'migrations';
public function __construct(PDO $db, $migrationsDir)
{
$this->db = $db;
$this->migrationsDir = $migrationsDir;
$this->createMigrationsTable();
}
private function createMigrationsTable()
{
$this->db->exec("CREATE TABLE IF NOT EXISTS {$this->tableName} (
id INT AUTO_INCREMENT PRIMARY KEY,
migration VARCHAR(255) NOT NULL,
batch INT NOT NULL,
executed_at DATETIME DEFAULT CURRENT_TIMESTAMP
)");
}
public function migrate()
{
$executed = $this->getExecutedMigrations();
$files = glob($this->migrationsDir . '/*.php');
$batch = $this->getNextBatchNumber();
$migrated = 0;
foreach ($files as $file) {
$migration = basename($file, '.php');
if (in_array($migration, $executed)) {
continue;
}
require_once $file;
$className = $this->getClassName($migration);
if (!class_exists($className)) {
throw new Exception("Migration class '$className' not found");
}
$instance = new $className();
$instance->up($this->db);
$this->recordMigration($migration, $batch);
echo "Migrated: $migration\n";
$migrated++;
}
echo "Migrated $migrated migration(s)\n";
}
public function rollback($steps = 1)
{
$lastBatch = $this->getLastBatchNumber();
$migrations = $this->getMigrationsByBatch($lastBatch);
$rolled = 0;
foreach (array_reverse($migrations) as $migration) {
if ($rolled >= $steps) break;
require_once $this->migrationsDir . '/' . $migration . '.php';
$className = $this->getClassName($migration);
$instance = new $className();
$instance->down($this->db);
$this->removeMigrationRecord($migration);
echo "Rolled back: $migration\n";
$rolled++;
}
}
private function getClassName($migration)
{
// 将文件名转换为类名: 20240101000001_create_users => CreateUsers
$parts = explode('_', $migration);
array_shift($parts); // 移除时间戳部分
return implode('', array_map('ucfirst', $parts));
}
private function getExecutedMigrations()
{
$stmt = $this->db->query("SELECT migration FROM {$this->tableName}");
return $stmt->fetchAll(PDO::FETCH_COLUMN);
}
private function getNextBatchNumber()
{
$stmt = $this->db->query("SELECT MAX(batch) FROM {$this->tableName}");
return ($stmt->fetchColumn() ?? 0) + 1;
}
private function recordMigration($migration, $batch)
{
$stmt = $this->db->prepare(
"INSERT INTO {$this->tableName} (migration, batch) VALUES (?, ?)"
);
$stmt->execute([$migration, $batch]);
}
private function removeMigrationRecord($migration)
{
$stmt = $this->db->prepare(
"DELETE FROM {$this->tableName} WHERE migration = ?"
);
$stmt->execute([$migration]);
}
}
高级场景处理
数据迁移验证
// MigrationValidator.php
class MigrationValidator
{
public function validate($sourceDb, $targetDb, $tables)
{
$results = [];
foreach ($tables as $table) {
$results[$table] = $this->compareTables($sourceDb, $targetDb, $table);
}
return $results;
}
private function compareTables($source, $target, $table)
{
$sourceCount = $source->query("SELECT COUNT(*) FROM $table")->fetchColumn();
$targetCount = $target->query("SELECT COUNT(*) FROM $table")->fetchColumn();
// 抽样验证
if ($sourceCount == $targetCount) {
$sample = $source->query("SELECT * FROM $table LIMIT 100")->fetchAll();
foreach ($sample as $row) {
$id = $row['id'];
$targetRow = $target->query("SELECT * FROM $table WHERE id = $id")->fetch();
if ($row != $targetRow) {
return ['status' => 'FAIL', 'message' => "Row $id mismatch"];
}
}
return ['status' => 'PASS', 'count' => $sourceCount];
}
return ['status' => 'FAIL', 'message' => "Count mismatch: $sourceCount vs $targetCount"];
}
}
回滚策略
// Migration with safety checks
class SafeMigration
{
private $backup;
public function beforeMigrate($db)
{
// 创建备份
$this->backup = $db->query("SELECT * FROM users")->fetchAll();
}
public function rollback($db)
{
// 清空并恢复数据
$db->exec("TRUNCATE TABLE users");
$stmt = $db->prepare("INSERT INTO users (id, username, email) VALUES (?, ?, ?)");
foreach ($this->backup as $row) {
$stmt->execute([$row['id'], $row['username'], $row['email']]);
}
}
}
性能优化建议
-
批量操作
- 使用
INSERT ... VALUES (...), (...), ...批量插入 - 使用事务包含大量操作
- 使用
-
索引管理
- 迁移后重建索引
- 使用
pt-online-schema-change进行在线DDL
-
分批处理
// 使用游标处理大量数据 $stmt = $db->prepare("SELECT * FROM big_table WHERE id > :lastId LIMIT 1000"); $lastId = 0; while (true) { $stmt->execute(['lastId' => $lastId]); $rows = $stmt->fetchAll(); if (empty($rows)) break; foreach ($rows as $row) { // 处理数据 $lastId = $row['id']; } } -
监控与日志
// 记录迁移日志 class MigrationLogger { public function log($message, $level = 'INFO') { $logEntry = sprintf( "[%s] [%s] %s\n", date('Y-m-d H:i:s'), $level, $message ); file_put_contents('migration.log', $logEntry, FILE_APPEND); } }
这套完整的迁移和种子管理方案涵盖了从开发到生产的全部流程,可以根据项目具体需求进行调整和扩展。