2

I am converting certain php data to json for use in my front end javascript.

I have an array of product variants, an array of images from a media field that is defined on my variants, and an array of the url attributes of that asset field. All this on the twig side works as expected.

However, when I log the js objects post-conversion, they seem to be missing some attributes. Notably, the variant objects in js don't have the variantMedia attribute, and the image objects don't have the url attribute, even though on the twig side the map filter is able to access those attributes just fine.

Why is this? Are some element properties "dynamic" in the sense that craft generates them on the fly? Is there a way to make these element fields resolve to properties on the source php array such that json_encode will capture them?

Minimal working example of code:

{% do craft.app.elements.eagerLoadElements(
  className(product),
  [product],
  [['variants.variantMedia']]
) %}
{% set variants = product.variants %}
{% set variantMedia = product.variants|map(variant => variant.variantMedia[0] ?? null)|filter(media => media is not null) %}
{% set variantMediaURL = variantMedia|map(media => media.url ?? null)|filter(media => media is not null) %}

<script>
    var variants = {{ to_json(variants) }}

    console.log(variants.map(v => v.variantMedia))
    var media = {{ to_json(variantMediae) }}
    console.log(media)

    console.log(media.map(m => m.url))
    var url = {{ to_json(variantMediaeURL) }}
    console.log(url)
</script>

to_json is just a twig macro for converting php arrays to json as defined here

{% macro to_json(php_array) %}
  {{ php_array|json_encode|replace("'", "&#39;")|raw }}
{% endmacro %}

Console logs, where attributes are missing:

//  console.log(variants.map(v => v.variantMedia)) - list is empty when generated through js map function
[null, null]
//  console.log(media) - list is non-empty when generated through twig map
[
    {
        "id": 51,
        ... // attributes trimmed
        "newFolderId": null,
        "tempFilePath": null,
        "avoidFilenameConflicts": false,
        "suggestedFilename": null,
        "conflictingFilename": null,
        "deletedWithVolume": false,
        "keepFileOnDelete": false
    }
]
// console.log(media.map(m => m.url)) - list is empty when generated through js map function
[null]
//  console.log(url) - list is non-empty when generated through twig map
["https://mystore.com/uploads/images/test-image.jpeg"]

2 Answers 2

2

The reason that you're missing certain attributes when JSON-encoding the raw element instances, is that these aren't real properties on the element classes. In short, when you do something like {{ product.url }} in Twig, what happens under the hood is that the product element class' so-called "magic" __get() method ends up calling a getUrl() method, and returns its value. Which explains why the url attribute is missing when you JSON-encode the products and variants, since only properties and not methods get included when you serialize a class instance.

Rather than JSON-encoding the raw elements, you'll probably have a bit more luck by converting them to arrays first, via the elements' toArray() method – which is provided by Yii to all models in Craft (elements basically being extravagant models). The toArray() method should drill down into the various getter methods, which should include most of the attributes you're missing (such as the url attribute).

A clean way to add the toArray() call in your current code could be via the |map Twig filter:

{% set variants = variants|map(variant => variant.toArray()) %}
<script>
    var variants = {{ to_json(variants) }}
    ...
</script>

To avoid the JSON being ridiculously huge by outputting the full element arrays with all of their attributes, you can consider passing an array of just the attributes you need, to the toArray() method:

{% set variants = variants|map(variant => variant.toArray([
    'title', 
    'url', 
    'variants'
])) %}

This even works recursively, so assuming you just needed the title and URLs for the products, and only the title and URLs for their variants as well, something like this should do it:

{% set variants = variants|map(variant => variant.toArray([
    'title', 
    'url', 
    'variants.title', 
    'variants.url'
])) %}
1
  • 1
    Thanks, I suspected something like this was the case, but wasn't sure how to verify or work around it. I actually ended up using the gql() function to create a query that got me what I needed, but this way definitely seems more appropriate. Feb 18 at 3:20
1

I ran into a situation similar to this, and without seeing exactly how you're querying these things, you may be running into an eager loading issue. Check out the documentation here: https://craftcms.com/docs/3.x/dev/eager-loading-elements.html#accessing-eager-loaded-elements

You may just need to use the "with" function to load the necessary fields?

I'm new to Craft, but that solved the issue I was having with unexpected "null" fields.

1
  • I am eager loading that field, so I don't think that's the issue here. I updated my question to include the code doing the eager loading. Feb 13 at 17:53

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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