rds/氢气

此包已被弃用且不再维护。未建议替代包。

更快、更便捷的 Doctrine ORM 抽象层

0.3.5 2020-03-11 13:00 UTC

This package is auto-updated.

Last update: 2020-03-11 13:01:13 UTC


README

Hydrogen

Travis CI Code coverage Scrutinizer CI Latest Stable Version Latest Unstable Version License MIT

简介

Hydrogen 为使用 Doctrine 查询提供了一个美丽、方便且简单的实现。它不会以任何方式影响现有代码,甚至可以在已构建的生产应用中使用。

安装

服务器要求

Hydrogen 库有几个系统要求。请确保您的服务器满足以下要求

安装 Hydrogen

Hydrogen 使用 Composer 来管理其依赖项。因此,在使用 Hydrogen 之前,请确保您的机器上已安装 Composer。

稳定版

composer require rds/hydrogen

开发版

composer require rds/hydrogen dev-master@dev

使用方法

Hydrogen 与 Doctrine 的存储库交互。为了利用额外的功能,您需要将主要特质添加到现有的存储库实现中。

<?php

use Doctrine\ORM\EntityRepository;
use RDS\Hydrogen\Hydrogen;

class ExampleRepository extends EntityRepository 
{
    use Hydrogen;
}

之后,您将获得对查询构建器的完全访问权限。

检索结果

检索所有实体

您可以使用存储库上的 ->query() 方法开始查询。此方法返回给定存储库的流畅查询构建器实例,允许您将更多约束添加到查询中,然后最终使用 ->get() 方法获取结果

<?php

use RDS\Hydrogen\Hydrogen;
use Doctrine\ORM\EntityRepository;

class UsersRepository extends EntityRepository 
{
    use Hydrogen;
    
    public function toArray(): iterable
    {
        return $this->query->get();
    }
}

get() 方法返回一个包含结果的 array,其中每个结果都是与指定存储库相关联的对象(实体)的实例

foreach ($users->toArray() as $user) {
    \var_dump($user);
}

此外,您还可以使用 collect() 方法获取一个与 ArrayCollection 兼容的集合

<?php

use RDS\Hydrogen\Hydrogen;
use Doctrine\ORM\EntityRepository;
use RDS\Hydrogen\Collection\Collection;

class UsersRepository extends EntityRepository 
{
    use Hydrogen;
    
    public function toCollection(): Collection
    {
        return $this->query->collect();
    }
}
$users->toCollection()->each(function (User $user): void {
    \var_dump($user);
});

注意:直接访问 Hydrogen 构建,而不是 Doctrine 提供的现有方法,完全 忽略 所有关系(如:@OneToMany(..., fetch="EAGER"))。

获取单个实体

如果您只需要从数据库表中检索一行,您可以使用第一种方法。此方法将返回一个单个实体对象

$user = $repository->query->where('name', 'John')->first();

echo $user->getName();

如果您甚至不需要整行,您可以使用 ->first() 方法的附加参数从记录中提取单个值。此方法将直接返回列的值

[$name, $email] = $repository->query->where('name', 'John')->first('name', 'email');

echo $name . ' with email ' . $email;

获取字段值列表

如果您希望检索包含单个实体字段值的数组或集合,您可以使用 ->get()->collect() 方法的附加参数。在此示例中,我们将检索用户ID和名称的集合

$users = $repository->query->get('id', 'name');

foreach ($users as ['id' => $id, 'name' => $name]) {
    echo $id . ': ' . $name;
}

聚合和标量结果

查询构建器还提供各种聚合方法,如 countmaxminavgsum。您可以在构建查询后调用这些方法中的任何一种

$count = $users->query->count(); 

$price = $prices->query->max('price');

当然,您可以与其他子句结合使用这些方法

$price = $prices->query
    ->where('user', $user)
    ->where('finalized', 1)
    ->avg('price');

如果您的数据库支持其他函数,则可以直接使用 ->scalar() 方法使用这些方法

->scalar() 方法的第一参数需要指定应包含在结果中的字段。第二个可选参数允许您将类型转换为所需的类型。

$price = $prices->query
    ->select('AVG(price) as price')
    ->scalar('price', 'int');

允许的类型

类型 描述
int 返回一个整数值
float 返回一个浮点值
string 返回一个字符串值
bool 返回布尔值
callable 返回闭包实例
object 返回一个对象
array 返回一个数组
iterable array 的别名

查询调用

方法 描述
get 返回实体数组
collect 返回实体集合
first 返回第一个结果
scalar 返回单个标量值
count 返回给定字段的计数
sum 返回给定字段的总和
avg 返回给定字段的平均值
max 返回给定字段的最大值
min 返回给定字段的最小值

选择

使用 select() 方法,您可以指定查询的自定义选择子句

['count' => $count] = $users->query
    ->select(['COUNT(id)' => 'count'])
    ->get();

echo $count;

此外,此表达式可以简化并重写为以下方式

$result = $users->query
    ->select(['COUNT(id)' => 'count'])
    ->scalar('count');

echo $result;

附加字段

实体

您会发现,如果我们指定选择,则在响应中我们得到选择的数据,忽略实体。为了在响应中获得任何实体,我们应该使用 withEntity 方法

['messages' => $messages, 'user' => $user] = $users->query
    ->select(['COUNT(messages)' => 'messages'])
    ->withEntity('user')
    ->where('id', 23)
    ->first();

原始列

有时某些字段可能不在实体中,例如,关系键。在这种情况下,我们别无选择,只能直接选择这些列,绕过实体的结构

$messages = $query
    ->select([$query->column('user_id') => 'user_id'])
    ->withEntity('message')
    ->get('message', 'user_id');
    
foreach ($messages as ['message' => $message, 'user_id' => $id]) {
    echo $message->title . ' of user #' . $id; 
}

Where 子句

简单的 Where 子句

您可以使用查询构建实例上的 where 方法向查询中添加 where 子句。最基本的 where 调用需要三个参数。第一个参数是列名。第二个参数是运算符,可以是数据库支持的所有运算符之一。最后,第三个参数是要与列进行比较的值。

例如,以下是一个验证 "votes" 实体字段值等于 100 的查询。

$users = $repository->query->where('votes', '=', 100)->get();

为了方便,如果您想验证列是否等于某个给定值,可以直接将值作为 where 方法的第二个参数传递。

$users = $repository->query->where('votes', 100)->get();

当然,在编写 where 子句时,您可以使用各种其他运算符。

$users = $repository->query
    ->where('votes', '>=', 100)
    ->get();
    
$users = $repository->query
    ->where('votes', '<>', 100)
    ->get();
    
$users = $repository->query
    ->where('votes', '<=', 100)
    ->get();

或语句

您可以将 where 约束链接起来,也可以向查询中添加 or 子句。orWhere 方法接受与 where 方法相同的参数。

$users = $repository->query
    ->where('votes', '>', 100)
    ->orWhere('name', 'John')
    ->get();

或者,您可以使用 ->or 魔术方法。

$users = $repository->query
    ->where('votes', '>', 100)
    ->or->where('name', 'John')
    ->get();

其他 where 子句

whereBetween

whereBetween 方法验证实体字段值是否介于两个值之间。

$users = $repository->query
    ->whereBetween('votes', 1, 100)
    ->get();

$users = $repository->query
    ->where('name', 'John')
    ->orWhereBetween('votes', 1, 100)
    ->get();

whereNotBetween

whereNotBetween 方法验证实体字段值是否不在两个值之间。

$users = $repository->query
    ->whereNotBetween('votes', 1, 100)
    ->get();

$users = $repository->query
    ->where('name', 'John')
    ->orWhereNotBetween('votes', 1, 100)
    ->get();

whereIn / whereNotIn

whereIn 方法验证给定实体字段值是否包含在给定的数组中。

$users = $repository->query
    ->whereIn('id', [1, 2, 3])
    ->get();

$users = $repository->query
    ->where('id', [1, 2, 3])
    // Equally: ->whereIn('id', [1, 2, 3])
    ->orWhere('id', [101, 102, 103])
    // Equally: ->orWhereIn('id', [101, 102, 103])
    ->get();

whereNotIn 方法验证给定实体字段值是否不包含在给定的数组中。

$users = $repository->query
    ->whereNotIn('id', [1, 2, 3])
    ->get();

$users = $repository->query
    ->where('id', '<>', [1, 2, 3])
    // Equally: ->whereNotIn('id', [1, 2, 3])
    ->orWhere('id', '<>', [101, 102, 103])
    // Equally: ->orWhereNotIn('id', [101, 102, 103])
    ->get();

whereNull / whereNotNull

whereNull 方法验证给定实体字段值是否为 NULL

$users = $repository->query
    ->whereNull('updatedAt')
    ->get();

$users = $repository->query
    ->where('updatedAt', null)
    // Equally: ->whereNull('updatedAt')
    ->orWhereNull('deletedAt', null)
    // Equally: ->orWhereNull('deletedAt')
    ->get();

whereNotNull 方法验证实体字段值是否不为 NULL

$users = $repository->query
    ->whereNotNull('updatedAt')
    ->get();

$users = $repository->query
    ->whereNotNull('updatedAt')
    ->or->whereNotNull('deletedAt')
    ->get();

like / notLike

like 方法验证给定实体字段值是否与给定值相似。

$messages = $repository->query
    ->like('description', '%some%')
    ->orLike('description', '%any%')
    ->get();

$messages = $repository->query
    ->where('description', '~', '%some%')
    ->orWhere('description', '~', '%any%')
    ->get();

notLike 方法验证给定实体字段值是否不与给定值相似。

$messages = $repository->query
    ->notLike('description', '%some%')
    ->orNotLike('description', '%any%')
    ->get();

$messages = $repository->query
    ->where('description', '!~', '%some%')
    ->orWhere('description', '!~', '%any%')
    ->get();

参数分组

有时您可能需要创建更复杂的 where 子句,如 "where exists" 子句或嵌套参数分组。Hydrogen 查询构建器可以处理这些情况。要开始,让我们看看一个分组约束的例子。

$users = $repository->query
    ->where('name', 'John')
    ->where(function (Query $query): void {
        $query->where('votes', '>', 100)
              ->orWhere('title', 'Admin');
    })
    ->get();

如你所见,传递一个 Closurewhere 方法指示查询构建器开始一个约束组。这个 Closure 将接收一个查询构建实例,您可以使用它来设置应包含在括号组内的约束。上面的例子将生成以下 DQL。

SELECT u FROM App\Entity\User u 
WHERE u.name = "John" AND (
    u.votes > 100 OR
    u.title = "Admin" 
)

此外,除了 whereorWhere 方法外,您还可以使用其他选项。方法 orand 会做同样的事情。

$users = $repository->query
    ->where('name', 'John')
    ->and(function (Query $query): void {
        $query->where('votes', '>', 100)
              ->orWhere('title', 'Admin');
    })
    ->get();
    
// SELECT u FROM App\Entity\User u 
// WHERE u.name = "John" AND (
//     u.votes > 100 OR
//     u.title = "Admin"
// )
    
$users = $repository->query
    ->where('name', 'John')
    ->or(function (Query $query): void {
        $query->where('votes', '>', 100)
              ->where('title', 'Admin');
    })
    ->get();
    
// SELECT u FROM App\Entity\User u 
// WHERE u.name = "John" OR (
//     u.votes > 100 AND
//     u.title = "Admin"
// )

排序

orderBy

orderBy 方法允许您根据给定的列对查询结果进行排序。orderBy 方法的第一个参数应该是您想要排序的列,而第二个参数控制排序的方向,可以是 asc 或 desc。

$users = $repository->query
    ->orderBy('name', 'desc')
    ->get();

此外,您还可以使用快捷方式 asc()desc() 来简化代码。

$users = $repository->query
    ->asc('id', 'createdAt')
    ->desc('name')
    ->get();

latest / oldest

最新和最老的方法允许您轻松根据日期对结果进行排序。默认情况下,结果将按 createdAt 实体字段排序。或者,您也可以传递您想要排序的列名。

$users = $repository->query
     ->latest()
     ->get();
     
$posts = $repository->query
    ->oldest('updatedAt')
    ->get();

分组

groupBy

groupBy 方法可用于对查询结果进行分组。

$users = $repository->query
     ->groupBy('account')
     ->get();

您可以向 groupBy 方法传递多个参数,以按多个列进行分组。

$users = $repository->query
     ->groupBy('firstName', 'status')
     ->get();

having

having方法的签名与where方法类似

$users = $repository->query
    ->groupBy('account')
    ->having('account.id', '>', 100)
    ->get();

限制和偏移量

跳过/获取

要限制查询返回的结果数量,或要跳过查询中给定数量的结果,可以使用skip()take()方法

$users = $repository->query->skip(10)->take(5)->get();

或者,您也可以使用limitoffset方法

$users = $repository->query
    ->offset(10)
    ->limit(5)
    ->get();

之前/之后

通常在数据库高负载期间,当向表中插入新记录时,offset可能会移动。在这种情况下,使用before()after()方法以确保后续样本严格遵循前一个样本是值得的。

让我们以获取id为15之后10篇文章的例子来说明

$articles = $repository->query
    ->where('category', 'news')
    ->after('id', 15)
    ->take(10)
    ->get();

范围

您可以使用range()方法来指定您想要作为结果接收的确切记录

$articles = $repository->range(10, 20)->get();

内嵌对象

内嵌对象是那些不是实体的类,但它们嵌入在实体中,并且也可以通过Hydrogen进行查询。您通常会使用它们来减少重复或分离关注点。例如,日期范围或地址这样的值对象是此功能的主要用例。

<?php

/**
 * @ORM\Entity(repositoryClass=UsersRepository::class)
 */
class User
{
    /**
     * @ORM\Embedded(class=Address::class) 
     */
    private $address;
}

/**
 * @ORM\Embeddable()
 */
class Address
{
    /**
     * @ORM\Column(type="string") 
     */
    private $city;

    /** 
     * @ORM\Column(type="string") 
     */
    private $country;
}

要使用查询来管理内嵌对象,您可以使用点运算符(.

<?php

class UsersRepository extends EntityRepository
{
    use Hydrogen;
    
    public function findAllOrderedByCountry(): iterable
    {
        return $this->query->asc('address.country')->get();
    }
}

关系

Doctrine ORM提供了几种不同类型的关系:@OneToOne@OneToMany@ManyToOne@ManyToMany。并且“贪婪”加载这些关系是在实体的元数据级别设置的。Doctrine不提供在查询期间管理关系和加载它们的机制,因此在检索数据时,您可能会遇到使用DQL时的N+1查询,特别是在@OneToOne关系上,因为根本就没有其他加载选项。

Hydrogen允许您在查询级别灵活地管理如何获取关系,以及这些关系的数量和适用于这些关系的附加聚合函数

<?php

/** 
 * @ORM\Entity() 
 */
class Customer
{
    /** .... */
    
    /**
     * @ORM\OneToOne(targetEntity=Cart::class, mappedBy="customer")
     */
    private $cart;
}

/** 
 * @ORM\Entity() 
 */
class Cart
{
    /** .... */
    
    /**
     * @ORM\OneToOne(targetEntity=Customer::class, inversedBy="cart")
     * @ORM\JoinColumn(name="customer_id", referencedColumnName="id")
     */
    private $customer;
}

如果您对存储库创建了一个基本查询,在这种情况下,您将得到相同的N+1,对于每个元素,对于每个相关实体,都会生成一个额外的查询块。

连接

在Doctrine中处理关系的一个选项是连接。为了使查询在一个查询中请求具有关系的数据,您应使用join(使用INNER JOIN)或leftJoin(使用LEFT JOIN)方法

$customers = $customerRepository->query
    ->join('cart')
    ->get();

foreach ($customers as $customer) {
    echo $customer->cart->id;
}

请注意,在使用连接时,您不能使用limit,因为它会影响响应中的总数据量(即,包括关系),而不是父实体的数量。

连接子查询

我们还可以对依赖实体执行其他操作。例如,我们想获取资产负债表上有超过100卢布的用户(客户)列表

$customers = $customerRepository->query
     ->join(['cart' => function (Query $query): void {
         $query->where('balance', '>', 100)
            ->where('currency', 'RUB');
     }])
     ->get();

注意:使用join的操作会影响底层查询。

嵌套关系

因此,如果我们需要所有订购过电影票等商品的客户,我们需要进行一个简单的请求

$customers = $customerRepository->query
     ->join(['cart.goods' => function (Query $query): void {
         $query->where('category', 'tickets')
            ->where('value', '>', 0);
     }])
     ->get();

查询作用域

有时构建整个查询需要很长时间,其中某些部分已经重复了。在这种情况下,我们可以使用作用域的机制,该机制允许您向查询添加一组方法,这些方法将返回我们需要的查询部分。

<?php

class UsersRepository extends EntityRepository
{
    use Hydrogen;
    
    public function banned(bool $positive = true): Query
    {
        return $positive
            ? $this->query->whereNotNull('bannedAt')
            : $this->query->whereNull('bannedAt');
    }
    
    public function findBanned(): iterable
    {
        // We supplement the query, call the existing method "banned"
        return $this->query->banned->get();
    }
    
    public function findActive(): iterable
    {
        // We supplement the query, call the existing method "banned" with additional argument "false"
        return $this->query->banned(false)->get();
    }
} 

集合

由于基础内核使用了Illuminate Collections,但增加了一些新功能。

高阶消息传递

在使用全局函数时,模式"_"用于指定高阶消息传递中函数参数中代理的位置。

use RDS\Hydrogen\Collection;

$data = [
    ['value' => '23'],
    ['value' => '42'],
    ['value' => 'Hello!'],
];


$example1 = Collection::make($data)
    ->map->value // ['23', '42', 'Hello!']
    ->toArray();
    
//
// $example1 = \array_map(function (array $item): string {
//      return $item['value']; 
// }, $data);
//

$example2 = Collection::make($data)
    ->map->value     // ['23', '42', 'Hello!']
    ->map->intval(_) // [23, 42, 0]
    ->filter()       // [23, 42]
    ->toArray();
    
//
//
// $example2 = \array_map(function (array $item): string {
//      return $item['value']; 
// }, $data);
//
// $example2 = \array_map(function (string $value): int {
//      return \intval($value);
//                      ^^^^^ - pattern "_" will replaced to each delegated item value. 
// }, $example1);
//
// $example2 = \array_filter($example2, function(int $value): bool {
//      return (bool)$value;
// });
//
//

$example3 = Collection::make($data)
    ->map->value            // ['23', '42', 'Hello!']
    ->map->mbSubstr(_, 1)   // Using "mb_substr(_, 1)" -> ['3', '2', 'ello!']
    ->toArray();

解构

use RDS\Hydrogen\Collection;

$collection = Collection::make([
    ['a' => 'A1', 'b' => 'B1' 'value' => '23'],
    ['a' => 'A2', 'b' => 'B2' 'value' => '42'],
    ['a' => 'A3', 'b' => 'B3' 'value' => 'Hello!'],
]);

// Displays all data
foreach($collection as $item) {
    \var_dump($item); // [a => 'A*', b => 'B*', value => '***'] 
}

// Displays only "a" field
foreach ($collection as ['a' => $a]) {
    \var_dump($a); // 'A'
}

贝多芬同意。

https://habrastorage.org/webt/lf/hw/dn/lfhwdnvjxlt9vrsbrd_ajpitubc.png