Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Matrices and Entries queries are over-written when filtered #2160

Closed
mzlock opened this issue Dec 4, 2017 · 12 comments
Closed

Matrices and Entries queries are over-written when filtered #2160

mzlock opened this issue Dec 4, 2017 · 12 comments

Comments

@mzlock
Copy link

mzlock commented Dec 4, 2017

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
@mzlock mzlock changed the title Matrices and Entries queries are over-written when filtered Matrices and Entries queries are over-written when filtered (in Twig) Dec 4, 2017
@mzlock mzlock changed the title Matrices and Entries queries are over-written when filtered (in Twig) Matrices and Entries queries are over-written when filtered Dec 5, 2017
@brandonkelly
Copy link
Member

brandonkelly commented Dec 18, 2017

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).

@mzlock
Copy link
Author

mzlock commented Dec 18, 2017

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.

@mzlock
Copy link
Author

mzlock commented Dec 18, 2017

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.

@narration-sd
Copy link
Contributor

narration-sd commented Dec 18, 2017

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...!

@monachilada
Copy link
Contributor

monachilada commented Dec 20, 2017

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.

@brandonkelly
Copy link
Member

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)

@jsunsawyer
Copy link

@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.

@brandonkelly
Copy link
Member

@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.

@jsunsawyer
Copy link

@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?

@brandonkelly
Copy link
Member

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

brandonkelly added a commit that referenced this issue Apr 4, 2018
@marten-wirelab
Copy link

This just took me 1,5 hours to debug. I'm rendering a template from code and accessing some entry properties at a later stage as well. The template performs a filtered query on a relation, which causes the same relation to still be filtered when I try to use it again later. My solution is to supply a cloned instance of the entry to Craft::$app->view->renderTemplate, which is simple enough, but it's really hard to understand this behaviour.

@brandonkelly
Copy link
Member

@marten-wirelab We’ve actually changed this behavior in Craft 4 – now field values are automatically cloned each time you fetch them from entry.<myFieldHandle>, so you generally won’t have to worry about cloning them yourself. (See #8781)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

6 participants