Skip to content

自定义错误处理器

概述

PHP 允许通过 set_error_handler() 注册自定义错误处理器,通过 set_exception_handler() 注册自定义异常处理器。自定义处理器可以在错误/异常发生时执行特定逻辑,如记录日志、发送告警、转换错误类型等。

在现代 PHP 应用中,自定义错误/异常处理器是框架和应用的基础设施组件,用于统一错误处理策略、实现优雅降级。

版本说明

  • set_error_handler() 从 PHP 4.0.1 起可用
  • PHP 7.0 中 set_error_handler() 的回调签名增加了返回类型 bool
  • PHP 8.0+ 中错误处理器推荐将错误转为 ErrorException

基础概念

set_error_handler

php
<?php

declare(strict_types=1);

// set_error_handler 注册自定义错误处理器
// function set_error_handler(
//     callable $callback,
//     int $error_levels = E_ALL
// ): ?callable

// 回调函数签名:
// function handler(
//     int $errno,        // 错误级别
//     string $errstr,    // 错误消息
//     string $errfile,   // 错误文件
//     int $errline,      // 错误行号
//     array $errcontext  // 错误上下文(PHP 8.0 弃用,8.2 移除)
// ): bool

// 返回值:
// true — 已处理,不调用 PHP 内置处理器
// false — 未处理,继续传递给内置处理器

set_exception_handler

php
<?php

declare(strict_types=1);

// set_exception_handler 注册未捕获异常的处理器
// function set_exception_handler(
//     callable $callback
// ): ?callable

// 回调函数签名:
// function handler(Throwable $exception): void

// 注意:
// - 异常处理器中抛出的异常会导致 Fatal Error
// - 异常处理器执行后脚本终止
// - 如果在异常处理器中调用了 exit/die,后续代码不执行

语法与代码

基本自定义错误处理器

php
<?php

declare(strict_types=1);

// 注册错误处理器
set_error_handler(function (int $severity, string $message, string $file, int $line): bool {
    $levels = [
        E_ERROR             => 'ERROR',
        E_WARNING           => 'WARNING',
        E_PARSE             => 'PARSE',
        E_NOTICE            => 'NOTICE',
        E_CORE_ERROR        => 'CORE_ERROR',
        E_CORE_WARNING      => 'CORE_WARNING',
        E_COMPILE_ERROR     => 'COMPILE_ERROR',
        E_COMPILE_WARNING   => 'COMPILE_WARNING',
        E_USER_ERROR        => 'USER_ERROR',
        E_USER_WARNING      => 'USER_WARNING',
        E_USER_NOTICE       => 'USER_NOTICE',
        E_STRICT            => 'STRICT',
        E_RECOVERABLE_ERROR => 'RECOVERABLE_ERROR',
        E_DEPRECATED        => 'DEPRECATED',
        E_USER_DEPRECATED   => 'USER_DEPRECATED',
    ];

    $levelName = $levels[$severity] ?? "LEVEL_{$severity}";
    $timestamp = date('Y-m-d H:i:s');

    $logMessage = "[{$timestamp}] [{$levelName}] {$message} in {$file}:{$line}";
    error_log($logMessage);

    // 返回 true 表示错误已处理
    return true;
});

// 触发一个 Notice/Warning(PHP 8+)
echo $undefinedVar;
// error_log 记录:[2025-01-01 12:00:00] [WARNING] Undefined variable $undefinedVar in /path/file.php:45

// 触发用户自定义错误
trigger_error('自定义错误消息', E_USER_WARNING);
// error_log 记录:[2025-01-01 12:00:01] [USER_WARNING] 自定义错误消息 in /path/file.php:50

将错误转为异常

php
<?php

declare(strict_types=1);

// 常见模式:将传统错误转为 ErrorException
set_error_handler(function (
    int $severity,
    string $message,
    string $file,
    int $line
): bool {
    // 检查错误是否在当前 error_reporting 级别内
    if (!(error_reporting() & $severity)) {
        return false;
    }

    // 将错误转为 ErrorException 并抛出
    throw new ErrorException($message, 0, $severity, $file, $line);
});

// 现在可以用 try/catch 捕获传统错误
try {
    $result = 1 / 0;  // Warning/DivisionByZeroError
    $file = fopen('/nonexistent', 'r');
} catch (ErrorException $e) {
    echo "捕获到 ErrorException: " . $e->getMessage();
    echo "严重性: " . $e->getSeverity();
} catch (DivisionByZeroError $e) {
    echo "除零错误: " . $e->getMessage();
}

自定义异常处理器

php
<?php

declare(strict_types=1);

set_exception_handler(function (Throwable $exception): void {
    $isCli = PHP_SAPI === 'cli';

    if ($isCli) {
        fwrite(STDERR, sprintf(
            "\n[ERROR] %s: %s\n  in %s:%d\n",
            get_class($exception),
            $exception->getMessage(),
            $exception->getFile(),
            $exception->getLine()
        ));
        exit(1);
    }

    // Web 环境
    http_response_code(500);

    $isDebug = getenv('APP_DEBUG') === 'true';

    if ($isDebug) {
        echo "<pre>" . htmlspecialchars((string) $exception) . "</pre>";
    } else {
        echo json_encode([
            'error' => [
                'message' => '服务器内部错误',
                'code'    => 500,
            ]
        ]);
    }

    // 记录日志
    error_log(sprintf(
        "[%s] [EXCEPTION] %s: %s in %s:%d\n%s",
        date('Y-m-d H:i:s'),
        get_class($exception),
        $exception->getMessage(),
        $exception->getFile(),
        $exception->getLine(),
        $exception->getTraceAsString()
    ));
});

// 触发未捕获异常
throw new RuntimeException('测试异常处理器');

详细说明

返回 true/false 的控制

php
<?php

declare(strict_types=1);

// 返回 true:错误已处理,PHP 内置处理器不再执行
// 返回 false:错误未处理,PHP 继续使用默认处理器

set_error_handler(function (int $severity, string $message, string $file, int $line): bool {
    // 只处理 Warning 级别
    if ($severity === E_WARNING) {
        error_log("[应用自定义处理] Warning: {$message}");
        return true;  // 已处理,不再输出到页面
    }

    // 其他级别交给 PHP 默认处理
    return false;
});

// E_WARNING 被自定义处理器处理
echo $undefinedVar;  // 由自定义处理器记录

// E_USER_ERROR 可能交给默认处理器
// trigger_error('fatal', E_USER_ERROR);  // 默认处理器处理

错误级别过滤

php
<?php

declare(strict_types=1);

// set_error_handler 第二个参数指定处理的错误级别
// 只处理 Warning 和 Notice
set_error_handler(function (int $severity, string $message, string $file, int $line): bool {
    error_log("[Handler] {$message}");
    return true;
}, E_WARNING | E_NOTICE);

// E_USER_ERROR 不会被上面的处理器捕获
// 因为第二个参数限制了处理范围

// 也可以在处理器内部过滤
set_error_handler(function (int $severity, string $message, string $file, int $line): bool {
    if ($severity === E_WARNING) {
        // 只处理 Warning
        error_log("[WARNING] {$message}");
        return true;
    }
    return false;  // 其他交给默认处理器
});

恢复之前的处理器

php
<?php

declare(strict_types=1);

// set_error_handler 返回之前的处理器
function myHandler(int $severity, string $message, string $file, int $line): bool
{
    error_log("[My] {$message}");
    return true;
}

$previousHandler = set_error_handler('myHandler');

// 恢复之前的处理器
restore_error_handler();

// set_exception_handler 返回之前的处理器
function myExceptionHandler(Throwable $e): void
{
    error_log("[Exception] " . $e->getMessage());
}

$previousExceptionHandler = set_exception_handler('myExceptionHandler');

// 恢复
restore_exception_handler();

PHP 8 中错误上下文的移除

php
<?php

declare(strict_types=1);

// PHP 7.x:错误处理器接收第 5 个参数 $errcontext
// PHP 8.0:$errcontext 被弃用
// PHP 8.2:$errcontext 参数被完全移除

// PHP 7.x 写法(不再推荐)
// set_error_handler(function (int $severity, string $message, string $file, int $line, array $context): bool {
//     // $context 包含触发错误时的所有变量
//     return true;
// });

// PHP 8.1+ 写法(推荐)
set_error_handler(function (int $severity, string $message, string $file, int $line): bool {
    error_log("[{$severity}] {$message} in {$file}:{$line}");
    return true;
});

PHP 8.2 移除 $errcontext

$errcontext 参数在 PHP 8.0 中被弃用,在 PHP 8.2 中完全移除。如果处理器函数签名包含该参数,PHP 8.2 会产生兼容性警告。

实战示例

完整的应用错误处理器

php
<?php

declare(strict_types=1);

class AppErrorHandler
{
    private string $env;
    private string $logPath;

    public function __construct(string $env = 'production', string $logPath = '/var/log/app_errors.log')
    {
        $this->env = $env;
        $this->logPath = $logPath;
    }

    public function register(): void
    {
        // 配置基础错误报告
        error_reporting(E_ALL);
        ini_set('display_errors', $this->env === 'development' ? '1' : '0');
        ini_set('log_errors', '1');

        // 注册错误处理器
        set_error_handler($this->handleError(...));

        // 注册异常处理器
        set_exception_handler($this->handleException(...));

        // 注册关机函数(处理 Fatal Error)
        register_shutdown_function($this->handleShutdown(...));
    }

    public function handleError(
        int $severity,
        string $message,
        string $file,
        int $line
    ): bool {
        if (!(error_reporting() & $severity)) {
            return false;
        }

        $this->log('error', $message, $file, $line, $severity);

        // Fatal 级别的错误转为异常
        if (in_array($severity, [E_ERROR, E_USER_ERROR, E_RECOVERABLE_ERROR], true)) {
            throw new ErrorException($message, 0, $severity, $file, $line);
        }

        return true;
    }

    public function handleException(Throwable $exception): void
    {
        $this->log(
            'exception',
            $exception->getMessage(),
            $exception->getFile(),
            $exception->getLine(),
            0,
            get_class($exception),
            $exception->getTraceAsString()
        );

        $this->renderErrorResponse($exception);
    }

    public function handleShutdown(): void
    {
        $error = error_get_last();
        if ($error === null) {
            return;
        }

        $fatalLevels = [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR];
        if (!in_array($error['type'], $fatalLevels, true)) {
            return;
        }

        $this->log(
            'fatal',
            $error['message'],
            $error['file'],
            $error['line'],
            $error['type']
        );
    }

    private function log(
        string $type,
        string $message,
        string $file,
        int $line,
        int $severity = 0,
        ?string $class = null,
        ?string $trace = null
    ): void {
        $entry = json_encode([
            'timestamp' => date('Y-m-d\TH:i:sP'),
            'type'      => $type,
            'severity'  => $severity,
            'message'   => $message,
            'file'      => $file,
            'line'      => $line,
            'class'     => $class,
            'trace'     => $trace,
            'env'       => $this->env,
        ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);

        error_log($entry . "\n", 3, $this->logPath);
    }

    private function renderErrorResponse(Throwable $exception): void
    {
        http_response_code(500);

        if ($this->env === 'development') {
            echo "<h1>Error</h1>";
            echo "<p>" . htmlspecialchars(get_class($exception)) . "</p>";
            echo "<p>" . htmlspecialchars($exception->getMessage()) . "</p>";
            echo "<pre>" . htmlspecialchars($exception->getTraceAsString()) . "</pre>";
        } else {
            echo json_encode(['error' => 'Internal Server Error']);
        }
    }
}

// 使用
$handler = new AppErrorHandler('production');
$handler->register();

注意事项

不能处理的错误

php
<?php

declare(strict_types=1);

// set_error_handler 不能处理以下错误:
// 1. E_ERROR — 致命错误
// 2. E_PARSE — 编译时解析错误
// 3. E_CORE_ERROR — PHP 核心初始化错误
// 4. E_CORE_WARNING — PHP 核心初始化警告
// 5. E_COMPILE_ERROR — 编译时错误
// 6. E_COMPILE_WARNING — 编译时警告

// 这些错误需要通过 register_shutdown_function 捕获
register_shutdown_function(function (): void {
    $error = error_get_last();
    if ($error !== null && in_array($error['type'], [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR], true)) {
        error_log("[FATAL] {$error['message']} in {$error['file']}:{$error['line']}");
    }
});

异常处理器中不能抛出异常

php
<?php

declare(strict_types=1);

// 异常处理器中再抛出异常会导致 Fatal Error
set_exception_handler(function (Throwable $e): void {
    // 危险!如果这里再 throw,会导致 Fatal Error
    // throw new RuntimeException('处理器内部错误');

    // 安全做法:使用 try/catch 包裹
    try {
        $this->sendAlert($e);
    } catch (Throwable $handlerError) {
        error_log("异常处理器内部错误: " . $handlerError->getMessage());
    }
});

最佳实践

  1. 在应用入口注册处理器:确保所有错误都被统一处理
  2. 错误转为 ErrorException:实现 try/catch 统一处理传统错误和异常
  3. 异常处理器中不要抛异常:捕获所有可能的内部异常
  4. 区分环境响应:开发环境显示详情,生产环境返回通用错误
  5. 使用 register_shutdown_function:捕获自定义处理器无法处理的 Fatal Error
php
<?php

declare(strict_types=1);

// 入口文件模板
set_error_handler(function (int $s, string $m, string $f, int $l): bool {
    if (!(error_reporting() & $s)) return false;
    throw new ErrorException($m, 0, $s, $f, $l);
});

set_exception_handler(function (Throwable $e): void {
    error_log($e->__toString());
    http_response_code(500);
    echo json_encode(['error' => 'Internal Server Error']);
});

register_shutdown_function(function (): void {
    $e = error_get_last();
    if ($e && in_array($e['type'], [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR], true)) {
        error_log("[FATAL] {$e['message']} in {$e['file']}:{$e['line']}");
    }
});

参考链接