强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

PHP 完全指南 / 第 16 章 — 错误处理

第 16 章 — 错误处理:错误级别、自定义处理器与日志

16.1 PHP 错误级别

常量说明
E_ERROR1致命运行时错误(不可恢复)
E_WARNING2运行时警告(不终止脚本)
E_PARSE4编译时解析错误
E_NOTICE8运行时通知(可能是 bug)
E_CORE_ERROR16PHP 内核启动错误
E_CORE_WARNING32PHP 内核启动警告
E_COMPILE_ERROR64编译时致命错误
E_COMPILE_WARNING128编译时警告
E_USER_ERROR256用户触发的致命错误
E_USER_WARNING512用户触发的警告
E_USER_NOTICE1024用户触发的通知
E_STRICT2048代码标准化建议
E_DEPRECATED8192弃用功能警告
E_USER_DEPRECATED16384用户触发的弃用警告
E_ALL32767所有错误和警告
<?php
// 设置错误报告级别(开发环境)
error_reporting(E_ALL);

// 生产环境
error_reporting(E_ALL & ~E_DEPRECATED & ~E_STRICT);

// 控制是否显示错误
ini_set('display_errors', '0');    // 生产:不显示
ini_set('display_errors', '1');    // 开发:显示
ini_set('log_errors', '1');        // 始终记录日志
ini_set('error_log', '/var/log/php/error.log');

16.2 自定义错误处理器

<?php
declare(strict_types=1);

class ErrorHandler
{
    private static array $errorLevels = [
        E_ERROR             => 'ERROR',
        E_WARNING           => 'WARNING',
        E_NOTICE            => 'NOTICE',
        E_STRICT            => 'STRICT',
        E_DEPRECATED        => 'DEPRECATED',
        E_USER_ERROR        => 'USER_ERROR',
        E_USER_WARNING      => 'USER_WARNING',
        E_USER_NOTICE       => 'USER_NOTICE',
        E_USER_DEPRECATED   => 'USER_DEPRECATED',
    ];

    public static function register(): void
    {
        set_error_handler([self::class, 'handleError']);
        set_exception_handler([self::class, 'handleException']);
        register_shutdown_function([self::class, 'handleShutdown']);
    }

    public static function handleError(
        int $errno,
        string $errstr,
        string $errfile,
        int $errline,
    ): bool {
        // 将错误转为异常(严格模式下推荐)
        if (error_reporting() & $errno) {
            throw new \ErrorException($errstr, 0, $errno, $errfile, $errline);
        }
        return true;  // 错误已处理
    }

    public static function handleException(\Throwable $e): void
    {
        self::logError($e);

        if (php_sapi_name() === 'cli') {
            fwrite(STDERR, self::formatCliError($e));
        } else {
            http_response_code(500);
            echo self::formatWebError($e);
        }
    }

    public static function handleShutdown(): void
    {
        $error = error_get_last();
        if ($error !== null && in_array($error['type'], [E_ERROR, E_PARSE, E_CORE_ERROR])) {
            self::logError(new \ErrorException(
                $error['message'],
                0,
                $error['type'],
                $error['file'],
                $error['line']
            ));
        }
    }

    private static function logError(\Throwable $e): void
    {
        $context = [
            'type'    => get_class($e),
            'message' => $e->getMessage(),
            'file'    => $e->getFile(),
            'line'    => $e->getLine(),
            'trace'   => $e->getTraceAsString(),
        ];

        $level = ($e instanceof \ErrorException)
            ? (self::$errorLevels[$e->getSeverity()] ?? 'UNKNOWN')
            : 'EXCEPTION';

        $logLine = sprintf(
            "[%s] %s: %s in %s:%d\n",
            date('Y-m-d H:i:s'),
            $level,
            $e->getMessage(),
            $e->getFile(),
            $e->getLine()
        );

        error_log($logLine, 3, '/var/log/php/error.log');
    }

    private static function formatCliError(\Throwable $e): string
    {
        return sprintf(
            "\033[31m[%s] %s: %s\033[0m\n  at %s:%d\n\n%s\n",
            date('H:i:s'),
            get_class($e),
            $e->getMessage(),
            $e->getFile(),
            $e->getLine(),
            $e->getTraceAsString()
        );
    }

    private static function formatWebError(\Throwable $e): string
    {
        if (getenv('APP_DEBUG') === 'true') {
            return "<pre>" . htmlspecialchars($e) . "</pre>";
        }
        return '<h1>500 Internal Server Error</h1>';
    }
}

// 注册
ErrorHandler::register();

16.3 trigger_error()

<?php
// 触发用户级错误
function divide(float $a, float $b): float
{
    if ($b === 0.0) {
        trigger_error('Division by zero', E_USER_ERROR);
    }
    return $a / $b;
}

// 弃用警告
class OldClass
{
    public function oldMethod(): void
    {
        trigger_error(
            'oldMethod() is deprecated, use newMethod() instead',
            E_USER_DEPRECATED
        );
        // 继续执行旧逻辑...
    }
}

16.4 错误与异常转换

<?php
// 方式 1:将所有错误转为异常
set_error_handler(function (int $errno, string $errstr, string $errfile, int $errline): never {
    throw new \ErrorException($errstr, 0, $errno, $errfile, $errline);
});

// 方式 2:只转换特定级别
set_error_handler(function (int $errno, string $errstr, string $errfile, int $errline) use ($levels): bool {
    if (in_array($errno, $levels, true)) {
        throw new \ErrorException($errstr, 0, $errno, $errfile, $errline);
    }
    return false;  // 不处理,让 PHP 默认处理器处理
});

// 注意:E_ERROR、E_PARSE、E_CORE_ERROR 不能被 set_error_handler 捕获
// 需要使用 register_shutdown_function

16.5 日志最佳实践

16.5.1 基本日志写入

<?php
// error_log — 内置日志
error_log('Something happened');

// 写入文件
error_log("User {$userId} logged in\n", 3, '/var/log/php/app.log');

// 发送到 syslog
error_log('Server error', 0);

// 发送到邮件
error_log('Critical error', 1, '[email protected]');

16.5.2 结构化日志

<?php
declare(strict_types=1);

class Logger
{
    private string $logFile;
    private string $minLevel;

    private const LEVELS = [
        'DEBUG'     => 0,
        'INFO'      => 1,
        'NOTICE'    => 2,
        'WARNING'   => 3,
        'ERROR'     => 4,
        'CRITICAL'  => 5,
        'ALERT'     => 6,
        'EMERGENCY' => 7,
    ];

    public function __construct(string $logFile, string $minLevel = 'INFO')
    {
        $this->logFile = $logFile;
        $this->minLevel = $minLevel;
    }

    public function log(string $level, string $message, array $context = []): void
    {
        if (self::LEVELS[$level] < self::LEVELS[$this->minLevel]) {
            return;
        }

        $entry = [
            'timestamp' => date('c'),
            'level'     => $level,
            'message'   => $message,
            'context'   => $context,
            'channel'   => 'app',
        ];

        if (isset($_SERVER['REQUEST_URI'])) {
            $entry['request'] = [
                'method' => $_SERVER['REQUEST_METHOD'],
                'uri'    => $_SERVER['REQUEST_URI'],
                'ip'     => $_SERVER['REMOTE_ADDR'] ?? '',
            ];
        }

        $line = json_encode($entry, JSON_UNESCAPED_UNICODE) . "\n";
        error_log($line, 3, $this->logFile);
    }

    public function debug(string $msg, array $ctx = []): void { $this->log('DEBUG', $msg, $ctx); }
    public function info(string $msg, array $ctx = []): void { $this->log('INFO', $msg, $ctx); }
    public function warning(string $msg, array $ctx = []): void { $this->log('WARNING', $msg, $ctx); }
    public function error(string $msg, array $ctx = []): void { $this->log('ERROR', $msg, $ctx); }
    public function critical(string $msg, array $ctx = []): void { $this->log('CRITICAL', $msg, $ctx); }
}

// 使用
$logger = new Logger('/var/log/php/app.log', 'DEBUG');
$logger->info('User logged in', ['user_id' => 42, 'ip' => '192.168.1.1']);
$logger->error('Payment failed', ['order_id' => 'ORD-001', 'amount' => 99.99]);

16.6 业务场景:生产环境错误追踪

<?php
declare(strict_types=1);

class ErrorTracker
{
    private array $breadcrumbs = [];
    private string $environment;
    private string $release;

    public function __construct(string $environment, string $release)
    {
        $this->environment = $environment;
        $this->release = $release;
    }

    public function addBreadcrumb(string $category, string $message, array $data = []): void
    {
        $this->breadcrumbs[] = [
            'timestamp' => microtime(true),
            'category'  => $category,
            'message'   => $message,
            'data'      => $data,
        ];
    }

    public function captureException(\Throwable $e, array $extra = []): string
    {
        $eventId = bin2hex(random_bytes(16));

        $payload = [
            'event_id'    => $eventId,
            'timestamp'   => date('c'),
            'environment' => $this->environment,
            'release'     => $this->release,
            'exception'   => [
                'type'    => get_class($e),
                'value'   => $e->getMessage(),
                'file'    => $e->getFile(),
                'line'    => $e->getLine(),
                'trace'   => $this->formatTrace($e->getTrace()),
            ],
            'breadcrumbs' => $this->breadcrumbs,
            'extra'       => $extra,
        ];

        if (isset($_SERVER['REQUEST_URI'])) {
            $payload['request'] = [
                'method'  => $_SERVER['REQUEST_METHOD'],
                'url'     => $_SERVER['REQUEST_URI'],
                'headers' => $this->getHeaders(),
            ];
        }

        // 发送到错误追踪服务(Sentry、Bugsnag 等)
        $this->sendToService($payload);

        return $eventId;
    }

    private function formatTrace(array $trace): array
    {
        return array_map(fn($frame) => [
            'file'     => $frame['file'] ?? '[internal]',
            'line'     => $frame['line'] ?? 0,
            'function' => $frame['function'] ?? '',
            'class'    => $frame['class'] ?? '',
        ], array_slice($trace, 0, 20));
    }

    private function getHeaders(): array
    {
        $headers = [];
        foreach ($_SERVER as $key => $value) {
            if (str_starts_with($key, 'HTTP_')) {
                $name = str_replace('_', '-', strtolower(substr($key, 5)));
                $headers[$name] = $value;
            }
        }
        return $headers;
    }

    private function sendToService(array $payload): void
    {
        // 实际实现中发送到 Sentry 等服务
        file_put_contents(
            '/var/log/php/errors-' . date('Y-m-d') . '.json',
            json_encode($payload, JSON_UNESCAPED_UNICODE) . "\n",
            FILE_APPEND
        );
    }
}

16.7 扩展阅读


上一章第 15 章 — Composer 下一章第 17 章 — PDO 数据库