2026 Community Survey results are here! See how the Craft CMS community works. results are live!

Easier Eager Loading with Collections

The first place you'll want to apply collection support in Craft 4 is anywhere you're using eager loading. The result will be one block of code that you can use for both eager-loaded and lazy-loaded elements, reducing errors in your code, as well as code duplication.

Image

In Craft 4 we have access to col­lec­tions, pow­ered by Laravel’s Col­lec­tions class.

Col­lec­tions are a wrap­per for work­ing with data arrays. Think about it as an array with spe­cial meth­ods avail­able that make it eas­i­er to work with the data.

Col­lec­tions sup­port in Craft comes in a cou­ple of dif­fer­ent forms. First, all eager-loaded ele­ments are now returned as Col­lec­tion arrays by default. Sec­ond, there’s a new collect() method avail­able for ele­ment queries. This returns the results of the query as a Lar­avel Collection. 

The first place you’ll want to apply col­lec­tion sup­port is any­where you’re using eager load­ing. The result will be one block of code that you can use for both eager-loaded and lazy-loaded ele­ments, reduc­ing errors in your code, as well as code duplication.

For this arti­cle, we’ll focus soley on how to use col­lec­tions sup­port with eager-loaded ele­ments in Craft CMS 4

Always Eager-Ready #

Since ver­sion 2.6, Craft returns eager-loaded ele­ments as arrays, and we have to iter­ate over them as a stan­dard array. 

When eager load­ing an asset ele­ment, you’re prob­a­bly famil­iar with hav­ing to access the image itself via the index (entry.asset[0]) instead of via .one() since that .one() method isn’t avail­able on a stan­dard array.

You have to update your Twig code if you decide to eager load an ele­ment and it makes some code not reusable because if a query isn’t eager-load­ing the ele­ment, you can’t access it as an array via the index. 

So, how does the addi­tion of Lar­avel Col­lec­tions help with eager loading?

The code is more straight­for­ward and reusable.

Craft 4 returns all eager-loaded ele­ments as col­lec­tions instead of stan­dard data arrays. Because of this change, we don’t need to have a spe­cial case using an array index to access the element.

Let’s look at an exam­ple with a matrix field and iter­at­ing over the block. With­out eager-load­ing, our query looks like this:

{% set entries = craft.entries()  
 .section('blog')  
 .limit(25)
 .all() %}

 {# ... #}

{% for entry in entries %}
	{% for block in entry.body.all() %}  
		 {% include ["matrix/" ~ block.type, "matrix/default"] %}  
	{% endfor %}
{% endfor %}
 

We call .all() on the ele­ment query and then again on the matrix block to exe­cute the query on each loop.

But we are good devel­op­ers, and we want to avoid the [[n+1]] per­for­mance issue, so we eager-load the matrix field, so it’s ready and wait­ing for us when it’s time to iter­ate over the blocks.

{% set entries = craft.entries()  
 .section('blog')
 .with(['body'])
 .limit(25)
 .all() %}

 {# ... #}

{% for entry in entries %}
	{% for block in entry.body.all() %}  
		 {% include ["matrix/" ~ block.type, "matrix/default"] %}  
	{% endfor %}
{% endfor %}
 

If we run the code as-is, you’re prob­a­bly famil­iar with the error that is returned:

Since we are eager-load­ing the Matrix block called body, Craft returns an array for the Matrix data instead of a Matrix iter­able object. So, we can’t use .all() since that method isn’t avail­able on an array. 

So we have to remove it.

{% set entries = craft.entries()  
 .section('blog')
 .with(['body'])
 .limit(25)
 .all() %}

 {# ... #}

{% for entry in entries %}
	{% for block in entry.body %}  
		 {% include ["matrix/" ~ block.type, "matrix/default"] %}  
	{% endfor %}
{% endfor %}
 

Arguably, not the biggest code issue; how­ev­er, this block of code is only func­tion­al when the Matrix field is eager-loaded. In instances where it’s okay to lazy-load, we’ll need to use a dif­fer­ent ver­sion of this code block. So, again, it’s not the worst thing, but it also reduces our reusable code.

We can make our code uni­ver­sal whether we’re eager-load­ing or not by adopt­ing Col­lec­tions. We do that by adopt­ing the .collect() method on all ele­ment queries where we used .all() pre­vi­ous­ly and on matrix blocks.

{% set entries = craft.entries()  
 .section('blog')
 .limit(25)
 .collect() %}

 {# ... #}

{% for entry in entries %}
	{% for block in entry.body.collect() %}  
		 {% include ["matrix/" ~ block.type, "matrix/default"] %}  
	{% endfor %}
{% endfor %}
 

Even if we eager-load the Matrix block data, the .collect() works just fine since Craft 4 returns all eager-loaded ele­ments as Col­lec­tions anyway.

The Matrix block code is usable no mat­ter if it’s eager-loaded or not. Win-win!

{% set entries = craft.entries()  
 .section('blog')
 .with(['body'])
 .limit(25)
 .collect() %}

 {# ... #}

{% for entry in entries %}
	{% for block in entry.body.collect() %}  
		 {% include ["matrix/" ~ block.type, "matrix/default"] %}  
	{% endfor %}
{% endfor %}
 

Eager-loaded Assets #

Let’s look at an exam­ple with an asset, which is anoth­er place we have to change our code depend­ing on whether we are eager-load­ing or not.

When not eager-load­ing an asset ele­ment, we would typ­i­cal­ly call the one() method and then .url to get the asset URL (in this exam­ple, we’re dis­play­ing an image).

<img src="{{ entry.teaserImage.one().url() }}" alt="{{ entry.title }}"/>  

But if we want to eager-load that image, we need to adjust the Twig code to sup­port eager load­ing. Craft returns an array for the eager-loaded data, so we can’t use .one() any­more. That means access­ing the array’s data via an index and then tack­ing on the .url() method to get the URL of the request­ed asset.

<img src="{{ entry.teaserImage[0].url() }}" alt="{{ entry.title }}"/>

But the goal here is to cre­ate one block of code we can use whether or not we are eager-load­ing the ele­ment or not. Just like with the Matrix ele­ment ear­li­er, we can do that for this asset ele­ment, too.

So what used to be this for eager-loaded Assets in Craft 3:

{% set entries = craft.entries()  
 .section('blog')  
 .with(['body', 'teaserImage'])  
 .limit(25)  
 .all() %}

 {% for entry in entries %}
	{% if entry.teaserImage %}  
		 <img src="{{ entry.teaserImage[0].url() }}" alt="{{ entry.title }}"/>  
	{% endif %}
 {% endfor %}

Is now this for all instances of this asset ele­ment, whether it is eager-loaded or not.

{% for entry in entries %}
	{% if entry.teaserImage %}  
		<img src="{{ entry.teaserImage.collect().first().url() }}" alt="{{ entry.title }}"/>  
	{% endif %}
{% endfor %}

This block of code works whether we eager-load the asset ele­ment or not because:

  • We are con­vert­ing it to a Lar­avel col­lec­tion (even if it’s eager-loaded and Craft returns it as a Col­lec­tion by default)
  • then, we’re call­ing the Lar­avel Col­lec­tions first() method on it to get the first ele­ment in the array. It is func­tion­al­ly the same as adding the array index on eager-loaded assets in Craft 3 and prior.
  • Because we now have a sin­gle asset, we can call the spe­cial .url() method that Craft pro­vides to gen­er­ate the URL for the asset.

Here, the key is call­ing collect() on both the main ele­ment query and again on the asset query, so we are always work­ing with Lar­avel Col­lec­tions no mat­ter the sit­u­a­tion: eager-loaded or lazy-loaded.

There’s no rea­son you can’t use the array index in all instances, as long as you call .collect() on the ele­ment query, but I find it’s nicer and more Craft-Twig-like to use the chained methods.