В Laravel 13.30 появился метод chunkBy(). Это удобное сокращение для самого распространённого сценария использования chunkWhile():
$products->chunkWhile(fn ($value, $key, $chunk) => $value->parent == $chunk->last()->parent);Теперь ту же операцию можно записать значительно короче:
$products->chunkBy('parent');До этого: chunkWhile() и сравнение
До Laravel 13.30 для такой задачи можно было использовать chunkWhile(). Метод принимает callback, которому передаются текущее значение, его ключ и формируемый в данный момент фрагмент. Новый фрагмент начинается, когда callback возвращает false:
$lineItems->chunkWhile(
fn ($value, $key, $chunk) => $value->order_id == $chunk->last()->order_id
);Самое интересное здесь это order_id, спрятанное внутри сравнения текущего значения с $chunk->last(). chunkBy() берёт ключ или callback и самостоятельно выполняет это сравнение:
$lineItems->chunkBy('order_id');Либо можно передать callback:
$lineItems->chunkBy(fn ($item) => $item->order_id);Ключ извлекается через data_get(), поэтому можно использовать точечную нотацию для доступа к вложенным массивам и объектам:
$users->chunkBy('address.city');Соседние элементы, а не все элементы с одинаковым значением
Важно не путать chunkBy() с groupBy(). Оба метода возвращают похожую структуру, но принципиально отличаются тем, как учитывают порядок элементов.
Например:
collect([1, 1, 2, 2, 1, 1])->chunkBy(fn ($v) => $v);
// [[1, 1], [2, 2], [1, 1]]В этом случае chunkBy() создаёт три группы. Две последовательности с 1 находятся в разных местах коллекции, поэтому они остаются отдельными фрагментами. groupBy() работает иначе:
collect([1, 1, 2, 2, 1, 1])->groupBy(fn ($v) => $v);
// [1 => [1, 1, 1, 1], 2 => [2, 2]]Здесь все элементы с одинаковым значением объединяются независимо от их положения в исходной коллекции.
Это не ограничение chunkBy(), которое нужно обходить. Именно такая работа с соседними элементами позволяет методу быть экономным. Если одинаковые значения, расположенные в разных частях коллекции, должны оказаться в одном фрагменте, исходные данные не соответствуют принципу работы chunkBy(). В таком случае их нужно сначала отсортировать либо использовать groupBy().
Ключи элементов внутри каждого фрагмента сохраняются:
collect(['a' => 1, 'b' => 1, 'c' => 2])->chunkBy(fn ($v) => $v);
// [['a' => 1, 'b' => 1], ['c' => 2]]Если внутри фрагмента нужен обычный список с последовательными индексами, можно вызвать values().
Потоковая обработка отсортированного запроса
chunkBy() доступен как для обычных коллекций, так и для LazyCollection. Поскольку метод наследует ленивое поведение chunkWhile(), при работе с ленивой коллекцией каждый новый фрагмент отдаётся сразу после изменения значения. В памяти при этом находится только текущий фрагмент.
Представим экспорт нескольких миллионов записей line_items в отдельные CSV-файлы для каждого заказа.
При использовании groupBy() все строки пришлось бы держать в памяти ещё до создания первого файла. В связке с курсором и chunkBy() максимальное потребление памяти будет зависеть только от самого большого отдельного заказа:
use App\Models\LineItem;
use Illuminate\Support\Facades\Storage;
LineItem::query()
->orderBy('order_id')
->orderBy('id')
->cursor()
->chunkBy('order_id')
->each(function ($items) {
$orderId = $items->first()->order_id;
Storage::disk('exports')->put(
"orders/{$orderId}.csv",
$items->map(fn ($item) => implode(',', [
$item->sku,
$item->quantity,
$item->unit_price,
]))->implode(PHP_EOL)
);
});orderBy('order_id') здесь нужен не для красоты. Это условие, на котором основана работа chunkBy(): база данных выполняет сортировку, используя индекс, а PHP разделяет поток данных на фрагменты по одной строке.
Такой же подход можно использовать при чтении лог-файла:
use Illuminate\Support\LazyCollection;
LazyCollection::make(function () {
$handle = fopen(storage_path('logs/laravel.log'), 'r');
while (($line = fgets($handle)) !== false) {
yield $line;
}
})
->chunkBy(fn ($line) => str_contains($line, 'ERROR') ? 'error' : 'other')
->each(function ($block) {
// Каждый блок содержит последовательную группу строк с ошибками
// или строк без ошибок.
});То же самое подходит для постраничного API или генератора, который читает CSV. Если источник данных упорядочен и его размер превышает доступную память, chunkBy() позволяет превратить обычную группировку в потоковую операцию.
Два важных нюанса
Сравнение выполняется нестрого.
Реализация сравнивает полученные значения через ==, а не ===:
collect(['1', 1, 1.0])->chunkBy(fn ($v) => $v);
// один фрагментДля значения, получаемого из столбца базы данных и имеющего стабильный тип, это обычно не проблема. Но при смешанных данных такое поведение может объединить фрагменты, которые вы ожидали получить отдельно.
Если необходимо, в callback можно вернуть нормализованное значение:
$rows->chunkBy(fn ($row) => (string) $row['code']);Два объекта также считаются равными при нестрогом сравнении, если они принадлежат одному классу и имеют одинаковые свойства. При группировке по value object это обычно соответствует ожидаемому поведению.
Resolver вызывается дважды для каждого элемента.
При каждой проверке границы chunkBy() получает значение текущего элемента, а затем повторно вычисляет значение последнего элемента текущего фрагмента.
Если callback выполняет дорогую операцию, например парсит дату или вычисляет хеш, значение лучше предварительно вычислить:
$entries
->map(fn ($entry) => [
$entry,
Carbon::parse($entry->logged_at)->toDateString()
])
->chunkBy(fn ($pair) => $pair[1]);Для обычного обращения к ключу или свойству это различие не имеет практического значения.