第 16 章 — 错误处理
第 16 章 — 错误处理:错误级别、自定义处理器与日志
16.1 PHP 错误级别
| 常量 | 值 | 说明 |
|---|---|---|
E_ERROR | 1 | 致命运行时错误(不可恢复) |
E_WARNING | 2 | 运行时警告(不终止脚本) |
E_PARSE | 4 | 编译时解析错误 |
E_NOTICE | 8 | 运行时通知(可能是 bug) |
E_CORE_ERROR | 16 | PHP 内核启动错误 |
E_CORE_WARNING | 32 | PHP 内核启动警告 |
E_COMPILE_ERROR | 64 | 编译时致命错误 |
E_COMPILE_WARNING | 128 | 编译时警告 |
E_USER_ERROR | 256 | 用户触发的致命错误 |
E_USER_WARNING | 512 | 用户触发的警告 |
E_USER_NOTICE | 1024 | 用户触发的通知 |
E_STRICT | 2048 | 代码标准化建议 |
E_DEPRECATED | 8192 | 弃用功能警告 |
E_USER_DEPRECATED | 16384 | 用户触发的弃用警告 |
E_ALL | 32767 | 所有错误和警告 |
<?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 数据库