« back published by @mmartin_joo on December 27, 2021
Laravel Higher Order Collections
We all know and love Laravel's Collection. It has all the necessary functions and it also comes with higher-order functions. If you don't know what I'm talking about, here's an example:
Order::whereUser($user)->get()->each->paid();
This is a shortcut for:
Order::whereUser()->get()->each(fn (Order $order) => $order->paid());
And this is the PHP7.4 equivalent for:
Order::whereUser()->get()->each(function (Order $order) {
$order->paid();
});
And the whereUser is a scope for:
Order::where('user_id', $user->id)
->get()
->each(function (Order $order) {
$order->paid();
});
Okay, let's get back to the original one:
Order::whereUser($user)->each->paid();
The each→paid() syntax is called higher-order function. And in this post we're gonna implement this. but first let's code some basic Collection stuff:
Basic collection
The basic collection functions are quite simple.
class Collection
{
public function__construct(private array $items)
{
}
#[Pure]public static function make(array $items): self
{
return new static($items);
}
public function each(callable $callback): self
{
foreach ($this->items as $key => $item) {
$callback($item, $key);
}
return $this;
}
public function map(callable $callback): self
{
$items = array_map($callback, $this->items);
return Collection::make($items);
}
public function filter(callable$callback): self
{
$items = array_filter($this->items, $callback);
return Collection::make($items);
}
public function dump(): self
{
var_dump($this->items);
return $this;
}
#[NoReturn]public function dd()
{
var_dump($this->items);
exit;
}
}
The basic collection methods: each, map, filter. They are really easy. In the map and filter we can even use the PHP array_map and array_filter helpers. The only important thing, that we need to return a Collection from each methods. In the each() we return $this, because it does not modify the elements. In the other methods we are making a new instance with the transformed elements. We can use our Collection like this:
Collection::make([1,2,3,4,5])
->filter(fn($item) => $item % 2 === 0)
->map(fn($item) => $item * 2)
->dump();
The output is:
[2, 8]
So it works. In fact the Laravel collection has a very similar, basic implementation like this one.
Higher-order functions
The first step is to implement the higher-order functions. To make it happen we need a new class that is "higher-order". It means that it holds a Collection instance and delegates to it. Because we use these higher-order functions by calling a property, like $items→each→doSomething() we need a magic getter on the Collection that will create a new higher-order class:
#[Pure]public function __get(string $name): HigherOrderCollectionProxy
{
return new HigherOrderCollectionProxy($name, $this);
}
I use the HigherOrderCollectionProxy as in the Laravel repo. It needs the name of the property and an instance of the collection. Keep in mind that the $name holds a value like 'each' or 'map'. So the name of the collection method. Now all we have to do is to write the actual class:
class HigherOrderCollectionProxy
{
public function __construct(
private readonly string $property,
private readonly Collection $collection
) {}
public function __call(string $name, array $arguments): Collection
{
return $this
->collection
->{$this->property}(fn($item) => $item->{$name}($arguments));
}
}
Okay, it's a little bit 'meta' but let me explain. When we do this:
$orders->each
In the HigherOrderCollectionProxy we have the following values:
$this->property = 'each';
$this->collection = [new Order, new Order];
When we call something after the each:
$orders->each->paid()
The __call method gets called:
__call('paid', []);
And after that:
return $this
->collection // This hold the orders
->{$this->property} // We call each. $orders->each
(fn ($item) => $item->{$name}($arguments)); // And the callback of each will execute the 'paid' method
So the method each will get a callback that calls paid() on the $item which is an Order. If I swap the variables it looks like this:
(fn ($order) => $order->paid([]));
Higher-order functions looks a bit magical, but as you can see we implemented them in 20 lines of code using 2 magic method and some dynamic property access. They are similarly simple in the Laravel code base, you can look them up in the Illuminate\Collections\HigherOrderCollectionProxy class.
That's it for today, I hope you have learned something new!