craftcms / cms

Build bespoke content experiences with Craft.
https://craftcms.com
Other
3.22k stars 627 forks source link

[3.x]: Categories Field Not Returning Selected Categories #13122

Closed mortscode closed 1 year ago

mortscode commented 1 year ago

What happened?

Description

I know Categories are on their way out, but recently I've noticed that using entry.categoryField.all() stopped returning my long-standing categories. We use categories to organize our recipes into categories (dinner, breakfast, low-carb, gluten free, etc), so this is a very important piece of our puzzle.

I have figured out a work around for the time being, but I never had to do it before.

Steps to reproduce

  1. On an entry with a Categories Field, select some categories and Save.
  2. In the template, render the selected categories using entry.categoryFieldHandle.all() and then use a for loop to display the categories.

Expected behavior

The categories would render on the frontend of the website.

Actual behavior

The entry.categoryFieldHandle.all() query returns an empty array.

Workaround

I noticed that entry.title and entry.id return the correct values. I've found that if I reference the entry by id, then it works as expected:

{% set entryRef = craft.entries.id(entry.id).one() %}
<ul>
  {% for cat in entryRef.categoryFieldHandle.all() %}
      <li>{{ cat.title }}</li>
  {% endfor %}
</ul>

Craft CMS version

3.8.8

PHP version

8

Operating system and version

No response

Database type and version

No response

Image driver and version

No response

Installed plugins and versions

i-just commented 1 year ago

Hi, thanks for reaching out. I cannot replicate this behaviour. Would you be able to send your composer files and database backup to support@craftcms.com so we can dig deeper? If you do that, could you also please mention which entry this is happening for (so that it’s easier for us to replicate)?

brandonkelly commented 1 year ago

I've found that if I reference the entry by id, then it works as expected:

How exactly is entry getting defined in the first place? It seems like it isn’t fully in-tact from the outset.

mortscode commented 1 year ago

First of all, I appreciate all of you. Thanks for Craft and all that you do. Ok, let's get into it.

Backstory

This site has been on Craft since 2013 and we've stayed up-to-date as best we can. I have 1 custom plugin that I need to update for us to migrate to Craft 4, and it's high on my priority list. We've been rendering Categories on this postEntries page for quite a few years in this fashion, so I'm not sure what changed, or perhaps how long I've been doing this incorrectly.

The Entry (Channel)

postEntreis is a Channel that provides all of the recipe blog posts on our website. We have ~970 recipes at this point. Here is how the Channel is defined in the CP:

image

The entrypoint template is found at craft/templates/post-entry/index.twig

The Field (Field Type: Categories)

We use this Category set to define the categories of our recipes (course, season, dietary needs, convenience, etc). Here's how the field is set up in the CP:

image

Here is an example of the field within a postEntries entry in the CP:

image

An Update

  1. I commented out all of the code in craft/templates/post-entry/index.twig and the following worked as expected:
    {% set testCats = entry.postCategories.all() %}
    <h1>Cats</h1>
    <ul>
    {% for item in testCats %}
        <li>{{ item.title }}</li>
    {% endfor %}
    </ul>
  2. I started moving the original code back in a bit at a time including the _layout extension and I started having the issue again when I un-commented the following line:
    {% set isRoundup = entry.postCategories.slug('roundups').exists() %}

    This line exists so I can provide some front-end and SEO differences if the post is a "roundup", for example "60 Best Chicken Recipes". I got this line from someone in the Craft Discord. Is there a better way to approach that isRoundup boolean?

mortscode commented 1 year ago

For the record, when I dump & dive the entry.postCategories.slug('roundups').exists() value, it seems to only return true or false. I'm not sure why it's emptying out the entry.postCategores query for the rest of the page.

mortscode commented 1 year ago

The following works as expected. It's a bummer to create that little array, but it leaves my category field in tact:

{% set categorySlugs = entry.postCategories|map(cat => cat.slug) %}
{% set isRoundup = 'roundups' in categorySlugs %}

However, if I add leaves() to the categorySlugs, then the level 1 categories disappear from any query following.

For example, when I follow the above code with this:

{% set testCats = entry.postCategories.all() %}
    <ul>
    {% for item in testCats %}
        <li>{{ item.title }}</li>
    {% endfor %}
    </ul>

I get all levels of slugs in the field:

Seasonal <-- Level 1
Spring
Fall
Winter
Courses <-- Level 1
Dinner
Dietary <-- Level 1
Dairy-Free
Gluten Free
Low-Carb
Convenience <-- Level 1
30 Minutes

But if I update the slug map using the leaves() param:

{% set categorySlugs = entry.postCategories.leaves()|map(cat => cat.slug) %}
{% set isRoundup = 'roundups' in categorySlugs %}

Then all of those Level 1 Category titles disappear from the testCats query.

brandonkelly commented 1 year ago

Sounds like #2929, #3477, #5637, etc. – when you do this:

entry.postCategories.slug('roundups').exists()

It is setting the slug param on the category query to roundups. Which will continue to be set on subsequent references to entry.postCategories.

This was resolved in Craft 4 (see #8781), but in the meantime, if you’re going to set any params on a relational field’s value, which you don’t intend to persist further down in the template, you should clone the value first:

clone(entry.postCategories).slug('roundups').exists()
mortscode commented 1 year ago

Makes total sense. Thanks @brandonkelly