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);
This is perfect for lookups where ambiguity is a bug — finding a user by email, resolving a config value, or selecting a default payment method.
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
This is especially powerful when building domain objects from API responses or database rows.
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]
This is invaluable for time-series data, detecting trends, calculating moving averages, or finding consecutive patterns in sequences.
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]
This is essential for session analysis, run-length encoding, grouping consecutive log entries by level, or detecting streaks in data.
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']));
This eliminates the pattern of breaking out of a collection pipeline just to check emptiness, then 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 application processes CSV imports, batch notifications, or large report generation, lazy collections are a game changer.
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.