一、为什么错误处理这么重要?

想象一下你正在开发一个电商网站,用户在下单时突然页面白屏了,但没有任何提示。用户不知道是网络问题、库存不足还是系统故障,这种体验有多糟糕?好的错误处理就像贴心的导购员,会明确告诉你"商品已售罄"或"请检查网络连接"。

PHP默认的错误处理其实很"懒"——它可能只在页面上显示一行小字,或者干脆什么都不显示。这就像家里烟雾报警器坏了,着火时你完全不知情。我们需要主动构建错误处理机制,让问题无处可藏。

二、PHP自带的错误处理三板斧

PHP其实提供了不少现成的工具,我们先看看这些"基础装备":

<?php
// 技术栈:PHP 8.2

// 1. 错误报告级别设置(放在入口文件最开头)
error_reporting(E_ALL); // 报告所有错误
ini_set('display_errors', 0); // 不直接显示给用户
ini_set('log_errors', 1); // 开启错误日志
ini_set('error_log', __DIR__.'/app_errors.log'); // 指定日志文件

// 2. 自定义错误处理器
set_error_handler(function($errno, $errstr, $errfile, $errline) {
    // 将错误信息格式化为JSON便于分析
    $error = [
        'time' => date('Y-m-d H:i:s'),
        'type' => $errno,
        'message' => $errstr,
        'file' => $errfile,
        'line' => $errline,
        'stack' => debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS)
    ];
    
    file_put_contents(__DIR__.'/error_log.json', json_encode($error)."\n", FILE_APPEND);
    return true; // 告诉PHP这个错误我们已经处理了
});

// 3. 致命错误捕获(注册关机函数)
register_shutdown_function(function() {
    $error = error_get_last();
    if ($error && in_array($error['type'], [E_ERROR, E_PARSE, E_CORE_ERROR])) {
        // 发送邮件或短信通知开发人员
        mail('dev@example.com', '系统崩溃警报', json_encode($error));
    }
});

这三个工具配合使用,就像给系统装上了:

  • 显微镜(error_reporting):发现所有问题
  • 记事本(error_log):记录问题细节
  • 警报器(shutdown_function):紧急情况通知

三、异常处理的艺术

如果说错误处理是消防演习,那么异常处理就是应急预案。看这个订单处理的例子:

<?php
// 技术栈:PHP 8.2

class InventoryException extends Exception {
    // 自定义库存异常类
    public function __construct($message, $sku) {
        parent::__construct("商品 {$sku} ".$message);
        $this->sku = $sku;
    }
}

class OrderService {
    public function createOrder($items) {
        try {
            foreach ($items as $item) {
                $this->checkInventory($item['sku'], $item['qty']);
            }
            // 正常订单处理逻辑...
        } catch (InventoryException $e) {
            // 库存不足的特殊处理
            return [
                'success' => false,
                'code' => 'INVENTORY_SHORTAGE',
                'message' => $e->getMessage(),
                'sku' => $e->sku
            ];
        } catch (Exception $e) {
            // 其他异常的通用处理
            error_log("订单创建失败: ".$e->getMessage());
            return [
                'success' => false,
                'code' => 'SYSTEM_ERROR',
                'message' => '系统繁忙,请稍后再试'
            ];
        }
    }
    
    private function checkInventory($sku, $qty) {
        $stock = $this->queryInventory($sku);
        if ($stock < $qty) {
            throw new InventoryException('库存不足', $sku);
        }
    }
}

// 使用示例
$service = new OrderService();
$result = $service->createOrder([
    ['sku' => 'IPHONE13', 'qty' => 2],
    ['sku' => 'AIRPODS', 'qty' => 1]
]);

if (!$result['success']) {
    // 给用户友好的提示
    echo "下单失败原因:".$result['message'];
    if ($result['code'] == 'INVENTORY_SHORTAGE') {
        echo ",您可以先购买其他商品";
    }
}

这种处理方式的好处是:

  1. 业务异常(如库存不足)和系统异常(如数据库连接失败)区分处理
  2. 给用户的信息友好且 actionable(可采取行动)
  3. 开发人员能通过日志准确定位问题

四、日志记录的进阶技巧

光是记录错误还不够,好的日志系统要像侦探的记事本,能还原案发现场。我们结合Monolog这个强大的日志库来看看:

<?php
// 技术栈:PHP 8.2 + Monolog

require __DIR__.'/vendor/autoload.php';

use Monolog\Logger;
use Monolog\Handler\StreamHandler;
use Monolog\Handler\SlackWebhookHandler;
use Monolog\Formatter\JsonFormatter;

// 创建日志频道
$log = new Logger('order_system');

// 文件日志(JSON格式)
$fileHandler = new StreamHandler(__DIR__.'/logs/order.log');
$fileHandler->setFormatter(new JsonFormatter());
$log->pushHandler($fileHandler);

// 严重错误发Slack通知
$slackHandler = new SlackWebhookHandler(
    'https://hooks.slack.com/services/XXX',
    '#tech-alerts',
    'PHP错误机器人',
    true,
    null,
    Logger::ERROR
);
$log->pushHandler($slackHandler);

// 添加上下文信息(自动记录在每个日志中)
$log->pushProcessor(function($record) {
    $record['extra']['ip'] = $_SERVER['REMOTE_ADDR'] ?? 'cli';
    $record['extra']['user_id'] = $_SESSION['user_id'] ?? null;
    return $record;
});

// 使用示例
try {
    $log->info("订单创建开始", ['sku' => 'IPHONE13', 'qty' => 2]);
    
    // 业务逻辑...
    if (rand(0, 10) > 8) {
        throw new Exception("模拟的随机错误");
    }
    
    $log->info("订单创建成功", ['order_id' => 10086]);
} catch (Exception $e) {
    $log->error("订单创建异常", [
        'error' => $e->getMessage(),
        'trace' => $e->getTraceAsString()
    ]);
    throw $e;
}

这样的日志系统可以:

  • 不同级别日志分开处理(info存文件,error发通知)
  • 自动记录请求上下文(用户ID、IP等)
  • 支持多种输出方式(文件、Slack等)
  • 结构化存储便于分析(JSON格式)

五、实战中的注意事项

  1. 敏感信息过滤
$log->pushProcessor(function($record) {
    // 过滤信用卡号
    if (isset($record['context']['card_no'])) {
        $record['context']['card_no'] = substr($record['context']['card_no'], -4).'****';
    }
    return $record;
});
  1. 日志轮转: 使用Linux的logrotate工具防止日志文件过大:
/path/to/your/logs/*.log {
    daily
    rotate 30
    missingok
    compress
    delaycompress
    notifempty
}
  1. 性能考量
  • 高频操作使用debug级别日志
  • 避免在循环中记录重复信息
  • 考虑使用syslog或异步写入

六、错误处理的最佳实践

  1. 用户视角
  • 前端显示友好的错误提示
  • 提供错误代码(便于客服查询)
  • 给出解决方案(如"请刷新重试")
  1. 开发视角
  • 在开发环境显示详细错误
  • 生产环境隐藏技术细节但记录完整日志
  • 使用Sentry/Bugsnag等工具收集错误
  1. 运维视角
  • 监控错误日志频率
  • 设置不同级别的报警阈值
  • 定期分析错误趋势

七、总结

好的错误处理系统就像飞机的黑匣子+驾驶舱警报系统的结合体:

  • 平时安静记录所有细节(黑匣子)
  • 出现问题时立即告警(警报系统)
  • 事后能完整复现问题(调查工具)

记住三个核心原则:

  1. 不要信任任何输入 - 验证、验证、再验证
  2. 失败要优雅 - 给用户留条退路
  3. 记录足够多 - 但不要太多(避免日志爆炸)

从今天开始,检查你的项目是否:

  • [ ] 有完整的错误分类处理
  • [ ] 关键操作都有日志记录
  • [ ] 生产环境不会暴露敏感信息
  • [ ] 有错误监控和报警机制

把这些做好,你的应用健壮性就能超过80%的项目了!