1

I have a loop like this:

{% set markers = [] %}
{% for product in products %}
        {% for category in product.categories %}
            {% if category.level == 1 %}
                {% set data = formatData(product,category)|json_decode %}
            {% endif %}
        {% endfor %}
        {% set markers = markers|merge([data]) %}
    {% endif %}
{% endfor %}

and then in the macro:

{% set properties = {
    id: product.id,
    title: product.title,
    category: category.slug
} %}

{{ properties|json_encode|raw }}

A lot of the products appear in multiple categories and I want properties to contain an item for every product/category combination.

At the moment, if I loop directly through the products then categories, I have an example like:

Product Example 1 / Category A
Product Example 1 / Category B

but the output of the markers only contains one instance, e.g.

[{"id":"111","title":"Product Example 1",category":"Category B"}]

I know that it's because if the key already exists, its value is overridden. But I don't want it overridden, and I'm not sure how to prevent that.

Edit: more context

I'm using if category.level == 1 instead of level(1) because products is actually an array that comes from a plugin with the query constructed like this:

    $products = Entry::find()
        ->section('products')
        ->with([
            'categories',
            'images',
        ])
        ->limit(-1)
        ->status('live')
        ->all();

Craft doesn't let me apply level to the array and I couldn't figure out how to include it in the query.

2
  • 1
    What's the context for what you're trying to do here? I see multiple things that don't make any sense. You're only merging the markers variable after the last loop iteration, so only the last category will be included. Also, why are you encoding to JSON in your macro and then decoding again? The architecture makes no sense. If you provide the context for what you're trying to do, there's probably a better approach that could be suggested.
    – MoritzLost
    Commented Nov 21, 2022 at 11:30
  • 1
    "You're only merging the markers variable after the last loop iteration, so only the last category will be included." And that's where my problem was and I didn't see it because I'd been staring at it too long. I had that line in the wrong place!
    – Tyssen
    Commented Nov 21, 2022 at 11:41

1 Answer 1

3

As noted in the comments, the problem came from the merge filter being placed outside the for-loop. However, the deeper problem is the procedural code causing a lot of noise, making it difficult to debug problems like this.

If you solve this using a more functional, declarative approach, you reduce the likelihood of errors like this one.

Something like this:

{% set markers = [] %}
{% for product in products %}
    {% set categories = product.categories.level(1).all() %}
    {% set data = categories|map(category => {
        id: product.id,
        title: product.title,
        category: category.slug,
    }) %}
    {% set markers = markers|merge([data]) %}
{% endfor %}
  • By using level(1), you remove the need for the additional if-statement and you need to load less data from the database.
  • By using map instead of a for-loop, you more clearly communicate your intent to other developers reading the code and reduce the error surface (by making things like incorrect merging or off-by-one errors impossible).
  • Encoding to JSON and then decoding again is wholly unnecessary, introduces a lot of potential bugs and is bad for performance. JSON-encoding should only happen as the very last step before the output.

The first for-loop is still necessary because flattening arrays is awkward in Twig. This could be further optimized by putting this logic into a PHP function and providing it to Twig through a custom extension.

2
  • 1
    I've added a bit more to my original question that addresses your first point. There's actually quite a bit more going on with this project and I tried to only post the parts I thought were relevant to troubleshooting the issue as I thought posting more would make things more confusing, not less.
    – Tyssen
    Commented Nov 21, 2022 at 23:14
  • @Tyssen Ok, got it, there's always more ^^ It should be possible to apply parameters to eager-loaded elements like this: ['categories', { level: 1, }]. See the documentation. Though I haven't tried that yet. By the way, if all that encoding and decoding is because the formatData macro is a shared piece of logic and you can't return an array from a macro, I would move that logic to PHP. Much simpler and makes it even more reusable.
    – MoritzLost
    Commented Nov 22, 2022 at 8:52

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.