Collections & Helpers
map, filter, groupBy, pluck and friends, plus the Str, Arr and date helpers you use daily.
A Collection is Laravel's wrapper around a PHP array with a hundred-odd chainable methods. Every Eloquent get() returns one, and collect([...]) turns any array into one. Add the Str and Arr helpers and Carbon dates, and you have the toolkit behind most day-to-day Laravel code.
This lesson shows the methods you'll use constantly, on the real task data behind this site. Code captioned with a file path is copied from this repository; blocks marked Illustrative — not in this app run on the same data but aren't in the codebase.
What a Collection is
Illustrative — not in this app.
$tasks = Task::with('project')->get(); // Illuminate\Database\Eloquent\Collection
$names = collect(['Ava', 'Ben', 'Chloe']); // Illuminate\Support\Collection
$tasks->count(); // 20
$tasks->first(); // a Task model
$tasks->isEmpty(); // false
$tasks->toArray(); // plain PHP arrays
Methods like map() and filter() return a new collection and never modify the original, so you can chain them into a readable pipeline:
Illustrative — not in this app.
$overdueTitles = Task::with('project')->get()
->filter(fn (Task $task) => $task->due_on?->isPast() && ! $task->isComplete())
->sortBy('due_on')
->map(fn (Task $task) => "{$task->project->name}: {$task->title}")
->values();
Task::where('priority', 'high')->get() asks MySQL for only the matching rows; Task::all()->where('priority', 'high') loads every row into PHP first. Collections are for shaping data you already have, or data too awkward to express in SQL.
Try the methods on live data
The explorer loads this app's 20 seeded tasks from /api/demo/tasks and runs each method in JavaScript the same way Laravel would. Pick a method to see the PHP and its result.
map, filter and reject
map()transforms every item.filter()keeps items where the callback returns true; with no callback, it removes falsy values.reject()is the opposite offilter().
The demo query-count endpoint uses nested map() to build its response:
app/Http/Controllers/Api/QueryCountController.php
$rows = $projects->map(fn (Project $project) => [
'project' => $project->name,
'tasks' => $project->tasks->map(fn ($task) => [
'title' => $task->title,
'assignee' => $task->assignee?->name,
]),
]);
filter() keeps the original keys, so the result can be [0 => …, 3 => …]. Call ->values() before returning JSON if you need a list rather than an object.
pluck, keyBy and mapWithKeys
pluck() pulls one field out of every item; with a second argument it builds a key => value map. The CMS admin uses it to fill dropdowns:
app/Filament/Resources/PostResource.php
->options(fn () => BlogType::orderBy('name')->pluck('name', 'id'))
mapWithKeys() lets you choose the key and value yourself. The demo seeder uses it to create users and tags and keep them addressable by name:
database/seeders/TutorialDemoSeeder.php
$tags = collect(['Frontend', 'Backend', 'Bug', 'Design', 'DevOps'])
->mapWithKeys(fn (string $name) => [$name => Tag::firstOrCreate(
['slug' => Str::slug($name)],
['name' => $name],
)]);
Later it picks some of them by key and plucks their ids, which is exactly what attach() needs:
database/seeders/TutorialDemoSeeder.php
$task->tags()->attach($tags->only($taskTags)->pluck('id'));
contains, search and first
These answer "is it there?" and "where is it?". The seeder that added the tutorials section to this site's homepage uses both:
database/seeders/HomeTutorialsBlockSeeder.php
if (collect($blocks)->contains('type', 'tutorials')) {
return;
}
$heroIndex = collect($blocks)->search(fn (array $block): bool => ($block['type'] ?? null) === 'hero');
$position = $heroIndex === false ? 0 : $heroIndex + 1;
Illustrative — not in this app.
$tasks->first(fn ($task) => $task->priority === TaskPriority::High); // first match or null
$tasks->firstWhere('title', 'Write API docs'); // shorthand
$tasks->contains(fn ($task) => $task->due_on?->isToday()); // bool
groupBy, countBy and partition
These split or summarise a collection, and come up whenever you build a report or a dashboard.
Illustrative — not in this app.
$byProject = $tasks->groupBy('project.name'); // ['Website Relaunch' => Collection, ...]
$perPriority = $tasks->countBy(fn ($t) => $t->priority->value); // ['high' => 5, 'medium' => 9, ...]
[$done, $open] = $tasks->partition(fn ($t) => $t->isComplete());
groupBy() accepts dot notation for nested fields and a closure for computed keys. partition() returns two collections, which array destructuring unpacks.
Sorting, sums and averages
Illustrative — not in this app.
$tasks->sortBy('due_on'); // ascending
$tasks->sortByDesc(fn ($t) => $t->tags->count());
$tasks->sortBy([['due_on', 'asc'], ['title', 'asc']]); // multiple columns
$projects->sum('tasks_count');
$projects->avg('open_tasks_count');
$tasks->max(fn ($t) => strlen($t->title));
each versus map
each() runs a callback for its side effects and returns the same collection; map() builds a new one. Return false from each() to stop early. On large tables, use Model::query()->each() or lazy() so Eloquent fetches rows in chunks instead of all at once.
Illustrative — not in this app.
Task::query()->open()->lazy()->each(function (Task $task) {
// processes one row at a time, 1000 per query behind the scenes
});
The Str helpers
Illuminate\Support\Str covers the string jobs you'd otherwise write by hand. The admin turns titles into URL slugs as you type:
app/Filament/Resources/PageResource.php
$set('slug', Str::slug($state));
And the demo seeder builds email addresses and throwaway passwords:
database/seeders/TutorialDemoSeeder.php
['email' => Str::slug($name, '.').'@demo.example'],
['name' => $name, 'password' => Str::random(64)],
Try the most common ones on your own text:
Illustrative — not in this app. Fluent strings chain like collections:
Str::of(' Build pricing calculator in Vue ')
->trim()
->lower()
->replace('vue', 'Vue')
->limit(20); // 'build pricing calcul...'
The Arr helpers and data_get
Arr works on plain arrays, especially nested ones such as config, JSON payloads and the CMS's blocks column.
Illustrative — not in this app.
$block = ['type' => 'hero', 'data' => ['title' => 'Demo Blog', 'image' => null]];
Arr::get($block, 'data.title'); // 'Demo Blog'
Arr::get($block, 'data.subtitle', ''); // '' (default)
Arr::only($block['data'], ['title']); // ['title' => 'Demo Blog']
Arr::pluck($blocks, 'type'); // ['hero', 'tutorials', 'blog_types', 'blog_posts']
data_get($page->blocks, '0.data.title'); // works on arrays and objects
Dates with Carbon
now(), today() and every date-cast model attribute are Carbon objects. Task::markComplete() stores the current time:
app/Models/Task.php
$this->update(['completed_at' => now()]);
and the date and datetime casts make $task->due_on and $task->completed_at Carbon instances, which is why the API resource can call ->toDateString() on them.
Illustrative — not in this app.
$task->due_on->isPast(); // bool
$task->due_on->diffForHumans(); // '3 days from now'
now()->addWeek()->startOfDay(); // next week, 00:00
$task->completed_at->format('j M Y H:i'); // '16 Oct 2026 15:24'
Task::whereDate('due_on', '<', today())->open()->count();
Key takeaways
- Eloquent queries return Collections;
collect()wraps any array. - Chain
filter,map,sortByandvaluesinto readable pipelines; each step returns a new collection. pluck,mapWithKeys,groupBy,countByandpartitioncover most reporting and dropdown needs.- Filter in SQL when you can, and use
lazy()oreach()on queries for large tables. Strhandles slugs, random strings and formatting;Arranddata_getread nested arrays safely.- Date casts give you Carbon objects, so date maths and formatting are one method call.