happyr / doctrine-specification
为您的 Doctrine 存储库提供规范模式
Requires
- php: >=7.2
- doctrine/orm: ~2.5
- symfony/polyfill-php80: ^1.20
- symfony/property-access: ^4.0 || ^5.2 || ^6.0 || ^7.0
Requires (Dev)
- phpspec/phpspec: ~6.3 || ^7.0
This package is auto-updated.
Last update: 2024-08-27 12:58:00 UTC
README
这个库为您提供了编写查询的新方法。使用规范模式,您将获得高度可重用的小的规范类。
编写 Doctrine 查询的问题在于,它会很快变得杂乱无章。当您的应用程序增长时,您将会有20+个函数在您的 Doctrine 存储库中。所有这些都有长而复杂的 QueryBuilder 调用。您也会发现您正在使用许多参数调用同一方法以适应不同的用例。
在与 Kacper Gunia 在Sound of Symfony 播客关于如何正确测试您的 Doctrine 存储库的讨论之后,我们(Kacper 和 Tobias)决定创建这个库。我们受到了 Benjamin Eberlei 在他的博客文章中的想法的启发。
目录
为什么我们需要这个库?
你可能想知道我们为什么要创建这个库。你的实体存储库工作得很好,对吧?
但如果你的朋友打开其中一个存储库类,他/她可能会发现代码并不像你认为的那么完美。实体存储库有变得杂乱无章的倾向。可能出现的问题包括
- 函数太多(
findActiveUser
、findActiveUserWithPicture
、findUserToEmail
等) - 某些函数有太多的参数
- 代码重复
- 难以测试
解决方案的要求
解决方案应具有以下功能
- 易于测试
- 易于扩展、存储和运行
- 可重用代码
- 单一责任原则
- 隐藏 ORM 的实现细节。(这听起来可能有些吹毛求疵,但会导致客户端代码在查询构建器上反复进行查询操作。)
实际差异
这是您使用此库的一个示例。假设您想获取一些广告并关闭它们。我们应该选择所有那些其endDate
在过去的广告。如果endDate
为 null,则将其设置为startDate
之后的4周。
// Not using the lib $qb = $this->em->getRepository('HappyrRecruitmentBundle:Advert') ->createQueryBuilder('r'); return $qb->where('r.ended = 0') ->andWhere( $qb->expr()->orX( 'r.endDate < :now', $qb->expr()->andX( 'r.endDate IS NULL', 'r.startDate < :timeLimit' ) ) ) ->setParameter('now', new \DateTime()) ->setParameter('timeLimit', new \DateTime('-4weeks')) ->getQuery() ->getResult();
// Using the lib $spec = Spec::andX( Spec::eq('ended', 0), Spec::orX( Spec::lt('endDate', new \DateTime()), Spec::andX( Spec::isNull('endDate'), Spec::lt('startDate', new \DateTime('-4weeks')) ) ) ); return $this->em->getRepository('HappyrRecruitmentBundle:Advert')->match($spec);
是的,看起来几乎一样。但后者是可重用的。假设您想执行另一个查询来获取我们应该关闭的广告,但仅限于特定公司。
Doctrine Specification
class AdvertsWeShouldClose extends BaseSpecification { public function getSpec() { return Spec::andX( Spec::eq('ended', 0), Spec::orX( Spec::lt('endDate', new \DateTime()), Spec::andX( Spec::isNull('endDate'), Spec::lt('startDate', new \DateTime('-4weeks')) ) ) ); } } class OwnedByCompany extends BaseSpecification { private $companyId; public function __construct(Company $company, ?string $context = null) { parent::__construct($context); $this->companyId = $company->getId(); } public function getSpec() { return Spec::andX( Spec::join('company', 'c'), Spec::eq('id', $this->companyId, 'c') ); } } class SomeService { /** * Fetch Adverts that we should close but only for a specific company */ public function myQuery(Company $company) { $spec = Spec::andX( new AdvertsWeShouldClose(), new OwnedByCompany($company) ); return $this->em->getRepository('HappyrRecruitmentBundle:Advert')->match($spec); } }
QueryBuilder
如果您打算只使用 QueryBuilder 来完成同样的工作,它将看起来像这样
class AdvertRepository extends EntityRepository { protected function filterAdvertsWeShouldClose(QueryBuilder $qb) { $qb ->andWhere('r.ended = 0') ->andWhere( $qb->expr()->orX( 'r.endDate < :now', $qb->expr()->andX('r.endDate IS NULL', 'r.startDate < :timeLimit') ) ) ->setParameter('now', new \DateTime()) ->setParameter('timeLimit', new \DateTime('-4weeks')) ; } protected function filterOwnedByCompany(QueryBuilder $qb, Company $company) { $qb ->join('company', 'c') ->andWhere('c.id = :company_id') ->setParameter('company_id', $company->getId()) ; } public function myQuery(Company $company) { $qb = $this->em->getRepository('HappyrRecruitmentBundle:Advert')->createQueryBuilder('r'); $this->filterAdvertsWeShouldClose($qb); $this->filterOwnedByCompany($qb, $company); return $qb->getQuery()->getResult(); } }
QueryBuilder 实现的问题包括
- 您只能在 AdvertRepository 中使用
filterOwnedByCompany
和filterAdvertsWeShouldClose
过滤器。 - 您不能使用 And/Or/Not 构建树。例如,您想获取每个广告,但不是那些由 $company 拥有的广告。在这种情况下,无法重用
filterOwnedByCompany()
。 - 由于API的创建方式,QueryBuilder过滤的不同部分不能组合在一起。假设我们有一个filterGroupsForApi()的调用,无法将其与另一个调用filterGroupsForPermissions()结合使用。相反,重用此代码将导致第三个方法filterGroupsForApiAndPermissions()。
检查单个实体
您可以对特定实体或数据集应用规范进行验证。
$highRankFemalesSpec = Spec::andX( Spec::eq('gender', 'F'), Spec::gt('points', 9000) ); // an array of arrays $playersArr = [ ['pseudo' => 'Joe', 'gender' => 'M', 'points' => 2500], ['pseudo' => 'Moe', 'gender' => 'M', 'points' => 1230], ['pseudo' => 'Alice', 'gender' => 'F', 'points' => 9001], ]; // or an array of objects $playersObj = [ new Player('Joe', 'M', 40, 2500), new Player('Moe', 'M', 55, 1230), new Player('Alice', 'F', 27, 9001), ]; foreach ($playersArr as $playerArr) { if ($highRankFemalesSpec->isSatisfiedBy($playerArr)) { // do something } } foreach ($playersObj as $playerObj) { if ($highRankFemalesSpec->isSatisfiedBy($playerObj)) { // do something } }
过滤集合
您可以对实体或数据集的集合应用规范进行过滤。
$highRankFemalesSpec = Spec::andX( Spec::eq('gender', 'F'), Spec::gt('points', 9000) ); // an array of arrays $playersArr = [ ['pseudo' => 'Joe', 'gender' => 'M', 'points' => 2500], ['pseudo' => 'Moe', 'gender' => 'M', 'points' => 1230], ['pseudo' => 'Alice', 'gender' => 'F', 'points' => 9001], ]; // or an array of objects $playersObj = [ new Player('Joe', 'M', 40, 2500), new Player('Moe', 'M', 55, 1230), new Player('Alice', 'F', 27, 9001), ]; $highRankFemales = $highRankFemalesSpec->filterCollection($playersArr); $highRankFemales = $highRankFemalesSpec->filterCollection($playersObj); $highRankFemales = $this->em->getRepository(Player::class)->match($highRankFemalesSpec);