什么是集合
Illuminate\Support\Collection 类是操作数组数据的流式封装。
不必逐个调用 PHP 数组函数,你可以通过方法链直观地对数据进行加工。
// 普通数组操作
$names = array_filter(
array_map(fn ($user) => $user['name'], $users),
fn ($name) => $name !== null
);
// 使用集合
$names = collect($users)
->pluck('name')
->filter()
->values();
集合是不可变的。每个方法都不会修改原集合,而是返回新的集合实例。你可以在保留原数据的同时安全操作。
创建集合
collect() 辅助函数
最常用的方式。传入数组生成集合:
use Illuminate\Support\Collection;
// 从数组创建集合
$users = collect([
['name' => '山田太郎', 'age' => 28, 'role' => 'admin'],
['name' => '铃木花子', 'age' => 34, 'role' => 'editor'],
['name' => '佐藤次郎', 'age' => 22, 'role' => 'viewer'],
]);
// 嵌套数组同样可以
$products = collect([
['name' => '笔记本电脑', 'price' => 120000, 'stock' => 5],
['name' => '鼠标', 'price' => 3500, 'stock' => 20],
['name' => '键盘', 'price' => 8000, 'stock' => 12],
]);
Collection::make()
等同于 collect()。若你偏好门面风格,可以用它。
$collection = Collection::make([1, 2, 3]);
Collection::fromJson()
从 JSON 字符串创建集合,便于处理外部 API 响应。
$collection = Collection::fromJson('[{"name":"山田太郎"},{"name":"铃木花子"}]');
常用方法
map — 转换每个元素
对每个元素执行处理,返回转换后的新集合。
$users = collect([
['name' => '山田太郎', 'email' => '[email protected]'],
['name' => '铃木花子', 'email' => '[email protected]'],
]);
// 邮箱脱敏
$masked = $users->map(function (array $user) {
$parts = explode('@', $user['email']);
return [
'name' => $user['name'],
'email' => substr($parts[0], 0, 2) . '***@' . $parts[1],
];
});
// [['name' => '山田太郎', 'email' => 'ya***@example.com'], ...]
filter / reject — 按条件过滤
filter() 保留满足条件的元素,reject() 排除满足条件的元素。
$products = collect([
['name' => '笔记本电脑', 'price' => 120000, 'in_stock' => true],
['name' => '鼠标', 'price' => 3500, 'in_stock' => false],
['name' => '键盘', 'price' => 8000, 'in_stock' => true],
]);
// 仅取库存商品
$inStock = $products->filter(fn ($product) => $product['in_stock']);
// 排除无库存(等同于反向 filter)
$available = $products->reject(fn ($product) => ! $product['in_stock']);
// 不带参数的 filter() 会移除 falsy 值
$names = collect(['山田', '', null, '铃木', false])->filter()->values();
// ['山田', '铃木']
filter() 之后索引会出现空洞。调用 values() 可以重新编号从 0 开始。
first / last — 获取单个元素
返回满足条件的第一个或最后一个元素。
$orders = collect([
['id' => 1, 'status' => 'shipped', 'amount' => 5000],
['id' => 2, 'status' => 'pending', 'amount' => 12000],
['id' => 3, 'status' => 'pending', 'amount' => 3500],
]);
// 第一个待处理订单
$nextOrder = $orders->first(fn ($order) => $order['status'] === 'pending');
// 无匹配时的默认值
$order = $orders->first(fn ($order) => $order['status'] === 'cancelled', null);
// 最后一个
$latest = $orders->last();
pluck — 抽取指定键的值
从嵌套数组或模型中抽出特定字段。
$users = collect([
['id' => 1, 'name' => '山田太郎', 'department' => '开发部'],
['id' => 2, 'name' => '铃木花子', 'department' => '销售部'],
['id' => 3, 'name' => '佐藤次郎', 'department' => '开发部'],
]);
// 抽取名字
$names = $users->pluck('name');
// ['山田太郎', '铃木花子', '佐藤次郎']
// 以 id 为键做映射
$nameById = $users->pluck('name', 'id');
// [1 => '山田太郎', 2 => '铃木花子', 3 => '佐藤次郎']
groupBy — 按键分组
$users = collect([
['name' => '山田太郎', 'department' => '开发部'],
['name' => '铃木花子', 'department' => '销售部'],
['name' => '佐藤次郎', 'department' => '开发部'],
['name' => '田中美咲', 'department' => '销售部'],
]);
$byDepartment = $users->groupBy('department');
// 用闭包动态指定分组键
$byFirstChar = $users->groupBy(fn ($user) => mb_substr($user['name'], 0, 1));
sortBy / sortByDesc — 排序
$products = collect([
['name' => '笔记本电脑', 'price' => 120000],
['name' => '鼠标', 'price' => 3500],
['name' => '键盘', 'price' => 8000],
]);
// 价格升序
$cheapFirst = $products->sortBy('price');
// 价格降序
$expensiveFirst = $products->sortByDesc('price');
// 多键排序
$sorted = $products->sortBy([
['price', 'asc'],
['name', 'asc'],
]);
each — 对每个元素执行处理
用于有副作用的操作(日志、发邮件等)。不会改变集合本身。
$orders = collect([
['id' => 1, 'user_id' => 10, 'amount' => 5000],
['id' => 2, 'user_id' => 11, 'amount' => 12000],
]);
$orders->each(function (array $order) {
\Log::info("处理订单 #{$order['id']}", ['amount' => $order['amount']]);
});
// 返回 false 可提前退出循环
$orders->each(function (array $order) {
if ($order['amount'] > 10000) {
return false;
}
});
flatMap — 映射后再拍平一层
将每个元素映射为数组,并将整体拍平为一维。
$users = collect([
['name' => '山田太郎', 'tags' => ['php', 'laravel']],
['name' => '铃木花子', 'tags' => ['javascript', 'vue']],
]);
$allTags = $users->flatMap(fn ($user) => $user['tags']);
// ['php', 'laravel', 'javascript', 'vue']
reduce — 聚合
把整个集合归约为单一值。
$orders = collect([
['product' => '笔记本电脑', 'quantity' => 1, 'price' => 120000],
['product' => '鼠标', 'quantity' => 2, 'price' => 3500],
['product' => '键盘', 'quantity' => 1, 'price' => 8000],
]);
$total = $orders->reduce(
fn ($carry, $order) => $carry + ($order['price'] * $order['quantity']),
0
);
// 135000
单纯求和可用 sum():$orders->sum(fn ($o) => $o['price'] * $o['quantity'])
reduceInto — 聚合到对象
与 reduce 类似,但回调不需要返回值。适合直接修改已有对象。
class OrderStats
{
public int $total = 0;
public int $count = 0;
}
$orders = collect([
['amount' => 100],
['amount' => 250],
['amount' => 50],
]);
$stats = $orders->reduceInto(new OrderStats, function (OrderStats $stats, array $order) {
$stats->total += $order['amount'];
$stats->count++;
});
$stats->total; // 400
$stats->count; // 3
reduce 通过回调返回值作为下一次 $carry,适合聚合基本类型。reduceInto 则一直修改传入的对象,写法更简洁。
若目标是标量或数组,可在回调中使用引用(&)。
$collection = collect([1, 2, 3, 4, 5]);
$even = $collection->reduceInto([], function (array &$result, int $value) {
if ($value % 2 === 0) {
$result[] = $value;
}
});
// [2, 4]
chunk — 分块
将大集合按指定大小切分。适合分批处理或分页展示。
$users = collect(range(1, 100))->map(fn ($i) => ['id' => $i, 'name' => "用户{$i}"]);
// 每 10 条一块
$chunks = $users->chunk(10);
// $chunks->count() === 10
$chunks->each(function (\Illuminate\Support\Collection $batch) {
// 每批处理
});
方法链
集合的价值体现在方法链上。可以将多个操作串联,实现复杂的数据变换。
$orders = collect([
['customer' => '山田太郎', 'status' => 'completed', 'amount' => 5000, 'category' => '电子设备'],
['customer' => '铃木花子', 'status' => 'pending', 'amount' => 12000, 'category' => '电子设备'],
['customer' => '佐藤次郎', 'status' => 'completed', 'amount' => 3500, 'category' => '文具'],
['customer' => '田中美咲', 'status' => 'completed', 'amount' => 8000, 'category' => '电子设备'],
['customer' => '伊藤健一', 'status' => 'cancelled', 'amount' => 2000, 'category' => '文具'],
]);
$result = $orders
->filter(fn ($order) => $order['status'] === 'completed')
->filter(fn ($order) => $order['category'] === '电子设备')
->sortByDesc('amount')
->map(fn ($order) => [
'customer' => $order['customer'],
'amount' => number_format($order['amount']) . '円',
])
->values();
与 Eloquent 联动
Eloquent 查询结果始终以 Illuminate\Database\Eloquent\Collection 实例返回。
它继承自基础 Collection,上述方法都可以使用。
use App\Models\User;
use App\Models\Order;
// get() 返回集合
$users = User::where('is_active', true)->get(); // Collection
// 集合方法可以直接使用
$adminEmails = User::all()
->filter(fn ($user) => $user->role === 'admin')
->pluck('email');
// 关联加载后的数据也是集合
$orders = Order::with('items')->where('status', 'completed')->get();
$summary = $orders->map(fn ($order) => [
'id' => $order->id,
'customer' => $order->user->name,
'item_count' => $order->items->count(),
'total' => $order->items->sum('price'),
]);
能通过数据库完成的过滤和排序,请使用查询构建器(where、orderBy 等),而不是先转为集合再操作。将全部数据加载到内存后再过滤会浪费资源。
Eloquent 集合特有方法
Eloquent\Collection 提供额外方法:
$users = User::all();
// 按 ID 查找
$user = $users->find(1);
// 取所有主键
$ids = $users->modelKeys();
// 批量加载关联
$users->load('orders', 'profile');
// 差集与交集
$diff = $users->diff($otherUsers);
$intersect = $users->intersect($otherUsers);
Lazy Collections
普通集合会把全部数据加载到内存。LazyCollection 借助 PHP Generator 逐条处理,适合数万条以上的大数据集,可节省内存。
use Illuminate\Support\LazyCollection;
// 普通集合:全部载入内存
$users = User::all(); // 10 万条就有 10 万条在内存里
// LazyCollection:逐条处理
User::cursor()->each(function (User $user) {
// 一次只留一条在内存
});
创建 LazyCollection
use Illuminate\Support\LazyCollection;
// 从闭包(generator)创建
$lazy = LazyCollection::make(function () {
$handle = fopen('large-file.csv', 'r');
while (($line = fgetcsv($handle)) !== false) {
yield $line;
}
fclose($handle);
});
// Eloquent 的 cursor() 返回 LazyCollection
$lazy = User::where('is_active', true)->cursor();
大数据批处理
use App\Models\Order;
// 逐条处理全部订单(内存友好)
Order::cursor()
->filter(fn ($order) => $order->amount > 10000)
->each(fn ($order) => $order->sendConfirmationEmail());
Order::where('status', 'completed')
->cursor()
->filter(fn ($order) => $order->amount > 10000)
->each(fn ($order) => $order->sendConfirmationEmail());
cursor() 逐条从数据库取回,处理大量记录时可大幅节省内存。但数据库连接会持续到处理完成。
用 take 与 skip 分页
$lazy = LazyCollection::make(function () {
foreach (range(1, 1000000) as $i) {
yield $i;
}
});
// 跳过前 1000,取接下来的 100
$page = $lazy->skip(1000)->take(100)->values();
| 方法 | 说明 |
|---|
collect($array) | 创建集合 |
map($callback) | 转换每个元素 |
filter($callback) | 过滤 |
reject($callback) | 排除匹配 |
first($callback) | 满足条件的第一个 |
pluck($key) | 抽取指定键 |
groupBy($key) | 按键分组 |
sortBy($key) | 升序 |
sortByDesc($key) | 降序 |
each($callback) | 逐个执行(副作用) |
flatMap($callback) | 映射并拍平 |
reduce($callback, $initial) | 聚合为单个值 |
chunk($size) | 按大小分块 |
values() | 重排索引 |
sum($key) | 求和 |
count() | 计数 |
集合在可读性上远胜数组函数。// 使用数组函数
$result = array_values(array_filter(
array_map(fn ($u) => $u['name'], $users),
fn ($name) => strlen($name) > 2
));
// 使用集合
$result = collect($users)
->pluck('name')
->filter(fn ($name) => strlen($name) > 2)
->values()
->all();
- 普通集合:几百到几千条数据,直观简洁。
- LazyCollection:数万条以上大数据,节省内存。搭配
cursor() 效果最佳。
- Eloquent 的
get() 返回普通集合,cursor() 返回 LazyCollection。