Laravel's higher order collection proxies

September 1, 2024

Higher-order collection proxies are a feature of Laravel that allows you to proxy methods to the underlying objects in a collection.

What the heck does that even mean?

Let's look at this simple example where we pull blocked users out of the database and send them an email informing them that they've been blocked.

Users::where('blocked', 1)->get()->each(function(User $user) {
$user->sendBlockedEmail();
});
Code highlighting powered by torchlight.dev (A service I created!)

This is a very simple implementation. We could clean it up by dropping the get, because each is implemented on the Eloquent Builder.

Users::where('blocked', 1)->each(function(User $user) {
$user->sendBlockedEmail();
});

However, get returns a Collection while the Builder's each does not. We want a Collection, so we'll restore the get and then call each on the Collection instead of the query builder.

Users::where('blocked', 1)->get()->each(function(User $user) {
$user->sendBlockedEmail();
});

This is where Higher Order Collection Proxies come in. Instead of calling a method named each, we're just going to chain a property named each. Like this:

Users::where('blocked', 1)->get()->each;

This is a magic property. If we were to dd the result here, we'd get back an instance of Illuminate\Support\HigherOrderCollectionProxy. (You can see a list of all these magic proxy properties here.)

The magic here is that any method you call on the proxy will be proxied (!) to the underlying collection item. So revisiting our initial example, here's what it would look like now:

Users::where('blocked', 1)->get()->each->sendBlockedEmail();

sendBlockedEmail is actually being called on each underlying User object. The neat part is that you can continue to chain collection methods on after that!

Users::where('blocked', 1)->get()
->each->sendBlockedEmail()
// Another collection method
->map(function(User $user) {
// ...
})
Code highlighting powered by torchlight.dev (A service I created!)

This can provide a super terse, readable collection chain. But! That's not all!

Advanced higher order collection proxies

Let's look at another example. This time using Laravel Dusk's browser automation.

Browser automation is a powerful tool for testing web applications. It allows you to simulate user interactions with your application, such as clicking buttons, filling out forms, and navigating between pages. Laravel Dusk is a browser automation tool that makes it easy to write and run browser tests for your Laravel applications.

We'll take a look at how you can use Laravel's higher-order collection proxies to make your code more readable and concise.

(Prefer to watch instead of read? Check out the video version of this article on YouTube: Poppin Off: Higher order collection proxies.)

In this example, suppose you are opening two different browsers in your test, doing the same thing in each browser to get it set up, and then doing different things. You could write this code like this:

public function testExample(): void
{
$this->browse(function (Browser $browser1, Browser $browser2) {
$browser1->resize(624, 696);
$browser1->move(12, 12);
 
$browser2->resize(624, 696);
$browser2->move(644, 12);
 
$browser1->visit('https://reverb.laravel.com');
$browser1->waitFor('figure');
$browser1->scrollIntoView('figure');
 
$browser2->visit('https://reverb.laravel.com');
$browser2->waitFor('figure');
$browser2->scrollIntoView('figure');
 
Sleep::for(10)->seconds();
}
}

In the example test we have two browsers, $browser1 and $browser2, that we are setting up in the same way. We are resizing and moving the browsers, visiting a URL, waiting for an element to appear, and scrolling to that element.

What I don't like here is that we're doing the same thing in each browser to prepare it.

Here's how we can make this a little more simplified:

Let's first collect $browser1 and $browser2 into a collection and save it to a variable called $both. Then we'll do dd($both) to see what we have.

public function testExample(): void
{
$this->browse(function (Browser $browser1, Browser $browser2) {
$both = collect([$browser1, $browser2]);
 
dd($both);
 
$browser1->resize(624, 696);
$browser1->move(12, 12);
 
$browser2->resize(624, 696);
$browser2->move(644, 12);
 
$browser1->visit('https://reverb.laravel.com');
$browser1->waitFor('figure');
$browser1->scrollIntoView('figure');
 
$browser2->visit('https://reverb.laravel.com');
$browser2->waitFor('figure');
$browser2->scrollIntoView('figure');
 
Sleep::for(10)->second();
}
}

Now when we run the test, we have two browsers that open up. From the terminal output, we can see that $both is a collection with two items in it, $browser1 and $browser2.

What if we just added each to the end of the collect statement? And this is not an each callback each() but just each.

public function testExample(): void
{
$this->browse(function (Browser $browser1, Browser $browser2) {
$both = collect([$browser1, $browser2])->each;
 
$dd($both);
 
$browser1->resize(624, 696);
$browser1->move(12, 12);
 
$browser2->resize(624, 696);
$browser2->move(644, 12);
 
$browser1->visit('https://reverb.laravel.com');
$browser1->waitFor('figure');
$browser1->scrollIntoView('figure');
 
$browser2->visit('https://reverb.laravel.com');
$browser2->waitFor('figure');
$browser2->scrollIntoView('figure');
 
Sleep::for(10)->second();
}
}

We run the test again and now we have an instance of HigherOrderCollectionProxy. And instead of using it right away, we just assign it to a variable!

We can call the duplicate actions on the HigherOrderCollectionProxy, $both, and it will apply the actions to each browser in the collection.

public function testExample(): void
{
$this->browse(function (Browser $browser1, Browser $browser2) {
$both = collect([$browser1, $browser2])->each;
 
$browser1->resize(624, 696);
$browser2->resize(624, 696);
$both->resize(624, 696);
 
// We still have access to the individual browsers!
$browser1->move(12, 12);
$browser2->move(644, 12);
 
$browser1->visit('https://reverb.laravel.com');
$browser2->visit('https://reverb.laravel.com');
$both->visit('https://reverb.laravel.com');
 
$browser1->waitFor('figure');
$browser2->waitFor('figure');
$both->waitFor('figure');
 
$browser1->scrollIntoView('figure');
$browser2->scrollIntoView('figure');
$both->scrollIntoView('figure');
 
Sleep::for(10)->second();
}
}

All cleaned up, it's a lot shorter and more readable:

public function testExample(): void
{
$this->browse(function (Browser $browser1, Browser $browser2) {
$both = collect([$browser1, $browser2])->each;
 
$both->resize(624, 696);
 
// We still have access to the individual browsers!
$browser1->move(12, 12);
$browser2->move(644, 12);
 
$both->visit('https://reverb.laravel.com');
$both->waitFor('figure');
$both->scrollIntoView('figure');
 
Sleep::for(10)->second();
}
}

Typehinting

If you don't like the fact that you lose the underlying type, you can typehint the Proxy:

public function testExample(): void
{
$this->browse(function (Browser $browser1, Browser $browser2) {
/** @var \Laravel\Dusk\Browser $both */
$both = collect([$browser1, $browser2])->each;
 
$both->resize(624, 696);
 
// We still have access to the individual browsers!
$browser1->move(12, 12);
$browser2->move(644, 12);
 
$both->visit('https://reverb.laravel.com');
$both->waitFor('figure');
$both->scrollIntoView('figure');
 
Sleep::for(10)->second();
}
}

When we run the test, the HigherOrderCollectionProxy is going to proxy under to every item in the collection. So it will run resize on all the items in the collection, for example.

We've simplified our code and made it more readable by using Laravel's higher-order collection proxies. It's also more maintainable, because if we want to change how we do something in both browsers, we only have to change it in one spot instead of two. Now imagine you've got 10 items in the collection and you'll see how valuable that is!

YouTube video

Check out the video version of this article on YouTube!

Me

Thanks for reading! My name is Aaron and I write, make videos , and generally try really hard .

If you ever have any questions or want to chat, I'm always on Twitter.

If you want to give me money (please do), you can buy my course on SQLite at HighPerformanceSQLite.com or my course on screencasting at Screencasting.com . On the off chance you're a sophomore at Texas A&M University, you can buy my accounting course at acct229.com .

You can find me on YouTube on my personal channel . If you love podcasts, I got you covered. You can listen to me on Mostly Technical .