ttskch / doctrine-orm-criteria
Doctrine ORM Criteria 允许您将任何复杂的 "搜索条件" 作为 Criteria 分离出来,并为 doctrine/orm 的 QueryBuilder 提供专门的 API。
Requires
- php: ^8.0
- doctrine/orm: ^2.8
Requires (Dev)
- ext-pcov: *
- bamarni/composer-bin-plugin: ^1.8
- phpspec/prophecy-phpunit: ^2.2
- phpunit/phpunit: ^9.5
README
动机
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 和工具
有一些内置的 Criteria
:OrderBy
、Andx
和 Orx
。使用 Andx
和 Orx
,您可以将多个 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
时,您可以使用 AddSelectTrait
和 JoinTrait
来确保即使在将 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