PHP项目迁移与种子管理

wen PHP项目 1

PHP项目迁移与种子管理完整指南

项目迁移最佳实践

迁移前评估

# 检查PHP版本兼容性
php -v
# 检查扩展依赖
php -m | grep -E "pdo|mysql|redis|mbstring"
# 代码静态分析
phpstan analyze --level=max src/

数据库迁移策略

Schema迁移 (使用Phinx)

PHP项目迁移与种子管理

// 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']]);
        }
    }
}

性能优化建议

  1. 批量操作

    • 使用INSERT ... VALUES (...), (...), ...批量插入
    • 使用事务包含大量操作
  2. 索引管理

    • 迁移后重建索引
    • 使用pt-online-schema-change进行在线DDL
  3. 分批处理

    // 使用游标处理大量数据
    $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'];
        }
    }
  4. 监控与日志

    // 记录迁移日志
    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);
        }
    }

这套完整的迁移和种子管理方案涵盖了从开发到生产的全部流程,可以根据项目具体需求进行调整和扩展。

抱歉,评论功能暂时关闭!