ttskch/doctrine-orm-criteria

Doctrine ORM Criteria allows you to separate any complex "search condition" as a Criteria with a specialized API for QueryBuilder of doctrine/orm.

1.0.0 2024-08-22 02:54 UTC

This package is auto-updated.

Last update: 2024-10-27 12:37:34 UTC


README

codecov Latest Stable Version Total Downloads

Motivation

QueryBuilder of doctrine/orm has a method called addCriteria() that allows you to build queries by combining Criteria of doctrine/collections. This allows you to separate the concerns of "search conditions" into a Criteria, improving the maintainability of your codebase.

However, Criteria of doctrine/collections only has a very limited matching language because it is designed to work both on the SQL and the PHP collection level, and therefore cannot be used to build complex queries.

Rejoice! Doctrine ORM Criteria allows you to separate any complex "search condition" as a Criteria with a specialized API for QueryBuilder of doctrine/orm just like below.

$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)
        ;
    }
}

Requirements

  • PHP: ^8.0
  • Doctrine ORM: ^2.8

Support for Doctrine ORM v3 is coming soon.

Installation

$ composer require ttskch/doctrine-orm-criteria

Usage

Basic

You can create your own Criteria by implementing CriteriaInterface and adding it to CriteriaAwareness to build queries.

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)
        ;
    }
}

Built-in Criteria and utilities

There are some built-in Criteria: OrderBy, Andx, and Orx. Using Andx and Orx, you can combine multiple Criteria to create a new 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);
    }
}

Additionally, when creating your own Criteria, you can use AddSelectTrait and JoinTrait to ensure that the addSelect() and join are IDEMPOTENT even if the Criteria is applied multiple times to the QueryBuilder.

<?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)
        ;
    }
}

Integration with Repository

You can also easily integrate with your repositories using 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

Getting involved

$ composer install

# Develop...

$ composer tests