2026 年 PHP 开发者应停止使用的坏实践
拥有十年以上 PHP 开发经验的人,通常都接手过让人怀疑职业选择的遗留代码库,也经历过需要先深呼吸才能写反馈的代码评审。
坦率地说,2026 年仍然反复出现的许多坏模式,和 2016 年许多开发者犯过的错误并无本质差异。
PHP 生态已经成熟了许多。PHP 8.3 速度快、表达力强,并且真正具备现代语言特征。Composer、PHPStan、Laravel 等工具也提高了行业基准。开发者已经没有理由继续用 mysql_query() 时代的方式编写 PHP。
下面以从初级到高级开发者都能理解的方式展开讨论。这里没有指责,只讨论应当停止的做法,以及更合适的替代方案。
1. 停止使用 @ 抑制错误
// ❌ Please, no.
$result = @file_get_contents('https://some-api.com/data');开发者这样做的动机很容易理解。错误提示很烦人,警告会污染日志,于是就在前面加上 @,然后继续往下写。
但实际发生的是:一个潜在的关键故障被静默吞掉了。这个 API 调用可能已经失败。此时 $result 是 false,代码却像一切正常一样继续把它传递到后续流程。
// ✅ Handle it properly
$result = file_get_contents('https://some-api.com/data');
if ($result === false) {
throw new \RuntimeException('Failed to fetch data from API');
}更好的方式是使用 Guzzle 这样的标准 HTTP 客户端。它默认会抛出异常,并提供真实的错误上下文。
use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;
$client = new Client();
try {
$response = $client->get('https://some-api.com/data');
$body = $response->getBody()->getContents();
} catch (RequestException $e) {
logger()->error('API call failed', ['error' => $e->getMessage()]);
throw $e;
}错误抑制是一种会持续产生利息的技术债。不要再让它继续累积。
2. 停止在没有预处理语句的情况下使用原始 mysql_* 风格查询
2026 年本不需要再强调这一点。然而,原始且未参数化的查询仍然会溜进代码库,有时甚至出现在新项目中。
// ❌ SQL injection waiting to happen
$id = $_GET['id'];
$result = $pdo->query("SELECT * FROM users WHERE id = $id");如果有人在 URL 中传入 1 OR 1=1,就等于把整个数据库交了出去。这不是假设场景,而是数据泄露真实发生的方式。
始终使用 PDO 预处理语句:
// ✅ Safe, modern, and actually readable
$pdo = new PDO('mysql:host=localhost;dbname=myapp', $user, $pass, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
$stmt = $pdo->prepare('SELECT * FROM users WHERE id = :id');
$stmt->execute(['id' => $_GET['id']]);
$user = $stmt->fetch();也可以使用 Eloquent 这样的 ORM(可通过 illuminate/database 独立使用),让基础 CRUD 摆脱手写原始 SQL。
规则很简单:永远不要把用户输入拼接进查询语句。
3. 停止把业务逻辑放进控制器
这一点在代码评审中尤其令人痛苦。打开一个控制器,里面有 300 行代码。它负责获取数据、转换数据、发送邮件、写入文件,还在同一个方法里完成三种不同的计算。
// ❌ The "God Controller" anti-pattern
class OrderController extends Controller
{
public function store(Request $request)
{
// validate...
// calculate totals manually...
// update inventory...
// send confirmation email...
// log to 3 different places...
// generate PDF invoice...
// notify Slack...
}
}控制器应该保持轻量。它唯一的职责是接收请求并返回响应。
把逻辑迁移到 Service 类中:
// ✅ Clean, testable, reusable
class OrderController extends Controller
{
public function __construct(private OrderService $orderService) {}
public function store(StoreOrderRequest $request): JsonResponse
{
$order = $this->orderService->createOrder($request->validated());
return response()->json($order, 201);
}
}
// The real work lives here, and it's actually testable
class OrderService
{
public function createOrder(array $data): Order
{
$order = Order::create($data);
$this->inventoryService->decrement($order);
$this->mailer->sendConfirmation($order);
return $order;
}
}现在可以在不启动 HTTP 内核的情况下单元测试 OrderService。未来维护这段代码的人会感谢这个决定。
4. 停止忽略类型声明
PHP 从 7.0 开始支持标量类型声明,从 8.x 开始拥有联合类型、枚举和只读属性。如果 2026 年的函数仍然像下面这样,就需要认真审视了:
// ❌ What even goes in here? What comes out?
function processPayment($amount, $currency, $user) {
// mystery box
}这会迫使每一个阅读代码的开发者通读整个函数,才能理解预期类型。同时,PHP 也无法在运行时帮助捕获错误。
// ✅ Self-documenting and safe
function processPayment(
float $amount,
string $currency,
User $user
): PaymentResult {
// Now we know exactly what this does
}对于状态值这类场景,可以进一步使用 PHP 8.1+ 的枚举:
// ✅ No more magic strings like 'pending', 'active', 'cancelled'
enum OrderStatus: string
{
case Pending = 'pending';
case Active = 'active';
case Cancelled = 'cancelled';
}
function updateStatus(Order $order, OrderStatus $status): void
{
$order->update(['status' => $status->value]);
}可以在 PHP 官方文档中进一步了解 PHP 8.x 提供的能力。如今 PHP 的类型系统已经足够优秀,应该充分使用它。
5. 停止手动管理依赖
如果仍然通过下载库的 .zip 文件、把它们放进 libs/ 文件夹,再用 require_once 引入,问题就已经很明确了。
// ❌ 2009 called
require_once 'libs/phpmailer/PHPMailer.php';
require_once 'libs/guzzle/guzzle.php';Composer 自 2012 年以来已经是事实标准。它会处理自动加载、版本管理和依赖解析。
composer require symfony/mailer
composer require guzzlehttp/guzzle
composer require --dev phpstan/phpstan然后在代码中这样使用:
// ✅ Clean autoloading, version-locked, reproducible builds
use Symfony\Component\Mailer\Mailer;
use GuzzleHttp\Client;把 composer.json 纳入版本控制。提交 composer.lock。永远不要提交 vendor/ 文件夹。这就是完整工作流。
如果项目今天还没有使用 Composer,就从现在开始:getcomposer.org。
6. 停止跳过静态分析
开发者写完代码,在浏览器里手动测试。它可以运行。于是发布上线。
三周后,某个用户触发了一个从未考虑过的边界情况,生产环境出现 TypeError: Cannot access property on null。
这是可以预防的。PHPStan、Psalm 等工具可以在不运行代码的情况下分析代码,并在问题到达用户之前捕获整类错误。
composer require --dev phpstan/phpstan
vendor/bin/phpstan analyse src --level=6可以从 level 0 开始,然后逐步提高等级。Level 6+ 能捕获 null 指针问题、错误返回类型和不可达代码。刚开始会有些不适,就像突然打开一间凌乱房间里的灯。但这正是它的价值所在。
把它加入 CI 流水线。让它成为合并门禁。当 PHPStan 发出严重警告时,不要让代码合并。
7. 停止用 die() 和 exit() 处理错误
// ❌ This is not error handling, this is giving up
$user = getUser($id) or die('User not found');die() 会直接终止整个脚本,而且几乎没有上下文。没有堆栈跟踪,没有日志记录,也没有向客户端返回优雅响应。只剩下中断。
使用异常。异常可以传播、捕获、记录,并且能让应用程序保持可控:
// ✅ Throw, catch, log, respond
function getUser(int $id): User
{
$user = User::find($id);
if (!$user) {
throw new UserNotFoundException("User with ID $id not found.");
}
return $user;
}
// In your controller or middleware:
try {
$user = getUser($id);
} catch (UserNotFoundException $e) {
return response()->json(['error' => $e->getMessage()], 404);
}如果使用 Laravel,app/Exceptions/Handler.php 中的异常处理器可以全局处理这类问题,因此只需要编写一次捕获逻辑。
8. 停止把测试留到最后,甚至完全不写测试
“功能完成后再写测试。”
许多开发者都说过这句话。功能完成后,截止日期迫近,测试也就没有发生。
测试并不是额外工作。它们是让开发者能够重构、升级依赖、发布变更而不必每次都紧张到出汗的基础。PHPUnit 文档质量很高,Laravel 在其上构建的测试层也确实非常易用。
// A basic feature test — takes 5 minutes, saves hours of debugging
public function test_user_can_place_an_order(): void
{
$user = User::factory()->create();
$product = Product::factory()->create(['stock' => 10]);
$response = $this->actingAs($user)
->postJson('/api/orders', ['product_id' => $product->id, 'quantity' => 2]);
$response->assertStatus(201);
$this->assertDatabaseHas('orders', ['user_id' => $user->id]);
$this->assertEquals(8, $product->fresh()->stock);
}这个测试覆盖了控制器、服务、数据库和库存扣减,全部集中在一个可读代码块中。先写测试,再写功能,实现会变得清晰得多。
坦诚总结
这些都不是新观点。大多数内容多年来一直是最佳实践。但它们仍然出现在真实代码库中,因为习惯很难改变,截止日期确实存在,捷径在当下看起来很快。
问题在于,这些“捷径”节省的时间,最终都会以更高成本偿还。凌晨 2 点导致数据损坏 bug 的被抑制错误。无人敢碰的上帝控制器。缺失测试让回归问题溜进生产环境。
高级开发者之所以高级,并不是因为他们知道更多语法。他们之所以高级,是因为他们感受过这些选择带来的痛苦,并决定停止重复它们。
从这个清单中选择一件事开始。只选一件。把它修复到当前项目中。然后再处理下一件。
能力就是这样提升的。一次提交,一次进步。