Problem/Motivation

Templates can include logic that requires cacheable metadata. Currently this can be done in preprocess why this is only normal task.

Proposed resolution

-

Remaining tasks

-

User interface changes

-

API changes

-

Data model changes

-

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

lauriii created an issue. See original summary.

Version: 8.1.x-dev » 8.2.x-dev

Drupal 8.1.0-beta1 was released on March 2, 2016, which means new developments and disruptive changes should now be targeted against the 8.2.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

lauriii’s picture

Status: Active » Needs review
FileSize
5.28 KB
dawehner’s picture

+++ b/core/lib/Drupal/Core/Template/TwigExtension.php
@@ -342,6 +343,39 @@ public function isUrlGenerationSafe(\Twig_Node $args_node) {
+    $attached_cacheable_metadata = [];
+    $items = [
+      'tags',
+      'contexts',
+      'max-age',
+    ];
+    foreach ($items as $item) {
+      if (!empty($cacheable_metadata[$item])) {
+        $attached_cacheable_metadata['#cache'][$item] = $cacheable_metadata[$item];
+      }
+    }

You can replace that by a single line:

$attached_cacheable_metadata['#cache'] = array_intersect_key($cacheable_metadata, ['tags' => 0, 'contexts' => 1, 'max-age' => 2]);
Wim Leers’s picture

The logic in templates should only operate on the variables it receives, not on request context (URL, cookies, headers, current user, permissions). As long as that is the case, this is not necessary.

I fear this would complicate our templates too much.

lauriii’s picture

The reason why this is necessary is that some themers are accessing data directly from render arrays and not actually rendering them. {{ render_array }} vs {{ render_array['#value'] }}.

In that case the cacheable metadata related to the data are not passed along and then you need to manage the cacheable metadata manually. Easiest ways to get around this now is to create a new render array and copying the values from #cache and then render that in the template or add the cacheable metadata in a preprocess.

What the patch here would allow to do is to take the cacheable metadata from the render array and manually bubble it up: {{ attach_cacheable_metadata(render_array['#cache']) }}

lauriii’s picture

dawehner’s picture

Another classical example is when you use one of the time related twig magics out there. People do it, and well so we need some support for it.

+++ b/core/lib/Drupal/Core/Template/TwigExtension.php
@@ -337,6 +338,27 @@ public function isUrlGenerationSafe(\Twig_Node $args_node) {
+
+    if (empty($attached_cacheable_metadata['#cache'])) {
+      return;
+    }

As talked earlier, we don't care about that case, its will almost never happen.

lauriii’s picture

lauriii’s picture

FileSize
597 bytes

Interdiff for the previous patch

Version: 8.2.x-dev » 8.3.x-dev

Drupal 8.2.0-beta1 was released on August 3, 2016, which means new developments and disruptive changes should now be targeted against the 8.3.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

lauriii’s picture

Just want to push this forward a little bit. IMHO the argument on #5 is not valid in the sense that the decision of complexity has been made elsewhere than in this issue. This issue tries to create an easy way to make the cacheable metadata bubble up when it otherwise wouldn't be doing so.

joelpittet’s picture

Curious to know what the alternative to this would be @Wim Leers when you take into consideration people will do what is described in #6?

lauriii’s picture

Category: Task » Feature request

I talked with this @Fabianx and he said that we should at least document this. We also agreed that the alternative way to this looks a bit awkward and therefore it might make sense to add this function. He didn't have a strong opinion since this is more about TX than any technical limitation.

This is a workaround for this inside a template:

{{ {'#cache': render_array['#cache']}|render }}
Wim Leers’s picture

Title: Add possibility to add cacheable metadata inside template » Allow explicit bubbling of cacheability metadata inside Twig template (when accessing data from instead of rendering render arrays)

I see, this is about drillability.

But let's not forget that often the use of drillability means that you'd need to bubble other cacheability metadata. So this would only be a partial solution.

Elijah Lynn’s picture

Elijah Lynn’s picture

Priority: Normal » Major

This is a pretty big issue considering that debugging it is not easy to come to the conclusion it is Twig related and what to do about it.

In the case I just came across, the client had blocks they were editing but the blocks were not updating on the node they were edited from. After checking general cache headers, varnish and pin pointing the render_cache I then found that the taginvalidator was correctly invalidating the block and after many more hours I found the workaround to {{ content|without() }} trick and finally got the cachetags to show in the page cacheability metadata (x-Drupal-Cache-Tags).

My point is that many will not easily arrive at Twig being the culprit and the site will just appear broken in that regard and it won't get fixed and the content editors will have a bad Drupal experience. I would consider this priority to be 'major' because of that.

Elijah Lynn’s picture

#14 - In my template on a breakpoint there isn't a {{ {'#cache': render_array['#cache']}|render }} only a ['#cache']['contexts'][0] available. I am able to get the cachetags to show up in the page cacheability metadata by doing {{ content|without('field_name_here') }} though, but that is quite tedious with many fields and if more fields are added then that will have to be updated, which would make sense anyways but just pointing out all the issues.

Elijah Lynn’s picture

Category: Feature request » Bug report

Also, I don't consider this a feature request, it is a bug.

Version: 8.3.x-dev » 8.4.x-dev

Drupal 8.3.0-alpha1 will be released the week of January 30, 2017, which means new developments and disruptive changes should now be targeted against the 8.4.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.4.x-dev » 8.5.x-dev

Drupal 8.4.0-alpha1 will be released the week of July 31, 2017, which means new developments and disruptive changes should now be targeted against the 8.5.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

pixelwhip’s picture

We've been architecting themes heavily based on https://www.drupal.org/project/components. We're running into this issue all over the place as this components-based approach involves a lot of drilling down into render arrays and passing the data we want into component templates as variables. It seems this undermines some of the promise of Twig in terms of drillability (at least how I've perceived it.)

I naively assumed the cache metadata would bubble up just by being present in a render array passed to a template. It makes sense to me now that the template needs to render the render array for the metadata to bubble up. Are there best practices for this documented anywhere? I imagine a lot of themers will shoot themselves in the foot without a better understanding of how drilling into render arrays can bypass the caching layer.

I'm happy to help spread the word, but want to make sure I fully understand the best practices.

moshe weitzman’s picture

Are folks using this patch successfully in production? If not, how do you solve the need?

froboy’s picture

I was able to get around enumerating fields in my custom block templates by adding {% set catch_cache = content|render %} instead of using the {{ content|without(..) }}. I'm not sure if that has any performance or other negative implications, but I think it worked for me.

1kenthomas’s picture

pixelwhip said:
>[I] want to make sure I fully understand the best practices.

Do we have any here? At this point I'm moving blocks that need this kind of drill-down access to variables + fine-grain caching to src/Blocks in a module, FWIW, but I'm not sure how good an idea that is or isn't.

1kenthomas’s picture

froboy: does

{% set catch_cache = content|render %}

work without patching? I'm heading to Try-It-And-See, of course.

froboy’s picture

1kenthomas: correct. No patch needed.

I started with the article linked above (which is broken up there now) but I was trying to figure out a way to get content to fire all the functions like it's being rendered, but not actually show up in the code anywhere. I tried searching for a twig | with() to try "with none" instead of "without all" but that's not a thing. I thought through the other twig options out there when I remembered seeing something similar on the docs at Twig Field Value. They recommend a similar method to deal with field-level caching:

The field_raw and field_target_entity filters do not provide any cache information. Without additional measures content printed with these filters will not be update when changed in the backend. You can workaround this by rendering the field in your template but not display the result. For example: {% set catch_cache = content.field_image|render %}

I thought I'd try the same thing on the entire content and it seemed to work for me. Adding this line to my block-level templates that don't call {{ content }} and then just clearing caches got all of the right things to bubble. A colleague who was testing my PR ran some Apache Bench tests and reported little performance impact.

So... I'm still not claiming it's great, but it works for us. :)

Version: 8.5.x-dev » 8.6.x-dev

Drupal 8.5.0-alpha1 will be released the week of January 17, 2018, which means new developments and disruptive changes should now be targeted against the 8.6.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.6.x-dev » 8.7.x-dev

Drupal 8.6.0-alpha1 will be released the week of July 16, 2018, which means new developments and disruptive changes should now be targeted against the 8.7.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

jonnyeom’s picture

Just an interesting note.

I found that without the {% set catch_cache = content|render %} line, certain block cache tags do not get properly invalidated, even though they are attached to the page (Seen in the response headers).

I do think this is a bug and having to add the {% set catch_cache = content|render %} to template files should only be a temporary workaround.

jonnyeom’s picture

Status: Needs review » Needs work

With this patch, the cache tags fail to properly invalidate.
At the moment, the {% set catch_cache = content|render %} in template files seems to be the best way.

Jonathan

Version: 8.7.x-dev » 8.8.x-dev

Drupal 8.7.0-alpha1 will be released the week of March 11, 2019, which means new developments and disruptive changes should now be targeted against the 8.8.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

PCate’s picture

I was able to get around enumerating fields in my custom block templates by adding {% set catch_cache = content|render %} instead of using the {{ content|without(..) }}.

Does anyone know if using the do tag would also work?

so:
{% do content|render %}

instead of:
{% set catch_cache = content|render %}

I use twigcs as a linter and it complains about un-used variables, so if it works, using do would be a little better.

Chi’s picture

What's wrong with setting cache metadata like follows? This seems to work for me pretty well.
{{ {'#cache': {contexts: ['url']}} }}

moshe weitzman’s picture

Thanks @chi - just worked for me.

markwittens’s picture

#34 doesn't work when you place a block on a page and then change the contents of the block. The block will never know about the updated content since it doesn't have the proper cache tags.

Rendering the content with |without() works but is error prone. The workaround using {% set catch_cache = content|render %} also works but I can see this having impact on performance since everything will be rendered twice that way.

Chi’s picture

The block will never know about the updated content since it doesn't have the proper cache tags.

Cache tags can be added the same way.
{{ {'#cache': {tags: ['block_content:123']}} }}

Version: 8.8.x-dev » 8.9.x-dev

Drupal 8.8.0-alpha1 will be released the week of October 14th, 2019, which means new developments and disruptive changes should now be targeted against the 8.9.x-dev branch. (Any changes to 8.9.x will also be committed to 9.0.x in preparation for Drupal 9’s release, but some changes like significant feature additions will be deferred to 9.1.x.). For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

Anybody’s picture

Thank you all for your helpful comments. To sum things up:
We currently don't have a proper solution currently to ensure cache tags bubble up in twig templates, if {{ content }} is not rendered, but only subcontents: {{ content.field_xxx }} which is widely used.

So I also consider this a bug or at least problematic for theme developers.

Available workarounds are:

  • {% set catch_cache = content|render %} (seems best but not really nice workaround and the theme developer has to know about this issue (which won't be the case in most situations, so I also consider this a logical bug!) #33 also shows a linting issue
  • {{ content|without('list up ALL fields here') }} (seems dirty because if a new field is added you have to expliclitly list it)
  • {{ {'#cache': {tags: ['block_content:123']}} }} which requires the theme developer to have deeper knowledge of caching logic which shouldn't be the case!

Further information can be found in this blog article: https://www.previousnext.com.au/blog/ensuring-drupal-8-block-cache-tags-...

As a reply to Win Leers in: #5:

The logic in templates should only operate on the variables it receives, not on request context (URL, cookies, headers, current user, permissions). As long as that is the case, this is not necessary.

The problem exists even when only using field data from content. which is widely used (like the blog entry shows).

Proposed solutions:

So from my perspective (but I have to admit, that I don't know whats technically possible and make sense) we might
a) inherit and render cache tags for single contents in {{ content.XX }} from their parent
b) Add a twig function like {{ content|only('field_name') }} to render the content field without everything but also the parent cache tag (name might be different like {{ content|with() }}) or {{ content|withoutAll }}, ...
c) Change {{ content|without() }} (with no arguments) to render nothing but the cache tags which seems a bit dirty and might break things. But {{ content|without() }} doesn't make much logical sense currently.
d) ?

If someone from the core team agrees or has thoughts, please feel free to copy things over to the issue description.

Chi’s picture

We currently don't have a proper solution currently to ensure cache tags bubble up in twig templates, if {{ content }} is not rendered, but only subcontents: {{ content.field_xxx }} which is widely used.

The problem exists even when only using field data from content. which is widely used (like the blog entry shows).

Bubbling cache metadata when printing content items i.e. {{ content.field_xxx }} works perfectly for me. If it does not work for you, please submit a bug report with details. This issue is about providing cache metadata in Twig templates when render arrays are not used at all.

lauriii’s picture

If {{ content.field_xxx }} is rendered without rendering {{ content }}, cacheable metadata from {{ content['#cache'] }} is not bubbled up because the cacheable metadata is bubbled up on render time. However, cacheable metadata is bubbled up as expected when both {{ content }} and {{ content.field_xxx }} are rendered.

I think we could use an update on the issue summary.

Anybody’s picture

@Chi: Well no - it doesn't work for me and it's like @lauriii describes so are you sure it works for you if you ONLY render {{ content.field_xxx }} and NOT {{ content }}?

Our concrete example from block--bundle--card.html.twig is:

   {{
    pattern('card', {
      title: label,
      text: content.body,
      image: content.field_bild,
      button_classes: ['button', 'primary'],
      target_url: target_url,
      link_label: link_label|default('read on'|t),
      link_ext: false
    })
  }}

And from the comments above and our results it seemed to me like I was right with my assumptions in #39. But perhaps I'm not?
Yes, we should clarify this first!

Berdir’s picture

{{ content }} doesn't contain #cache by default, it only contains field render arrays, copied in template_preprocess_node() from 'elements'.

The cacheability metadata is either in fields or one level up on the render array that has #theme => 'node', which should be processed automatically by the Renderer and you shouldn't be able to not have that. However, if you have something else, like another preprocess function that adds cacheability to $variables['content'] then yes, that might get lost.

What information specifically is missing in your example?

In preprocess, you can set $variables['#cache'] directly, ThemeManager supports and processes that, always.

Chi’s picture

If {{ content.field_xxx }} is rendered without rendering {{ content }}, cacheable metadata from {{ content['#cache'] }} is not bubbled up because the cacheable metadata is bubbled up on render time.

Are we talking about content variable that is part of node.html.twig template? If so how did you find {{ content['#cache'] }}?
On my fresh Drupal 8 installation it is missing.

 array:4 [▼
  "field_image" => array:18 [▶]
  "body" => array:2 [▶]
  "field_tags" => array:2 [▶]
  "links" => array:2 [▶]
]
Anybody’s picture

In our case we experienced the problem in a block_custom template: block--bundle--card.html.twig
where we use pattern() via https://www.drupal.org/project/ui_patterns
We're not changing cache tags.

I'm really sorry if my assumptions and sum-up of the issue were wrong. Maybe this issue shows the same fix (which works for us), but the true reason is the combination with ui_patterns and not rendering (any part of) content directly in the block template file. I'm not sure and will have a closer look into the arrays. Code will speak the truth. Sorry!

Chi’s picture

@Anybody you may create an issue for ui_patterns project.

Anybody’s picture

FileSize
47.84 KB

Just to provide final information for others who run into a similar issue like described in the blog post and my comments...
Attached you can find the {{ kint() }} dump from within block--bundle--card.html.twig (See #42 for details about the twig contents).

It may or may not be part or related to this issue, at least a case like ours needs a solution like proposed in this issue.

Berdir’s picture

So the difference is that your example is actually inside a block template for a block_content entity and the content there is a very different beast compared to a node.

block_content doesn't have its own template, so yes, content is the full render array and despite the same name, has not much in common with content inside a node template-

I would suggest you create a separate issue for that. It's somewhat related to #2704331: Ability to display block_content entities independently, also outside of Blocks, but I think we can improve that by pushing the cacheability metadata up in the block content block plugin to make it less likely to run into such things.

samuel.mortenson’s picture

I added a similar feature to Single File Components that also supports passing cacheable objects, which I found useful: #3109362: Make correctly caching components easier. Also see https://git.drupalcode.org/project/sfc#component-caching for some documentation.

Phil Wolstenholme’s picture

We're currently using the {% set catch_cache = content|render %} trick a lot at work in our Drupal templates that reference non-Drupal Twig files coming from Pattern Lab. We've had some problems with the double rendering causing form errors to be displayed twice when a a Webform inside a Paragraph being rendered by our 'Page section' Twig file (which uses the {% set catch_cache = content|render %} trick) is submitted with form errors.

@samuel.mortenson's approach from his SFC module looks interesting. Am I right in thinking that the cache function linked to in the Gitlab link below would avoid the double rendering as rather than render a render array passed to it, it uses CacheableMetadata::createFromObject to extract just the cache metadata and then returns a render array containing just the metadata?

https://git.drupalcode.org/project/sfc/-/blob/8.x-1.x/src/TwigExtension....

Version: 8.9.x-dev » 9.1.x-dev

Drupal 8.9.0-beta1 was released on March 20, 2020. 8.9.x is the final, long-term support (LTS) minor release of Drupal 8, which means new developments and disruptive changes should now be targeted against the 9.1.x-dev branch. For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

ckaotik’s picture

Just want tot chime in to note that #attached libraries are also prone to get lost, which can result in missing styling/js functionality.

Version: 9.1.x-dev » 9.2.x-dev

Drupal 9.1.0-alpha1 will be released the week of October 19, 2020, which means new developments and disruptive changes should now be targeted for the 9.2.x-dev branch. For more information see the Drupal 9 minor version schedule and the Allowed changes during the Drupal 9 release cycle.

Version: 9.2.x-dev » 9.3.x-dev

Drupal 9.2.0-alpha1 will be released the week of May 3, 2021, which means new developments and disruptive changes should now be targeted for the 9.3.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 9.3.x-dev » 9.4.x-dev

Drupal 9.3.0-rc1 was released on November 26, 2021, which means new developments and disruptive changes should now be targeted for the 9.4.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

botanic_spark’s picture

Is there some update on recommended approach?

mxr576’s picture

it uses CacheableMetadata::createFromObject to extract just the cache metadata and then returns a render array containing just the metadata?

Yes, it looks like this is what it does.

Drupal 9.4.0 is close so we have a limited time to add any improvements for this problem.

Version: 9.4.x-dev » 9.5.x-dev

Drupal 9.4.0-alpha1 was released on May 6, 2022, which means new developments and disruptive changes should now be targeted for the 9.5.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Luke.Leber’s picture

Correct me if I'm wrong, but wouldn't this also be a viable, albeit ugly workaround for folks looking for something more light-weight?

{% extends 'block.html.twig' %}
{% block content %}
  {# manage cacheability #}
  {{ { '#cache': content['#cache'] } }}

  {# Call through to the design system #}
  {% include '@design-system/thing.twig' with content only %}
{% endblock %}
Chi’s picture

@Luke.Leber that approach works well. The other possible appoach is using cache_metadata filter from Twig Tweak module.

It must be noted that when the content is rendered lazily some cache metadata may not bubble up. So that rendering content is only 100% way to get all cache metata.

Version: 9.5.x-dev » 10.1.x-dev

Drupal 9.5.0-beta2 and Drupal 10.0.0-beta2 were released on September 29, 2022, which means new developments and disruptive changes should now be targeted for the 10.1.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 10.1.x-dev » 11.x-dev

Drupal core is moving towards using a “main” branch. As an interim step, a new 11.x branch has been opened, as Drupal.org infrastructure cannot currently fully support a branch named main. New developments and disruptive changes should now be targeted for the 11.x branch, which currently accepts only minor-version allowed changes. For more information, see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

markwittens’s picture

-- Edit: My problem seems to be related to Views in combination with Search API and possibly Facets. So this probably doesn't affect all sites --

Any news about this issue? I just came across a website we've update to Drupal 10.1.8 where both workarounds from this issue are not working anymore:

Workaround I always used:

  {% set _render_cache = content|render %}

Alternative workaround:

  {{ { '#cache': content['#cache'] } }}

The provided patches to add the twig function don't apply anymore.

It seems the only way to render the proper cache tags is now to render the entire content variable.