本文目录导读:

为PHP项目编写升级脚本是一个需要谨慎处理的任务,旨在将数据库结构、配置文件、代码逻辑等从旧版本平滑过渡到新版本。
以下是编写PHP项目升级脚本的完整指南,包括策略、步骤、代码示例和最佳实践。
核心升级类型
通常升级脚本需要处理以下几类变更:
- 数据库变更:新增/修改/删除表、字段、索引、外键。
- 配置文件变更:
config.php或.env中新增/废弃/修改配置项。 - 逻辑及数据迁移:需要批量转换旧数据格式(如将用户头像从本地路径改为云端URL)。
- 依赖更新:
composer.json或package.json中的依赖更新。 - 文件系统变更:移动/删除/新增目录或文件。
升级脚本设计模式
基于“里程碑”版本号(Version-Based)
每个版本对应一个独立的PHP脚本文件,按照版本号顺序执行。
文件结构示例:
migrations/
├── V1_0_1__add_email_column.php
├── V1_0_2__rename_status_field.php
├── V1_1_0__init_user_logs_table.php
└── V1_2_0__update_config_format.php
核心执行逻辑:
// upgrade.php (主入口)
class UpgradeRunner {
private $db;
private $currentVersion;
private $migrationDir;
private $versionTable = 'schema_versions';
public function __construct() {
$this->db = new PDO('mysql:host=localhost;dbname=myapp', 'user', 'pass');
$this->migrationDir = __DIR__ . '/migrations/';
$this->ensureVersionTable();
$this->currentVersion = $this->getCurrentVersion();
}
private function ensureVersionTable() {
$sql = "CREATE TABLE IF NOT EXISTS {$this->versionTable} (
version VARCHAR(50) PRIMARY KEY,
applied_at DATETIME DEFAULT CURRENT_TIMESTAMP,
checksum VARCHAR(64)
)";
$this->db->exec($sql);
}
private function getCurrentVersion() {
$stmt = $this->db->query("SELECT version FROM {$this->versionTable} ORDER BY version DESC LIMIT 1");
$row = $stmt->fetch(PDO::FETCH_ASSOC);
return $row ? $row['version'] : '0.0.0';
}
private function getMigrationFiles() {
$files = glob($this->migrationDir . 'V*.php');
usort($files, 'version_compare'); // 按版本号自然排序
return $files;
}
public function run() {
$files = $this->getMigrationFiles();
foreach ($files as $file) {
// 提取版本号:V1_0_1__xxx.php -> 1.0.1
preg_match('/V(\d+_\d+_\d+)__/', basename($file), $matches);
$fileVersion = str_replace('_', '.', $matches[1]);
// 检查是否已执行
if (version_compare($fileVersion, $this->currentVersion, '<=')) {
continue;
}
echo "Applying migration: " . basename($file) . "\n";
// 执行迁移脚本(注意:脚本应包含一个可调用的函数或类)
$migration = require $file; // 假设返回一个闭包或对象
if (is_callable($migration)) {
$migration($this->db);
}
// 记录版本信息(计算文件哈希,防止篡改)
$checksum = hash_file('sha256', $file);
$stmt = $this->db->prepare("INSERT INTO {$this->versionTable} (version, checksum) VALUES (?, ?)");
$stmt->execute([$fileVersion, $checksum]);
$this->currentVersion = $fileVersion; // 更新内部状态
}
echo "All migrations applied. Current version: " . $this->currentVersion . "\n";
}
}
// 使用
$runner = new UpgradeRunner();
$runner->run();
直接“增量执行”(SQL文件方式)
将升级脚本以 .sql 文件存放,搭配批量执行工具。
简单执行器:
class SqlMigrator {
private $pdo;
private $tableName = 'migrations';
public function executeSqlFile($filePath) {
$sql = file_get_contents($filePath);
// 去除注释和空行
$sql = preg_replace('/--.*\n/', '', $sql);
$sql = preg_replace('/#.*\n/', '', $sql);
$sql = preg_replace('/\/\*.*?\*\//s', '', $sql);
$statements = explode(';', $sql);
foreach ($statements as $stmt) {
$stmt = trim($stmt);
if (!empty($stmt)) {
$this->pdo->exec($stmt);
}
}
}
}
缺点:对于复杂的逻辑迁移(如逐行遍历旧数据并插入新表)能力不足。
数据库迁移详解
安全的DDL操作
// V1_0_1__add_email_column.php
return function (PDO $db) {
// 使用 IF NOT EXISTS / ALTER 之前检查
try {
$db->exec("ALTER TABLE users ADD COLUMN email VARCHAR(255) NULL AFTER username");
echo "✓ Added 'email' column to users table\n";
} catch (PDOException $e) {
// 如果列已存在,直接忽略
if (strpos($e->getMessage(), 'Duplicate column') !== false) {
echo "~ Column 'email' already exists, skipping\n";
} else {
throw $e;
}
}
};
复杂数据迁移(带事务和批量处理)
// V1_2_0__encrypt_sensitive_data.php
return function (PDO $db) {
$db->beginTransaction();
try {
// 1. 创建临时字段
$db->exec("ALTER TABLE users ADD COLUMN email_encrypted VARCHAR(512) NULL");
// 2. 分批迁移旧数据
$offset = 0;
$limit = 1000;
while (true) {
$stmt = $db->prepare("SELECT id, email FROM users WHERE email IS NOT NULL AND email != '' LIMIT :limit OFFSET :offset");
$stmt->execute(['limit' => $limit, 'offset' => $offset]);
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
if (empty($rows)) break;
foreach ($rows as $row) {
$encrypted = openssl_encrypt($row['email'], 'AES-128-CBC', 'your-secure-key', 0, 'iv-16bytes-long');
$updateStmt = $db->prepare("UPDATE users SET email_encrypted = :encrypted WHERE id = :id");
$updateStmt->execute(['encrypted' => $encrypted, 'id' => $row['id']]);
}
$offset += $limit;
echo " Processed {$offset} rows...\n";
}
// 3. 删除旧字段,重命名新字段
$db->exec("ALTER TABLE users DROP COLUMN email");
$db->exec("ALTER TABLE users CHANGE COLUMN email_encrypted email VARCHAR(512) NULL");
$db->commit();
echo "✓ Encrypted all existing email data\n";
} catch (Exception $e) {
$db->rollBack();
throw $e;
}
};
回滚策略
编写 down() 方法(可选,但强烈建议):
// 升级脚本结构
return [
'up' => function (PDO $db) {
$db->exec("ALTER TABLE users ADD COLUMN avatar_url VARCHAR(500) NULL");
},
'down' => function (PDO $db) {
$db->exec("ALTER TABLE users DROP COLUMN avatar_url");
}
];
主执行器增加 --down 参数支持回滚。
配置文件升级
通用配置合并逻辑
// V1_3_0__add_cache_config.php
return function () {
$configFile = __DIR__ . '/../config.php';
$backupFile = $configFile . '.bak';
// 1. 备份旧配置(关键步骤!)
copy($configFile, $backupFile);
// 2. 读取旧配置(假设是返回数组的PHP文件)
$oldConfig = include $configFile;
// 3. 合并新配置默认值
$newConfig = array_merge($oldConfig, [
'cache' => [
'driver' => 'file',
'prefix' => 'myapp_',
'ttl' => 3600
],
'app' => array_merge($oldConfig['app'] ?? [], [
'debug' => strtolower(getenv('APP_DEBUG') ?: 'false')
])
]);
// 4. 写回(保留用户原有值,新增默认值)
$content = '<?php' . PHP_EOL . 'return ' . var_export($newConfig, true) . ';' . PHP_EOL;
file_put_contents($configFile, $content);
echo "✓ Configuration updated with cache settings\n";
};
环境变量文件升级
// V1_4_0__add_env_vars.php
return function () {
$envFile = __DIR__ . '/../.env';
$backupFile = $envFile . '.backup';
// 1. 备份
copy($envFile, $backupFile);
// 2. 读取现有内容
$existingLines = file($envFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
$existingKeys = [];
foreach ($existingLines as $line) {
if (strpos($line, '=') !== false) {
list($key) = explode('=', $line, 2);
$existingKeys[trim($key)] = true;
}
}
// 3. 新变量定义
$newVars = [
'CACHE_DRIVER' => 'file',
'CACHE_PREFIX' => 'myapp_',
'REDIS_HOST' => '127.0.0.1'
];
// 4. 追加不存在的变量
$fp = fopen($envFile, 'a');
foreach ($newVars as $key => $defaultValue) {
if (!isset($existingKeys[$key])) {
fwrite($fp, PHP_EOL . "$key=$defaultValue");
}
}
fclose($fp);
echo "✓ .env variables appended (existing values preserved)\n";
};
依赖更新脚本
// V1_5_0__update_composer_and_cache.php
return function () {
// 1. 更新 composer.json (可通过新增或修改 require)
$composerFile = __DIR__ . '/../composer.json';
$composer = json_decode(file_get_contents($composerFile), true);
$composer['require']['monolog/monolog'] = '^2.0';
$composer['require']['guzzlehttp/guzzle'] = '^7.0';
file_put_contents($composerFile, json_encode($composer, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
// 2. 执行 composer update
$output = [];
exec('cd ' . escapeshellarg(dirname($composerFile)) . ' && composer update --no-dev 2>&1', $output, $returnCode);
if ($returnCode !== 0) {
throw new RuntimeException("Composer update failed: " . implode("\n", $output));
}
// 3. 清除旧缓存目录(可选)
$cacheDir = __DIR__ . '/../storage/cache/';
if (is_dir($cacheDir)) {
array_map('unlink', glob($cacheDir . '*'));
echo "✓ Old cache cleared\n";
}
echo "✓ Dependencies updated to new versions\n";
};
安全与最佳实践
| 实践 | 说明 |
|---|---|
| 原子性 | 每个升级脚本应包含在事务中(如 BEGIN / COMMIT),失败时自动回滚 |
| 幂等性 | 升级脚本可重复执行而不产生副作用(使用 IF NOT EXISTS、checksum 校验) |
| 备份 | 修改配置文件或数据库前,自动备份到 .backup 或 migrations/backups/ |
| 进度反馈 | 长任务(如数据迁移上千条)应输出进度,避免用户误以为卡死 |
| 失败容忍 | 非关键失败(如已存在的列)应视为警告而非错误,允许继续 |
| 版本锁 | 在 schema_versions 表中增加 checksum 字段,防止版本被手动篡改 |
| 测试模式 | 增加 --dry-run 参数,仅输出将执行的操作但不实际操作 |
典型升级执行命令
# 执行所有待处理的升级 php upgrade.php # 仅运行到特定版本 php upgrade.php --to=1.5.0 # 回滚上一个版本 php upgrade.php --rollback=1 # 测试模式(不实际执行) php upgrade.php --dry-run
一个健壮的PHP项目升级脚本应具备:
- 版本追踪:记录已执行的每个数据库/文件变更。
- 顺序执行:按版本号自然排序,逐级升级。
- 错误处理:严格的事务控制,关键操作可回滚。
- 用户反馈:清晰的进度、成功/失败信息。
- 容错性:允许重复执行、忽略已存在的变更。
通过以上模式,你可以安全地将任何PHP项目的数据库、配置和逻辑从任意旧版本升级到最新版本。