本文系统讲解 Symfony 7.x 的异常与错误处理流程,涵盖 HttpKernel 异常体系、404/500 错误页面定制、Twig 例外模板、kernel.exception 监听器、API JSON 错误响应、生产环境 debug 关闭与 Monolog 日志调试。适用于需要构建稳定、可观测、用户友好的 PHP Web 应用与接口服务的开发场景。

在 Symfony 7.x 应用中,错误并不只是「页面白屏」或「抛出一个 Exception」这么简单 😅。路由未匹配、参数校验失败、权限不足、数据库异常、第三方服务超时,都会进入统一的异常调度链路。合理的异常处理策略可以实现三件事:
-
对用户返回清晰、品牌化的错误页面,避免暴露服务器路径与堆栈信息 🎨;
-
对 API 客户端返回结构化 JSON 错误体,包含 code、message、traceId 等可用字段 📦;
-
对运维与开发团队输出可检索日志,定位异常源头、请求上下文与复现条件 🔍。
一、Symfony 7.x 异常体系与处理流程
Symfony 基于 HttpKernel 组件调度请求。控制器、事件、服务中抛出的异常会先被 Kernel 捕获,再交由错误处理器生成 Response。
常用内置异常位于 Symfony\Component\HttpKernel\Exception:
-
NotFoundHttpException:对应 404,路由不存在或资源未找到; -
AccessDeniedHttpException:对应 403,鉴权通过但无权访问; -
UnauthorizedHttpException:对应 401,未认证或凭证缺失; -
BadRequestHttpException:对应 400,请求参数非法; -
MethodNotAllowedHttpException:对应 405,路由存在但 HTTP 方法不允许; -
HttpException:所有 HTTP 状态码异常的基类,可传 statusCode; -
普通
\Exception、\RuntimeException未显式映射时,生产环境通常归为 500。
处理链路简化如下:
-
控制器/服务抛异常;
-
Kernel 触发
kernel.exception事件; -
内置监听器与自定义监听器可修改异常、补充上下文、替换 Response;
-
若未生成 Response,则进入 ErrorController/Twig 例外模板;
-
返回最终错误 Response,同时按日志配置写入文件或外部系统。
小提示:开发环境开启
debug: true会展示 Web Debug Toolbar 与详细堆栈;生产环境应关闭debug,仅展示自定义错误页,原始堆栈写入日志 🧪。
二、基础配置:debug 模式与错误控制
Symfony 7.x 通常通过环境变量与服务配置控制错误行为。
.env 或 .env.prod 示例:
APP_ENV=prod
APP_DEBUG=0
config/packages/framework.yaml 关键项:
framework:
secret: '%env(APP_SECRET)%'
# 错误页控制器,可自定义覆盖
error_controller: 'error_controller'
# 生产环境不向浏览器暴露底层错误
php_errors:
log: true
throw: true
若使用 Monolog,建议在 config/packages/prod/monolog.yaml 中单独配置错误渠道:
monolog:
handlers:
main:
type: rotating_file
path: '%kernel.logs_dir%/app.log'
level: info
max_files: 14
exception:
type: rotating_file
path: '%kernel.logs_dir%/exception.log'
level: error
max_files: 30
channels: ['!event']
生产环境配合 PHP 本身关闭显示错误更安全:
display_errors = Off
log_errors = On
error_log = /var/log/php/php_error.log
这样浏览器只看到友好页,服务器与 Symfony 日志保留排查依据 🔐。
三、Twig 自定义错误页面:404/500 按状态码模板
Symfony 使用 TwigBundle 的例外模板渲染 HTML 错误页。最规范的覆盖路径为:
templates/bundles/TwigBundle/Exception/
├── error.html.twig # 全部状态码兜底
├── error404.html.twig # 仅 404
├── error500.html.twig # 仅 500
├── error403.html.twig # 仅 403
└── error.html.twig.json # API/JSON 格式可配合格式判断
error.html.twig 基础示例:
{# templates/bundles/TwigBundle/Exception/error.html.twig #}
{% extends 'base.html.twig' %}
{% block title %}系统错误 {{ status_code }} {% endblock %}
{% block body %}
<div class="container py-5 text-center">
<h1>{{ status_code }} 😢</h1>
<p class="lead">{{ status_text }}</p>
{% if app.environment == 'dev' %}
<div class="alert alert-danger text-start">
<strong>Debug:</strong>{{ exception.message }}
</div>
{% else %}
<p>当前请求未能正常处理,请稍后重试或联系技术支持。🛠️</p>
{% endif %}
<a href="{{ path('homepage') }}" class="btn btn-primary">返回首页</a>
</div>
{% endblock %}
仅覆盖 404 的友好页:
{# templates/bundles/TwigBundle/Exception/error404.html.twig #}
{% extends 'base.html.twig' %}
{% block title %}页面未找到 404 {% endblock %}
{% block body %}
<div class="container py-5 text-center">
<h1>404 🔍</h1>
<p class="lead">访问的页面不存在,可能被移除、改名或输入错误。</p>
<a href="{{ path('homepage') }}" class="btn btn-outline-primary">回到首页</a>
</div>
{% endblock %}
按状态码细分模板优先级高于通用 error.html.twig。修改后清缓存:
php bin/console cache:clear
php bin/console cache:warmup
开发环境可直接访问不存在路由验证 404 模板;生产环境需将 APP_DEBUG=0 后再用 curl 验证,避免一直看到调试页 🧹。
四、自定义异常类与业务错误码
业务系统不建议在控制器随意 throw new \Exception(‘fail’)。应定义语义异常,便于监听器、日志、API 层统一处理。
示例:订单业务异常
<?php
namespace App\Exception;
use Symfony\Component\HttpKernel\Exception\HttpException;
class OrderBusinessException extends HttpException
{
public function __construct(
string $message = '订单处理失败',
int $statusCode = 422,
private array $extra = [],
?\Throwable $previous = null
) {
parent::__construct($statusCode, $message, $previous);
$this->extra = $extra;
}
public function getExtra(): array
{
return $this->extra;
}
}
控制器中使用:
#[Route('/api/orders/{id}/pay', name: 'api_order_pay', methods: ['POST'])]
public function pay(int $id, OrderService $service): JsonResponse
{
if (!$service->exists($id)) {
throw new NotFoundHttpException('订单不存在');
}
if (!$service->canPay($id)) {
throw new OrderBusinessException('当前订单状态不可支付', 422, [
'order_id' => $id,
'state' => $service->getState($id),
]);
}
// 支付逻辑...
return $this->json(['ok' => true]);
}
这样异常自带 HTTP 状态与业务字段,后续监听器无需反查消息字符串 📌。
五、异常监听器:kernel.exception 统一拦截
需要统一加 traceId、转 JSON、隐藏内部消息、上报日志时,使用事件订阅者监听 kernel.exception。
Symfony 7.x 推荐实现 EventSubscriberInterface 并监听 KernelEvents::EXCEPTION。
<?php
namespace App\EventListener;
use App\Exception\OrderBusinessException;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Symfony\Component\HttpKernel\KernelEvents;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
class ApiExceptionSubscriber implements EventSubscriberInterface
{
public function __construct(
private LoggerInterface $logger,
private string $environment,
private array $jsonPrefixes = ['/api']
) {}
public static function getSubscribedEvents(): array
{
return [
KernelEvents::EXCEPTION => ['onException', 10],
];
}
public function onException(ExceptionEvent $event): void
{
$request = $event->getRequest();
$uri = $request->getRequestUri();
// 仅对 API 前缀或 Accept: application/json 做 JSON 响应
$acceptJson = str_contains($request->headers->get('accept', ''), 'application/json');
$isApi = $this->isApiUri($uri) || $acceptJson;
$throwable = $event->getThrowable();
// 记录日志
$this->logger->error('Unhandled exception', [
'uri' => $uri,
'status' => $throwable instanceof HttpException ? $throwable->getStatusCode() : 500,
'class' => get_class($throwable),
'message' => $throwable->getMessage(),
'trace' => $this->environment === 'dev' ? $throwable->getTraceAsString() : null,
]);
if (!$isApi) {
// 非 API 交给默认错误页/ErrorController,不在此替换 Response
return;
}
$status = $throwable instanceof HttpException ? $throwable->getStatusCode() : 500;
$data = [
'success' => false,
'status' => $status,
'message' => $this->publicMessage($throwable),
];
if ($throwable instanceof OrderBusinessException) {
$data['extra'] = $throwable->getExtra();
}
if ($this->environment === 'dev') {
$data['debug'] = [
'class' => get_class($throwable),
'detail' => $throwable->getMessage(),
'file' => $throwable->getFile(),
'line' => $throwable->getLine(),
];
}
$event->setResponse(new JsonResponse($data, $status));
}
private function isApiUri(string $uri): bool
{
foreach ($this->jsonPrefixes as $prefix) {
if (str_starts_with($uri, $prefix)) {
return true;
}
}
return false;
}
private function publicMessage(\Throwable $e): string
{
if ($e instanceof HttpException) {
return match ($e->getStatusCode()) {
404 => '资源不存在',
403 => '无访问权限',
401 => '未认证或登录失效',
400 => '请求参数错误',
422 => $e->getMessage() ?: '业务校验未通过',
default => '服务暂时不可用',
};
}
// 非 Http 异常在生产环境不暴露原始信息
return $this->environment === 'dev' ? $e->getMessage() : '服务器内部错误';
}
}
注册服务并启用标签:
services:
App\EventListener\ApiExceptionSubscriber:
tags:
- { name: kernel.event_subscriber }
arguments:
$environment: '%kernel.environment%'
$jsonPrefixes: ['/api', '/mobile']
要点说明:
-
优先级
10可保证在默认错误页之前介入;若还要让其他监听器补充上下文,可调低优先级; -
非 API 请求不要强制返回 JSON,否则会破坏 Twig 错误页;
-
生产环境对 500 类异常统一输出「服务器内部错误」,详细原因只进日志,防止泄露 SQL、路径、第三方密钥 🔏。
六、自定义 ErrorController:完全接管错误响应
若希望所有错误走自有控制器而不是 TwigBundle 默认逻辑,可定义 error_controller。
framework.yaml:
framework:
error_controller: App\Controller\CustomErrorController::show
控制器示例:
<?php
namespace App\Controller;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Twig\Environment;
class CustomErrorController
{
public function __construct(private Environment $twig) {}
public function show(\Throwable $exception): Response
{
$status = $exception instanceof HttpException ? $exception->getStatusCode() : 500;
$template = sprintf('error/error%s.html.twig', $status);
if (!($this->twig->getLoader()->exists($template))) {
$template = 'error/error.html.twig';
}
$html = $this->twig->render($template, [
'status_code' => $status,
'status_text' => Response::$statusTexts[$status] ?? 'Unknown',
'exception' => $exception,
]);
return new Response($html, $status);
}
}
该方式适合多站点、多主题、按主机名切换错误模板的场景;普通项目用 TwigBundle 例外模板更省事 🧩。
七、API 项目错误响应规范
REST/JSON API 建议统一结构:
{
"success": false,
"status": 422,
"code": "ORDER_CANNOT_PAY",
"message": "当前订单状态不可支付",
"extra": {
"order_id": 123,
"state": "refunded"
},
"trace_id": "a1b2c3d4"
}
实现建议:
-
自定义异常增加
code、traceId字段; -
监听器在
kernel.exception生成traceId并写入日志; -
用
Accept头、URL 前缀、请求格式_format判断是否需要 JSON; -
校验失败可用
Symfony\Component\HttpKernel\Exception\BadRequestHttpException或UnprocessableEntityHttpException(Symfony 6+ 起UnprocessableEntityHttpException可用于 422); -
表单/Validator 异常可单独监听
form.error或转换 ConstraintViolationList 为字段级错误数组。
生成 traceId 示例:
$traceId = $_SERVER['HTTP_X_REQUEST_ID']
?? sprintf('%08x', crc32(uniqid((string) mt_rand(), true)));
并在日志上下文与 JSON 响应中同时返回,便于后端按 traceId 拉取全链路日志 🧵。
八、生产环境调试与日志排错
生产环境原则:页面友好、日志完整、信息不外泄。
推荐组合:
-
APP_DEBUG=0; -
framework.php_errors.log=true,不向浏览器 throw detail; -
Monolog 按 level 分流:
info/warn/error/critical分文件或接入 ELK、Loki; -
对 500 异常配置邮件/钉钉/Webhook 报警(通过 Monolog handler 或独立监听器);
-
使用
bin/console debug:event kernel.exception查看异常监听器顺序; -
使用
bin/console router:match /some-url验证路由是否命中,定位伪 404; -
清缓存后复现:
bin/console cache:clear --env=prod。
查看已注册错误相关服务:
php bin/console debug:container error_controller
php bin/console debug:event kernel.exception
若某些异常未写日志,检查:
-
自定义监听器是否提前
setResponse但未调用 logger; -
Monolog level 是否高于异常等级,例如 handler level=error 会忽略 warning;
-
使用 Sentry/Bugsnag 等 APM 时,确认 client 在 kernel.exception 中捕获而非仅 Monolog。
九、常见排错清单
-
404 模板不生效:确认路径为
templates/bundles/TwigBundle/Exception/error404.html.twig;清缓存;检查是否存在自定义 ErrorController 抢先返回;开发环境 debug 开启时会优先显示调试页。 -
500 仍显示堆栈:生产环境检查
APP_DEBUG、web 服务器display_errors、frameworkphp_errors.throw配置;若自定义监听器设了 Response,需确保不把原始 message 发到前端。 -
API 返回 HTML 错误页:监听器未识别 JSON 请求;增加
/api前缀或Accept: application/json判断;确认响应Content-Type: application/json。 -
异常监听器不执行:服务未打
kernel.event_subscriber标签;优先级被其他监听器覆盖;在 dev 用debug:event核查。 -
日志无异常:Monolog handler level 过高、channel 被排除、异常在 controller 中被 try/catch 吞掉且未 rethrow 或 log。
-
状态码不正确:业务异常未继承 HttpException;手动
new Response($html, 200)会误导监控系统,错误页必须返回对应 4xx/5xx。
结尾
Symfony 7.x 的异常处理不是单一「catch 块」,而是「内置 Http 异常 + kernel.exception 事件 + ErrorController/Twig 例外模板 + Monolog 日志」的整套体系 🧠。HTML 站点优先用按状态码 Twig 模板做品牌化错误页;API 服务用异常订阅者统一输出 JSON、traceId 与业务 code;生产环境坚持关闭调试输出、只记日志、按需报警。按上述分层落地后,错误率监控、用户反馈、开发定位三者可以同时兼顾,系统稳定性会明显提升 🚀。

