skip to content

What are higher-order messages on Laravel collections, such as $responses->each->markReviewed(), and where do they stop working?

level: middleimportance: should knowfreq 30%

answer

  1. a property that names a method
  2. HigherOrderCollectionProxy
  3. $c->sum->score, $c->each->save()
  4. one hop only
  5. Collection::proxy() adds methods

basics

~10 s

A higher-order message is a shortcut where a collection method name, used as a property, forwards the next property read or method call to every item: $responses->sum->score or $responses->each->markReviewed().

solid answer

~40 s

Reading a property such as `->each` or `->sum` on a collection triggers `__get`, which returns a `HigherOrderCollectionProxy` if the name is in the static `$proxies` list. The proxy turns the next property access or method call into a closure and passes it to the real method, so `$responses->filter->isComplete()` means `filter(fn ($r) => $r->isComplete())`. It works for array items too. Only listed methods qualify — in Laravel 13 `map`, `filter`, `reject`, `each`, `sum`, `groupBy`, `keyBy`, `partition`, `sortBy`, `unique` and others, but not `pluck` or `reduce`; any other name throws `Property [x] does not exist on this collection instance.` It is one hop only, cannot take comparisons, and `Collection::proxy('name')` registers extra methods.

code

php · 16 lines
php
<?php

use Illuminate\Support\Collection;

$answers = collect([
    ['department' => 'Sales', 'score' => 4],
    ['department' => 'IT',    'score' => 2],
    ['department' => 'Sales', 'score' => 5],
]);

$answers->sum->score;                     // 11
$answers->groupBy('department')->map->count(); // ['Sales' => 2, 'IT' => 1]

// $answers->pluck->score;  // Exception: Property [pluck] does not exist on this collection instance.

Collection::proxy('pluck'); // opt pluck in (usually in a service provider)

go deeper

for a junior

Recognise $collection->each->method() and $collection->sum->field as shorthand for the closure versions.

for a middle

Explain __get, the $proxies list and HigherOrderCollectionProxy, and name the cases that throw or silently misbehave.

for a senior

Keep higher-order messages to one clear hop in reviews, and catch ones hiding per-model queries such as each->delete() on large collections.

for a principal

Decide on a style rule for higher-order messages versus explicit closures so a codebase stays readable to newcomers and static analysis.

## What the syntax means Laravel collections let you write ```php <?php $responses->each->markReviewed(); $total = $responses->sum->score; $complete = $responses->filter->isComplete(); ``` instead of ```php <?php $responses->each(fn ($r) => $r->markReviewed()); $total = $responses->sum(fn ($r) => $r->score); $complete = $responses->filter(fn ($r) => $r->isComplete()); ``` The docs call these **higher-order messages**: the method name is accessed as a **dynamic property**, and whatever you do next is applied to each item. ## How it works inside 1. `$responses->each` is a property read on an object that has no `each` property, so PHP calls the collection's **`__get('each')`**. 2. `__get` checks the name against the static **`$proxies`** list. If it is there, it returns a **`HigherOrderCollectionProxy`** holding the collection and the method name. 3. On that proxy, a **property read** (`->score`) goes to the proxy's `__get`, which calls `$collection->sum(fn ($value) => …)` with a closure reading `$value['score']` for arrays or `$value->score` for objects. 4. A **method call** (`->markReviewed()`) goes to the proxy's `__call`, which builds a closure calling that method on each item with the same arguments — or statically, if the item is a class-name string. So `$responses->each->notify($message)` passes `$message` to every item's `notify()`. ## Which methods qualify In Laravel 13 the proxy list includes `average`, `avg`, `contains`, `doesntContain`, `each`, `every`, `filter`, `first`, `flatMap`, `groupBy`, `hasMany`, `hasSole`, `keyBy`, `last`, `map`, `max`, `min`, `partition`, `percentage`, `reject`, `skipUntil`, `skipWhile`, `sole`, `some`, `sortBy`, `sortByDesc`, `sum`, `takeUntil`, `takeWhile`, `unique`, `unless`, `until` and `when`. Not included: `pluck`, `reduce`, `count`, `mapWithKeys`, `values`. `$responses->pluck->score` therefore throws a plain `Exception` with the message `Property [pluck] does not exist on this collection instance.` You can register more with the static `Collection::proxy('methodName')`, typically in a service provider. ## Where they stop working | Attempt | What happens | Why | |---|---|---| | `$c->map->respondent->email` | exception on `email` | `map->respondent` already returned a collection; the second `->email` is a property read on that collection, and `email` is not a proxied method | | `$c->filter->score > 3` | filters by truthy `score`, then compares the resulting collection object with `3` | the proxy cannot capture operators; only the property read is forwarded | | `$c->sortBy->score` on array items missing `score` | an "Undefined array key" warning, which Laravel's error handler turns into an `ErrorException` | the proxy reads `$value['score']` directly, with no `data_get` default | | a nested path such as `respondent.email` | not supported by the property form | use the string form, e.g. `sortBy('respondent.email')`, which goes through `data_get` | The rule of thumb: a higher-order message covers **one property or one method call per item**. Anything with a condition, a default, a nested path or a second step needs a closure or the string form (`sum('score')`, `sortBy('respondent.name')`). ## Higher-order messages versus string arguments Many of the same methods also accept a **string key**: `sum('score')`, `sortBy('score')`, `groupBy('department')`, `unique('email')`, `keyBy('respondent_id')`. The two forms are not identical: | | Higher-order message | String argument | |---|---|---| | How the value is read | direct `$item->score` or `$item['score']` | `data_get($item, 'score')` | | Nested paths | no | yes, `respondent.email` | | Calls a **method** on each item | yes, `->filter->isComplete()` | no, a string is read as a key | | Missing key on an array item | error | `null` | So the string form is the safer choice for plain data and nested paths, while the higher-order form is the concise way to call a **method** on every item without writing a closure. Both compile down to an ordinary closure passed to the same method, so there is no performance difference worth arguing about. ## When to use them - **Good fits:** side-effect loops (`$responses->each->markReviewed()`), simple aggregates (`$group->sum->score`), boolean filters on a method (`->filter->isComplete()`), and aggregating grouped data (`->groupBy('department')->map->count()`). - **Poor fits:** anything a reader must decode, anything with arguments that differ per item, and hot loops where an explicit closure is clearer to profile. One more caution with models: `$responses->each->delete()` issues **one query per model** and fires model events for each, which is sometimes what you want and sometimes an accidental N-query loop that a single query-builder `delete()` would replace.

  • How does a Laravel higher-order message pass arguments to the method it calls on each item?
    The proxy's `__call($method, $parameters)` builds a closure that calls `$value->{$method}(...$parameters)` on each item, so `$responses->each->notify($alert)` hands the same `$alert` to every item's `notify()`. If the item is a class-name string, it calls the method statically instead.
  • Why does $answers->map->respondent->email fail on a Laravel collection?
    Only the first hop is proxied. `$answers->map->respondent` runs `map` and returns a new collection of respondents; `->email` is then a property read on that collection, whose `__get` only accepts proxied method names, so it throws. Use `$answers->map(fn ($a) => $a->respondent->email)` or `pluck('respondent.email')`.

saying these in an interview costs you the question

  • Thinks every collection method can be used as a higher-order message.
  • Chains two property hops and expects both to reach each item.
  • Writes ->filter->score > 3 and expects a numeric comparison per item.
  • Believes higher-order messages only work on Eloquent models, not arrays.
  • Says the proxy caches results between calls.