happyr/doctrine-specification
Composer 安装命令:
composer require happyr/doctrine-specification
包简介
Specification Pattern for your Doctrine repositories
README 文档
README
This library gives you a new way for writing queries. Using the Specification pattern you will get small Specification classes that are highly reusable.
The problem with writing Doctrine queries is that it soon will be messy. When your application grows you will have 20+ function in your Doctrine repositories. All with long and complicated QueryBuilder calls. You will also find that you are using a lot of parameters to the same method to accommodate different use cases.
After a discussion with Kacper Gunia on Sound of Symfony podcast about how to test your Doctrine repositories properly, we (Kacper and Tobias) decided to create this library. We have been inspired by Benjamin Eberlei's thoughts in his blog post.
Table of contents
- Motivation and basic understanding (this page)
- Usage examples
- Create your own spec
- Contributing to the library
Why do we need this lib?
You are probably wondering why we created this library. Your entity repositories are working just fine as they are, right?
But if your friend open one of your repository classes he/she would probably find that the code is not as perfect as you thought. Entity repositories have a tendency to get messy. Problems may include:
- Too many functions (
findActiveUser,findActiveUserWithPicture,findUserToEmail, etc) - Some functions have too many arguments
- Code duplication
- Difficult to test
Requirements of the solution
The solution should have the following features:
- Easy to test
- Easy to extend, store and run
- Re-usable code
- Single responsibility principle
- Hides the implementation details of the ORM. (This might seen like nitpicking, however it leads to bloated client code doing the query builder work over and over again.)
The practical differences
This is an example of how you use the lib. Say that you want to fetch some Adverts and close them. We should select all Adverts that have their endDate in the past. If endDate is null make it 4 weeks after the startDate.
// 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);
Yes, it looks pretty much the same. But the later is reusable. Say you want another query to fetch Adverts that we should close but only for a specific company.
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
If you were about to do the same thing with only the QueryBuilder it would look like this:
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(); } }
The issues with the QueryBuilder implementation are:
- You may only use the filters
filterOwnedByCompanyandfilterAdvertsWeShouldCloseinside AdvertRepository. - You can not build a tree with And/Or/Not. Say that you want every Advert but not those owned by $company. There
is no way to reuse
filterOwnedByCompany()in that case. - Different parts of the QueryBuilder filtering cannot be composed together, because of the way the API is created. Assume we have a filterGroupsForApi() call, there is no way to combine it with another call filterGroupsForPermissions(). Instead reusing this code will lead to a third method filterGroupsForApiAndPermissions().
Check single entity
You can apply specifications to validate specific entities or dataset.
$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 } }
Filter collection
You can apply specifications to filter collection of entities or datasets.
$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);
Continue reading
You may want to take a look at some usage examples or find out how to create your own spec.
happyr/doctrine-specification 适用场景与选型建议
happyr/doctrine-specification 是一款 基于 PHP 开发的 Composer 扩展包,目前已累计 939.36k 次下载、GitHub Stars 达 450, 最近一次更新时间为 2014 年 07 月 17 日, 在 PHP 生态内属于活跃度较高的组件。
它主要适用于以下技术方向: 「repository」 「doctrine」 「specification」 等业务场景。在实际项目中,围绕这些方向常见需要落地的问题包括:接口对接、性能调优、并发安全、与既有框架(Laravel / ThinkPHP / Yii / Webman 等)的兼容适配,以及生产环境的日志埋点与稳定性保障。
我们在过去多个企业项目中使用过 happyr/doctrine-specification 或与其功能相近的方案,如果你在选型或落地过程中遇到问题,例如 版本兼容、二次改造、私有化封装、与内部系统对接、生产 BUG 排查,欢迎联系我们协助评估。
基于 happyr/doctrine-specification 在你已有业务上做功能扩展、字段裁剪、UI 适配、与内部账号 / 权限 / 日志系统的深度对接。
线上偶发问题、内存泄漏、慢查询、并发异常等排查修复;针对高流量场景做缓存、队列、索引层面的调优。
承接完整的项目从需求 → 设计 → 开发 → 上线 → 长期运维;也可按月提供技术保姆服务。
与 happyr/doctrine-specification 相关的其它包
同方向 / 同关键字的高下载量 PHP Composer 包推荐,方便对比选型:
Helper that decorates any SUS with a phpspec lazy object wrapper
repository php library
Specification support for Doctrine
Laravel 5 - Repositories to the database layer
Specifications like JSON schemas and other things for mosparo
Make pimcore migration simple
统计信息
- 总下载量: 939.36k
- 月度下载量: 0
- 日度下载量: 0
- 收藏数: 450
- 点击次数: 23
- 依赖项目数: 8
- 推荐数: 0
其他信息
- 授权协议: MIT
- 更新时间: 2014-07-17