ttskch/doctrine-orm-criteria

Doctrine ORM Criteria 允许您将任何复杂的 "搜索条件" 作为 Criteria 分离出来,并为 doctrine/orm 的 QueryBuilder 提供专门的 API。

1.0.0 2024-08-22 02:54 UTC

This package is auto-updated.

Last update: 2024-08-27 12:19:43 UTC


README

codecov Latest Stable Version Total Downloads

动机

QueryBuilder of doctrine/orm 有一个名为 addCriteria() 的方法,允许您通过组合 Criteria of doctrine/collections 来构建查询。这允许您将 "搜索条件" 的关注点分离到 Criteria 中,从而提高代码库的可维护性。

然而,doctrine/collections 的 Criteria 只拥有一个非常有限的匹配语言,因为它旨在同时在工作于 SQL 和 PHP 集合级别,因此不能用于构建复杂的查询。

欢呼!Doctrine ORM Criteria 允许您将任何复杂的 "搜索条件" 作为 Criteria 分离出来,并为 doctrine/orm 的 QueryBuilder 提供专门的 API,就像下面这样。

$qb = (new CriteriaAwareness($fooRepository->createQueryBuilder('f')))
    ->addCriteria(new IsPublic(), 'f')
    ->addCriteria(new IsAccessibleBy($user), 'f')
    ->addCriteria(new CategoryIs($category), 'f')
    ->addCriteria(new OrderByRandom(), 'f')
    ->getQueryBuilder()
;
$foos = $qb->getQuery()->getResult();

// Or, using the Repository integration:

$foos = $fooRepository->findByCriteria([
    new IsPublic(),
    new IsAccessibleBy($user),
    new CategoryIs($category),
    new OrderByRandom(),
]);
final readonly class IsPublic implements CriteriaInterface
{
    public ?\DateTimeInterface $at;

    public function __construct(?\DateTimeInterface $at = null)
    {
        $this->at = $at ?? new \DateTimeImmutable();
    }

    public function apply(QueryBuilder $qb, string $alias): void
    {
        $qb
            ->andWhere("$alias.state = :state")
            ->andWhere($qb->expr()->andX(
                $qb->expr()->orX(
                    "$alias.openedAt IS NULL",
                    "$alias.openedAt <= :at",
                ),
                $qb->expr()->orX(
                    "$alias.closedAt IS NULL",
                    "$alias.closedAt > :at",
                ),
            ))
            ->setParameter('state', Foo::STATE_PUBLIC)
            ->setParameter('at', $this->at)
        ;
    }
}

要求

  • PHP: ^8.0
  • Doctrine ORM: ^2.8

即将支持 Doctrine ORM v3。

安装

$ composer require ttskch/doctrine-orm-criteria

使用

基本

您可以通过实现 CriteriaInterface 并将其添加到 CriteriaAwareness 中来创建自己的 Criteria,以构建查询。

use App\Repository\Criteria\Foo\IsPublic;
use Ttskch\DoctrineOrmCriteria\CriteriaAwareness;

$qb = (new CriteriaAwareness($fooRepository->createQueryBuilder('f')))
    ->addCriteria(new IsPublic(), 'f')
    ->getQueryBuilder()
;
<?php

declare(strict_types=1);

namespace App\Repository\Criteria\Foo;

final readonly class IsPublic implements CriteriaInterface
{
    public ?\DateTimeInterface $at;

    public function __construct(?\DateTimeInterface $at = null)
    {
        $this->at = $at ?? new \DateTimeImmutable();
    }

    public function apply(QueryBuilder $qb, string $alias): void
    {
        $qb
            ->andWhere("$alias.state = :state")
            ->andWhere($qb->expr()->andX(
                $qb->expr()->orX(
                    "$alias.openedAt IS NULL",
                    "$alias.openedAt <= :at",
                ),
                $qb->expr()->orX(
                    "$alias.closedAt IS NULL",
                    "$alias.closedAt > :at",
                ),
            ))
            ->setParameter('state', Foo::STATE_PUBLIC)
            ->setParameter('at', $this->at)
        ;
    }
}

内置 Criteria 和工具

有一些内置的 CriteriaOrderByAndxOrx。使用 AndxOrx,您可以将多个 Criteria 组合起来创建一个新的 Criteria

<?php

declare(strict_types=1);

namespace App\Repository\Criteria\Foo;

use App\Entity\User;
use Doctrine\ORM\QueryBuilder;
use Ttskch\DoctrineOrmCriteria\Criteria\CriteriaInterface;
use Ttskch\DoctrineOrmCriteria\Criteria\Andx;
use Ttskch\DoctrineOrmCriteria\Criteria\Orx;

final readonly class IsViewable implements CriteriaInterface
{
    public ?\DateTimeInterface $at;

    public function __construct(
        public User $me,
        ?\DateTimeInterface $at = null,
    ) {
        $this->at = $at ?? new \DateTimeImmutable();
    }

    public function apply(QueryBuilder $qb, string $alias): void
    {
        (new Andx([
            new Orx([
                new IsPublic($this->at),
                ...array_map(fn (string $category) => new CategoryIs($category), Foo::PUBLIC_CATEGORIES),
            ]),
            new IsAccessibleBy($this->me),
        ]))->apply($qb, $alias);
    }
}

此外,在创建自己的 Criteria 时,您可以使用 AddSelectTraitJoinTrait 来确保即使在将 Criteria 应用多次到 QueryBuilder 的情况下,addSelect()join 仍然是 IDEMPOTENT 的。

<?php

declare(strict_types=1);

namespace App\Repository\Criteria\Foo;

use App\Entity\User;
use Doctrine\ORM\QueryBuilder;
use Ttskch\DoctrineOrmCriteria\Criteria\CriteriaInterface;
use Ttskch\DoctrineOrmCriteria\Criteria\Traits\JoinTrait;

final readonly class IsAccessibleBy implements CriteriaInterface
{
    use JoinTrait;

    private const string CRITERIA_KEY = 'Foo_IsAccessibleBy'; // some unique key

    public function __construct(public User $me)
    {
    }

    public function apply(QueryBuilder $qb, string $alias): void
    {
        $userAlias = sprintf('%s_%s_user', self::CRITERIA_KEY, $alias);

        $this->leftJoin($qb, sprintf('%s.user', $alias), $userAlias);

        $qb
            ->andWhere(sprintf('%s = :user', $userAlias))
            ->setParameter('user', $this->me)
        ;
    }
}

与仓库集成

您还可以使用 CriteriaAwareRepositoryTrait 轻松地将它与您的仓库集成。

  <?php

  declare(strict_types=1);

  namespace App\Repository;

  use App\Entity\Foo;
  use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
  use Doctrine\Persistence\ManagerRegistry;
+ use Ttskch\DoctrineOrmCriteria\Repository\CriteriaAwareRepositoryTrait;

  /**
   * @extends ServiceEntityRepository<Foo>
   */
  class FooRepository extends ServiceEntityRepository
  {
+     /** @use CriteriaAwareRepositoryTrait<Foo> */
+     use CriteriaAwareRepositoryTrait;
+
      public function __construct(ManagerRegistry $registry)
      {
          parent::__construct($registry, Foo::class);
      }
  }
$foos = $fooRepository->findByCriteria([
    new IsPublic(),
    new IsAccessibleBy($user),
    new CategoryIs($category),
    new OrderByRandom(),
]);

\PHPStan\dumpType($foos); // Dumped type: array<App\Entity\Foo>

$foo = $fooRepository->findOneByCriteria([
    new IsPublic(),
    new IsAccessibleBy($user),
    new CategoryIs($category),
    new OrderByRandom(),
]);

\PHPStan\dumpType($foo); // Dumped type: App\Entity\Foo|null

$count = $fooRepository->countByCriteria([
    new IsPublic(),
    new IsAccessibleBy($user),
    new CategoryIs($category),
]);

\PHPStan\dumpType($count); // Dumped type: int

参与其中

$ composer install

# Develop...

$ composer tests