repository类必须显式声明并正确配置,否则无法支持自定义查询;需用命令生成、注入entitymanager、在实体中声明repositoryclass,且复杂逻辑应移至service层。

Repository 类必须显式声明,不能靠 auto_mapping 自动创建
Doctrine 的 auto_mapping: true 会为每个实体生成默认 Repository,但它只提供 find()、findAll() 这类基础方法,**不会自动注入 EntityManager 或支持自定义 DQL/QueryBuilder**。一旦你要写带条件、JOIN、聚合或分页的查询,就必须手动创建 Repository 类。
正确做法是用命令生成:
php bin/console make:repository ProductRepository
它会在 src/Repository/ProductRepository.php 中生成带 EntityManagerInterface 构造注入的骨架,并在实体类注解里补上 @ORM\Repository("App\Repository\ProductRepository")(或 Symfony 6+ 的 PHP 属性语法 #[ORM\Repository(ProductRepository::class)])。
常见错误:
- 直接在控制器里 new ProductRepository() —— 破坏依赖注入,无法 mock 测试
- 忘了在实体上声明 repositoryClass,导致
$em->getRepository(Product::class)返回的是 Doctrine 默认仓库,调不到你的自定义方法 - 把自定义查询逻辑写在 Controller 或 Service 里,导致复用性差、难以单元测试
自定义查询优先用 QueryBuilder,不用原生 DQL 除非必要
QueryBuilder 是类型安全、可组合、易调试的首选。它在编译阶段就能暴露字段名错误(比如拼错 p.prce),而 DQL 字符串要到运行时才报错。
示例:一个带动态条件的列表查询
$qb = $this->createQueryBuilder('p');
$qb->where('p.status = :status')
->setParameter('status', Product::STATUS_ACTIVE);
if ($minPrice !== null) {
$qb->andWhere('p.price >= :minPrice')
->setParameter('minPrice', $minPrice);
}
return $qb->getQuery()->getResult();
注意点:
- 不要在
where()后连续链式调用多个setParameter()—— 参数绑定必须在对应条件之后立即完成,否则可能被后续andWhere()覆盖 - 避免在循环里反复调用
setParameter()同一名字,会覆盖前值;批量 IN 查询要用setParameter('ids', [1,2,3], Connection::PARAM_STR_ARRAY) - 原生 DQL 只在跨库、复杂函数(如 MySQL JSON_EXTRACT)、或性能敏感场景下使用,且必须配
setParameters()防注入,不能拼字符串
查询方法命名要反映返回值类型和业务语义
Symfony 不强制方法名格式,但团队协作中,清晰的命名能减少理解成本。别用 getProducts() 这种模糊名,它不说明是否分页、是否过滤、是否只返回 ID。
推荐模式:
-
findActiveByCategory(string $category): array—— 返回对象数组,明确条件 -
countPublishedByAuthor(int $authorId): int—— 方法名含count,返回标量,不查全量数据 -
findLatestTen(): array—— “Latest” 暗示按时间倒序,“Ten” 暗示有数量限制 -
findOneBySlugAndLocale(string $slug, string $locale): ?Product—— 明确单结果 + 可空返回,比findBy()更安全
陷阱:
- 写
findByStatus()却没在方法签名里注明返回array|Collection,调用方误以为是单对象 - 用
get*前缀(如getProductsForReport())暗示是 getter,实际却触发 DB 查询 + 复杂计算,违反直觉 - 方法返回
null但没加 PHPDoc 或返回类型提示,静态分析工具(PHPStan)无法捕获空指针风险
复杂查询逻辑拆到独立 Service,Repository 只管“怎么查”,不管“为什么查”
Repository 的职责边界很窄:封装对单个实体的数据访问细节。一旦出现跨实体 JOIN、缓存策略、权限过滤、或需要调用外部 API 决定查询条件,就该移出 Repository。
比如“获取用户可见的商品列表”,涉及:
- 用户角色检查(Security)
- 商品状态 + 库存 + 时间窗口过滤
- 关联分类、标签、促销信息
- 结果缓存键生成
这些都不该出现在 ProductRepository 里。正确结构是:
→ Controller 注入 ProductVisibilityService
→ Service 内部协调 ProductRepository、CategoryRepository、SecurityHelper 等,组装最终 QueryBuilder 并执行
→ Repository 仅暴露 addVisibilityConditions(QueryBuilder $qb, User $user) 这类纯数据层辅助方法
这个边界最容易被忽略:很多人把所有“跟查库有关”的代码都塞进 Repository,结果它越来越重,越来越难测,最后变成黑盒。











