最近把项目从PHP 5.6升级到了PHP 7.2还提前体验了PHP 7.3的RC版本(PHP 7.3预计年底正式发布)。

升级的,同时也对项目的老代码进行了重构把很多烂代码改造成了优雅的代码。

PHP这门语言,因为入门简单很多人写PHP代码都比较随意导致项目里有很多烂代码难维护难扩展bug多。

但是PHP 7之后PHP有了很大的进步性能提升类型系统完善新特性丰富完全可以写出优雅的代码。

今天想分享一下PHP 7.2/7.3代码重构的经验和,技巧从烂代码到优雅代码的改造过程。

一、为什么要重构

在讲重构技巧之前,先说说为什么要重构。

1. 老代码难维护

很多老的PHP代码写得很随意函数几千行变量名看不懂逻辑混乱注释少新人接手根本看不懂维护成本很高。

2. bug多

烂代码,因为逻辑混乱没有测试很容易出bug而且出了bug很难排查修复一个bug可能又引入新的bug。

3. 难扩展

烂代码耦合严重改一个地方可能影响很多其他地方加新功能很困难经常需要改很多代码。

4. 性能差

很多老代码写得不合理有很多性能问题,比如循环里查数据库重复加载文件等等导致系统性能差。

5. 利用PHP 7新特性

PHP 7有很多新特性,比如类型声明匿名类null合并运算符太空船运算符等等能让代码更简洁更安全重构能充分利用这些新特性提升代码质量。

二、重构的原则

重构不是随便改代码有一些基本原则要遵守。

1. 小步重构

不要一下子重构整个项目要小步重构每次改一小部分测试没问题再继续。这样风险小出了问题也容易回滚。

2. 保持功能不变

重构的目的是改善代码结构而不是改变功能。重构过程中要保持功能不变不能,因为重构引入新的bug或者改变原有的行为。

3. 有测试保障

重构之前,最好有测试用例保障重构之后,跑测试确保功能正常。如果没有测试要先补测试再重构。

4. 持续重构

重构不是一次性能完就完事了要持续重构写代码的过程中随时重构保持代码的整洁。

三、烂代码的特征

先看看烂代码有哪些特征对照一下自己的项目有没有这些问题。

1. 函数过长

一个函数几百行甚至几千行什么都干逻辑混乱看不懂。

2. 命名不规范

变量名函数名随便起,比如$a$b$c$data$info$temp看不懂什么意思。

3. 全局变量滥用

到处用global或者$GLOBALS变量作用域不清晰副作用大难调试。

4. 重复代码

同样的逻辑复制粘贴到处都是改一个地方要改很多地方容易漏改。

5. 魔法数字和字符串

代码里到处是魔法数字,比如if ($status == 1)不知道1代表什么意思。

6. 嵌套过深

if里面套if套for套foreach嵌套四五层看得头晕。

7. 没有注释

复杂的逻辑没有注释看不懂为什么这么写。

8. 耦合严重

函数之间,类之间,耦合严重改一个地方影响很多其他地方。

9. 没有错误处理

代码里不处理错误调用函数不检查返回值出了问题不知道。

10. 数据库操作混乱

到处是mysql_query或者拼接SQL没有用PDO或者,ORMSQL注入风险大。

四、重构技巧

下面讲解一些具体的重构技巧结合PHP 7.2/7.3的新特性。

技巧1:类型声明

PHP 7支持标量类型声明和返回值类型声明能让代码更安全更易读。

烂代码:

function add($a, $b) {
    return $a + $b;
}

优雅代码:

function add(int $a, int $b): int {
    return $a + $b;
}

类型声明能在调用函数的时候,检查参数类型防止传错类型导致bug。而且看函数签名就知道参数和返回值的类型不用看实现代码。

PHP 7.2还支持object类型PHP 7.3对类型系统也有一些改进。

技巧2:null合并运算符

PHP 7新增了null合并运算符(??)能简化判断变量是否存在的代码。

烂代码:

$name = isset($_GET['name']) ? $_GET['name'] : 'default';

优雅代码:

$name = $_GET['name'] ?? 'default';

还可以链式使用:

$name = $_GET['name'] ?? $_POST['name'] ?? 'default';

技巧3:太空船运算符

PHP 7新增了太空船运算符(<=>)能简化比较的代码特别适合usort的回调。

烂代码:

usort($arr, function($a, $b) {
    if ($a['price'] == $b['price']) return 0;
    return $a['price'] > $b['price'] ? 1 : -1;
});

优雅代码:

usort($arr, function($a, $b) {
    return $a['price'] <=> $b['price'];
});

技巧4:匿名函数和闭包

PHP 5.3就支持匿名函数了,但是很多老代码还是用create_function或者写单独的函数。PHP 7匿名函数性能更好更推荐。

烂代码:

$func = create_function('$a, $b', 'return $a + $b;');

优雅代码:

$func = function($a, $b) {
    return $a + $b;
};

闭包还能用use关键字捕获外部变量:

$factor = 2;
$multiply = function($n) use ($factor) {
    return $n * $factor;
};

技巧5:提取函数/方法

对于过长的函数把相关的逻辑提取成单独的函数,或者方法让每个函数只做一件事。

烂代码:

function processOrder($order) {
    // 验证订单
    if (!$order['id']) {
        throw new Exception('Invalid order');
    }
    if (!$order['items']) {
        throw new Exception('Empty order');
    }
    
    // 计算总价
    $total = 0;
    foreach ($order['items'] as $item) {
        $total += $item['price'] * $item['qty'];
    }
    
    // 保存订单
    $db = getDB();
    $db->query("INSERT INTO orders ...");
    
    // 发送通知
    mail($order['email'], 'Order confirmed', '...');
    
    return $total;
}

优雅代码:

function processOrder(array $order): float {
    validateOrder($order);
    $total = calculateTotal($order);
    saveOrder($order, $total);
    sendOrderNotification($order);
    return $total;
}

function validateOrder(array $order): void {
    if (empty($order['id'])) {
        throw new InvalidArgumentException('Invalid order id');
    }
    if (empty($order['items'])) {
        throw new InvalidArgumentException('Order has no items');
    }
}

function calculateTotal(array $order): float {
    return array_sum(array_map(function($item) {
        return $item['price'] * $item['qty'];
    }, $order['items']));
}

提取函数后每个函数职责单一易读易测试易维护。

技巧6:用常量代替魔法数字

烂代码:

if ($order['status'] == 1) {
    // 已支付
} elseif ($order['status'] == 2) {
    // 已发货
}

优雅代码:

class OrderStatus {
    const PENDING = 0;
    const PAID = 1;
    const SHIPPED = 2;
    const COMPLETED = 3;
}

if ($order['status'] == OrderStatus::PAID) {
    // 已支付
} elseif ($order['status'] == OrderStatus::SHIPPED) {
    // 已发货
}

用常量代替魔法数字代码更易读也不容易写错。

技巧7:减少嵌套

烂代码:

function calculateDiscount($user, $order) {
    if ($user) {
        if ($user['is_vip']) {
            if ($order['total'] > 1000) {
                return 0.2;
            } else {
                return 0.1;
            }
        } else {
            if ($order['total'] > 1000) {
                return 0.05;
            } else {
                return 0;
            }
        }
    } else {
        return 0;
    }
}

优雅代码:

function calculateDiscount(?array $user, array $order): float {
    if (!$user) {
        return 0;
    }
    
    $baseRate = $user['is_vip'] ? 0.1 : 0;
    $extraRate = $order['total'] > 1000 ? 0.05 : 0;
    
    return $baseRate + $extraRate;
}

用提前返回和合并条件减少嵌套代码更清晰。

*技巧8:用PDO代替mysql_函数**:

烂代码:

$link = mysql_connect('localhost', 'user', 'pass');
mysql_select_db('db', $link);
$name = $_GET['name'];
$query = "SELECT * FROM users WHERE name = '$name'";
$result = mysql_query($query);
while ($row = mysql_fetch_assoc($result)) {
    // ...
}

优雅代码:

$pdo = new PDO('mysql:host=localhost;dbname=db;charset=utf8mb4', 'user', 'pass');
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

$stmt = $pdo->prepare('SELECT * FROM users WHERE name = :name');
$stmt->execute([':name' => $_GET['name'] ?? '']);
$users = $stmt->fetchAll(PDO::FETCH_ASSOC);

用PDO和预处理语句能防止SQL注入代码更安全更简洁。

技巧9:用命名空间和自动加载

烂代码:到处requireinclude文件类名容易冲突。

优雅代码:

// composer.json
{
    "autoload": {
        "psr-4": {
            "App\\": "src/"
        }
    }
}

// src/Models/User.php
namespace App\Models;

class User {
    // ...
}

// 使用
use App\Models\User;
$user = new User();

用命名空间和Composer自动加载代码结构清晰不用手动require文件。

技巧10:用异常处理错误

烂代码:

function getUser($id) {
    $user = db_query("SELECT * FROM users WHERE id = $id");
    if (!$user) {
        return false;
    }
    return $user;
}

$user = getUser($id);
if (!$user) {
    echo 'User not found';
}

优雅代码:

class UserNotFoundException extends RuntimeException {}

function getUser(int $id): array {
    $user = dbQuery('SELECT * FROM users WHERE id = ?', [$id]);
    if (!$user) {
        throw new UserNotFoundException("User $id not found");
    }
    return $user;
}

try {
    $user = getUser($id);
} catch (UserNotFoundException $e) {
    // 处理用户不存在
}

用异常处理错误代码更清晰错误处理和业务逻辑分离。

五、PHP 7.3的新特性

提前体验了PHP 7.3的RC版本有一些新特性很实用重构的时候,也能用上。

1. Heredoc/Nowdoc更灵活

PHP 7.3对Heredoc和Nowdoc的语法做了改进闭合标记可以缩进也可以不换行代码更整洁。

2. 数组解构支持引用

PHP 7.3支持在数组解构中使用引用:

$arr = [1, 2];
[$a, &$b] = $arr;
$b = 3;
// $arr[1] 现在是 3

3. JSON错误处理改进

PHP 7.3新增了JSONTHROWONERROR选项jsonencode和jsondecode出错的时候,会抛异常不用再手动检查jsonlast_error。

try {
    $data = json_decode($json, true, 512, JSON_THROW_ON_ERROR);
} catch (JsonException $e) {
    // 处理JSON解析错误
}

4. 新增is_countable函数

PHP 7.3新增了is_countable函数判断变量是否可计数避免count非可数变量的警告。

5. 新增arraykeyfirst和,arraykeylast函数

PHP 7.3新增了arraykeyfirst和arraykeylast函数获取数组的第一个和,最后一个键不用再用reset和end了。

这些新特性能让代码更简洁更安全重构的时候,可以逐步用上。

六、重构的步骤

最后总结一下重构的步骤供大家参考。

步骤1:建立测试

重构之前,先建立测试用例确保重构不会改变功能。如果项目没有测试先补一些关键路径的测试。

步骤2:识别烂代码

用工具扫描代码找出烂代码的热点,比如过长的函数重复代码复杂度过高的代码等等。可以用PHPMDPHP_CodeSniffer等工具。

步骤3:小步重构

从最简单的开始小步重构每次改一小部分跑测试确保没问题再继续。

步骤4:代码审查

重构完一部分做代码审查看看有没有问题有没有更好的写法。

步骤5:持续重构

重构不是一次性能完就完事了要持续重构写新代码的时候,也要注意代码质量随时重构。

七、写在最后

以上就是PHP 7.2/7.3代码重构的经验和技巧。

PHP这门语言,虽然入门简单容易写出烂代码,但是,只要我们用心完全可以写出优雅的代码。特别是PHP 7之后PHP有了很大的进步性能提升类型系统完善新特性丰富完全不输其他语言。

代码重构是一个持续的过程不是一次性能完就完事了。我们要在日常开发中注意代码质量随时重构保持代码的整洁和优雅。

希望这篇文章能帮大家提升代码质量写出更优雅的PHP代码。

如果有什么问题,或者不同的看法欢迎在评论区留言我们一起交流。

最后用一句话结束这篇文章:"代码是写给人看的只是顺便能在机器上运行。"

愿大家都能写出优雅的代码。