编写整洁、易读且可维护的 PHP 代码
本章将重点介绍如何让你的 PHP 代码随着时间的推移依然易于理解、调试和修改。遵循这些原则能够显著提升团队协作效率,减少代码错误,并延长应用程序的生命周期。
1. 保持一致的格式与缩进
一致的格式是代码可读性的基石。当代码遵循一种可预测的视觉结构时,阅读者就能更容易地扫描并理解其中的逻辑流。 缩进在视觉上呈现了代码块的层级和嵌套关系,扮演着至关重要的角色。
例如,不一致的缩进会让人很难快速辨别出一个 if 语句或 for 循环的作用范围,这在开发或调试阶段极易引发逻辑错误。
<?php
// 不一致的缩进示例(难以阅读)
function calculatePrice($quantity, $unitPrice, $taxRate) {
if ($quantity > 10) {
$discount = 0.10;
$totalPrice = ($quantity * $unitPrice * (1 - $discount));
} else {
$totalPrice = ($quantity * $unitPrice);
}
return $totalPrice * (1 + $taxRate);
}
// 一致的缩进示例(清晰易读)
function calculatePriceConsistent($quantity, $unitPrice, $taxRate) {
if ($quantity > 10) {
$discount = 0.10;
$totalPrice = ($quantity * $unitPrice * (1 - $discount));
} else {
$totalPrice = ($quantity * $unitPrice);
}
return $totalPrice * (1 + $taxRate);
}
// 使用格式一致的函数
echo "12 件商品的价格: " . calculatePriceConsistent(12, 10, 0.05) . "\n";
echo "5 件商品的价格: " . calculatePriceConsistent(5, 10, 0.05) . "\n";
?>正如上一章所讲,PHP 的 PSR 标准(特别是 PSR-12)为 PHP 代码格式化提供了被广泛接受的指南,包括缩进(通常是 4 个空格)、行长以及大括号的位置。遵循这些标准可以确保在不同的项目和团队之间保持高度的一致性。
2. 意义明确的命名规范
为变量、函数、类和常量挑选的名称,必须能够清晰、准确地描述它们的用途或内容。模棱两可或过于简短的命名,会大大增加阅读代码时的认知负担(即使是原作者过段时间再看自己的代码也会一头雾水)。
假设你正在应用程序中管理用户数据:
<?php
// 糟糕的命名规范
$u = "John Doe"; // 'u' 代表什么?
$db = "user_data"; // 这是一个数据库连接、一个表名,还是实际的用户数据?
function proc($d) { // 'proc' 代表 process (处理),'d' 代表 data (数据)?太宽泛了。
// ...
}
// 意义明确的命名规范
$userName = "John Doe"; // 清楚地表明这是一个用户名
$userDataTableName = "users"; // 表明这是一个用于存储用户的数据库表名
function processUserData(array $data) { // 函数目的和参数类型一目了然
// ...
}
?>有意义的命名能够提升代码的自文档化(self-documentation)能力,从而减少为了解释基础功能而编写大量注释的需求。这也让在代码库中搜索特定元素变得更加容易。例如,搜索 $userId 显然比搜索通用的 $id 更加精确。
3. 谨慎且合理地使用注释
虽然有意义的命名和整洁的代码结构能减少注释的必要性,但注释在解释**“为什么”**做出某些决策、阐明复杂的算法或强调非直观的行为时,依然具有不可替代的价值。注释绝不应该仅仅是复述代码已经表达的内容。
想象一个复杂的计算逻辑或针对特定 Bug 的临时解决方案:
<?php
function calculateDiscountedPrice(float $originalPrice, int $itemCount): float {
$discountRate = 0;
// 针对订购量超过 100 件的订单应用批量折扣。
// 具体的阈值是由市场部 Q3 活动策略决定的。
if ($itemCount >= 100) {
$discountRate = 0.15; // 15% 折扣
} elseif ($itemCount >= 50) {
$discountRate = 0.10; // 10% 折扣
}
// 为高级会员调整价格。
// 注意:此调整必须在批量折扣*之后*应用,以确保基于初始价格计算的正确性。
// 这是一个因集成遗留系统而产生的已知边缘情况。
$finalPrice = $originalPrice * (1 - $discountRate);
if (/* 检查客户是否为高级会员 */ true) { // 此处为实际检查逻辑的占位符
$finalPrice *= 0.95; // 高级会员额外减免 5%
}
return $finalPrice;
}
// 糟糕的注释示例(纯属废话)
$i = 0; // 将 i 初始化为 0
// 优秀的注释示例(解释原因或复杂性)
// 此循环会迭代至允许的最大重试次数,
// 专门用于处理容易出现间歇性失败的网络超时问题。
for ($i = 0; $i < MAX_RETRIES; $i++) {
// ...
}
?>注释应该保持简洁、及时更新,并且必须提供超越代码字面意思的价值。过时或具有误导性的注释比没有注释还要糟糕,因为它们会把未来的开发者带进沟里。
4. 拒绝“魔术数字”和“魔术字符串”
“魔术数字 (Magic numbers)” 或 “魔术字符串 (Magic strings)” 指的是直接硬编码在代码中、且没有任何上下文解释其含义的字面值。它们使得代码极难理解和维护,因为它们的意义并不直观,而且一旦需要修改,你必须在整个项目中找出它们出现的所有位置。
更好的做法是,将这些值定义为具名常量,让它们的用途明确化,并集中进行管理。
<?php
// 包含魔术数字的糟糕示例
function checkOrderStatus($statusId) {
if ($statusId == 1) { // 这里的 '1' 是什么意思?
echo "订单待处理。\n";
} elseif ($statusId == 2) { // 这里的 '2' 又是什么意思?
echo "订单已处理。\n";
}
}
function calculateShippingCost($weight) {
// 为什么是 0.5?单位是什么?
$baseCost = $weight * 0.5;
// 这里的 10 又是用来干嘛的?
return $baseCost + 10;
}
// 使用具名常量的优秀示例
const ORDER_STATUS_PENDING = 1;
const ORDER_STATUS_PROCESSED = 2;
const SHIPPING_RATE_PER_KG = 0.50;
const SHIPPING_BASE_FEE = 10.00;
function checkOrderStatusWithConstants($statusId) {
if ($statusId == ORDER_STATUS_PENDING) {
echo "订单待处理。\n";
} elseif ($statusId == ORDER_STATUS_PROCESSED) {
echo "订单已处理。\n";
}
}
function calculateShippingCostWithConstants($weight) {
$baseCost = $weight * SHIPPING_RATE_PER_KG;
return $baseCost + SHIPPING_BASE_FEE;
}
// 使用方式
checkOrderStatusWithConstants(ORDER_STATUS_PENDING);
echo "5kg 的运费: " . calculateShippingCostWithConstants(5) . "\n";
?>如前面常量章节所述,使用常量能够让代码自我解释,更易于修改(只需在一个地方更改常量的值即可),并减少因手动输入不一致的字面值而导致的错误。
5. 拆分臃肿的函数与类
执行各种不同任务的“巨石型(Monolithic)”函数或类,是非常难以阅读、测试和维护的。它们违反了单一职责原则(Single Responsibility Principle),该原则指出:一个模块、类或函数应该只有一个被修改的理由(即只做一件事)。
将庞大的实体拆分成更小、更专注的单元,可以大幅提高代码的可读性和可维护性。每个独立的小单元都可以被单独理解、独立测试,并且更容易被复用。
看下面这个负责处理用户注册流程的函数:
<?php
// 臃肿的函数(糟糕的设计)
function handleUserRegistration($username, $email, $password) {
// 1. 验证输入
if (empty($username) || empty($email) || empty($password)) {
return ['success' => false, 'message' => '所有字段必填。'];
}
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
return ['success' => false, 'message' => '邮箱格式无效。'];
}
// 2. 检查用户名或邮箱是否已存在于数据库
global $db; // 假设有一个全局数据库连接 $db
$stmt = $db->prepare("SELECT COUNT(*) FROM users WHERE username = ? OR email = ?");
$stmt->execute([$username, $email]);
if ($stmt->fetchColumn() > 0) {
return ['success' => false, 'message' => '用户名或邮箱已被占用。'];
}
// 3. 密码哈希处理
$hashedPassword = password_hash($password, PASSWORD_DEFAULT);
// 4. 将用户存入数据库
$stmt = $db->prepare("INSERT INTO users (username, email, password) VALUES (?, ?, ?)");
if (!$stmt->execute([$username, $email, $hashedPassword])) {
return ['success' => false, 'message' => '用户注册失败。'];
}
$userId = $db->lastInsertId();
// 5. 发送欢迎邮件
if (!mail($email, '欢迎使用我们的应用', '感谢您的注册!')) {
// 记录邮件发送失败,但不中断注册流程
error_log("向 $email 发送欢迎邮件失败");
}
return ['success' => true, 'message' => '用户注册成功!', 'userId' => $userId];
}改进:拆分成更小、更专注的函数或类
// 拆分后的类(更好的设计)
class UserRegistrationService {
private $db;
public function __construct(PDO $db) {
$this->db = $db;
}
// 专门负责输入验证
private function validateRegistrationInputs(string $username, string $email, string $password): array {
if (empty($username) || empty($email) || empty($password)) {
return ['isValid' => false, 'message' => '所有字段必填。'];
}
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
return ['isValid' => false, 'message' => '邮箱格式无效。'];
}
return ['isValid' => true];
}
// 专门负责查重
private function checkExistingUser(string $username, string $email): bool {
$stmt = $this->db->prepare("SELECT COUNT(*) FROM users WHERE username = ? OR email = ?");
$stmt->execute([$username, $email]);
return $stmt->fetchColumn() > 0;
}
// 专门负责入库
private function storeUser(string $username, string $email, string $hashedPassword): ?int {
$stmt = $this->db->prepare("INSERT INTO users (username, email, password) VALUES (?, ?, ?)");
if ($stmt->execute([$username, $email, $hashedPassword])) {
return (int) $this->db->lastInsertId();
}
return null;
}
// 专门负责发邮件
private function sendWelcomeEmail(string $email, string $username): bool {
$sent = mail($email, '欢迎使用我们的应用', "你好 $username, 感谢注册!");
if (!$sent) {
error_log("向 $email 发送欢迎邮件失败");
}
return $sent;
}
// 主控流程方法
public function registerUser(string $username, string $email, string $password): array {
$validation = $this->validateRegistrationInputs($username, $email, $password);
if (!$validation['isValid']) {
return ['success' => false, 'message' => $validation['message']];
}
if ($this->checkExistingUser($username, $email)) {
return ['success' => false, 'message' => '用户名或邮箱已被占用。'];
}
$hashedPassword = password_hash($password, PASSWORD_DEFAULT);
$userId = $this->storeUser($username, $email, $hashedPassword);
if ($userId === null) {
return ['success' => false, 'message' => '用户注册失败。'];
}
$this->sendWelcomeEmail($email, $username); // 发送邮件对注册成功与否是非关键的
return ['success' => true, 'message' => '用户注册成功!', 'userId' => $userId];
}
}在改进后的例子中,UserRegistrationService 类负责协调整个注册流程,但将验证、数据库检查、存储和发送邮件等具体的任务,分别委派给了单独的私有方法。这使得每个部分都更容易理解、测试(例如:可以单独测试 validateRegistrationInputs)和维护。
6. DRY 原则 (Don't Repeat Yourself / 拒绝重复)
DRY 原则主张避免代码的重复。当相同的逻辑或数据出现在多个地方时,它就成了一个维护负担:对该逻辑的任何更改都需要修改每一个出现它的地方,这大大增加了出现不一致和 Bug 的风险。
解决之道是:将重复的代码提取到可复用的函数、类或常量中。
假设一个应用程序需要频繁地格式化商品价格:
<?php
// 违反 DRY 原则
function displayProductPrice($price) {
return '¥' . number_format($price, 2);
}
function generateInvoiceLineItem($productName, $quantity, $unitPrice) {
$total = $quantity * $unitPrice;
$formattedTotal = '¥' . number_format($total, 2); // 逻辑重复了
return "$productName x $quantity @ ¥" . number_format($unitPrice, 2) . " = $formattedTotal";
}
// 遵循 DRY 原则
function formatCurrency(float $amount): string {
return '¥' . number_format($amount, 2);
}
function generateInvoiceLineItemDRY(string $productName, int $quantity, float $unitPrice): string {
$total = $quantity * $unitPrice;
return "$productName x $quantity @ " . formatCurrency($unitPrice) . " = " . formatCurrency($total);
}
// 使用
echo "商品价格: " . displayProductPrice(99.99) . "\n";
echo "发票项目 (DRY): " . generateInvoiceLineItemDRY("笔记本电脑", 2, 1200.50) . "\n";
?>通过将价格格式化逻辑提取到 formatCurrency() 中,未来如果需要更改价格显示方式(例如:改变货币符号或小数精度),你只需要修改这一处代码即可。
7. 综合实战演练
让我们将上述几个原则结合到一个更全面的例子中:为一个简单的博客系统编写处理新文章验证逻辑的代码。
<?php
// 定义验证规则和错误信息的常量
const MIN_TITLE_LENGTH = 5;
const MAX_TITLE_LENGTH = 100;
const MIN_CONTENT_LENGTH = 20;
const ERROR_TITLE_EMPTY = "文章标题不能为空。";
const ERROR_TITLE_TOO_SHORT = "文章标题至少需要 " . MIN_TITLE_LENGTH . " 个字符。";
const ERROR_TITLE_TOO_LONG = "文章标题不能超过 " . MAX_TITLE_LENGTH . " 个字符。";
const ERROR_CONTENT_EMPTY = "文章内容不能为空。";
const ERROR_CONTENT_TOO_SHORT = "文章内容至少需要 " . MIN_CONTENT_LENGTH . " 个字符。";
/**
* 净化并验证博客文章输入数据。
*
* @param array $postData 一个包含 'title' 和 'content' 的关联数组。
* @return array 返回一个包含 'isValid' (布尔值) 以及 'errors' (错误信息数组) 或 'data' (净化后数据) 的数组。
*/
function sanitizeAndValidatePost(array $postData): array {
$errors = [];
$sanitizedData = [
'title' => '',
'content' => ''
];
// --- 验证标题 ---
if (!isset($postData['title']) || empty(trim($postData['title']))) {
$errors[] = ERROR_TITLE_EMPTY;
} else {
$sanitizedData['title'] = htmlspecialchars(trim($postData['title']), ENT_QUOTES, 'UTF-8');
if (strlen($sanitizedData['title']) < MIN_TITLE_LENGTH) {
$errors[] = ERROR_TITLE_TOO_SHORT;
}
if (strlen($sanitizedData['title']) > MAX_TITLE_LENGTH) {
$errors[] = ERROR_TITLE_TOO_LONG;
}
}
// --- 验证内容 ---
if (!isset($postData['content']) || empty(trim($postData['content']))) {
$errors[] = ERROR_CONTENT_EMPTY;
} else {
$sanitizedData['content'] = htmlspecialchars(trim($postData['content']), ENT_QUOTES, 'UTF-8');
if (strlen($sanitizedData['content']) < MIN_CONTENT_LENGTH) {
$errors[] = ERROR_CONTENT_TOO_SHORT;
}
}
if (empty($errors)) {
return ['isValid' => true, 'data' => $sanitizedData];
} else {
return ['isValid' => false, 'errors' => $errors];
}
}
// 示例 1: 有效的文章数据
echo "--- 示例 1: 有效文章 ---\n";
$validPost = [
'title' => ' 我的第一篇博客文章 ',
'content' => '这是我第一篇博客文章的内容。它需要至少二十个字符才能通过验证。'
];
$result1 = sanitizeAndValidatePost($validPost);
if ($result1['isValid']) {
echo "验证成功!\n";
print_r($result1['data']);
}
// 示例 2: 无效的文章数据 (字段为空)
echo "\n--- 示例 2: 无效文章 (空字段) ---\n";
$invalidPostEmpty = [
'title' => '',
'content' => '太短'
];
$result2 = sanitizeAndValidatePost($invalidPostEmpty);
if (!$result2['isValid']) {
echo "验证失败:\n";
foreach ($result2['errors'] as $error) { echo "- " . $error . "\n"; }
}
?>在这个例子中:
- 命名明确:函数名和常量名清楚地表明了它们的用途。
- 消灭魔术值:使用常量来设置长度限制和错误信息。
- 注释规范:函数带有一个
/** ... */文档块(Docblock),解释了它的功能、参数和返回值。 - 逻辑清晰:标题和内容的验证逻辑被分组管理,易于追踪流程。
- 类型声明:参数使用了类型提示(array $postData),确保接收到了正确类型的数据。