Demystifying Laravel's Higher Order Messaging
I was browsing some Laravel project codebases when I came across code that looked like this:
$participants = collect([...]);
$team2Participants = $participants->filter->belongsToTeam(2);
$totalScore = $team2Participants->sum->score;
I had a good idea of what this code does, but I had never seen collection methods written like this before. When I call collection methods, I usually provide a callback, like this:
$currentYearOrders = $participants->filter(function ($participant) {
return $participant->belongsToTeam(2);
});
$currentYearAmount = $currentYearOrders->sum(function ($order) {
return $order->score;
});
This made me wonder: what is this syntax, and how does it work? Spoiler alert: it involves some magic!
After some research, I learned that Laravel calls this pattern Higher Order Messaging. To better understand how it works, I decided to reimplement it in vanilla PHP.
Creating a Basic Collection Class
First, I needed a collection class, so I created one with filter()
and sum()
methods:
class MyCollection
{
public function __construct(private array $items = []) {}
public function filter(callable $callback): static
{
return new static(array_filter($this->items, $callback));
}
public function sum(callable $callback): int|float
{
return array_reduce($this->items, fn($result, $item) => $result + $callback($item), 0);
}
}
This is a simple wrapper class. The filter()
method calls PHP’s array_filter()
and returns a new instance of MyCollection
with items filtered using the provided callback. The sum()
method iterates over array items and adds them up.
Creating a Basic Participant Class
Next, I created a basic Participant
data class. It holds the team number a participant belongs to and their score. It also has a method to check if a participant belongs to a specific team:
class Participant
{
public function __construct(private int $teamNumber, public int $score = 0) {}
public function belongsToTeam(int $teamNumber): bool
{
return $this->teamNumber === $teamNumber;
}
}
Testing the Functionality
Now, let’s check if our implementation works as expected:
$participants = new MyCollection([
new Participant(teamNumber: 1, score: 11),
new Participant(teamNumber: 1, score: 6),
new Participant(teamNumber: 2, score: 5),
new Participant(teamNumber: 2, score: 8),
new Participant(teamNumber: 2, score: 7),
new Participant(teamNumber: 3, score: 20),
]);
// Result: 3 items
$team2Participants = $participants->filter(fn($participant) => $participant->belongsToTeam(2));
// Result: 20
$totalScore = $team2Participants->sum(fn($participant) => $participant->score);
// Chaining the methods
$totalScore = $participants
->filter(fn($participant) => $participant->belongsToTeam(2))
->sum(fn($participant) => $participant->score);
Everything works as expected. We create six participants and filter out everyone except those from team 2. We are left with three participants. When we sum their scores, the result is 20. We can even chain sum()
after filter()
because filter()
returns a new instance of MyCollection
. With our basic collection functionality in place, it’s time for the fun part—replicating Laravel’s Higher Order Messaging.
Implementing Higher Order Messaging
When we call a property on Laravel’s collection that doesn’t exist, such as:
$collection->food;
Laravel triggers the magic __get()
method on the collection, which instantiates a class named HigherOrderCollectionProxy and passes $collection
and food
as parameters (HigherOrderCollectionProxy($collection, 'food')
). When we call a method or try to get a property on the HigherOrderCollectionProxy
class, it uses __get()
and __call()
magic methods to proxy that method or property call back to $collection
.
Let’s modify our MyCollection
class to add the __get()
method:
class MyCollection
{
// ...
public function __get(string $key)
{
return new HigherOrderCollectionProxy($this, $key);
}
// ...
}
Now, when $participants->filter
is called, it creates an instance of HigherOrderCollectionProxy
, passing the collection and method name (filter
) to that instance.
Creating the HigherOrderCollectionProxy Class
class HigherOrderCollectionProxy
{
public function __construct(private $collection, private string $collMethod) {}
public function __call(string $method, array $parameters)
{
return $this->collection->{$this->collMethod}(fn($value) => $value->{$method}(...$parameters));
}
public function __get(string $key)
{
return $this->collection->{$this->collMethod}(fn($value) => $value->{$key});
}
}
This is a simplified reimplementation of Laravel’s actual HigherOrderCollectionProxy
to demonstrate the concept. The key idea is:
- When calling
$participants->filter->belongsToTeam(2)
, the proxy class's__call()
magic method is triggered. This method then invokes thefilter
method on the collection, passing a callback that callsbelongsToTeam(2)
on each item in the collection. - Similarly, when calling
$team2Participants->sum->score
, the proxy class's__get()
magic method is triggered. This retrieves thescore
property from each item in the collection before summing the values.
// Higher Order Messages
$team2Participants = $participants->filter->belongsToTeam(2);
$totalScore = $team2Participants->sum->score;
// chaining
$currentYearAmount = $participants->filter->belongsToTeam(2)->sum->score;
Conclusion
With this approach, we’ve successfully replicated Laravel’s Higher-Order Messaging in vanilla PHP. Surprisingly, it was easier than expected, proving that true ingenuity lies in simplicity.
You can find the complete code in this GitHub Gist.