Provide a mechanism for deprecation of variables used in twig templates

Created on 17 March 2022, over 2 years ago
Updated 7 June 2023, over 1 year ago

Problem/Motivation

We currently have no way to warn themers that certain variables consumed in their twig templates are deprecated and their templates should be updated.

Proposed resolution

If an array of deprecations is present in a render array under the #deprecations key, provide that as a variable to twig. Use a twig node visitor to check if used variables are present in the deprecations array and if so trigger a deprecation error when they are used.

Example

Currently we have

function template_preprocess_menu_local_task(&$variables) {
  ...
  if (!empty($variables['element']['#active'])) {
    $variables['is_active'] = TRUE;

If we wished to deprecate 'is_active' we could do this:

function template_preprocess_menu_local_task(&$variables) {
  ...
  if (!empty($variables['element']['#active'])) {
    $variables['is_active'] = TRUE;
    $variables['deprecations']['is_active] = "The 'is_active' property of 'menu_local_task' render arrays is deprecated in Drupal 9.4 and will be unavailable from Drupal 10.0";

Remaining tasks

Decide in principle.

User interface changes

None.

API changes

None.

Data model changes

Render arrays will sometimes have a #deprecations subarray.

Release notes snippet

None.

📌 Task
Status

Fixed

Version

11.0 🔥

Component
Theme 

Last updated about 7 hours ago

Created by

🇬🇧United Kingdom jonathanshaw Stroud, UK

Live updates comments and jobs are added and updated live.
  • Performance

    It affects performance. It is often combined with the Needs profiling tag.

  • needs profiling

    It may affect performance, and thus requires in-depth technical reviews and profiling.

Sign in to follow issues

Comments & Activities

Not all content is available!

It's likely this issue predates Contrib.social: some issue and comment data are missing.

  • Status changed to Needs review almost 2 years ago
  • Status changed to RTBC almost 2 years ago
  • 🇬🇧United Kingdom catch

    I may be missing something, but why can't we do all of the deprecation logic when actually compiling the templates and trigger the deprecation notices then?

  • 🇬🇧United Kingdom jonathanshaw Stroud, UK

    I may be missing something, but why can't we do all of the deprecation logic when actually compiling the templates and trigger the deprecation notices then?

    I had to think for a while to remember the answer from when I coded this :) I think it's because the Twig $context (i.e. the render array) is not available at compile time so there is no way then to inform Twig which variables are deprecated.

    Your question did make me consider the use of frontmatter here, as that is the only way the template itself could declare a variable deprecated.

    A lot depends on who we see as providing the API here and who is consuming it. Are templates providing an API surface called by render arrays, or are render arrays the API being consumed by templates.

    It seems to me that we expect templates to be the locus of customisation, and render arrays make information available to templates without requiring them to make use of any particular piece of it. Therefore it's render arrays that need to be able to deprecate pieces of information that templates can no longer rely on. And this is what this patch offers, which frontmatter could not.

  • Status changed to Needs work almost 2 years ago
  • 🇬🇧United Kingdom alexpott 🇪🇺🌍

    I think we should add test coverage of a template that is not using a deprecated variable but one is being passed to it. Maybe we could use state and alter in a suggestion for the theme_test_deprecate theme so it uses a template that is not using {{ foo }} but is using {{ bar }}.

  • Status changed to Needs review almost 2 years ago
  • 🇧🇷Brazil murilohp

    Just update the MR fixing the code standards, @alexpott, it would be nice to have your review here, and thanks to @jonathanjfshaw for the test. Moving back to NR

  • Status changed to RTBC almost 2 years ago
  • 🇧🇷Brazil murilohp

    My bad here, I putted to NR, but #32 was addressed and the tests seems fine, so moving back to RTBC to see what you think.

  • Status changed to Needs work almost 2 years ago
  • 🇬🇧United Kingdom longwave UK

    A few nitpick comments.

  • 🇬🇧United Kingdom jonathanshaw Stroud, UK

    OK, so as well as fixing @longwave's MR comments, I also identified and fixed an edge case: if a deprecated variable is overridden inside a template, then the usage of that variable is no longer the usage of a deprecated variable.

    However, I think the tests need expanding and refactoring to cover more cases. I will do that now.

  • First commit to issue fork.
  • Status changed to Needs review almost 2 years ago
  • 🇬🇧United Kingdom jonathanshaw Stroud, UK
  • 🇧🇷Brazil murilohp

    Great job with the tests @jonathanshaw!

    Moving back to NW, just some nitpicks and I think you've missed this comment from Dave, otherwise it's RTBC for me.

  • Status changed to Needs work almost 2 years ago
  • Status changed to RTBC almost 2 years ago
  • 🇧🇷Brazil murilohp

    Just reviewed and the MR looks good, #35 and #39 were addressed, all the tests are ok so moving back to RTBC.

  • 🇬🇧United Kingdom catch

    Wanted to commit this, but I think we need to add to core documentation somewhere to show how this can be done.

    https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Render%21... might be the only obvious place?

  • 🇫🇷France andypost

    It needs to measure effect of the new node visitor, as kill-switch is not needed (#19) some diff in blackfire can help

    Having $variables['deprecations'] could have collisions within custom theme functions so I think it's better to introduce new key deprecated besides variables to be inline with https://github.com/twigphp/Twig/blob/3.x/doc/tags/deprecated.rst

    $items['custom_template'] = [
        'variables' => [
          'foo' => 'foo',
          'bar' => 'bar',
        ],
        'deprecated' => [
          'foo' => 'foo is deprecated in version X and is removed from Y. Use "bar" instead.',
        ],
      ];
    

    I think it will be cheaper to catch this variables inside of render and substitute variables with some DeprecatedMarkup object which will throw deprecation if used in templates

  • Status changed to Needs work over 1 year ago
  • 🇫🇷France andypost

    @jonathanjfshaw re #31 - front-matter will not work here because template could be overriden and nobody going to use it except help topics, see 📌 Supply front matter metadata to preprocess functions Active

  • 🇬🇧United Kingdom jonathanshaw Stroud, UK

    Having $variables['deprecations'] could have collisions within custom theme functions

    True. Although a very very very edge case.

    so I think it's better to introduce new key deprecated besides variables

    items['custom_template'] = [
        'variables' => [
          'foo' => 'foo',
          'bar' => 'bar',
        ],
        'deprecated' => [
          'foo' => 'foo is deprecated in version X and is removed from Y. Use "bar" instead.',
        ],
      ];

    We'd still have to combine variables and deprecated into a single context array to pass to Twig, so this doesn't prevent a collision with a variable named 'deprecated'.

    I think it will be cheaper to catch this variables inside of render and substitute variables with some DeprecatedMarkup object which will throw deprecation if used in templates

    It might be cheaper, but it's not backwards compatible as the DeprecatedMarkup object will not be of the same type as the original variable so it will behave differently in Twig logic e.g. {% if foo is same as(false) %}

  • Status changed to Needs review over 1 year ago
  • 🇬🇧United Kingdom jonathanshaw Stroud, UK

    Added documentation

  • 🇬🇧United Kingdom catch

    #31 definitely explains why we can't do this at the template level, because the template doesn't know what's being passed into it. A big reason for introducing this API is for getting rid of cruft from preprocess.

    But re-reading it has me thinking: preprocess hooks only get called at runtime, so the deprecation information can only be picked up at runtime if it's in there.

    But... what if we added a new hook, hook_deprecated_preprocess_variables($theme_key), and get the variables from that during template compilation, and compare them against what's in the template?

    If we did that, there's zero chance of variable collisions, and no runtime overhead either.

  • 🇬🇧United Kingdom jonathanshaw Stroud, UK

    what if we added a new hook, hook_deprecated_preprocess_variables($theme_hook), and get the variables from that during template compilation, and compare them against what's in the template?

    The NodeVisitor has access to the environment:

      protected function doLeaveNode(Node $node, Environment $env) {
        // At the end of the template, check the used variables are not deprecated.
        if ($node instanceof ModuleNode) {

    The ModuleNode has a getTemplateName() method.

    So in theory we should be able to add a getDeprecatedVariables($template_name) method to Drupal's TwigEnvironment and do:

      protected function doLeaveNode(Node $node, Environment $env) {
        // At the end of the template, check the used variables are not deprecated.
        if ($node instanceof ModuleNode) {
          $deprecations = $env->getDeprecatedVariableNames($node->getTemplateName());
          foreach ($this->usedNames as $name) {
            if (isset($deprecations[$name])) {
              @trigger_error($deprecations[$name], E_USER_DEPRECATED);
            }
          }
        }
    

    A problem is that in getDeprecatedVariables($template_name) we may well have to reverse engineer the hook names out of the template name. The variables contain the hook names, but the node visitor doesn't have access to the variables. This feels like it could be a maintenance complication.

    An alternative approach to remove the hypothetical collision risk would be to keep the current architecture, but fire hook_deprecated_preprocess_variables($theme_hook) from within the TwigContext at runtime, as the context with the variables and so (I think) the hook names is available there. However, this would compound the performance concerns dramatically.

    Lastly, with hook_deprecated_preprocess_variables() we'd be doing the deprecation in a seperate place from where the variable is defined or used, which makes the code base harder to understand.

    TLDR; I suspect the harms of hook_deprecated_preprocess_variables() outweigh the gains.

  • 🇷🇺Russia Chi

    How often does Drupal deprecate Twig variables? Don't you think it's over engineering?

  • 🇬🇧United Kingdom jonathanshaw Stroud, UK

    How often does Drupal deprecate Twig variables? Don't you think it's over engineering?

    The issue is that Drupal's committment to backwards compatability is very strong these days; and because of this, developers come to expect it. But there is simply no other backwards compatabile way to cease to provide twig variables to a template, so we have no way to remove variables and clean up the "preprocess cruft" @catch refers to which can have a history going back 15 years. The more we can modernise and standardise the variables delivered to twig templates, the better frontend DX we can have.

    Something like that would be the argument ...

  • 🇬🇧United Kingdom jonathanshaw Stroud, UK

    @catch and I discussed the first idea in #49 about having hook_deprecated_preprocess_variables().

    The problem with it is that there are any falavours of preprcess hook that can provide variables, in addition to hook_theme. Which means that all of them need to be able to deprecate variables. But the specificity and overriding of all of this has to be respected; more later hooks should be able to 'undo' deprecations. Just because a module provides a variable in hook_theme() and then deprecates it, doesn't mean that a theme shouldn't be able to provide an undeprecated variable with that same name using THEME_preprocess_HOOK() .

    To enhance the fun we also have hook_template_preprocess_default_variables_alter we should be respecting which could 'undo' stuff too.

    We might end up:
    - needing multiple versions of hook_deprecated_preprocess_variables called in sequence
    - passing already discovered deprecations as an argument to hook_deprecated_preprocess_variables() so they can be undone
    - trying to hack onto the theme registry's preprocess hook discovery mechanism and repurpose it so that we can call all these hook_deprecated_preprocess_variables_HOOK() in the right sequence.

    We agreed this seems too complex to be worthwhile.

  • 🇬🇧United Kingdom jonathanshaw Stroud, UK
  • 🇬🇧United Kingdom jonathanshaw Stroud, UK

    This is RTBC once rerolled

  • Open in Jenkins → Open on Drupal.org →
    Environment: PHP 8.1 & MySQL 5.7
    last update over 1 year ago
    29,299 pass
  • Status changed to RTBC over 1 year ago
  • 🇺🇸United States smustgrave

    Removing credit from myself for the reroll and rebase.

  • 🇺🇸United States dww
  • Open in Jenkins → Open on Drupal.org →
    Environment: PHP 8.1 & MySQL 5.7
    last update over 1 year ago
    29,310 pass
  • Open in Jenkins → Open on Drupal.org →
    Environment: PHP 8.1 & MySQL 5.7
    last update over 1 year ago
    29,312 pass
  • Open in Jenkins → Open on Drupal.org →
    Environment: PHP 8.1 & MySQL 5.7
    last update over 1 year ago
    29,351 pass
  • Open in Jenkins → Open on Drupal.org →
    Environment: PHP 8.1 & MySQL 5.7
    last update over 1 year ago
    29,374 pass
  • Open in Jenkins → Open on Drupal.org →
    Environment: PHP 8.1 & MySQL 5.7
    last update over 1 year ago
    29,374 pass
  • Open in Jenkins → Open on Drupal.org →
    Environment: PHP 8.1 & MySQL 5.7
    last update over 1 year ago
    29,379 pass
  • Open in Jenkins → Open on Drupal.org →
    Environment: PHP 8.1 & MySQL 5.7
    last update over 1 year ago
    29,386 pass
  • Open in Jenkins → Open on Drupal.org →
    Environment: PHP 8.1 & MySQL 5.7
    last update over 1 year ago
    29,387 pass
  • Open in Jenkins → Open on Drupal.org →
    Environment: PHP 8.1 & MySQL 5.7
    last update over 1 year ago
    29,388 pass
  • Open in Jenkins → Open on Drupal.org →
    Environment: PHP 8.1 & MySQL 5.7
    last update over 1 year ago
    29,391 pass
  • Open in Jenkins → Open on Drupal.org →
    Environment: PHP 8.1 & MySQL 5.7
    last update over 1 year ago
    29,396 pass
  • Open on Drupal.org →
    Environment: PHP 8.1 & MySQL 5.7
    last update over 1 year ago
    Waiting for branch to pass
  • Open in Jenkins → Open on Drupal.org →
    Environment: PHP 8.1 & MySQL 5.7
    21:42
    17:57
    Running
  • Open in Jenkins → Open on Drupal.org →
    Environment: PHP 8.1 & MySQL 5.7
    last update over 1 year ago
    29,395 pass, 2 fail
  • Open in Jenkins → Open on Drupal.org →
    Environment: PHP 8.1 & MySQL 5.7
    last update over 1 year ago
    29,396 pass
  • Open on Drupal.org →
    Environment: PHP 8.1 & MySQL 5.7
    last update over 1 year ago
    Waiting for branch to pass
  • Open in Jenkins → Open on Drupal.org →
    Environment: PHP 8.1 & MySQL 5.7
    last update over 1 year ago
    29,403 pass
  • Open in Jenkins → Open on Drupal.org →
    Environment: PHP 8.1 & MySQL 5.7
    last update over 1 year ago
    29,407 pass
  • Open in Jenkins → Open on Drupal.org →
    Environment: PHP 8.1 & MySQL 5.7
    last update over 1 year ago
    29,407 pass
  • Open in Jenkins → Open on Drupal.org →
    Environment: PHP 8.1 & MySQL 5.7
    last update over 1 year ago
    29,408 pass
  • Open in Jenkins → Open on Drupal.org →
    Environment: PHP 8.1 & MySQL 5.7
    last update over 1 year ago
    29,417 pass
  • Open in Jenkins → Open on Drupal.org →
    Environment: PHP 8.1 & MySQL 5.7
    last update over 1 year ago
    29,417 pass
  • Open in Jenkins → Open on Drupal.org →
    Environment: PHP 8.1 & MySQL 5.7
    last update over 1 year ago
    29,423 pass
  • Open in Jenkins → Open on Drupal.org →
    Environment: PHP 8.1 & MySQL 5.7
    last update over 1 year ago
    29,423 pass
  • 🇬🇧United Kingdom catch

    I re-reviewed this again and I think it looks good.

    Nearly committed it then noticed one thing - I've pushed one additional commit to use \array_key_exists() instead of array_key_exists() which allows PHP to use an opcode implementation instead of a function call. Some background in 📌 Eliminate anti-pattern isset($arr[$key]) || array_key_exists($key, $arr) Fixed .

    Assuming we get a green test run with that one character change, I'll commit this soon (to 11.x/10.2.x, probably a bit late for 10.1.x now).

    Hopefully we can deprecate lots of twig variables in 10.2.x and onwards :)

  • 🇬🇧United Kingdom catch

    Committed/pushed to 11.x, thanks!

  • Status changed to Fixed over 1 year ago
    • catch committed 3c8128a0 on 11.x
      Issue #3270148 by jonathanshaw, murilohp, catch, smustgrave, longwave,...
  • 🇬🇧United Kingdom catch

    Updated and published the change record.

  • Automatically closed - issue fixed for 2 weeks with no activity.

Production build 0.71.5 2024