Collection::countBy
Tallies how many times each value occurs, optionally grouping by a key or computed value.
countBy(callable|string|null $countBy = null): Collection- Added in Laravel 5.8
Parameters
| Parameter | Type | Description |
|---|---|---|
$$countByoptional | (callable(TValue, TKey): (array-key|\UnitEnum))|string|null= null | A key to read from each item, or a closure returning the value to tally by. Omit it to tally the items themselves. |
Return value
Collection<array-key, int>A new collection keyed by the tallied value, with the number of occurrences as each value.
Errors
TypeError when the tallied value is not usable as an array key, such as an array or a plain object.
Changelog
- PHP 5.8Introduced on both Collection and LazyCollection.
- PHP 10.xPassing a string key began resolving through data_get(), so model attributes and dot paths work as well as array keys.
countBy() produces a frequency table: keys are the distinct values, values are how many times each appeared. It is array_count_values() generalised to work on a key or a closure.
Three shapes
With no argument it tallies the items themselves — the classic word-frequency one-liner. With a string it reads that key from every item via data_get(), so countBy('status') works on arrays, objects and Eloquent models, and dot paths reach into nesting. With a closure it tallies whatever the closure returns, which is how you bucket by a derived value such as an email domain or a rounded price band.
Result ordering
The result is in first-seen order, not sorted by count. Chain ->sortDesc() (or sort()->reverse()) before take() to get a genuine top-N; skipping that is the most common bug on this method.
Keys are coerced
Whatever you tally becomes an array key, so PHP's key rules apply: true becomes 1, null becomes '', floats truncate to integers, and numeric strings become integers. That means '1' and 1 merge into one bucket. Cast to a deliberate string in the closure when the distinction matters. Returning an array or an object raises a TypeError.
Example
When to use it
- Build a word-frequency table from a body of text.
- Count orders per status without a GROUP BY query.
- Bucket signups by email domain or by day of the week.
Frequently asked questions
How do I get the most frequent value from countBy()?
Chain sortDesc() before take(), because countBy() returns results in first-seen order rather than by count. $collection->countBy()->sortDesc()->take(5) gives a genuine top five.
What is the difference between countBy() and groupBy()?
countBy() returns a count per bucket, while groupBy() returns the actual items in each bucket. Use groupBy()->map->count() only if you need both the items and the tallies.
Why did countBy() merge values that were different?
Because the tallied value becomes an array key, so PHP coerces it — true and '1' both become the integer 1, and null becomes an empty string. Return an explicit string from a closure to keep the buckets apart.
More examples
Why sortDesc() matters for a top-N
The second dataset exposes the bug: 'go' appears once but was seen first, so take(2) hands it back as a top result. sortDesc() orders by the tally before take() slices, which is the only correct way to get a top-N.
<?php
$tags = collect(['php', 'js', 'php', 'go', 'php', 'js', 'rust']);
// Insertion order — 'php' happens to be first here, but only by luck.
print_r($tags->countBy()->all());
// Array ( [php] => 3 [js] => 2 [go] => 1 [rust] => 1 )
// Wrong: takes the first two DISTINCT values, not the two most common.
print_r($tags->countBy()->take(2)->all());
// Array ( [php] => 3 [js] => 2 )
$reordered = collect(['go', 'php', 'php', 'php', 'js', 'js']);
print_r($reordered->countBy()->take(2)->all());
// Array ( [go] => 1 [php] => 3 ) <- 'go' is not a top tag
// Right:
print_r($reordered->countBy()->sortDesc()->take(2)->all());
// Array ( [php] => 3 [js] => 2 )Tallying models and coerced keys
The first two blocks tally models by attribute and by a formatted date. The third shows key coercion in action: true, 1 and '1' all become the integer key 1, merging three distinct inputs into one bucket of 3. Returning a deliberate string from the closure keeps them separate. Needs a booted application with a database.
<?php
$orders = Order::whereMonth('created_at', 3)->get();
// String key resolves through data_get() on the model.
print_r($orders->countBy('status')->all());
// Array ( [paid] => 118 [pending] => 9 [refunded] => 4 )
// Bucket by weekday using a closure.
print_r($orders->countBy(fn (Order $o) => $o->created_at->format('D'))->all());
// Array ( [Mon] => 24 [Tue] => 19 ... )
// Key coercion: booleans and numeric strings collapse.
print_r(collect([true, 1, '1', false, 0])->countBy()->all());
// Array ( [1] => 3 [0] => 2 )
// Cast deliberately when the types must stay apart.
print_r(
collect([true, 1, '1'])
->countBy(fn ($v) => get_debug_type($v))
->all()
);
// Array ( [bool] => 1 [int] => 1 [string] => 1 )
Discussion