Коллекции
Lift\Support\Collection — это текучая, неизменяемая по умолчанию обёртка над массивами PHP. Она превращает цепочки вроде «отфильтровать активных пользователей, сгруппировать по стране, отобразить в email, посчитать каждую группу» в одно выражение.
Ментальная модель:
Collectionдляarray— то же, что поток для списка: сцепляемый объект, где каждый метод возвращает новую Collection с преобразованными данными. Мутация включается явно через несколько ясно названных методов.
Когда её использовать
- Вас тянет вложить
array_map(array_filter(array_values($x), …), …). - Вы пишете
foreachтолько чтобы вычислить одно итоговое значение. - Вам нужны
pluck,groupBy,keyBy,sortBy('field')и т. д. без ручной возни с ключами.
Для одноразовой работы (один array_map) оставайтесь с массивами PHP — Collection нужны для цепочек.
Демо за две секунды
use Lift\Support\Collection;
$activeEmails = Collection::make($users)
->filter(fn($u) => $u['active'])
->sortBy('name')
->pluck('email')
->values();
// $activeEmails — это Collection. Вернитесь к массиву, когда закончите:
$array = $activeEmails->all(); // ['[email protected]', …]
Создание
Collection::make(); // пустая
Collection::make([1, 2, 3]);
Collection::make(['a' => 1, 'b' => 2]);
new Collection($items); // то же самое
Преобразование — возвращает новую Collection
->map(fn($v, $k) => $v * 2)
->flatMap(fn($v) => [$v, $v]) // map + выравнивание на один уровень
->filter(fn($v) => $v > 0) // оставить совпадения; values() переиндексирует
->reject(fn($v) => $v > 0) // противоположность filter
->reduce(fn($acc, $v) => $acc + $v, 0) // возвращает аккумулятор (не Collection)
Примечание: filter() и reject() переиндексируют результат (последовательные целочисленные ключи). Используйте where(...), если хотите сохранить ключи.
Извлечение и нарезка
->first(); // первый элемент или null
->first(fn($v) => $v > 5); // первый совпадающий
->first(fn($v) => $v > 5, $default); // со значением по умолчанию
->last();
->last(fn($v) => $v > 5, $default);
->take(3); // первые 3
->take(-3); // последние 3
->skip(2); // отбросить первые 2
->slice(2, 5); // [2, 7)
->chunk(2); // Collection из Collection по 2 каждая
Группировка / создание ключей / извлечение
$users = [
['id' => 1, 'role' => 'admin', 'email' => 'a@x'],
['id' => 2, 'role' => 'user', 'email' => 'b@x'],
['id' => 3, 'role' => 'admin', 'email' => 'c@x'],
];
Collection::make($users)
->groupBy('role');
// [
// 'admin' => Collection [ user1, user3 ],
// 'user' => Collection [ user2 ],
// ]
Collection::make($users)
->keyBy('id');
// [
// 1 => user1,
// 2 => user2,
// 3 => user3,
// ]
Collection::make($users)
->pluck('email'); // ['a@x', 'b@x', 'c@x']
Collection::make($users)
->pluck('email', 'id'); // [1 => 'a@x', 2 => 'b@x', 3 => 'c@x']
groupBy и keyBy также принимают callback: keyBy(fn($u) => "user-{$u['id']}").
Сортировка
->sort(); // базовая по возрастанию
->sort(fn($a, $b) => $a['age'] <=> $b['age']);
->sortBy('age'); // по возрастанию по полю
->sortByDesc('age');
->sortBy(fn($u) => strtolower($u['name'])); // по вычисленному значению
->sortKeys();
->reverse();
Операции над множествами
->unique(); // дедупликация
->unique('email'); // дедупликация по полю
->flatten(); // рекурсивно
->flatten(1); // один уровень
->merge([10, 11]); // добавить в конец
->diff([2, 3]); // значения, которых нет в аргументе
->intersect([2, 3]); // значения, присутствующие в обоих
Ключи / значения / переворот
->keys(); // Collection ключей
->values(); // Collection значений (переиндексирована)
->flip(); // поменять местами ключи/значения (каждое значение должно быть хешируемым)
Поиск / проверка
->contains(42);
->contains(fn($v) => $v > 50);
->has('email'); // проверка ключа (не значения)
->where('status', 'active'); // фильтр, сохраняющий ключи
Агрегаты
->count();
->sum();
->sum('amount'); // по полю
->sum(fn($x) => $x['price'] * $x['qty']);// по callback
->avg();
->avg('rating');
->min();
->min('price');
->max();
->max('price');
->isEmpty();
->isNotEmpty();
Доступ и экспорт
$c->get('email'); // '[email protected]' или null
$c->get('email', '[email protected]');
$c->all(); // сырой нижележащий массив
$c->toArray(); // рекурсивно (JsonSerializable разворачиваются)
$c->toJson(); // строка JSON
$c->jsonSerialize(); // для json_encode()
Collection реализует JsonSerializable, Countable, IteratorAggregate и ArrayAccess, поэтому:
foreach ($collection as $key => $value) { … } // итерируема
count($collection); // работает
$collection[0]; // работает
return $collection; // обработчик маршрута автоматически кодирует в JSON
Изменяющие помощники — возвращают $this
Эти действительно мутируют на месте. Используйте умеренно; они для редкого случая, когда неизменяемость вредит производительности:
$c->push($value); // добавить в конец
$c->put($key, $value); // установить по ключу
$c->forget($key); // удалить
$c->each(fn($v, $k) => …); // foreach с ранним выходом, если callback возвращает false
$c->transform(fn($v) => $v * 2); // map на месте
Реальный пример — отчёт о продажах
$sales = Collection::make($orders)
->filter(fn($o) => $o['status'] === 'paid')
->groupBy(fn($o) => substr($o['paid_at'], 0, 7)) // 'YYYY-MM'
->map(fn(Collection $month) => [
'count' => $month->count(),
'revenue' => $month->sum('total'),
'top_country' => $month
->groupBy('country')
->map->count() // сокращение не поддерживается — делайте так:
->sortByDesc(fn($x) => $x)
->keys()
->first(),
])
->sortKeys();
return Response::json($sales);
Вывод — это карта месяц → статистика, целиком построенная без единого явного цикла.
Идиомы
Преобразовать элементы Paginator
$page = $db->table('posts')->paginate(1, 20);
$tags = Collection::make($page->items())
->pluck('tags')
->flatten()
->unique()
->values();
Сгруппировать строки по внешнему ключу (ручная жадная загрузка)
$users = $db->table('users')->whereIn('id', $userIds)->get();
$byId = Collection::make($users)->keyBy('id')->all();
foreach ($posts as $post) {
$post['author'] = $byId[$post['user_id']] ?? null;
}
Быстро что-то агрегировать
$avgRating = Collection::make($reviews)
->where('product_id', $productId)
->avg('rating');
Разбить на чанки для пакетной обработки
Collection::make($emails)
->chunk(100)
->each(fn(Collection $batch) => $mailer->sendBulk($batch->all()));
Заметки о производительности
- Каждый неизменяющий метод выделяет новую Collection с новым массивом. Для очень горячих циклов по миллионам элементов обычный
foreachбыстрее. sortBy('field')— этоO(n log n)с вызываемым компаратором — годится для тысяч, медленно для миллионов.Collectionне вычисляет лениво. Для ленивых конвейеров над генераторами пишитеforeachсyield.
Для типичных веб-нагрузок (десятки–тысячи строк) выигрыш в читаемости доминирует над потерей производительности.
Частые подводные камни
| Симптом | Причина | Исправление |
|---|---|---|
Результат имеет ключи вместо [0, 1, 2…] |
filter() переиндексирует, но where() и unique('field') сохраняют ключи |
Вызовите ->values() в конце, если нужен список. |
pluck('foo') на объектах возвращает null |
Объекты не предоставляют foo как публичное свойство (или элемент массива) |
Сделайте свойство публичным или сначала извлеките: map(fn($o) => $o->getFoo()). |
merge() перезаписал мои числовые ключи |
array_merge перенумеровывает целочисленные ключи |
Используйте семантику + вручную, если нужно их сохранить: $c->all() + $other. |
groupBy создаёт Collection из Collection, а не массивов |
Это так задумано — продолжайте цепочку или вызовите ->toArray(). |
|
first(fn ...) возвращает null для ложных, но валидных совпадений вроде 0 |
По умолчанию — null; вам может понадобиться сигнальное значение |
Передайте явное значение по умолчанию: first($cb, $sentinel). |
Шпаргалка
Collection::make($items)
->filter(fn($x) => $x['active'])
->map(fn($x) => $x['email'])
->unique()
->sort()
->values()
->all();
// Агрегаты
Collection::make($items)->sum('amount');
Collection::make($items)->groupBy('country')->map->count(); // (не поддерживается; см. реальный пример)
// Итерация
foreach (Collection::make($items) as $item) { … }
count($collection);
$collection[0];
return $collection; // сериализуется в JSON автоматически