CatchAdmin PHP 后台管理框架 Logo CatchAdmin

2026 年 PHP 开发者应停止使用的坏实践

拥有十年以上 PHP 开发经验的人,通常都接手过让人怀疑职业选择的遗留代码库,也经历过需要先深呼吸才能写反馈的代码评审。

坦率地说,2026 年仍然反复出现的许多坏模式,和 2016 年许多开发者犯过的错误并无本质差异。

PHP 生态已经成熟了许多。PHP 8.3 速度快、表达力强,并且真正具备现代语言特征。Composer、PHPStan、Laravel 等工具也提高了行业基准。开发者已经没有理由继续用 mysql_query() 时代的方式编写 PHP。

下面以从初级到高级开发者都能理解的方式展开讨论。这里没有指责,只讨论应当停止的做法,以及更合适的替代方案。

1. 停止使用 @ 抑制错误

php
// ❌ Please, no.
$result = @file_get_contents('https://some-api.com/data');

开发者这样做的动机很容易理解。错误提示很烦人,警告会污染日志,于是就在前面加上 @,然后继续往下写。

但实际发生的是:一个潜在的关键故障被静默吞掉了。这个 API 调用可能已经失败。此时 $resultfalse,代码却像一切正常一样继续把它传递到后续流程。

php
// ✅ 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 客户端。它默认会抛出异常,并提供真实的错误上下文。

php
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 年本不需要再强调这一点。然而,原始且未参数化的查询仍然会溜进代码库,有时甚至出现在新项目中。

php
// ❌ SQL injection waiting to happen
$id = $_GET['id'];
$result = $pdo->query("SELECT * FROM users WHERE id = $id");

如果有人在 URL 中传入 1 OR 1=1,就等于把整个数据库交了出去。这不是假设场景,而是数据泄露真实发生的方式。

始终使用 PDO 预处理语句:

php
// ✅ 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 行代码。它负责获取数据、转换数据、发送邮件、写入文件,还在同一个方法里完成三种不同的计算。

php
// ❌ 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 类中:

php
// ✅ 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 年的函数仍然像下面这样,就需要认真审视了:

php
// ❌ What even goes in here? What comes out?
function processPayment($amount, $currency, $user) {
    // mystery box
}

这会迫使每一个阅读代码的开发者通读整个函数,才能理解预期类型。同时,PHP 也无法在运行时帮助捕获错误。

php
// ✅ Self-documenting and safe
function processPayment(
    float $amount,
    string $currency,
    User $user
): PaymentResult {
    // Now we know exactly what this does
}

对于状态值这类场景,可以进一步使用 PHP 8.1+ 的枚举:

php
// ✅ 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 引入,问题就已经很明确了。

php
// ❌ 2009 called
require_once 'libs/phpmailer/PHPMailer.php';
require_once 'libs/guzzle/guzzle.php';

Composer 自 2012 年以来已经是事实标准。它会处理自动加载、版本管理和依赖解析。

bash
composer require symfony/mailer
composer require guzzlehttp/guzzle
composer require --dev phpstan/phpstan

然后在代码中这样使用:

php
// ✅ 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 等工具可以在不运行代码的情况下分析代码,并在问题到达用户之前捕获整类错误。

bash
composer require --dev phpstan/phpstan
vendor/bin/phpstan analyse src --level=6

可以从 level 0 开始,然后逐步提高等级。Level 6+ 能捕获 null 指针问题、错误返回类型和不可达代码。刚开始会有些不适,就像突然打开一间凌乱房间里的灯。但这正是它的价值所在。

把它加入 CI 流水线。让它成为合并门禁。当 PHPStan 发出严重警告时,不要让代码合并。

7. 停止用 die()exit() 处理错误

php
// ❌ This is not error handling, this is giving up
$user = getUser($id) or die('User not found');

die() 会直接终止整个脚本,而且几乎没有上下文。没有堆栈跟踪,没有日志记录,也没有向客户端返回优雅响应。只剩下中断。

使用异常。异常可以传播、捕获、记录,并且能让应用程序保持可控:

php
// ✅ 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 在其上构建的测试层也确实非常易用。

php
// 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 的被抑制错误。无人敢碰的上帝控制器。缺失测试让回归问题溜进生产环境。

高级开发者之所以高级,并不是因为他们知道更多语法。他们之所以高级,是因为他们感受过这些选择带来的痛苦,并决定停止重复它们。

从这个清单中选择一件事开始。只选一件。把它修复到当前项目中。然后再处理下一件。

能力就是这样提升的。一次提交,一次进步。

本作品采用《CC 协议》,转载必须注明作者和本文链接