Skip to content

错误基础概要

概述

PHP 的错误处理机制经历了重大演进。PHP 5.x 中错误和异常是两套完全独立的系统;PHP 7.0 引入 Throwable 接口统一了两者的处理方式;PHP 8.0+ 中错误默认都转换为异常抛出。

理解 PHP 错误机制的演进对于编写健壮的应用程序至关重要。现代 PHP 推荐使用异常(Exception)作为主要的错误处理手段,错误(Error)仅在不可恢复的程序级问题中使用。

版本说明

  • PHP 5.x:错误和异常独立,错误不能被 try/catch 捕获
  • PHP 7.0:引入 Throwable,Error 实现了 Throwable,可被 try/catch 捕获
  • PHP 8.0+:大多数错误转为 Error 异常,默认错误报告更严格

基础概念

错误 vs 异常

php
<?php

declare(strict_types=1);

// 错误(Error)— PHP 引擎层面的故障
// 通常表示程序存在严重问题,不应被忽略
// 例如:语法错误、类型错误、内存不足
echo $undefinedVar;  // PHP 8+: Warning: Undefined variable

// 异常(Exception)— 应用层面的可预期异常
// 表示程序遇到了可处理的异常情况
// 例如:参数无效、文件不存在、网络超时
throw new InvalidArgumentException('参数不能为空');
特性错误 (Error)异常 (Exception)
来源PHP 引擎 / 内部函数应用代码 / 库代码
可恢复性通常不可恢复通常可恢复
处理方式try/catch 或 set_error_handlertry/catch
基类ErrorException
严重性高(程序级问题)中(业务级问题)
PHP 7+实现 Throwable,可被 try/catch实现 Throwable

PHP 错误机制演进

php
<?php

declare(strict_types=1);

// PHP 5.x — 两套独立体系
// 错误:trigger_error(), E_WARNING, E_ERROR
// 异常:throw new Exception()
// 错误不能被 try/catch 捕获

// PHP 7.0 — 统一为 Throwable
// Error 和 Exception 都实现 Throwable
// 所有错误和异常都可以用 try/catch 捕获
try {
    $result = 1 / 0;  // DivisionByZeroError
} catch (Throwable $e) {
    echo $e->getMessage();  // Division by zero
}

// PHP 8.0+ — 错误默认更严格
// 许多之前只是 Warning/Notice 的现在抛出 Error
// 例如:访问未定义属性、参数类型不匹配

Throwable:统一的错误/异常接口

php
<?php

declare(strict_types=1);

// PHP 7+ 的错误/异常层次
// Throwable
// ├── Error
// │   ├── TypeError
// │   ├── ValueError
// │   ├── ArgumentCountError
// │   ├── ArithmeticError
// │   ├── CompileError
// │   └── ...
// └── Exception
//     ├── RuntimeException
//     ├── LogicException
//     └── ...

语法与代码

错误触发示例

php
<?php

declare(strict_types=1);

// 1. 类型错误 (TypeError)
function greet(string $name): string
{
    return "Hello, {$name}";
}

try {
    greet(123);  // PHP 8+ strict mode: TypeError
} catch (TypeError $e) {
    echo "TypeError: " . $e->getMessage();
}

// 2. 参数数量错误 (ArgumentCountError)
function add(int $a, int $b): int
{
    return $a + $b;
}

try {
    add(1);  // ArgumentCountError
} catch (ArgumentCountError $e) {
    echo "参数数量错误: " . $e->getMessage();
}

// 3. 值错误 (ValueError) — PHP 8.0+
try {
    $num = intval('abc', 37);  // 无效的进制
} catch (ValueError $e) {
    echo "值错误: " . $e->getMessage();
}

// 4. 算术错误 (ArithmeticError)
try {
    $result = intdiv(PHP_INT_MIN, -1);  // 整数溢出
} catch (ArithmeticError $e) {
    echo "算术错误: " . $e->getMessage();
}

错误报告级别

php
<?php

declare(strict_types=1);

// 查看当前错误报告级别
echo error_reporting();
// -1 表示所有错误
// E_ALL 表示所有错误和警告

// 设置错误报告级别
error_reporting(E_ALL);
error_reporting(E_ALL & ~E_NOTICE);  // 排除 Notice

// PHP 8.0+ 推荐设置
error_reporting(E_ALL | E_STRICT);

详细说明

PHP 5 到 PHP 8 的错误处理变化

php
<?php

declare(strict_types=1);

// PHP 5.x:错误是传统机制
// 调用未定义函数 → Fatal Error(脚本终止)
// 访问未定义变量 → Notice(继续执行)
// 参数类型不匹配 → Catchable Fatal Error

// PHP 7.x:统一为 Throwable
// 调用未定义函数 → Error (PHP 7+)
// 参数类型不匹配 → TypeError
// 除以零 → DivisionByZeroError (PHP 7+)

// PHP 8.x:更严格的默认行为
// 访问未定义属性 → Warning (PHP 8.0)
// 访问未定义变量 → Warning (PHP 8.0, 之前是 Notice)
// 未定义常量 → Error (PHP 8.0, 之前是 Notice)
// 函数返回值类型不匹配 → TypeError

// PHP 8.1:
// 内部函数参数类型不匹配(之前是 Warning)→ TypeError
// 返回值类型不匹配(内部函数)→ TypeError

// PHP 8.2:
// 动态属性弃用(使用 #[AllowDynamicProperties] 保留)
// 更多严格类型检查

错误转换为异常

php
<?php

declare(strict_types=1);

// 在 PHP 7.0+ 中,使用 ErrorException 将传统错误转为异常
// 这是兼容旧代码和统一异常处理的桥梁

class ErrorHandler
{
    public function register(): void
    {
        // 将 PHP 错误转为 ErrorException
        set_error_handler(
            function (int $severity, string $message, string $file, int $line): bool {
                // 只处理 error_reporting 包含的错误级别
                if (!(error_reporting() & $severity)) {
                    return false;
                }

                throw new ErrorException($message, 0, $severity, $file, $line);
            }
        );
    }
}

// 使用
$handler = new ErrorHandler();
$handler->register();

try {
    // 触发 Warning
    $file = fopen('/nonexistent/file.txt', 'r');
} catch (ErrorException $e) {
    echo "捕获到错误(转为异常): " . $e->getMessage();
}

错误抑制运算符 @

php
<?php

declare(strict_types=1);

// @ 运算符抑制错误输出
// 但不推荐使用,因为它:
// 1. 影响性能
// 2. 隐藏了可能有用的调试信息
// 3. 不能抑制所有错误(Fatal Error、ParseException)

$result = @file_get_contents('nonexistent.txt');
// 错误被抑制,$result 为 false

// 替代方案:使用错误控制或异常
$result = file_get_contents('nonexistent.txt');
if ($result === false) {
    // 优雅地处理错误
    echo "文件不存在";
}

// PHP 8.0+:@ 运算符不影响错误处理器
// 如果自定义了 set_error_handler,@ 不会阻止错误处理器执行
// 但 error_reporting() 在 @ 作用域内会返回 0

不推荐使用 @ 运算符

PHP 官方不推荐使用 @ 运算符抑制错误。应使用 try/catch 或条件检查来处理预期的错误场景。

实战示例

生产环境错误处理框架

php
<?php

declare(strict_types=1);

class ProductionErrorHandler
{
    private bool $debugMode;

    public function __construct(bool $debugMode = false)
    {
        $this->debugMode = $debugMode;
    }

    public function register(): void
    {
        // 注册错误处理器
        set_error_handler($this->handleError(...));

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

        // 注册致命错误处理器
        register_shutdown_function($this->handleShutdown(...));
    }

    public function handleError(
        int $severity,
        string $message,
        string $file,
        int $line
    ): bool {
        $errorTypes = [
            E_ERROR             => 'Error',
            E_WARNING           => 'Warning',
            E_PARSE             => 'Parse Error',
            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',
        ];

        $type = $errorTypes[$severity] ?? "Unknown({$severity})";

        $this->logError($type, $message, $file, $line);

        // 返回 true 表示已处理,不调用 PHP 内置错误处理器
        return true;
    }

    public function handleException(Throwable $e): void
    {
        $this->logError(
            get_class($e),
            $e->getMessage(),
            $e->getFile(),
            $e->getLine()
        );

        if (!$this->debugMode) {
            echo '服务器发生内部错误';
        } else {
            echo "<pre>{$e}</pre>";
        }
    }

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

        if (
            $error['type'] === E_ERROR
            || $error['type'] === E_PARSE
            || $error['type'] === E_CORE_ERROR
            || $error['type'] === E_COMPILE_ERROR
        ) {
            $this->logError(
                'Fatal ' . $error['type'],
                $error['message'],
                $error['file'],
                $error['line']
            );
        }
    }

    private function logError(
        string $type,
        string $message,
        string $file,
        int $line
    ): void {
        $entry = sprintf(
            "[%s] [%s] %s in %s:%d",
            date('Y-m-d H:i:s'),
            $type,
            $message,
            $file,
            $line
        );

        error_log($entry);
    }
}

// 使用
$handler = new ProductionErrorHandler(debugMode: false);
$handler->register();

注意事项

错误处理的性能影响

php
<?php

declare(strict_types=1);

// 错误处理会带来性能开销
// 1. @ 运算符会临时将 error_reporting 设为 0,然后再恢复
// 2. set_error_handler 每次错误都会调用
// 3. try/catch 块本身开销极小(没有异常时)

// 错误处理性能建议:
// - 使用条件检查代替 @ 运算符
// - 在生产环境中减少 error_reporting 级别
// - 避免在热路径中频繁触发错误

// 好:条件检查
if (file_exists($path)) {
    $content = file_get_contents($path);
}

// 差:使用 @ 运算符
$content = @file_get_contents($path);

不能被 try/catch 捕获的错误

php
<?php

declare(strict_types=1);

// 以下错误不能被 try/catch 捕获:
// 1. 语法错误(ParseError 除外的一些编译时错误)
// 2. 某些内部错误(如内存限制超出)

// ParseError 可以被捕获(PHP 7+)
try {
    eval('invalid php code ===');
} catch (ParseError $e) {
    echo "语法错误: " . $e->getMessage();
}

最佳实践

  1. 统一使用 Throwable 处理:在全局处理器中使用 catch(Throwable) 捕获所有
  2. 错误级别按环境区分:开发环境显示所有错误,生产环境只记录
  3. 避免使用 @ 抑制错误:使用条件检查或 try/catch 替代
  4. 注册全局处理器:set_error_handler + set_exception_handler + register_shutdown_function
  5. 使用 ErrorException 桥接旧代码:将传统错误转为异常以统一处理
php
<?php

declare(strict_types=1);

// 入口文件统一错误处理
error_reporting(E_ALL);
ini_set('display_errors', '0');
ini_set('log_errors', '1');

set_error_handler(function (int $severity, string $msg, string $file, int $line): bool {
    if (!(error_reporting() & $severity)) {
        return false;
    }
    throw new ErrorException($msg, 0, $severity, $file, $line);
});

set_exception_handler(function (Throwable $e): void {
    error_log(sprintf(
        "[%s] %s: %s in %s:%d",
        date('Y-m-d H:i:s'),
        get_class($e),
        $e->getMessage(),
        $e->getFile(),
        $e->getLine()
    ));
    http_response_code(500);
    echo json_encode(['error' => 'Internal Server Error']);
});

参考链接