必须用 kernel.exception 而不是 set_exception_handler,因为前者是 Symfony 框架预留的语义明确拦截点,能集成 Monolog 日志、开发环境调试页、API JSON 响应及监听器优先级控制;后者绕过框架机制导致日志丢失、调试页失效、响应格式错乱。

直接用 kernel.exception 事件监听器 + ErrorHandler 组件组合,就能覆盖 99% 的未捕获异常场景。不需要在每个控制器里写 try/catch,也不该靠 set_exception_handler() 手动接管。
为什么必须用 kernel.exception 而不是 set_exception_handler
Symfony 的 HTTP 内核在异常抛出后、响应生成前会主动分发 kernel.exception 事件,这是框架预留的、语义明确的拦截点;而 set_exception_handler() 是 PHP 底层钩子,会绕过 Symfony 的整个事件循环、日志通道、环境适配逻辑,导致:
- Monolog 不记录堆栈(除非你手动调用)
- 开发环境不显示调试页(
HtmlErrorRenderer失效) - API 请求本该返回 JSON 却返回 HTML 错误页
- 无法利用
priority控制多个监听器执行顺序
如何注册一个基础异常监听器
创建类 src/EventListener/ExceptionListener.php,注意构造函数不要依赖太多服务(避免监听器自身抛异常导致死循环):
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\KernelInterface;
<p>class ExceptionListener
{
public function __construct(private readonly KernelInterface $kernel) {}</p><pre class="brush:php;toolbar:false;">public function onKernelException(ExceptionEvent $event): void
{
$exception = $event->getThrowable();
$request = $event->getRequest();
// 记录到 Monolog(自动注入)
$this->kernel->getContainer()->get('logger')->error(
'Uncaught exception: {message} in {file}:{line}',
[
'message' => $exception->getMessage(),
'file' => $exception->getFile(),
'line' => $exception->getLine(),
'trace' => $exception->getTraceAsString(),
]
);
// API 请求返回 JSON,其他返回 500 页面
if ($request->headers->get('Accept') === 'application/json') {
$response = new Response(
json_encode(['error' => 'Internal Server Error']),
Response::HTTP_INTERNAL_SERVER_ERROR,
['Content-Type' => 'application/json']
);
} else {
$response = new Response('Server Error', Response::HTTP_INTERNAL_SERVER_ERROR);
}
$event->setResponse($response);
}}
在 config/services.yaml 中注册:
App\EventListener\ExceptionListener:
tags:
- { name: 'kernel.event_listener', event: 'kernel.exception', method: 'onKernelException', priority: 10 }注意 priority: 10 是为了确保它比框架默认监听器(如 Twig 错误页渲染)更早执行。
常见漏掉的异常类型和应对方式
有些异常不会走到 kernel.exception,需要额外处理:
-
Doctrine\DBAL\Exception类异常:可能在 Doctrine 事件监听器或生命周期回调中抛出,且未被内核捕获 → 在监听器里加instanceof判断,转为 503 并隐藏细节 - CLI 命令异常:
kernel.exception不触发 → 依赖ErrorHandler组件,默认已注册,无需额外操作,但自定义日志需确认monolog.handlers.console是否启用 - PHP 错误(E_WARNING、E_NOTICE):由
ErrorHandler统一转为ErrorException,再进kernel.exception→ 确保error_handler.throw_at配置合理(如生产环境设为0避免警告中断流程) - 异步任务(Messenger、Scheduler)中的异常:不走 HTTP 内核 → 必须单独配置
messenger.transport.amqp或failure_transport的重试与失败处理
真正容易被忽略的是「异常在监听器里又被抛出」——比如日志服务不可用、模板渲染失败、或 $event->setResponse() 后又调用了 $event->getResponse() 导致空响应。上线前务必在 APP_ENV=prod 下用真实请求压测一次监听器路径。


















