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.
In Craft 4 we have access to collections, powered by Laravel’s Collections class.
Collections are a wrapper for working with data arrays. Think about it as an array with special methods available that make it easier to work with the data.
Collections support in Craft comes in a couple of different forms. First, all eager-loaded elements are now returned as Collection arrays by default. Second, there’s a new collect()
method available for element queries. This returns the results of the query as a Laravel Collection.
The first place you’ll want to apply collection support 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.
For this article, we’ll focus soley on how to use collections support with eager-loaded elements in Craft CMS 4.
Since version 2.6, Craft returns eager-loaded elements as arrays, and we have to iterate over them as a standard array.
When eager loading an asset element, you’re probably familiar with having to access the image itself via the index (entry.asset[0]
) instead of via .one()
since that .one()
method isn’t available on a standard array.
You have to update your Twig code if you decide to eager load an element and it makes some code not reusable because if a query isn’t eager-loading the element, you can’t access it as an array via the index.
So, how does the addition of Laravel Collections help with eager loading?
The code is more straightforward and reusable.
Craft 4 returns all eager-loaded elements as collections instead of standard data arrays. Because of this change, we don’t need to have a special case using an array index to access the element.
Let’s look at an example with a matrix field and iterating over the block. Without eager-loading, 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 element query and then again on the matrix block to execute the query on each loop.
But we are good developers, and we want to avoid the [[n+1]] performance issue, so we eager-load the matrix field, so it’s ready and waiting for us when it’s time to iterate 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 probably familiar with the error that is returned:
Since we are eager-loading the Matrix block called body
, Craft returns an array for the Matrix data instead of a Matrix iterable object. So, we can’t use .all()
since that method isn’t available 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; however, this block of code is only functional when the Matrix field is eager-loaded. In instances where it’s okay to lazy-load, we’ll need to use a different version of this code block. So, again, it’s not the worst thing, but it also reduces our reusable code.
We can make our code universal whether we’re eager-loading or not by adopting Collections. We do that by adopting the .collect()
method on all element queries where we used .all()
previously 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 elements as Collections anyway.
The Matrix block code is usable no matter 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 %}
Let’s look at an example with an asset, which is another place we have to change our code depending on whether we are eager-loading or not.
When not eager-loading an asset element, we would typically call the one()
method and then .url
to get the asset URL (in this example, we’re displaying 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 support eager loading. Craft returns an array for the eager-loaded data, so we can’t use .one()
anymore. That means accessing the array’s data via an index and then tacking on the .url()
method to get the URL of the requested asset.
<img src="{{ entry.teaserImage[0].url() }}" alt="{{ entry.title }}"/>
But the goal here is to create one block of code we can use whether or not we are eager-loading the element or not. Just like with the Matrix element earlier, we can do that for this asset element, 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 element, 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 element or not because:
first()
method on it to get the first element in the array. It is functionally the same as adding the array index on eager-loaded assets in Craft 3 and prior..url()
method that Craft provides to generate the URL for the asset.Here, the key is calling collect()
on both the main element query and again on the asset query, so we are always working with Laravel Collections no matter the situation: eager-loaded or lazy-loaded.
There’s no reason you can’t use the array index in all instances, as long as you call .collect()
on the element query, but I find it’s nicer and more Craft-Twig-like to use the chained methods.