PHP项目全局异常捕获:从原理到落地的完整代码实现指南
目录导读

为什么需要全局异常捕获?
在PHP项目开发中,每处代码都可能抛出异常,如果每个地方都写try-catch,代码会变得臃肿且难以维护,更严重的是,未捕获的异常会导致页面白屏、数据库连接泄露或敏感信息暴露。
全局异常捕获的价值:
- 统一错误处理逻辑,避免重复代码
- 防止敏感信息(如数据库密码、堆栈路径)暴露给用户
- 记录错误日志便于排查
- 在API项目中返回规范化的JSON错误响应
典型场景: 用户访问一个不存在的路由、数据库连接超时、第三方接口返回500错误。
PHP异常处理基础回顾
PHP提供了两种错误机制:异常(Exception) 和 错误(Error),全局捕获必须同时处理这两种。
// 异常示例
throw new \Exception("自定义异常");
// 错误示例(传统错误)
trigger_error("这是一个错误", E_USER_WARNING);
关键函数:
set_exception_handler()- 捕获未处理的异常set_error_handler()- 捕获错误(Warning/Notice等)register_shutdown_function()- 捕获致命错误(Fatal Error)
全局异常捕获的核心机制
全局捕获的思想是:在程序入口(入口文件)统一设置钩子,无论代码何处抛出异常,都交由预设的回调函数处理。
捕获流程图解
用户请求 → index.php → 业务代码 → 异常抛出
↓ ↓
set_exception_handler set_error_handler
↓ ↓
统一处理函数(记录日志+返回响应)
↓
shutdown回调处理致命错误
代码实现:三种主流方案详解
基础版本(原生PHP)
// 入口文件 index.php
<?php
// 1. 捕获未处理的异常
set_exception_handler(function ($exception) {
$message = "异常信息: " . $exception->getMessage() . "\n";
$message .= "文件: " . $exception->getFile() . " (第" . $exception->getLine() . "行)\n";
file_put_contents(__DIR__ . '/logs/error.log', $message, FILE_APPEND);
// 生产环境返回友好提示
http_response_code(500);
echo json_encode(["error" => "系统繁忙,请稍后重试"]);
});
// 2. 捕获错误(Warning/Notice)
set_error_handler(function ($severity, $message, $file, $line) {
$log = "[错误等级: $severity] $message in $file on line $line\n";
file_put_contents(__DIR__ . '/logs/error.log', $log, FILE_APPEND);
return true; // 防止PHP继续处理
});
// 3. 捕获致命错误(Fatal Error)
register_shutdown_function(function () {
$lastError = error_get_last();
if ($lastError && in_array($lastError['type'], [E_ERROR, E_PARSE, E_CORE_ERROR])) {
$log = "致命错误: " . json_encode($lastError) . "\n";
file_put_contents(__DIR__ . '/logs/error.log', $log, FILE_APPEND);
}
});
// 业务代码
include 'router.php';
面向对象封装
创建一个全局异常处理类:
<?php
class GlobalExceptionHandler
{
public static function register()
{
set_exception_handler([self::class, 'handleException']);
set_error_handler([self::class, 'handleError']);
register_shutdown_function([self::class, 'handleShutdown']);
}
public static function handleException($e)
{
self::log($e->getMessage(), $e->getFile(), $e->getLine());
self::renderResponse(500, ['error' => '服务器内部错误']);
}
public static function handleError($severity, $message, $file, $line)
{
self::log($message, $file, $line);
return true;
}
public static function handleShutdown()
{
$error = error_get_last();
if ($error && in_array($error['type'], [E_ERROR, E_PARSE])) {
self::log($error['message'], $error['file'], $error['line']);
}
}
private static function log($message, $file, $line)
{
$log = sprintf("[%s] %s in %s:%d\n", date('Y-m-d H:i:s'), $message, $file, $line);
file_put_contents(__DIR__ . '/logs/app.log', $log, FILE_APPEND);
}
private static function renderResponse($code, $data)
{
http_response_code($code);
header('Content-Type: application/json');
echo json_encode($data);
exit;
}
}
// 在入口调用
GlobalExceptionHandler::register();
支持不同环境模式
区分开发和生产环境:
<?php
class ExceptionHandler
{
private $debug;
public function __construct($debug = false)
{
$this->debug = $debug;
$this->register();
}
public function register()
{
set_exception_handler([$this, 'handleException']);
set_error_handler([$this, 'handleError']);
register_shutdown_function([$this, 'handleShutdown']);
}
public function handleException($e)
{
$this->log($e);
if ($this->debug) {
// 开发环境显示详细错误
echo "<pre>Error: " . $e->getMessage() . "\n";
echo "File: " . $e->getFile() . " (Line: " . $e->getLine() . ")\n";
echo "Trace:\n" . $e->getTraceAsString() . "</pre>";
} else {
// 生产环境返回JSON
http_response_code(500);
header('Content-Type: application/json');
echo json_encode(['code' => 500, 'message' => '系统故障']);
}
exit;
}
private function log($e)
{
$trace = $e->getTraceAsString();
$log = sprintf("[%s] %s\nFile: %s:%d\nTrace: %s\n",
date('Y-m-d H:i:s'),
$e->getMessage(),
$e->getFile(),
$e->getLine(),
$trace
);
error_log($log, 3, __DIR__ . '/../logs/errors.log');
}
// handleError 和 handleShutdown 类似,略
}
实战:在框架中集成
在Laravel中集成
Laravel已内置全局异常处理,但可以自定义:
// app/Exceptions/Handler.php
public function render($request, \Throwable $exception)
{
// 针对API请求返回JSON
if ($request->expectsJson()) {
return response()->json([
'code' => $exception->getCode() ?: 500,
'message' => $exception->getMessage(),
'debug' => config('app.debug') ? [
'file' => $exception->getFile(),
'line' => $exception->getLine()
] : null
], 500);
}
return parent::render($request, $exception);
}
在ThinkPHP6中集成
// config/app.php 或通过中间件
use think\exception\Handle;
class GlobalExceptionHandle extends Handle
{
public function render($request, \Throwable $e): mixed
{
// 记录日志
\think\facade\Log::error($e->getMessage(), [
'file' => $e->getFile(),
'line' => $e->getLine()
]);
// 返回JSON响应
return json([
'code' => 500,
'message' => '服务器错误'
], 500);
}
}
常见问答Q&A
Q1:set_exception_handler和set_error_handler有什么区别?
A:set_exception_handler只捕获Exception对象,而set_error_handler捕获PHP错误(如E_WARNING、E_NOTICE),致命错误(E_ERROR)无法被这两个捕获,需依赖register_shutdown_function。
Q2:全局捕获会影响性能吗?
A:不会,这些钩子只在异常/错误发生时才会被调用,正常执行流程没有额外开销,但日志写入操作建议使用异步队列,避免阻塞响应。
Q3:如何在全局捕获中获取请求上下文?
A:可以在入口文件中将$_SERVER或请求对象存入全局变量,$GLOBALS['request'] = $_SERVER;,然后在处理函数中引用。
Q4:为什么我设置了异常处理但页面还是白屏?
A:可能是致命错误在shutdown函数之前已经输出了内容,或者register_shutdown_function未正确注册,建议先输出一个ob_clean()清除缓冲。
Q5:全局捕获能否捕获所有PHP错误?
A:不能。E_PARSE(解析错误)和E_CORE_ERROR不能被自定义错误处理捕获,register_shutdown_function可以捕获它们的最后痕迹,但无法阻止程序终止。
性能优化与注意事项
最佳实践清单
- 日志分离:将异常日志与业务日志分开存储,便于排查
- 敏感信息过滤:在生产环境隐藏文件路径、数据库密码等
- 优雅退出:在shutdown函数中确保输出缓冲刷新
- 连接清理:在异常处理中关闭数据库连接和文件句柄
- 兼容PHP8:PHP8中
Throwable接口同时包含Exception和Error
完整配置示例
// 入口文件顶部
define('APP_DEBUG', env('APP_DEBUG', false));
// 初始化全局异常处理
(new ExceptionHandler(APP_DEBUG))->register();
性能陷阱提醒
- 不要在异常处理函数中进行数据库查询或远程调用(容易导致连锁错误)
- 避免在shutdown函数中抛出异常(PHP7+会报Fatal Error)
通过以上实现,你的PHP项目将具备健壮的全局异常捕获能力,根据项目复杂度选择方案:小型项目用方案一,中型项目用方案二,框架项目直接用框架内置机制。全局捕获是最后一道防线,不能替代代码中的主动异常处理。