andanteproject/shared-query-builder

一个 Doctrine 2 查询构建器装饰器,使在共享上下文中构建查询更加容易

2.0.0 2024-06-14 08:29 UTC

This package is auto-updated.

Last update: 2024-09-14 09:05:57 UTC


README

Andante Project Logo

共享查询构建器

Doctrine 2 查询构建器装饰器 - AndanteProject

Latest Version Github actions Php8 PhpStan

一个 Doctrine 2 查询构建器装饰器,使在共享上下文中构建查询更加容易。

为什么我需要这个?

当你的查询业务逻辑很大且复杂时,你可能需要将其构建过程拆分到不同的地方/类。

没有 SharedQueryBuilder,除非 猜测实体别名 并与 连接语句 糟糕地打交道,否则无法做到这一点。

这个 查询构建器装饰器解决了一些你在实际情况下会遇到的问题,你通常会用工作程序和业务惯例来解决这个问题。

特性

  • 当你在创建上下文之外时,询问查询构建器用于实体的别名;
  • 延迟连接,仅当定义了相关标准时才执行连接语句;
  • 不可变和唯一的查询 参数
  • 像魔法一样 ✨。

要求

Doctrine 2 和 PHP 7.4。

安装

通过 Composer

$ composer require andanteproject/shared-query-builder

设置

在创建你的 查询构建器之后,将其包裹在我们的 SharedQueryBuilder 中。

use Andante\Doctrine\ORM\SharedQueryBuilder;

// $qb instanceof Doctrine\ORM\QueryBuilder
// $userRepository instanceof Doctrine\ORM\EntityRepository
$qb = $userRepository->createQueryBuilder('u');
// Let's wrap query builder inside our decorator.
// We use $sqb as acronym of "Shared Query Builder"
$sqb = SharedQueryBuilder::wrap($qb);

从现在起,你可以像通常使用 查询构建器一样使用 $sqbQueryBuilder 的每个方法都在 SharedQueryBuilder 上可用),但有一些有用的额外方法 🤫。

当你完成查询构建后,只需 解包 你的 SharedQueryBuilder

// $sqb instanceof Andante\Doctrine\ORM\SharedQueryBuilder
// $qb instanceof Doctrine\ORM\QueryBuilder
$qb = $sqb->unwrap();

请注意

  • 构建 SharedQueryBuilder 的唯一条件是尚未声明任何连接语句。
  • SharedQueryBuilderQueryBuilder装饰器,这意味着它不是 QueryBuilder 的实例,即使它有所有方法(遗憾的是,Doctrine 没有QueryBuilder 接口 🥺)。
  • SharedQueryBuilder 不允许你使用不同的别名多次连接实体。

我有什么额外的方法?

实体方法

你可以询问 SharedQueryBuilder 是否在 from 语句或某些 join 语句中具有实体。

if($sqb->hasEntity(User::class)) // bool returned 
{ 
    // Apply some query criteria only if this query builder is handling the User entity
}

你可以询问在构建的查询中实体的别名是什么(无论它是否用于 from 语句或 join 语句)。

$userAlias = $sqb->getAliasForEntity(User::class); // string 'u' returned 

你可以使用 withAlias 方法轻松添加对该实体属性的条件的条件。

if($sqb->hasEntity(User::class)) // bool returned 
{ 
    $sqb
        ->andWhere(
            $sqb->expr()->eq(
                $sqb->withAlias(User::class, 'email'), // string 'u.email'
                ':email_value'
            )
        )
        ->setParameter('email_value', 'user@email.com')
    ;    
} 

给定一个别名,你可以检索其实体类。

$entityClass = $sqb->getEntityForAlias('u'); // string 'App\Entity\User' returned

QueryBuilder::getAllAliases 方法扩展了一个可选的 bool 参数 $includeLazy(默认:false),用于包含 延迟连接 别名。

$allAliases = $sqb->getAllAliases(true);

延迟连接

所有查询构建器的 join 方法都可以像通常一样使用,但您也可以使用带 "lazy" 前缀的方式使用它们。

// Common join methods
$sqb->join(/* args */);
$sqb->innerJoin(/* args */);
$sqb->leftJoin(/* args */);

// Lazy join methods
$sqb->lazyJoin(/* args */);
$sqb->lazyInnerJoin(/* args */);
$sqb->lazyLeftJoin(/* args */);

// They works with all the ways you know you can perform joins in Doctrine
// A: $sqb->lazyJoin('u.address', 'a') 
// or B: $sqb->lazyJoin('Address::class', 'a', Expr\Join::WITH, $sqb->expr()->eq('u.address','a')) 

通过这样做,您定义了一个 join 语句,但实际上并未将其 添加 到您的 DQL 查询中。它将在您添加 另一个条件/DQL 部分 并引用它时自动添加。神奇!✨。

根据您现在有多困惑,您可以查看 为什么您需要这个一些示例 来达到您的“OMG”顿悟时刻。

示例

假设我们需要列出 User 实体,但我们还有一个可选的过滤器,可以根据 Building 名称搜索用户。

在决定使用该过滤器之前,我们无需执行任何连接。我们可以使用 延迟连接 来实现这一点。

$sqb = SharedQueryBuilder::wrap($userRepository->createQueryBuilder('u'));
$sqb
    ->lazyJoin('u.address', 'a')
    ->lazyJoin('a.building', 'b')
    //Let's add a WHERE condition that do not need our lazy joins 
    ->andWhere(
        $sqb->expr()->eq('u.verifiedEmail', ':verified_email')
    )
    ->setParameter('verified_email', true)
;

$users = $sqb->getQuery()->getResults();
// DQL executed:
//     SELECT u
//     FROM App\entity\User
//     WHERE u.verifiedEmail = true

// BUT if we use the same Query Builder to filter by building.name:
$buildingNameFilter = 'Building A';
$sqb
    ->andWhere(
        $sqb->expr()->eq('b.name', ':name_value')
    )
    ->setParameter('name_value', $buildingNameFilter)
;
$users = $sqb->getQuery()->getResults();
// DQL executed:
//     SELECT u
//     FROM App\entity\User
//       JOIN u.address a
//       JOIN a.building b
//     WHERE u.verifiedEmail = true
//       AND b.name = 'Building A'

您可能正在想:为什么我们不用以下更常见的方式达到相同的结果?(请注意,避免执行不必要的连接仍然是要求之一)

// How you could achieve this without SharedQueryBuilder
$buildingNameFilter = 'Building A';
$qb = $userRepository->createQueryBuilder('u');
$qb
    ->andWhere(
        $qb->expr()->eq('u.verifiedEmail', ':verified_email')
    )
    ->setParameter('verified_email', true);
    
if(!empty($buildingNameFilter)){
    $qb
        ->lazyJoin('u.address', 'a')
        ->lazyJoin('a.building', 'b')
        ->andWhere(
            $qb->expr()->eq('b.name', ':building_name_value')
        )
        ->setParameter('building_name_value', $buildingNameFilter)
    ;
}

$users = $qb->getQuery()->getResults(); // Same result as example shown before
// But this has some down sides further explained

如果在这个 同一上下文 中构建整个查询,上面的代码是完全可以的。

  • 👍 您 清楚 整个查询构建过程;
  • 👍 您 清楚 哪些实体被涉及;
  • 👍 您 清楚 每个实体定义了哪些别名;
  • 👍 您 清楚 定义了哪些查询参数及其用途。

但您有问题

  • 👎 您将查询结构定义与可选的过滤标准混合在一起。
  • 👎 代码很快就会变成一个难以阅读的混乱。

现实世界的案例

如果您的查询结构随着许多连接和过滤标准而增长,您可能需要将所有这些业务逻辑拆分到不同的类中。

例如,在一个后台管理员的用户列表中,您可能会在控制器中定义您的 主查询 来列出实体,并在某些 其他类 中处理 可选的过滤器

// UserController.php
class UserController extends Controller
{
    public function index(Request $request, UserRepository $userRepository) : Response
    {
        $qb = $userRepository->createQueryBuilder('u');
        $qb
            ->andWhere(
                $qb->expr()->eq('u.verifiedEmail', ':verified_email')
            )
            ->setParameter('verified_email', true);
        
        // Now Apply some optional filters from Request
        // Let's suppose we have an "applyFilters" method which is giving QueryBuilder and Request
        // to and array of classes responsable to take care of filtering query results.  
        $this->applyFilters($qb, $request);
        
        // Maybe have some pagination logic here too. Check KnpLabs/knp-components which is perfect for this.
        
        $users = $qb->getQuery()->getResults();
        // Build our response with User entities list.
    }
}

过滤器类可能看起来像这样

// BuildingNameFilter.php
class BuildingNameFilter implements FilterInterface
{
    public function filter(QueryBuilder $qb, Request $request): void
    {
        $buildingNameFilter = $request->query->get('building-name');
        if(!empty($buildingNameFilter)){
            $qb
                ->join('u.address', 'a')
                ->join('a.building', 'b')
                ->andWhere(
                    $qb->expr()->eq('b.name', ':building_name_value')
                )
                ->setParameter('building_name_value', $buildingNameFilter)
            ;
        }
    }
}

我们在这里犯了几个多罪!💀 上下文已经改变。

  • 👎 您 不清楚 整个查询构建过程。给定的 QueryBuilder 确实是对用户实体的查询吗?
  • 👎 您 不清楚 哪些实体被涉及。哪些实体已经被连接?
  • 👎 您 不清楚 为每个实体定义了哪些别名。我们怎么能按照惯例调用 u.address 呢?🤨
  • 👎 您 清楚 已经定义了哪些参数($qb->getParameters()),但您 不清楚 它们为什么被定义,其用途,并且您还可以通过在其他地方更改它们来 覆盖 它们,从而改变其他地方的行为;
  • 👎 在这个上下文中,我们的工作只是应用一些过滤器。我们 可以 通过添加一些连接语句来更改查询,但我们 应该避免 这样做。如果另一个过滤器也需要执行这些连接怎么办?灾难性的。😵

这就是为什么 SharedQueryBuilder 将在这些情况下拯救您的屁股。

让我们看看如何使用 SharedQueryBuilder 解决所有这些问题(现在您可以猜测为什么它会被命名为这样)。

使用 SharedQueryBuilder,您可以

  • 👍 定义 延迟连接,以便仅在需要时执行它们;
  • 👍 定义一些 不可变 参数,以确保值不会被其他地方更改;
  • 👍 您可以 检查一个实体是否在查询中,然后应用一些业务逻辑;
  • 👍 您可以 询问查询构建器 用于特定实体的 别名,这样您就不需要猜测别名,或者使用常量在类之间共享别名(我知道您已经想到了这个 🧐)。
// UserController.php
use Andante\Doctrine\ORM\SharedQueryBuilder;

class UserController extends Controller
{
    public function index(Request $request, UserRepository $userRepository) : Response
    {
        $sqb = SharedQueryBuilder::wrap($userRepository->createQueryBuilder('u'));
        $sqb
            // Please note: Sure, you can mix "normal" join methods and "lazy" join methods
            ->lazyJoin('u.address', 'a')
            ->lazyJoin('a.building', 'b')
            ->andWhere($sqb->expr()->eq('u.verifiedEmail', ':verified_email'))
            ->setImmutableParameter('verified_email', true);
        
        // Now Apply some optional filters from Request
        // Let's suppose we have an "applyFilters" method which is giving QueryBuilder and Request
        // to and array of classes responsable to take care of filtering query results.  
        $this->applyFilters($sqb, $request);
        
        // Maybe have some pagination logic here too.
        // You probably need to unwrap the Query Builder now for this
        $qb = $sqb->unwrap();
        
        $users = $qb->getQuery()->getResults();
        // Build our response with User entities list.
    }
}

过滤器类将如下所示

// BuildingNameFilter.php
use Andante\Doctrine\ORM\SharedQueryBuilder;

class BuildingNameFilter implements FilterInterface
{
    public function filter(SharedQueryBuilder $sqb, Request $request): void
    {
        $buildingNameFilter = $request->query->get('building-name');
        // Let's check if Query has a Building entity in from or join DQL parts 🙌
        if($sqb->hasEntity(Building::class) && !empty($buildingNameFilter)){
            $sqb
                ->andWhere(
                    // We can ask Query builder for the "Building" alias instead of guessing it/retrieve somewhere else 💋
                    $sqb->expr()->eq($sqb->withAlias(Building::class, 'name'), ':building_name_value')
                    // You can also use $sqb->getAliasForEntity(Building::class) to discover alias is 'b';
                )
                ->setImmutableParameter('building_name_value', $buildingNameFilter)
            ;
        }
    }
}
  • 👍 无需时不会执行额外的连接语句;
  • 👍 一旦定义了参数值,就无法更改/覆盖;
  • 👍 我们可以检测查询构建器是否正在处理实体,然后应用我们的业务逻辑;
  • 👍 我们不会猜测实体别名;
  • 👍 我们的过滤器类仅负责过滤;
  • 👍 可以有多个过滤器类处理同一实体的不同条件,而不需要重复的连接语句;

不可变参数

共享查询构建器有 不可变参数。一旦定义,它们就不能更改,否则会引发 异常

// $sqb instanceof Andante\Doctrine\ORM\SharedQueryBuilder

// set a common Query Builder parameter, as you are used to 
$sqb->setParameter('parameter_name', 'parameterValue');

// set an immutable common Query Builder parameter. It cannot be changed otherwise an exception will be raised.
$sqb->setImmutableParameter('immutable_parameter_name', 'parameterValue');

// get a collection of all query parameters (commons + immutables!)
$sqb->getParameters();

// get a collection of all immutable query parameters (exclude commons)
$sqb->getImmutableParameters();

// Sets a parameter and return parameter name as string instead of $sqb.
$sqb->withParameter(':parameter_name', 'parameterValue');
$sqb->withImmutableParameter(':immutable_parameter_name', 'parameterValue');
// This allows you to write something like this:
$sqb->expr()->eq('building.name', $sqb->withParameter(':building_name_value', $buildingNameFilter));

// The two following methods sets "unique" parameters. See "Unique parameters" doc section for more...
$sqb->withUniqueParameter(':parameter_name', 'parameterValue');
$sqb->withUniqueImmutableParameter(':parameter_name', 'parameterValue');

同时设置参数并在表达式中使用它

如果您确信您不会在查询的多个位置使用参数,您可以编写以下代码 🙌

$sqb
    ->andWhere(
        $sqb->expr()->eq(
            $sqb->withAlias(Building::class, 'name'), 
            ':building_name_value'
        )
    )
    ->setImmutableParameter('building_name_value', $buildingNameFilter)
;

这种方式 👇👇👇

$sqb
    ->andWhere(
        $sqb->expr()->eq(
            $sqb->withAlias(Building::class, 'name'), 
            $sqb->withImmutableParameter(':building_name_value', $buildingNameFilter) // return ":building_name_value" but also sets immutable parameter
        )
    )
;

唯一参数

除了 不可变参数 之外,您还可以要求查询构建器生成一个参数名。使用以下方法,查询构建器将装饰名称以避免与已声明的名称冲突(即使不可变参数也不会发生这种情况)。

$sqb
    ->andWhere(
        $sqb->expr()->eq(
           'building.name', 
            $sqb->withUniqueParameter(':name', $buildingNameFilter) // return ":param_name_4b3403665fea6" making sure parameter name is not already in use and sets parameter value.
        )
    )
    ->andWhere(
        $sqb->expr()->gte(
           'building.createdAt', 
            $sqb->withUniqueImmutableParameter(':created_at', new \DateTime('-5 days ago'))  // return ":param_created_at_5819f3ad1c0ce" making sure parameter name is not already in use and sets immutable parameter value.
        )
    )
    ->andWhere(
        $sqb->expr()->lte(
           'building.createdAt',
            $sqb->withUniqueImmutableParameter(':created_at', new \DateTime('today midnight'))  // return ":param_created_at_604a8362bf00c" making sure parameter name is not already in use and sets immutable parameter value.
        )
    )
;

/* 
 * Query Builder has now 3 parameters:
 *  - param_name_4b3403665fea6 (common)
 *  - param_created_at_5819f3ad1c0ce (immutable)
 *  - param_created_at_604a8362bf00c (immutable)
 */

结论

世界变得更美好 💁。

如果您觉得现在世界变得更美好,请给我们一个 ⭐️!💃🏻

AndanteProject 团队用爱建造 ❤️。