Laravel 教程

Group Adjacent Collection Items in Laravel with chunkBy()

发布
Group Adjacent Collection Items in Laravel with chunkBy() image

Laravel 13.30 introduces the chunkBy() method, which is a convenient shorthand for the most common chunkWhile() use case:

$产品 -> chunkWhile fn ($value, $key, $chunk) => $value -> 父母 == $块 -> 最后的 () -> parent);

Which, you can now write as simply:

$产品 -> chunkBy 'parent' (英文):

前: chunkWhile() and a Comparison

As shown above, before Laravel 13.30, you could use chunkWhile() to handle this. It takes a callback receiving the current value, its key, and the chunk being built, and it starts a new chunk whenever the callback returns false:

$lineItems -> chunkWhile
fn ($value, $key, $chunk) => $value -> 订单编号 == $块 -> 最后的 () -> 订单编号
(英文):

The interesting part of the line is one word ( order_id ) buried in a comparison between a value and $chunk->last()chunkBy() takes a key or a callback and builds the comparison for you:

$lineItems -> chunkBy 'order_id' (英文):
$lineItems -> chunkBy fn ($item) => $item -> order_id);

The key resolves through data_get() , so dot notation reaches into nested arrays and objects:

$用户 -> chunkBy 'address.city' (英文):

Adjacent, Not Grouped

The distinction from groupBy() is the thing to internalize, because the methods return the same shape and disagree only about your data's order:

收集 ([ 1 , 1 , 2 , 2 , 1 , 1 ]) -> chunkBy fn ($v) => $v);
// [[1, 1], [2, 2], [1, 1]]
收集 ([ 1 , 1 , 2 , 2 , 1 , 1 ]) -> groupBy fn ($v) => $v);
// [1 => [1, 1, 1, 1], 2 => [2, 2]]

chunkBy() method produces three chunks because the two runs of 1 are not next to each other. Nothing about that is a bug to work around; it is the property that makes the method cheap. If non-adjacent items with the same value need to end up together, the data is not sorted the way chunkBy() needs it, and either sort it first or use groupBy()

Keys are preserved inside each chunk:

收集 ([ '一个' => 1 , ‘b’ => 1 , ‘c’ => 2 ]) -> chunkBy fn ($v) => $v);
// [['a' => 1, 'b' => 1], ['c' => 2]]

称呼 values() on a chunk if you want a list.

Streaming a Sorted Query

chunkBy() method is added to normal collections as well as LazyCollection . Since chunkBy() inherits chunkWhile() 's laziness, on a lazy collection it yields each chunk as soon as the value changes and never holds more than the current chunk in memory.

Consider exporting a per-order CSV for a table with a few million line items. With groupBy() , every row is stored in memory before the first file is written. With a cursor and chunkBy() , the highest memory point will be during the largest single order:

使用 App\Models\LineItem ;
使用 照明\支持\立面\存储 ;
LineItem :: 询问 ()
-> orderBy 'order_id'
-> orderBy 'ID'
-> 光标 ()
-> chunkBy 'order_id'
-> 每个 功能 ($items) {
$订单号 = $items -> 第一的 () -> order_id;
贮存 :: 磁盘 'exports' ->
"orders/{ $订单号 }.csv" ,
$items -> 地图 fn ($item) => 内爆 “,” ,[
$item -> sku,
$item -> quantity,
$item -> unit_price,
])) -> 内爆 PHP_EOL
(英文):
});

orderBy('order_id') is not decoration. It is the contract chunkBy() runs on: the database does the sorting, in an index, and PHP does the splitting, one row at a time.

The same shape works over a log file:

使用 Illuminate\Support\LazyCollection ;
惰性收集 :: 制作 功能 (){
$handle = 打开 存储路径 'logs/laravel.log' ), 'r' (英文):
尽管 (($line = fgets ($handle)) !== 错误的 ){
屈服 $line;
}
})
-> chunkBy fn ($line) => str_contains ($line, 'ERROR' '错误' : '其他'
-> 每个 功能 ($block) {
// Each block is a consecutive run of error or non-error lines.
});

Or over a paginated API, or a generator reading a CSV. Anywhere the source is ordered and larger than memory, chunkBy() turns "group by" into a streaming operation.

Two Things Worth Knowing

The comparison is loose. The implementation compares the resolved values with == , 不是 === :

收集 ([ ‘1’ , 1 , 1.0 ]) -> chunkBy fn ($v) => $v);
// one chunk

For a database column that yields a consistent type this isn't an issue. For mixed input it can merge chunks you expected to be separate. Return a normalized value from the callback if needed:

$行 -> chunkBy fn ($row) => ( 细绳 ) $row[ '代码' ]);

Two objects compare loosely as equal when they are the same class with equal properties, which is usually what you want when chunking by a value object.

The resolver runs twice per item. Each boundary check resolves the current item and re-resolves the last item of the chunk. If the callback is expensive, say a date parse or a hash, precompute the value first:

$entries
-> 地图 fn ($entry) => [$entry, :: 解析 ($entry -> logged_at) -> 日期字符串 ()])
-> chunkBy fn ($pair) => $pair[ 1 ]);

For a plain key or property lookup this is irrelevant.

边缘情况

An empty collection returns an empty collection. A single item returns one chunk containing it. Both eager and lazy collections return the same class they were called on, so chunkBy()LazyCollection gives you a LazyCollectionLazyCollection 实例。

Contributed by @约瑟夫西尔伯#61357

保罗·雷德蒙德照片

Laravel News 特约撰稿人。全栈 Web 开发人员兼作家。

赞助

laravelcloud logo
Laravel 云

轻松创建和管理服务器,并在几秒钟内部署 Laravel 应用程序。

访问 Laravel Cloud
Exclude Vendor and Default Commands in `php artisan dev` image

Exclude Vendor and Default Commands in `php artisan dev`

阅读文章
The Laracon Archive image

The Laracon Archive

阅读文章
Laravel queue:work Now Prints Why the Worker Stopped image

Laravel queue:work Now Prints Why the Worker Stopped

阅读文章
Sidecar Brings Statamic's Control Panel to Your Existing Markdown Sites image

Sidecar Brings Statamic's Control Panel to Your Existing Markdown Sites

阅读文章
Collections chunkBy() and Storage Path Hardening in Laravel 13.30 image

Collections chunkBy() and Storage Path Hardening in Laravel 13.30

阅读文章
Forte: Parse and Rewrite Laravel Blade Templates image

Forte: Parse and Rewrite Laravel Blade Templates

阅读文章