Cms: Matrices and Entries queries are over-written when filtered

Created on 5 Dec 2017  Â·  10Comments  Â·  Source: craftcms/cms

Description

Matrices and entries queries are over-written when filtered.

Example 1:

{% set query = entry.matrixField %}
# `query` matches 10 blocks

{% set textBlock = query.type('text').one() %}
# `query` only matches text blocks now

Use case:
I want to get the first text block to generate the meta description for the page, but still use the full set of blocks for the actual content of the page.

Example 2:

{% set query = craft.entries({ section: 'products' }) %}
# `query` matches 10 products

{% set productSelected = query.slug(productSlug).one() %}
# `query` only matches the product with the matching slug now

Use case:
I need all of the Products for a drop-down filter, but I also need just the current Product that is selected, which I'd rather get out of the set I already have than query again.

Steps to reproduce

Follow the methods above.

Additional info

  • Craft version: Craft CMS 3.0.0-beta.34
  • PHP version: 7.0.8
rfc under consideration

Most helpful comment

We just released Craft 3 RC8, which adds a new clone() Twig function that provides a relatively elegant solution to this issue:

{% set query = entry.matrixField %}
# `query` matches 10 blocks

{% set textBlock = clone(matrixBlocks).type('text').one() %}
# `query` still matches 10 blocks
# (The `type` param was only applied to a clone of the query)

All 10 comments

Yeah this is a bit of an awkward one… In Craft 2, when you chain-set element query params (or any model’s attributes), each method call would clone the model, set the new attribute value on the clone, and return the clone.

$model = new MyModel();
$model->foo('bar');
echo $model->foo;

You would expect that the above would output bar, but in Craft 2 it would actually output nothing, because foo was set on a completely new instance of MyModel, that never actually gets assigned to anything.

To fix that awkwardness, and for consistency with how chain-setting methods works for Yii's base Query classes (and how chain-setting usually works in general), Craft 3 chain-setter methods now set the new attribute value on the current instance of the model, and return that same current instance.

It does result in a change in behavior for templating though, as demonstrated above. Not totally sure what we should do here (if anything).

It's worth noting that this was easier to get around when I switched to the .one/.all syntax.

This worked pretty well:

{% set products = craft.entries({ section: 'products' }) %}
{% set allProducts = products.all %}
{% set productSelected = products.slug(productSlug).one %}

products is still only 1 item after that, but allProducts is preserved.

The issue with the matrix fields stands though. In order to get meta data in the <head> of the file, before any other content renders, I couldn't use matrixField.type('text').one and had to loop through all blocks to find the first text one instead.

As far as a fixup on Craft2, quite possibly this situation doesn't come up too often, because people have already worked around it.

It does seem no errors would be introduced by making the change, because chaining expectations hadn't before worked, while future modifications to extant Craft 2 installations would be safer?

I might be missing something, it's always safe to add...!

For me this deteriorates brevity in templating in a pretty serious way. In my case I'm trying to filter a list of films based on a category. I want to pull one category from the full list of entries, and filter my films based on relatedness to that category. But I also need to list all categories in a dropdown, which breaks by dropping to one as soon as the filtering occurs above.

{% set films = craft.entries.section('films') %}
{% set collections = craft.categories.group('collections') %}
{% set collectionFilter = craft.app.request.getQueryParam('collection') %}
{% if collectionFilter %}
    {% set films = films.relatedTo(collections.slug(collectionFilter).one) // Filters the collections variable instance persistently %}
{% endif %}

<select name="collection">
    <option value="">{{ 'Collections'|t }}</option>
    {% for collection in collections.all // Collections is now only one category %}
        <option value="{{ collection.slug }}" {{ collection.slug == collectionFilter? 'selected' }}>{{ collection.title }}</option>
    {% endfor %}
</select>

// Loop through films down here

The only way to achieve this in 3 now would be to create a new instance of the list each time replacing line 5 with:

{% set films = films.relatedTo(craft.categories.group('collections').slug(collectionFilter).one) %}

Which, granted, doesn't seem like a huge deal in one case, but now imagine 4 or five different filters, searches etc. The code gets really redundant and repetitive very quickly.

I've used variations of this pattern countless times in my templates over the last few years on Craft 2 installs, and couldn't say how difficult it would be to update all my sites to work around this. It's not just a matter of a few find and replaces, whole swathes of structure would have to be entirely rewritten. It would definitely be enough to strongly dissuade me from upgrading to 3 on any of them, if only due to the fact that acquiring additional budget from clients would be a hard sell.

Maybe I'm the only one that uses a similar style to this, but I would doubt it. I'd love to hear how this would effect others moving from Craft 2 to 3.

We just released Craft 3 RC8, which adds a new clone() Twig function that provides a relatively elegant solution to this issue:

{% set query = entry.matrixField %}
# `query` matches 10 blocks

{% set textBlock = clone(matrixBlocks).type('text').one() %}
# `query` still matches 10 blocks
# (The `type` param was only applied to a clone of the query)

@brandonkelly I find myself having to use entry.getNext(clone(posts)) or entry.getPrev(clone(posts)) to not break the various uses of my posts entries. Is this how we should be using clone?

Just having a bit of trouble understanding the purpose of the change from Craft 2 to 3. I was really struggling to figure out what my issue stemmed from and doesn't seem very intuitive, especially to more front-end focused devs like me.

@jsunsawyer there’s not really any benefit to using clone() like that. Just use it when you need to make changes to an element query, but you don't want those changes to persist in subsequent references to the query later on in the template.

@brandonkelly I had to use clone() here or further instances of posts would only have a single entry. Simply using entry.getNext(posts), where posts is a contextual list of entries based on a custom category route (if defined), would change my posts.

I guess my real question is, why does .getNext(posts) alter the posts query?

@jsunsawyer Ah in this case, that’s a bug. We’ll get that fixed. Sorry for the confusion!

Was this page helpful?
0 / 5 - 0 ratings