shlinkio/doctrine-specification

为您的 Doctrine 存储库提供规范模式。从 https://github.com/Happyr/Doctrine-Specification 分支而来

v2.1.1 2024-02-17 09:09 UTC

This package is auto-updated.

Last update: 2024-09-17 10:18:07 UTC


README

Build Status Travis (.org) Latest Stable Version Packagist Monthly Downloads Packagist Total Downloads Packagist Quality Score

这个库为您提供了一种编写查询的新方法。使用 规范模式,您将获得高度可重用的小的规范类。

编写 Doctrine 查询的问题在于,它会很快变得混乱。当您的应用程序增长时,您将在 Doctrine 存储库中有 20+ 个函数。所有这些都有长而复杂的 QueryBuilder 调用。您还会发现,您正在使用很多参数来适应不同的用例。

在与 Kacper Gunia 在 Sound of Symfony播客 上讨论如何正确测试 Doctrine 存储库之后,我们(Kacper 和 Tobias)决定创建这个库。我们受到了 Benjamin Eberlei 在他的 博客文章 中想法的启发。

目录

  1. 动机基本理解(本页)
  2. 使用示例
  3. 创建自己的规范
  4. 为库做出贡献

我们为什么需要这个库?

您可能想知道我们为什么创建了这个库。您的实体存储库按现状工作得很好,对吧?

但如果有朋友打开您的存储库类,他们可能会发现代码并不像您想象的那么完美。实体存储库倾向于变得混乱。可能出现的问题包括

  • 太多函数(如 findActiveUserfindActiveUserWithPicturefindUserToEmail 等)
  • 一些函数有太多参数
  • 代码重复
  • 难以测试

解决方案的需求

解决方案应具有以下特性

  • 易于测试
  • 易于扩展、存储和运行
  • 可重用代码
  • 单一责任原则
  • 隐藏 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 规范

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 中使用过滤器 filterOwnedByCompanyfilterAdvertsWeShouldClose
  • 您不能使用 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);

继续阅读

您可以查看一些使用示例,或者了解如何创建自己的规范