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.

CommentFileSizeAuthor
#20 interdiff.txt7.7 KBmurilohp
#20 3270148-18.patch9.75 KBmurilohp

Issue fork drupal-3270148

Command icon Show commands

Start within a Git clone of the project using the version control instructions.

Or, if you do not have SSH keys set up on git.drupalcode.org:

Comments

jonathanshaw created an issue. See original summary.

jonathanshaw’s picture

I have a working PoC, coming later today.

jonathanshaw’s picture

Status: Active » Needs review
longwave’s picture

Status: Needs review » Needs work
FILE: ...core/tests/Drupal/KernelTests/Core/Theme/TwigEnvironmentTest.php
----------------------------------------------------------------------
FOUND 0 ERRORS AND 1 WARNING AFFECTING 1 LINE
----------------------------------------------------------------------
 261 | WARNING | [x] A comma should follow the last multiline array
     |         |     item. Found: ]
jonathanshaw’s picture

OK, this is causing functional tests to fail with:
Fatal error: Can't use method return value in write context in /app/vendor/twig/twig/src/Environment.php(497) : eval()'d code on line 62

I had missed this in my local testing with inline templates. I think I can fix it easily enough using a new type of node instead of a twig function.

jonathanshaw’s picture

Issue summary: View changes
Status: Needs work » Needs review
adamps’s picture

Seems interesting. It would be useful to have some examples of how this would actually be used.

In #3176673: Deprecate non-standard display of title and other base fields we are deprecating some TWIG variables, but we don't need this issue to do it. How come? Well in this case we are also deprecating the code in template_preprocess_*() that sets the variables. So we can put a standard PHP deprecation notice in the code and the job is done very simply.

In general we should check performance implications of this patch - we likely don't want to slow down every test that runs using TWIG.

jonathanshaw’s picture

Issue summary: View changes

I've added an example to the IS.

We're able to trigger_error in preprocess in #3176673: Deprecate non-standard display of title and other base fields because of the complex preparatory work we've done to wrap the deprecated variables in conditionals; some of that preparatory work might not have been necessary if we'd had this. However, the node base field issues are particularly complex and high impact, so I'm not sure how much this would have helped them and it probably won't help to evaluate that more here.

The good news is that the performance impact of this is absolutely negligible. Twig itself is using node visiotrs and custom nodes to do things much more complex than we are adding here; the runtime impact when there are no deprecations is simply testing a single isset($context['deprecations']) statement for every template being rendered.

adamps’s picture

Thanks for the explanation. Yes I agree #3176673 is likely unusual in that it has another, simpler way to deprecate variables.

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.

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.

smustgrave’s picture

Status: Needs review » Needs work
Issue tags: +Needs Review Queue Initiative, +Needs change record, +Needs framework manager review

This issue is being reviewed by the kind folks in Slack, #needs-review-queue-initiative. We are working to keep the size of Needs Review queue [2700+ issues] to around 400 (1 month or less), following Review a patch or merge require as a guide.

At this time we will need a D10 MR of this issue.

Such a feature will require a change record so others may know.

Tagging for framework manager review for their input.

larowlan’s picture

I like the idea of this, is it something we could make configurable like debug mode? i.e we probably don't want this running in production but we would in test environments, local development etc.

jonathanshaw’s picture

It's performance impact is trivial AFAICS, as the twig templates get compiled and cached and it's just a simple function call.

But yes, we can make it opt-in in services.yml or settings.php. However, we don't really have any precedent for switching something on for tests. And if we're not confident that developers will switch it on, then we can't be confident the deprecation error will get triggered so it's all a waste of time.

I'm not sure what's best here, but happy to implement if someone can advise.

larowlan’s picture

I'll get some additional opinions

larowlan’s picture

I guess it could default to on, but we could allow people to turn it off in production

longwave’s picture

Added a couple of minor comments to the MR.

Regarding a killswitch, we already have twig.config as a service parameter, we could add deprecations: true as a default there and allow users to turn it off in production?

alexpott’s picture

I'm not sure the complexity of maintaining a killswitch is necessary - we trigger all the other silenced deprecations in production - right? I can't see the MR atm because gitlab appears to be down but if this is triggering silenced deprecations then I think we should be doing this in production just like we do for other deprecations.

murilohp’s picture

StatusFileSize
new9.75 KB
new7.7 KB

Hey, I was taking a look at the MR, I think I can help with that, I know some stuff needs to be decided(@larowlan will provide more info), but, meanwhile, I've made a POC using your code @jonathanshaw, just made a reroll to branch 10.1.x and also added a new option to the services.yml. I hope this patch can be useful.

I'll keep this as NW due to the CR.

murilohp’s picture

Woow I haven't seen you there @longwave and @alexpott, sorry for the noise

jonathanshaw’s picture

To elaborate a little more on the performance implications:
for every variable used in a template, TwigExtension::checkDeprecations() gets called once, and if there are no deprecations all that function will need to do is make a single if (isset($context['deprecations'])) {} call.

If 1000 named variables are used in all the templates for a single uncached page, that seems like a drop in the ocean of all the function calls necessary to render that page. With active caches, this seems like a complete non-issue.

On balance, I think the maintainability considerations for having this feature, and not having the complexity of an option for it, are compelling.

longwave’s picture

I am convinced against the need for a killswitch by the arguments in #19 and #22. If it turns out there is a performance issue or need for a killswitch, we can still add one in the future, but to me it's not necessary to add one here.

murilohp’s picture

Status: Needs work » Needs review

I've rebased the MR with 9.4.x and also tried to address the code review comments, I'm moving back to NR to see what you think about the MR. About the branch 9.4.x, I think we need to do a reroll for 9.5.x, 10.0.x and 10.1.x right? Or this will be a feature for D10 only?

smustgrave’s picture

Status: Needs review » Needs work

At this time I think we should focus on 10.1 as a task.

Would recommend starting a new MR as switching for 9.x to 10 is a pain.

Also will still need a change record

longwave’s picture

This feature will only be committed to 10.1.x (or later, if it misses the 10.1.x window) - we cannot add new deprecations or features to Drupal 9 now.

murilohp’s picture

Status: Needs work » Needs review
Issue tags: -Needs change record

Thanks @smustgrave and @longwave! A new MR to 10.1.x is available and a new CR, so moving back to NR.

jonathanshaw’s picture

Status: Needs review » Reviewed & tested by the community

Technically I think this can be called RTBC now.

I leave as questions for the committer:

1) Should this get eyes on it from a front-end manager, as it's their world we are trying to provide this functionality for.

2) Has this been adequately reviewed by people who understand twig node visitors? It's a bit of an esoteric subject.

catch’s picture

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?

jonathanshaw’s picture

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.

alexpott’s picture

Status: Reviewed & tested by the community » Needs work

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

murilohp’s picture

Status: Needs work » Needs review

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

murilohp’s picture

Status: Needs review » Reviewed & tested by the community

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.

FYI: I had a quick chat with @jonathanshaw through slack and we agreed with the RTBC here.

longwave’s picture

Status: Reviewed & tested by the community » Needs work

A few nitpick comments.

jonathanshaw’s picture

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.

VladimirAus made their first commit to this issue’s fork.

jonathanshaw’s picture

Status: Needs work » Needs review
murilohp’s picture

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.

murilohp’s picture

Status: Needs review » Needs work
murilohp’s picture

Status: Needs work » Reviewed & tested by the community

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

catch’s picture

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?

andypost’s picture

Status: Reviewed & tested by the community » Needs work
Issue tags: +Performance

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

andypost’s picture

Issue tags: +needs profiling
andypost’s picture

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

jonathanshaw’s picture

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) %}

jonathanshaw’s picture

Status: Needs work » Needs review

Added documentation

catch’s picture

#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_hook), 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.

jonathanshaw’s picture

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.

chi’s picture

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

jonathanshaw’s picture

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

jonathanshaw’s picture

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

jonathanshaw’s picture

Issue tags: +Needs reroll
jonathanshaw’s picture

This is RTBC once rerolled

smustgrave’s picture

Status: Needs review » Reviewed & tested by the community

Removing credit from myself for the reroll and rebase.

dww’s picture

Issue tags: -Needs reroll

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.

catch credited quietone.

catch’s picture

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 #3346645: Eliminate anti-pattern isset($arr[$key]) || array_key_exists($key, $arr).

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

catch’s picture

Status: Reviewed & tested by the community » Fixed

Committed/pushed to 11.x, thanks!

  • catch committed 3c8128a0 on 11.x
    Issue #3270148 by jonathanshaw, murilohp, catch, smustgrave, longwave,...
catch’s picture

Updated and published the change record.

Status: Fixed » Closed (fixed)

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