Laravel Collections: 10 Methods That Will Change How You Write PHP
Bryan Heath
If you've been writing Laravel for a while, you're probably comfortable with map, filter, and each. But Laravel's Collection class has over 100 methods, and some of the most powerful ones fly completely under the radar.
These ten methods will help you replace verbose loops and conditionals with expressive, readable pipelines that communicate intent instead of implementation.
1. sole() — Find Exactly One or Fail
When you expect a collection to contain exactly one matching item, sole() is stricter and safer than first(). It throws an exception if zero or multiple items match.
// Instead of this
$admin = $users->filter(fn ($user) => $user->is_admin);
if ($admin->count() !== 1) {
throw new \Exception('Expected exactly one admin.');
}
$admin = $admin->first();
// Do this
$admin = $users->sole(fn ($user) => $user->is_admin);
Use it anywhere ambiguity is a bug — finding a user by email, resolving a config value, selecting a default payment method. If your collection has zero or multiple matches, you want to know about it immediately, not silently grab the wrong one.
2. reduce() — Build Anything from a Collection
While map transforms each item independently, reduce carries an accumulator across every item. It's the Swiss army knife for building totals, nested structures, or any derived value.
$cart = collect([
['product' => 'Keyboard', 'price' => 89, 'qty' => 1],
['product' => 'Mouse', 'price' => 49, 'qty' => 2],
['product' => 'Monitor', 'price' => 399, 'qty' => 1],
]);
$total = $cart->reduce(
fn ($carry, $item) => $carry + ($item['price'] * $item['qty']),
0
);
// $total = 586
The key insight: if you find yourself initializing a variable before a foreach loop, reduce is almost always cleaner.
3. partition() — Split a Collection in Two
Need to separate items into two groups based on a condition? partition returns a pair of collections without iterating twice.
[$active, $inactive] = $users->partition(
fn ($user) => $user->is_active
);
This is cleaner than running filter and reject separately. Common uses include separating valid from invalid records, splitting billable from free accounts, or grouping tasks by completion status.
4. mapInto() — Transform Items into Class Instances
When you need to wrap raw data into value objects or DTOs, mapInto saves you from writing a closure.
class Temperature
{
public function __construct(
public readonly float $celsius
) {}
public function toFahrenheit(): float
{
return ($this->celsius * 9 / 5) + 32;
}
}
$readings = collect([22.5, 18.3, 25.1]);
$temperatures = $readings->mapInto(Temperature::class);
$temperatures->first()->toFahrenheit(); // 72.5
I reach for this constantly when hydrating domain objects from API responses or database rows. Cleaner than writing out the map closure every time.
5. pipeThrough() — Chain Transformation Classes
While pipe passes the entire collection through a single callback, pipeThrough sends it through a series of invokable classes. This is ideal for multi-step data processing.
class NormalizeEmails
{
public function __invoke($collection)
{
return $collection->map(
fn ($user) => $user->setAttribute(
'email',
strtolower(trim($user->email))
)
);
}
}
class RemoveDuplicatesByEmail
{
public function __invoke($collection)
{
return $collection->unique('email');
}
}
$cleaned = $users->pipeThrough([
NormalizeEmails::class,
RemoveDuplicatesByEmail::class,
]);
Each step is a testable, reusable unit. The pipeline reads like a recipe.
6. sliding() — Create Overlapping Windows
The sliding method creates a "sliding window" over your collection — useful for comparisons between adjacent items.
$prices = collect([100, 105, 98, 112, 107]);
$changes = $prices->sliding(2)->map(
fn ($window) => $window->last() - $window->first()
);
// [5, -7, 14, -5]
If you work with time-series data, stock prices, sensor readings, or anything where you need to compare adjacent values — this one's a lifesaver. Try implementing a moving average without it and you'll appreciate the difference.
7. undot() — Expand Dot-Notation Keys into Nested Arrays
Working with flat config arrays or form submissions that use dot notation? undot rebuilds the nested structure instantly.
$flat = collect([
'database.host' => 'localhost',
'database.port' => 3306,
'database.name' => 'forge',
'cache.driver' => 'redis',
'cache.ttl' => 3600,
]);
$nested = $flat->undot();
// [
// 'database' => ['host' => 'localhost', 'port' => 3306, 'name' => 'forge'],
// 'cache' => ['driver' => 'redis', 'ttl' => 3600],
// ]
The inverse, dot(), flattens nested arrays back to dot notation. Together they're powerful for config manipulation and data restructuring.
8. chunkWhile() — Group by Consecutive Condition
Unlike groupBy which groups all matching items regardless of position, chunkWhile only groups consecutive items that satisfy a condition.
$events = collect([
['type' => 'click', 'time' => 1],
['type' => 'click', 'time' => 2],
['type' => 'scroll', 'time' => 3],
['type' => 'scroll', 'time' => 4],
['type' => 'click', 'time' => 5],
]);
$sessions = $events->chunkWhile(
fn ($current, $key, $chunk) => $current['type'] === $chunk->last()['type']
);
// Three chunks: [click, click], [scroll, scroll], [click]
I first discovered this when building a session replay feature. It's perfect for grouping consecutive log entries, detecting streaks, or any scenario where the *sequence* matters as much as the values themselves.
9. whenNotEmpty() — Conditional Logic Without If Statements
Instead of checking if ($collection->isNotEmpty()) before operating on a collection, use whenNotEmpty to keep your pipeline fluent.
return $errors
->whenNotEmpty(fn ($errors) => throw ValidationException::withMessages(
$errors->toArray()
))
->whenEmpty(fn () => response()->json(['status' => 'ok']));
No more breaking out of your fluent chain just to check emptiness, then awkwardly jumping back in.
10. lazy() — Process Massive Collections Without Memory Limits
The lazy method converts an eager collection into a LazyCollection, enabling you to process millions of items using generators under the hood.
// Instead of loading everything into memory
$allUsers = User::all();
// Stream results lazily
User::cursor()->lazy()->each(function ($user) {
$user->notify(new AnnualSummary);
});
Combine lazy() with chunk() for even more control. If your app processes CSV imports, batch notifications, or large report generation, lazy collections will save you from hitting PHP's memory limit.
Putting It All Together
The real power of these methods is composition. Here's a real-world example that processes an order export:
$report = $orders
->partition(fn ($order) => $order->is_fulfilled)
->map(fn ($group) => $group
->sliding(2)
->map(fn ($pair) => $pair->last()->total - $pair->first()->total)
->reduce(fn ($carry, $change) => $carry + $change, 0)
);
Each method communicates a specific intent. Anyone reading this code can understand what it does without tracing through loop variables and conditional branches.
Start Using Them Today
You don't need to memorize all 100+ Collection methods. Start with one. Next time you write a foreach loop, pause and ask: is there a collection method that expresses what I actually mean?
The Laravel Collection documentation is worth reading end to end — at least once. You'll find methods you didn't know existed that solve problems you've been solving the hard way.