错误处理的核心原则

在多年的PHP开发中,我发现很多团队在错误处理方面存在误区。正确的错误处理不仅仅是捕获异常,更重要的是建立一套完整的错误响应机制。

生产环境错误配置

首先,生产环境的PHP配置应该是这样的:

// 生产环境错误设置
ini_set('display_errors', '0');
ini_set('log_errors', '1');
ini_set('error_log', '/var/log/php/error.log');
error_reporting(E_ALL);

关键点在于:关闭错误显示,但开启错误日志记录,并且设置适当的错误报告级别。

异常处理的实用模式

业务异常分类

我习惯将异常分为三类:

  • 系统异常:数据库连接失败、文件系统错误等
  • 业务异常:用户输入验证失败、业务规则冲突等
  • 第三方服务异常:API调用失败、外部服务不可用等
class BusinessException extends Exception {
    private $errorCode;
    
    public function __construct(string $message, int $errorCode = 0) {
        parent::__construct($message);
        $this->errorCode = $errorCode;
    }
    
    public function getErrorCode(): int {
        return $this->errorCode;
    }
}

class ValidationException extends BusinessException {
    private $fieldErrors = [];
    
    public function setFieldError(string $field, string $message) {
        $this->fieldErrors[$field] = $message;
    }
    
    public function getFieldErrors(): array {
        return $this->fieldErrors;
    }
}

全局异常处理器

建立一个统一的异常处理入口非常关键:

set_exception_handler(function (Throwable $e) {
    $logContext = [
        'message' => $e->getMessage(),
        'code' => $e->getCode(),
        'file' => $e->getFile(),
        'line' => $e->getLine(),
        'trace' => $e->getTraceAsString(),
        'timestamp' => date('Y-m-d H:i:s'),
        'request_id' => $_SERVER['REQUEST_ID'] ?? uniqid()
    ];
    
    // 根据异常类型采用不同处理策略
    if ($e instanceof BusinessException) {
        // 业务异常,记录警告级别日志
        error_log(json_encode($logContext, JSON_UNESCAPED_UNICODE));
        
        // 返回用户友好的错误信息
        http_response_code(400);
        echo json_encode([
            'success' => false,
            'error' => $e->getMessage(),
            'error_code' => $e->getErrorCode()
        ]);
    } else {
        // 系统异常,记录错误级别日志
        error_log('SYSTEM_ERROR: ' . json_encode($logContext, JSON_UNESCAPED_UNICODE));
        
        // 生产环境不暴露系统错误详情
        http_response_code(500);
        echo json_encode([
            'success' => false,
            'error' => '系统繁忙,请稍后重试'
        ]);
    }
});

结构化日志记录

日志分级策略

我推荐使用以下日志级别:

  • DEBUG: 详细的调试信息,生产环境通常关闭
  • INFO: 重要的业务流程节点
  • WARN: 不影响系统运行的异常情况
  • ERROR: 需要立即关注的问题

日志上下文信息

每次日志记录都应该包含足够的上下文:

class StructuredLogger {
    private $requestId;
    
    public function __construct() {
        $this->requestId = $_SERVER['REQUEST_ID'] ?? uniqid();
    }
    
    public function info(string $message, array $context = []) {
        $this->log('INFO', $message, $context);
    }
    
    public function error(string $message, array $context = []) {
        $this->log('ERROR', $message, $context);
    }
    
    private function log(string $level, string $message, array $context) {
        $logEntry = [
            'timestamp' => date('c'),
            'level' => $level,
            'message' => $message,
            'request_id' => $this->requestId,
            'uri' => $_SERVER['REQUEST_URI'] ?? '',
            'method' => $_SERVER['REQUEST_METHOD'] ?? '',
            'ip' => $_SERVER['REMOTE_ADDR'] ?? '',
            'context' => $context
        ];
        
        // 写入文件或发送到日志服务
        file_put_contents(
            '/var/log/php/app.log',
            json_encode($logEntry, JSON_UNESCAPED_UNICODE) . PHP_EOL,
            FILE_APPEND | LOCK_EX
        );
    }
}

实际应用场景

数据库操作异常处理

在处理数据库操作时,我采用以下模式:

try {
    $pdo->beginTransaction();
    
    // 执行多个数据库操作
    $userStmt = $pdo->prepare("INSERT INTO users (...) VALUES (...)");
    $userStmt->execute([...]);
    
    $profileStmt = $pdo->prepare("INSERT INTO user_profiles (...) VALUES (...)");
    $profileStmt->execute([...]);
    
    $pdo->commit();
    
    $logger->info('用户注册成功', ['user_id' => $pdo->lastInsertId()]);
    
} catch (PDOException $e) {
    $pdo->rollBack();
    
    // 根据错误码判断异常类型
    if ($e->errorInfo[1] == 1062) {
        throw new BusinessException('用户名已存在', 1001);
    } else {
        $logger->error('数据库操作失败', [
            'error' => $e->getMessage(),
            'sql_state' => $e->errorInfo[0],
            'driver_code' => $e->errorInfo[1]
        ]);
        throw new Exception('系统错误,请稍后重试');
    }
}

API接口统一响应

对于API接口,我建议统一的响应格式:

class ApiResponse {
    public static function success($data = null, string $message = '成功') {
        http_response_code(200);
        echo json_encode([
            'success' => true,
            'message' => $message,
            'data' => $data,
            'timestamp' => time()
        ]);
    }
    
    public static function error(string $message, int $code = 0, int $httpCode = 400) {
        http_response_code($httpCode);
        echo json_encode([
            'success' => false,
            'message' => $message,
            'error_code' => $code,
            'timestamp' => time()
        ]);
    }
}

监控与告警

关键指标监控

在生产环境中,需要监控以下关键指标:

  • 错误率:统计不同级别错误的数量
  • 响应时间:记录API接口的响应时间分布
  • 业务异常:特定业务异常的出现频率

日志分析策略

我通常使用以下方式分析日志:

# 查找最近一小时内的错误
cat /var/log/php/app.log | grep '"level":"ERROR"' | jq '.'

# 统计不同接口的错误数量
cat /var/log/php/app.log | grep '"level":"ERROR"' | jq '.uri' | sort | uniq -c

总结的经验教训

经过多个项目的实践,我总结了以下几点:

  1. 尽早失败原则:在发现问题时立即抛出异常,而不是继续执行可能出错的操作
  2. 上下文完整性:确保每个错误日志都包含足够的问题定位信息
  3. 异常分类处理:不同类型的异常应该有不同的处理策略
  4. 监控自动化:建立自动化的错误监控和告警机制
  5. 定期回顾:定期分析错误日志,找出系统瓶颈和改进点

这套错误处理和日志记录策略帮助我们在多个大型项目中快速定位和解决问题,大幅提升了系统的稳定性和可维护性。