控制器仅负责调度,业务逻辑必须移出;数据库查询交Repository、权限判断交Voter、JSON渲染交响应器、数据转换交DataTransformer;路由须类型约束、实体映射用#[MapEntity]、验证写独立约束类、资源路由分控制器、依赖注入用接口、禁new对象、模板与表单不掺业务逻辑。

控制器只做调度,业务逻辑必须移出;否则半年后你改个状态判断,得翻遍三个文件才能定位到那行 $userRepository->findBy(['status' => 'active'])。
控制器里不能出现数据库查询或条件分支
常见错误现象:控制器方法里直接调用 $entityManager->getRepository()、if ($user->getRole() === 'admin')、json_encode($data) 等操作。
- 实体查询必须交给 Repository 或自定义服务,控制器只接收已处理好的数据
- 权限判断交给 Voter 或 Security 授权系统,不是
if块 - JSON 渲染交给
JsonResponse或序列化器,不手拼字符串 - 表单提交后的数据转换(如标签数组 → 字符串)应由
DataTransformer处理,不在handleRequest()后写implode()
路由参数必须带类型约束和实体映射
别信“ID 总是数字”,{id} 不加约束等于把 WHERE id = ? 的 ? 直接暴露给用户。
- 强制使用正则约束:
/post/{id}或requirements: ['id' => '\d+'] - 查实体优先用
#[MapEntity],它自动校验存在性并抛NotFoundHttpException,而不是返回 null 导致后续 NPE - 自定义验证(如 UUID 格式)写独立约束类,不塞
if (!Uuid::isValid($id))到控制器里 - 路径中避免裸变量:
/api/{resource}/{id}是反模式,应为每个资源建独立控制器(ProductController、UserController)
依赖注入必须用接口,不能用具体类
写 private Logger $logger 看似省事,但换日志驱动时你得改所有构造函数签名。
- 声明依赖时用标准接口:
private LoggerInterface $logger、private CacheInterface $cache - 自定义服务也先定义接口,比如
PaymentProcessorInterface,实现类可随时替换 Stripe/PayPal - 配置层用
bind或 autowire 绑定实现,不碰业务代码 - 禁止在控制器里
new任何对象——包括 DTO、工具类,全走 DI 容器
模板和表单严禁混入业务逻辑
你在 Twig 里写 {% set users = getActiveUsers() %},就等于把 Repository 调用藏进了视图层,缓存失效、测试断裂、复用无门。
- 模板只负责渲染:变量插值、循环、继承、
include静态区块 - 禁止
{{ dump() }}上线,禁止render(controller(...))做非幂等操作 - 表单类(
PostType)只管字段映射、验证规则、DataTransformer,不处理业务状态流转 - 复杂表单逻辑(如根据类别动态加载选项)用 Form Event(
PRE_SET_DATA),不靠 JS 或后端 if-else 拼 HTML
最常被跳过的其实是路由约束和接口注入——它们不报错,但三个月后你加个新 API,会发现一半逻辑卡在控制器里动弹不得。


















