集合

介绍

Illuminate\Support\Collection 类提供了一个流畅、方便的包装器,用于处理数组数据。例如,查看以下代码。我们将使用 collect 辅助函数从数组创建一个新的集合实例,对每个元素运行 strtoupper 函数,然后移除所有空元素:

$collection = collect(['taylor', 'abigail', null])->map(function (?string $name) {
    return strtoupper($name);
})->reject(function (string $name) {
    return empty($name);
});

如你所见,Collection 类允许你链式调用其方法,执行对底层数组的流畅映射和缩减。通常,集合是不可变的,这意味着每个 Collection 方法都会返回一个全新的 Collection 实例。

创建集合

如上所述,collect 辅助函数会为给定的数组返回一个新的 Illuminate\Support\Collection 实例。因此,创建集合非常简单:

$collection = collect([1, 2, 3]);

Eloquent 查询的结果总是以 Collection 实例的形式返回。

扩展集合

集合是 “可扩展的”(macroable),这使得你可以在运行时向 Collection 类添加额外的方法。Illuminate\Support\Collection 类的 macro 方法接受一个闭包,该闭包将在调用宏时执行。宏的闭包可以通过 $this 访问集合的其它方法,就像它是集合类的一个真实方法一样。例如,以下代码将一个 toUpper 方法添加到 Collection 类:

use Illuminate\Support\Collection;
use Illuminate\Support\Str;

Collection::macro('toUpper', function () {
    return $this->map(function (string $value) {
        return Str::upper($value);
    });
});

$collection = collect(['first', 'second']);

$upper = $collection->toUpper();

// ['FIRST', 'SECOND']

通常,你应该在【服务提供者】的 boot 方法中声明集合宏。

宏的参数

如果需要,你可以定义接受额外参数的宏:

use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Lang;

Collection::macro('toLocale', function (string $locale) {
    return $this->map(function (string $value) use ($locale) {
        return Lang::get($value, [], $locale);
    });
});

$collection = collect(['first', 'second']);

$translated = $collection->toLocale('es');

可用方法列表

在接下来的大部分集合文档中,我们将讨论 Collection 类中每个可用的方法。请记住,所有这些方法都可以链式调用,以流畅地操作底层数组。此外,几乎每个方法都会返回一个新的 Collection 实例,这样你就可以在必要时保留集合的原始副本。

TODO

高级有序消息

集合还支持“高阶消息”(higher order messages),它们是用于在集合上执行常见操作的快捷方式。提供高阶消息的集合方法包括:averageavgcontainseacheveryfilterfirstflatMapgroupBykeyBymapmaxminpartitionrejectskipUntilskipWhilesomesortBysortByDescsumtakeUntiltakeWhileunique

每个高阶消息都可以作为集合实例的动态属性进行访问。例如,下面我们使用 each 高阶消息来调用集合中每个对象的方法:

use App\Models\User;

$users = User::where('votes', '>', 500)->get();

$users->each->markAsVip();

同样,我们可以使用 sum 高阶消息来计算一个用户集合中所有 “votes” 的总和:

$users = User::where('group', 'Development')->get();

return $users->sum->votes;

懒集合

介绍

在深入学习 Laravel 的懒集合(lazy collections)之前,先花一些时间熟悉 【PHP 的生成器】(generators)。

为了补充已经非常强大的 Collection 类,LazyCollection 类利用了 PHP 的【生成器】,让你在处理非常大的数据集时保持低内存使用。

例如,假设你的应用程序需要处理一个多吉字节的日志文件,并利用 Laravel 的集合方法来解析日志。与一次性将整个文件读取到内存中不同,懒集合可以让你在给定的时间内仅将文件的一小部分保留在内存中:

use App\Models\LogEntry;
use Illuminate\Support\LazyCollection;

LazyCollection::make(function () {
    $handle = fopen('log.txt', 'r');

    while (($line = fgets($handle)) !== false) {
        yield $line;
    }
})->chunk(4)->map(function (array $lines) {
    return LogEntry::fromLines($lines);
})->each(function (LogEntry $logEntry) {
    // 处理日志条目...
});

或者,假设你需要遍历 10,000 个 Eloquent 模型。当使用传统的 Laravel 集合时,所有 10,000 个 Eloquent 模型必须一次性加载到内存中:

use App\Models\User;

$users = User::all()->filter(function (User $user) {
    return $user->id > 500;
});

然而,查询构建器的 cursor 方法返回的是一个 LazyCollection 实例。这允许你仍然只对数据库执行一次查询,并且每次只加载一个 Eloquent 模型到内存中。在这个例子中,filter 回调只有在我们真正遍历每个用户时才会执行,从而大大减少了内存使用:

use App\Models\User;

$users = User::cursor()->filter(function (User $user) {
    return $user->id > 500;
});

foreach ($users as $user) {
    echo $user->id;
}

创建懒集合

要创建一个懒集合实例,你应该将一个 PHP 生成器函数传递给集合的 make 方法:

use Illuminate\Support\LazyCollection;

LazyCollection::make(function () {
    $handle = fopen('log.txt', 'r');

    while (($line = fgets($handle)) !== false) {
        yield $line;
    }
});

Enumerable 合约

几乎所有在 Collection 类上可用的方法也都可以在 LazyCollection 类上使用。这两个类都实现了 Illuminate\Support\Enumerable 接口,该接口定义了以下方法:

TODO

懒集合方法

除了 Enumerable 接口中定义的方法外,LazyCollection 类还包含以下方法:

takeUntilTimeout()

takeUntilTimeout 方法返回一个新的懒集合,该集合会在指定的时间之前枚举值。超过该时间后,集合将停止枚举:

$lazyCollection = LazyCollection::times(INF)
    ->takeUntilTimeout(now()->addMinute());

$lazyCollection->each(function (int $number) {
    dump($number);

    sleep(1);
});

// 1
// 2
// ...
// 58
// 59

为了说明这个方法的使用,假设一个应用程序需要从数据库中提交发票。你可以定义一个计划任务,每 15 分钟运行一次,并且每次只处理最多 14 分钟的发票:

use App\Models\Invoice;
use Illuminate\Support\Carbon;

Invoice::pending()->cursor()
    ->takeUntilTimeout(
        Carbon::createFromTimestamp(LARAVEL_START)->add(14, 'minutes')
    )
    ->each(fn (Invoice $invoice) => $invoice->submit());

tapEach()

each 方法会立即为集合中的每个项调用给定的回调,而 tapEach 方法则只有在项一个一个地从列表中提取时,才会调用给定的回调。

// 到目前为止没有任何输出...
$lazyCollection = LazyCollection::times(INF)->tapEach(function (int $value) {
    dump($value);
});

// 输出三个项...
$array = $lazyCollection->take(3)->all();

// 1
// 2
// 3

throttle()

throttle 方法会限制懒集合的返回速度,使得每个值在指定的秒数后返回。这个方法特别适用于与外部 API 交互的场景,尤其是当外部 API 对请求有速率限制时:

use App\Models\User;

User::where('vip', true)
    ->cursor()
    ->throttle(seconds: 1)
    ->each(function (User $user) {
        // 调用外部 API...
    });

remember()

remember 方法返回一个新的懒集合,该集合会记住已经枚举过的任何值,并且在后续的集合枚举中不会重新获取它们:

// 查询尚未执行...
$users = User::cursor()->remember();

// 查询已执行...
// 数据库中获取前 5 个用户...
$users->take(5)->all();

// 前 5 个用户来自集合的缓存...
// 其余的从数据库中获取...
$users->take(20)->all();