« 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!