应使用 kernel.terminate 事件异步记录访问日志,避免阻塞请求;分页查询须用 QueryBuilder 并禁用 totalCount,同时透传时间筛选参数以保证分页链接有效性。

直接在 Symfony2 中监听请求并入库访问记录,同时支持分页查询,关键不是“写个监听器就完事”,而是得让日志可查、不拖慢主流程、分页不崩。
用 KernelEvent 监听请求并异步写入日志表
别在 kernel.request 里直接执行 INSERT —— 那会阻塞每次请求,尤其高并发时。正确做法是用 kernel.terminate 事件,在响应已发给客户端后才落库:
- 创建监听器类,实现
EventSubscriberInterface,订阅kernel.terminate - 在监听器中从
$event->getRequest()提取路径、方法、IP、User-Agent、状态码(需从 Response 拿) - 用 Doctrine 的
EntityManager::persist()+flush()写入自定义日志实体(如AccessLog),不要用原生 SQL 或executeStatement() - 避免在监听器里调用
$em->clear()或手动管理事务 —— 默认 flush 就在当前请求事务外,天然解耦
分页查询日志时必须绕开 COUNT(*) 性能陷阱
访问日志表动辄百万行,knp_paginator_bundle 默认的 COUNT(*) + LIMIT/OFFSET 在 MySQL 上会越来越慢,甚至锁表:
开箱即用的技能链路由引擎。13 条预定义链覆盖搜索、开发、审查、MLOps、法律、创意等场景,三层路由架构(触发词→SAD反馈→DAG编排),recall@10=96.97%。配置驱动(chains.yaml),零代码扩展。pip install skill-weave-chains 一键安装。
- 控制器里别传
$repository->findAll()给$paginator->paginate(),那会把全部日志加载进内存 - 改用
QueryBuilder:先$qb->select('l')->from('App:AccessLog', 'l'),再传$qb->getQuery()进paginate() - 明确禁用总条数:
$paginator->paginate($query, $request->query->getInt('page', 1), 50, ['totalCount' => false]),模板里只显示「下一页」按钮 - 如果真要总页数,确保
created_at字段有索引,并加WHERE created_at > :since缩小范围
路由和模板里最容易漏掉的两个参数透传
日志列表页通常带时间筛选(?from=2026-08-01&to=2026-08-13),但默认分页链接会丢掉这些:
- 调用
paginate()时必须传第 4 个参数:['from' => 'from', 'to' => 'to'],否则点「第2页」就回到全量数据 - Twig 模板里不能只写
{{ knp_pagination_render(pagination) }},得确认变量名是pagination;如果控制器返回的是logs,就得写{{ knp_pagination_render(logs) }} - 检查路由是否允许 query string:别用
path('log_list')生成链接,它不带查询参数;改用url('log_list')或显式拼接{{ app.request.uri }}
真正麻烦的从来不是“怎么记一次访问”,而是“怎么查得快、翻得稳、不拖垮线上服务”。COUNT 和参数透传这两处,90% 的分页日志页面上线后第一周就会暴露问题。


















