2

I am working on a site that needs to both list all categories and also pull out individual categories by their id.

I would like to query the DB once for all the categories (to improve performance), and then pull out which ones I want, but am struggling to find a solution.

{% set catNavigation = craft.categories.group('products').limit(null).find() %}

<h1><a href="{{ catNavigation.id(102).first.url }}">{{ catNavigation.id(102).first.title|raw }}</a></h1>
<h2>{{ catNavigation.id(126).first.title|raw }}</h2>

This is giving me an error.

Array to string conversion

So I guess I can't grab pieces of an element criteria model after using find.

If I don't use find, then I query the DB multiple times.

I thought about creating my own array of categories dynamically with twig, but that has not gone well.

Is there a way to accomplish what I'm after? It's as if I need to search within an array of data that has already been returned from the DB. Hope that makes sense.

3
  • 1
    If you've applied .find() (which you have), then you're no longer dealing with an Element Criteria Model. The find parses the ECM into an array of elements (in this case, an array of Category Models).
    – Lindsey D
    Commented Aug 10, 2016 at 17:13
  • I see, is there a way to grab individual categories from that array. Or would trying to search through an array of category models negate any performance increase gained by reducing queries?
    – mjr
    Commented Aug 10, 2016 at 17:28
  • Unless you have a serious amount of categories, looping through them a few times will almost certainly be more performant than having an X amount of additional database queries. You can even make use of the NM Break and Continue plugin in order to break out of the for loop as soon as you have a match. If that doesn't float your boat, I added an answer with a different approach below – it requires looping through all the categories, but only once :) Commented Aug 10, 2016 at 18:58

1 Answer 1

4

Your issue is that as soon as you call .find(), your ElementCriteriaModel returns an array of elements. In other words, the ElementCriteriaModel is gone, and it's no longer possible to add parameters (i.e. id()) or call other methods that you'd otherwise be able to call on that object (i.e. .first()).

On the other hand – like you say in your description; if you don't use .find() (keeping the ElementCriteriaModel around) you'll hit the database every time you use the .id() method.

A workaround could be to loop over the categories and add them to an indexed array (using the category IDs as keys) and then pull your individual categories from that index instead of the original array.

Important: One gotcha to note is that the |merge filter will actually renumber numeric keys, so you have to add an arbitrary character to the ID to retain the actual ID values (the example below uses an underscore prefix):

{# Get the categories #}
{% set categories = craft.categories.group('products').limit(null).find() %}

{# Create category index #}
{% set categoryIndex = {} %}
{% for category in categories %}
    {% set categoryIndex = categoryIndex|merge({('_'~category.id): category}) %}
{% endfor %}

{# Do your thing #}
<h1><a href="{{ categoryIndex['_102'].url }}">{{ categoryIndex['_102'].title|raw }}</a></h1>
<h2>{{ categoryIndex['_126'].title|raw }}</h2>

Note that when you want to pull a category from the index, categoryIndex['_102'] and categoryIndex._102 will both work – I just find that the former looks a bit cleaner, with the underscore in front of it (obviously you can use any character, it doesn't have to be an underscore).

Edit

As pointed out by @carlcs in the comments below, another approach to creating the index is to use the group filter. With this approach you'll avoid the underscore hack and the for loop. The gotcha here is that this filter will actually create an array of arrays, meaning you'll have to use categoryIndex[102][0] (note the [0] at the end) to access a specific category (you could also use categoryIndex[102]|first, of course):

{% set categories = craft.categories.group('products').limit(null).find() %}
{% set categoryIndex = categories|group('id') %}

<h1><a href="{{ categoryIndex[102][0].url }}">{{ categoryIndex[102][0].title|raw }}</a></h1>
<h2>{{ categoryIndex[126][0].title|raw }}</h2>
2
  • 1
    Good solution, I was just thinking that you might be able to achieve the same using the group filter, and It might even be possible to do so without this underscore hack that is required because of how merge works. categories|group('id')
    – carlcs
    Commented Aug 11, 2016 at 7:48
  • Good idea, @carlcs – the group filter is definitely an option, too. I'll edit my answer to include it. Commented Aug 11, 2016 at 11:24

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.