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.
| Comment | File | Size | Author |
|---|
Issue fork drupal-3270148
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
Comment #2
jonathanshawI have a working PoC, coming later today.
Comment #4
jonathanshawComment #5
longwaveComment #6
jonathanshawOK, 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 62I 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.
Comment #7
jonathanshawComment #8
adamps commentedSeems 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.
Comment #9
jonathanshawI'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.Comment #10
adamps commentedThanks for the explanation. Yes I agree #3176673 is likely unusual in that it has another, simpler way to deprecate variables.
Comment #13
smustgrave commentedThis 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.
Comment #14
larowlanI 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.
Comment #15
jonathanshawIt'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.
Comment #16
larowlanI'll get some additional opinions
Comment #17
larowlanI guess it could default to on, but we could allow people to turn it off in production
Comment #18
longwaveAdded a couple of minor comments to the MR.
Regarding a killswitch, we already have
twig.configas a service parameter, we could adddeprecations: trueas a default there and allow users to turn it off in production?Comment #19
alexpottI'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.
Comment #20
murilohp commentedHey, 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.
Comment #21
murilohp commentedWoow I haven't seen you there @longwave and @alexpott, sorry for the noise
Comment #22
jonathanshawTo 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.
Comment #23
longwaveI 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.
Comment #24
murilohp commentedI'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?
Comment #25
smustgrave commentedAt 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
Comment #27
longwaveThis 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.
Comment #28
murilohp commentedThanks @smustgrave and @longwave! A new MR to 10.1.x is available and a new CR, so moving back to NR.
Comment #29
jonathanshawTechnically 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.
Comment #30
catchI 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?
Comment #31
jonathanshawI 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.
Comment #32
alexpottI 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 }}.Comment #33
murilohp commentedJust 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
Comment #34
murilohp commentedMy 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.
Comment #35
longwaveA few nitpick comments.
Comment #36
jonathanshawOK, 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.
Comment #38
jonathanshawComment #39
murilohp commentedGreat 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.
Comment #40
murilohp commentedComment #41
murilohp commentedJust reviewed and the MR looks good, #35 and #39 were addressed, all the tests are ok so moving back to RTBC.
Comment #42
catchWanted 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?
Comment #43
andypostIt 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 keydeprecatedbesides variables to be inline with https://github.com/twigphp/Twig/blob/3.x/doc/tags/deprecated.rstI think it will be cheaper to catch this variables inside of render and substitute variables with some
DeprecatedMarkupobject which will throw deprecation if used in templatesComment #44
andypostComment #45
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 #3092385: Supply front matter metadata to preprocess functions
Comment #46
jonathanshawTrue. Although a very very very edge case.
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'.
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) %}Comment #47
jonathanshawAdded documentation
Comment #48
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_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.
Comment #49
jonathanshawThe NodeVisitor has access to the environment:
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: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.
Comment #50
chi commentedHow often does Drupal deprecate Twig variables? Don't you think it's over engineering?
Comment #51
jonathanshawThe 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 ...
Comment #52
jonathanshaw@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.
Comment #53
jonathanshawComment #54
jonathanshawThis is RTBC once rerolled
Comment #55
smustgrave commentedRemoving credit from myself for the reroll and rebase.
Comment #56
dwwComment #59
catchI 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 ofarray_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 :)
Comment #60
catchCommitted/pushed to 11.x, thanks!
Comment #62
catchUpdated and published the change record.