木块九宫格
93.64M · 2026-02-13
通常是这样开始的。
有人让你修一个小 bug,可能是个验证问题,可能是个只在生产环境出现的奇怪边界情况。你打开文件,想着很快就能搞定。
然后你看到了:
$x、$temp、$data2这时候你才意识到:这就是遗留 PHP 代码。
不是"老"PHP,不是"烂"PHP,只是活得够久、变成了核心系统的代码。
现在它归你了。
重构遗留 PHP 的名声一直不好——痛苦、高风险、精神损耗大。但大部分痛苦来自重构的方式,而不是代码本身。
这篇文章讲的是怎么重构遗留 PHP,同时保持心态不崩、系统不炸、也不用推翻重来。
遗留代码不等于"烂代码"。
遗留代码是没有安全网的代码:
有些遗留 PHP 是 15 年前写的。有些是去年写的——赶工期,没时间收拾。
怪过去没有用。你的任务不是评判,而是让下一次改动比上一次更安全。
光是这个心态转变,就能改变很多事。
在动结构、格式、架构之前,你需要的是可见性。
不理解行为就动手重构,是线上事故的典型成因。
先问这几个问题:
从这些地方开始,而不是从最丑的那个文件开始。
遗留 PHP 系统通常能活下来,是因为核心路径是稳定的——哪怕代码很难看。
你不需要完整的测试覆盖率,你需要的是行为锚点。
特征测试(Characterization Tests)
不是测代码"应该"做什么,而是测它"目前"在做什么。
PHPUnit 示例:
public function testLegacyDiscountCalculation()
{
$calculator = new LegacyDiscountCalculator();
$result = $calculator->calculate(100, 'VIP');
$this->assertEquals(85, $result);
}
你可能不喜欢这个逻辑,可能还没看懂。没关系。
目标很简单:如果我重构了这段代码,我要能知道行为变了没有。
这些测试是临时的安全带——但关键时刻能救命。
在"清理"之前,先修掉那些正在持续制造问题的东西。
遗留 PHP 常见的出血点
全局状态
global $db;
global $user;
全局变量让重构几乎不可能。第一步:把它包起来。
class Database
{
public function query(string $sql): array
{
return $GLOBALS['db']->query($sql);
}
}
不完美——但你有了一条缝(seam)。
职责混杂
function processOrder()
{
// validate input
// query database
// calculate totals
// send email
// render HTML
}
不要一口气全改,一次只抽离一个职责。
重构不是重写。
目标是划出清晰的边界,而不是打磨漂亮的内部实现。
遗留代码往往做的事情是对的,只是形式不对。
$total = 0;
foreach ($items as $item) {
if ($item['type'] === 'digital') {
$total += $item['price'];
} else {
$total += $item['price'] + 10;
}
}
第一步重构:
$total = calculateOrderTotal($items);
function calculateOrderTotal(array $items): float
{
$total = 0;
foreach ($items as $item) {
$total += itemTotal($item);
}
return $total;
}
行为没变,但可读性和可测试性变了。
这就是一次有效的改进。
不需要第一天就全面开启严格类型。
从 bug 容易藏身的地方开始:
function findUser($id)
{
// returns array or false or null
}
重构方向:
function findUser(int $id): ?User
{
// return User or null
}
类型做两件事:表达意图,暴露隐藏的假设。
遗留 PHP 能跑通,往往是因为什么都是"灵活"的。类型会逼你面对真实情况。
遗留 PHP 特别喜欢用标志位。
if ($status === 1) {
// active
} elseif ($status === 2) {
// pending
}
用常量或枚举重构:
enum UserStatus: int
{
case Active = 1;
case Pending = 2;
}
然后:
if ($user->status === UserStatus::Active) {
// ...
}
代码一下子就能自己解释自己了。
重复代码是生存策略,不是无能的表现。
遗留 PHP 之所以有大量重复,是因为当时做抽象的风险太大。
你的做法:
例如这段代码到处都是:
$tax = $price * 0.1;
$total = $price + $tax;
提取出来:
function calculateTax(float $price): float
{
return $price * 0.1;
}
不要过早做过度抽象。重构的方向是清晰,不是理论上的复用。
遗留 PHP 重构中收益最大的一件事,就是把下面这些东西和真正的业务规则分开:
重构前:
$result = mysqli_query($conn, "SELECT * FROM users WHERE id = $id");
if ($row['type'] === 'premium') {
$discount = 0.2;
}
重构后(渐进式):
$user = $userRepository->findById($id);
$discount = $discountPolicy->forUser($user);
即使 repository 内部仍然用的是裸 SQL,你也已经创造了一条缝。
有缝,重构才安全。
在遗留系统中升级 PHP 版本是件让人紧张的事,因为升级会把隐藏的问题暴露出来。
把升级当成反馈,而不是失败:
每一条警告都在告诉你:"代码的这个部分不够清晰,或者不够安全。"
这是有价值的信息。
测试缺失的时候,日志能给你信心。
logger()->info('Calculating discount', [
'user_id' => $user->id,
'type' => $user->type,
]);
重构之后对比日志。如果行为出现了意料之外的偏差,你能及时发现。
永远不要一次性重构:
一次改一条路径、一个函数、一个职责。
小改进会累积。
遗留系统不是瞬间崩塌的——它是慢慢被侵蚀的。你的重构也应该以同样的节奏推进。
完美代码是个陷阱。
以下情况就该收手了:
你不是在创作艺术品,你是在降低风险。
重构遗留 PHP 之所以让人难受,是因为:
但别忘了:这些代码能活到今天,说明它一直在发挥作用。
你的任务不是抹掉过去,而是让未来没那么痛苦。
重构遗留 PHP 不需要英雄主义。
它需要的是:
你不是通过重写来重构遗留 PHP 的。你是通过一次又一次安全的小改动,一点一点赢得它的信任。
如果做对了——你不会崩溃。说不定还会觉得挺有意思。 如何重构遗留 PHP 代码 不至于崩溃