« back published by Martin 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!