<p>As noted in the comments, the problem came from the <code>merge</code> 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.</p>
<p>If you solve this using a more functional, declarative approach, you reduce the likelihood of errors like this one.</p>
<p>Something like this:</p>
<pre class="lang-twig prettyprint-override"><code>{% set markers = [] %}
{% for product in products %}
    {% set categories = product.categories.level(1).all() %}
    {% set data = categories|map(category =&gt; {
        id: product.id,
        title: product.title,
        category: category.slug,
    }) %}
    {% set markers = markers|merge([data]) %}
{% endfor %}
</code></pre>
<ul>
<li>By using <code>level(1)</code>, you remove the need for the additional if-statement and you need to load less data from the database.</li>
<li>By using <code>map</code> 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).</li>
<li>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.</li>
</ul>
<p>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.</p>

CC BY-SA 4.0
2022-11-21T23:14:36.630Z
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
CC BY-SA 4.0
2022-11-22T08:52:34.517Z
@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](https://craftcms.com/docs/4.x/dev/eager-loading-elements.html#defining-custom-parameters-on-eager-loaded-elements). 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
CC BY-SA 4.0
2022-11-21T16:44:22.960Z
2022-11-21T16:44:22.960Z
MoritzLost
<p>I have a loop like this:</p>
<pre><code>{% 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 %}
</code></pre>
<p>and then in the macro:</p>
<pre><code>{% set properties = {
    id: product.id,
    title: product.title,
    category: category.slug
} %}

{{ properties|json_encode|raw }}
</code></pre>
<p>A lot of the products appear in multiple categories and I want <code>properties</code> to contain an item for every product/category combination.</p>
<p>At the moment, if I loop directly through the products then categories, I have an example like:</p>
<pre><code>Product Example 1 / Category A
Product Example 1 / Category B
</code></pre>
<p>but the output of the <code>markers</code> only contains one instance, e.g.</p>
<pre><code>[{&quot;id&quot;:&quot;111&quot;,&quot;title&quot;:&quot;Product Example 1&quot;,category&quot;:&quot;Category B&quot;}]
</code></pre>
<p>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.</p>
<p>Edit: more context</p>
<p>I'm using <code>if category.level == 1</code> instead of <code>level(1)</code> because <code>products</code> is actually an array that comes from a plugin with the query constructed like this:</p>
<pre><code>    $products = Entry::find()
        -&gt;section('products')
        -&gt;with([
            'categories',
            'images',
        ])
        -&gt;limit(-1)
        -&gt;status('live')
        -&gt;all();
</code></pre>
<p>Craft doesn't let me apply <code>level</code> to the array and I couldn't figure out how to include it in the query.</p>

CC BY-SA 4.0
2022-11-21T11:30:33.280Z
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
CC BY-SA 4.0
2022-11-21T11:41:29.247Z
"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
<array><merge>
Prevent merge from removing data
# Prevent merge from removing data

(Asked by: username_0 on 2022-11-21)

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.

**Tags:** array, merge

> **Comments:**
> **username_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.  
> **username_0**: "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!  

---

## 1 answers

### Answer by username_1 on 2022-11-21. Score: 3 | Accepted answer

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.

> **Comments:**
> **username_0**: 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.  
> **username_1**: @username_0 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](https://craftcms.com/docs/4.x/dev/eager-loading-elements.html#defining-custom-parameters-on-eager-loaded-elements). 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.  

---

