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), получающее текущее значение, его ключ и формируемый чанк (chunk), и начинает новый чанк каждый раз, когда замыкание возвращает false:
$lineItems->chunkWhile(
fn ($value, $key, $chunk) => $value->order_id == $chunk->last()->order_id
);
Самая интересная часть этой строки — одно слово ( order_id ), скрытое среди сравнений значения с $chunk->last() . Метод chunkBy() принимает ключ или замыкание и формирует сравнение за вас:
$lineItems->chunkBy('order_id');
$lineItems->chunkBy(fn ($item) => $item->order_id);
Ключ обрабатывается с помощью функции data_get() , поэтому точечная нотация (dot notation) позволяет обращаться к вложенным массивам и объектам:
$users->chunkBy('address.city');
#По соседству, а не сгруппировано
Важно усвоить главное отличие от groupBy() : эти методы возвращают данные одинаковой структуры и различаются лишь требованиями к порядку элементов в вашей коллекции:
collect([1, 1, 2, 2, 1, 1])->chunkBy(fn ($v) => $v);
// [[1, 1], [2, 2], [1, 1]]
collect([1, 1, 2, 2, 1, 1])->groupBy(fn ($v) => $v);
// [1 => [1, 1, 1, 1], 2 => [2, 2]]
Метод chunkBy() создает три чанка, потому что две группы элементов 1 не находятся рядом друг с другом. В этом нет никакой ошибки, которую нужно исправлять — именно это свойство делает метод эффективным. Если элементы с одинаковым значением, находящиеся в разных частях коллекции, должны оказаться вместе, значит, данные не отсортированы так, как требуется для chunkBy() . В таком случае предварительно отсортируйте их или используйте groupBy() .
Ключи внутри каждого чанка сохраняются:
collect(['a' => 1, 'b' => 1, 'c' => 2])->chunkBy(fn ($v) => $v);
// [['a' => 1, 'b' => 1], ['c' => 2]]
Если вам нужен обычный список (массив с числовыми индексами), вызовите values() для чанка.
#Стриминг отсортированного запроса
Метод chunkBy() добавлен как в обычные коллекции, так и в LazyCollection . Поскольку chunkBy() наследует ленивость (laziness) от chunkWhile() , при работе с ленивой коллекцией он возвращает каждый чанк сразу же при изменении значения, никогда не удерживая в памяти больше данных, чем содержится в текущем чанке.
Представьте экспортеры CSV-файлов по каждому заказу для таблицы с несколькими миллионами позиций. При использовании groupBy() каждая строка сохраняется в памяти до того, как будет записан первый файл. При использовании курсора (cursor) и 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) {
// Each block is a consecutive run of error or non-error lines.
});
Или при работе с постраничным (paginated) API, либо с генератором, читающим CSV. Везде, где источник данных упорядочен и превышает объем доступной памяти, chunkBy() превращает группировку в потоковую операцию.
#Две вещи, которые стоит знать
Сравнение является нестрогим. Реализация сравнивает полученные значения с помощью оператора == , а не === :
collect(['1', 1, 1.0])->chunkBy(fn ($v) => $v);
// one chunk
Для колонок базы данных, возвращающих данные стабильного типа, это не проблема. Но при смешанных входных данных это может привести к объединению чанков, которые вы рассчитывали получить раздельно. При необходимости возвращайте из замыкания нормализованное значение:
$rows->chunkBy(fn ($row) => (string) $row['code']);
Два объекта считаются равными при нестрогом сравнении, если они принадлежат к одному классу и имеют равные свойства, что обычно и требуется при разбивке на чанки по объекту-значению (value object).
Резолвер запускается дважды для каждого элемента. Каждая проверка границы определяет текущий элемент и заново определяет последний элемент чанка. Если колбэк требует больших вычислительных затрат (например, парсинг даты или хеширование), предварительно вычислите значение:
$entries
->map(fn ($entry) => [$entry, Carbon::parse($entry->logged_at)->toDateString()])
->chunkBy(fn ($pair) => $pair[1]);
Для простого поиска по ключу или свойству это не имеет значения.
#Краевые случаи
Пустая коллекция возвращает пустую коллекцию. Один элемент возвращает один чанк, содержащий его. Как жадные, так и ленивые коллекции возвращают тот же класс, на котором они были вызваны, поэтому вызов chunkBy() для LazyCollection возвращает LazyCollection экземпляров LazyCollection .
Автор кода: @JosephSilber в #61357.
Комментарии (0)
Пока нет комментариев — будьте первым.