控制器唯一职责是协调请求与响应:解析Request、调用服务、构造Response;所有业务逻辑必须移出,用DTO隔离输入输出,领域动作封装为接口注入的服务,副作用通过事件解耦。

控制器里别写业务逻辑,这是硬边界
Symfony 控制器的唯一职责是协调请求与响应:解析 Request、调用服务、构造 Response。所有计算、数据转换、外部调用、状态变更都必须移出控制器。常见错误包括在控制器里直接调用 $entityManager->flush()、拼接 SQL、手动处理表单验证失败逻辑、或调用 $this->redirectToRoute() 以外的响应方法(如 $this->json() 就算越界)。一旦控制器开始做这些事,测试会变脆弱,复用会变困难,API 和 HTML 路由难以共享同一份核心逻辑。
用 DTO 明确输入输出,切断 Entity 泄露路径
控制器接收参数时,不能直接依赖 User 实体或 Request 对象本身。应该定义一个只含必要字段的 readonly 类,比如:
readonly class CreateUserDto
{
public function __construct(
public string $email,
public string $password,
public ?string $inviteCode,
) {}
}然后在控制器中用表单、序列化器或手动构造它。这样能防止前端传入非法字段(如 id、roles),也避免后续修改 Entity 字段影响 API 兼容性。注意:CreateUserDto 不继承任何类、不实现 jsonSerialize()、不带 setter —— 它只是个容器。
把领域动作封装成服务,按接口注入
业务动作要抽成独立服务类,命名体现意图而非技术,例如:UserRegistrationService、OrderCancellationService。关键点:
- 构造函数只接受接口,如
UserRepositoryInterface、PasswordHasherInterface,而非具体实现 - 方法签名清晰:输入是 DTO 或值对象,输出是领域对象或简单类型(
void、bool、OrderId) - 不访问
$this->request、不调用$this->redirectToRoute()、不读写 session 或 cookie - 配置项(如密码最小长度)通过构造参数注入,而不是硬编码或从
ParameterBag直接取
services.yaml 中启用 autowire: true 和 autoconfigure: true,让容器自动解析依赖。
事件解耦副作用,别让主流程卡在发邮件上
注册成功后发欢迎邮件、下单后更新库存统计、用户删除后清理头像文件——这类操作不该阻塞主业务流程。用 symfony/event-dispatcher 发布领域事件,比如:UserRegisteredEvent,再写一个监听器去异步处理。监听器本身是无状态的,只做一件事,且可以被禁用、替换或延迟执行。不要在服务里直接 new 一个 Mailer 或调用 $mailer->send();也不要让控制器等邮件发完才返回响应。复杂点在于:事件监听器的执行顺序、失败重试策略、以及如何保证事务一致性(比如监听器在数据库事务提交后才触发)——这些必须显式设计,不能靠默认行为。


















