Support for Drupal 7 is ending on 5 January 2025—it’s time to migrate to Drupal 10! Learn about the many benefits of Drupal 10 and find migration tools in our resource center.
The docs say that drupal_render() will pass a renderable array to theme() if #theme is set and in this case it is the responsibility of the theme function called to render children as required.
Currently drupal_render() does this:
// Call the element's #theme function if it is set. Then any children of the
// element have to be rendered there. If the internal #render_children
// property is set, do not call the #theme function to prevent infinite
// recursion.
if (isset($elements['#theme']) && !isset($elements['#render_children'])) {
$elements['#children'] = theme($elements['#theme'], $elements);
}
// If #theme was not set and the element has children, render them now.
// This is the same process as drupal_render_children() but is inlined
// for speed.
if ($elements['#children'] === '') {
foreach ($children as $key) {
$elements['#children'] .= drupal_render($elements[$key]);
}
}
The problem is that if theme() returns an empty string for some reason, drupal_render() then ignores the theme function's decision to not render the children elements and tries to go ahead and recursively render the children anyway.
Comment | File | Size | Author |
---|---|---|---|
#44 | 2012812-44.patch | 11.01 KB | thedavidmeister |
#44 | interdiff-43-44.patch | 7.01 KB | thedavidmeister |
#43 | 2012812-43.patch | 10.38 KB | thedavidmeister |
#41 | 2012812-41.patch | 9.89 KB | thedavidmeister |
#37 | 2012812-37.patch | 9.13 KB | thedavidmeister |
Comments
Comment #1
thedavidmeister CreditAttribution: thedavidmeister commentedComment #2
thedavidmeister CreditAttribution: thedavidmeister commentedComment #4
thedavidmeister CreditAttribution: thedavidmeister commented#1: 2012812-1.patch queued for re-testing.
Comment #6
thedavidmeister CreditAttribution: thedavidmeister commented#1: 2012812-1.patch queued for re-testing.
Comment #8
thedavidmeister CreditAttribution: thedavidmeister commentedhmmmmmmmmmmmmmmmmmmmmmm
Comment #9
thedavidmeister CreditAttribution: thedavidmeister commentedWow, yep, the installer doesn't appear which implies that it declares #theme but then relies on drupal_render() to be called recursively make children appear. Fail :/
Going to look into this further.
Comment #10
thedavidmeister CreditAttribution: thedavidmeister commentedIn drupal_prepare_form() leads to #theme => array('install_select_language_form'). This theme function doesn't exist and theme('install_select_language_form') returns an empty string.
If this is the intended behaviour then this in drupal_prepare_form():
and this inside drupal_render():
and this above drupal_render():
are really misleading statements in the documentation.
Comment #11
thedavidmeister CreditAttribution: thedavidmeister commentedI'm not really sure what to do here.
There's multiple ways to get theme() to return an empty string that don't justify drupal_render() having a "second go" at rendering children itself.
The documentation is also inaccurate.
I can also see why it would be beneficial to want to "fallback" to the drupal_render() behaviour, like in this exact case where drupal_get_form() merges in default theme suggestions into the form renderable array, which returns an empty string when none of those suggestions match, despite the fact that you would clearly want your form to render it's children even without matching suggestions.
I can't tell just by looking at drupal_render() and the documentation what the intended behaviour is here, so I don't know if the code or the docs are wrong. If the intended behaviour is to allow "fallbacks" then I believe we should be more careful about when we "fallback" to avoid undermining the work done by theme().
Comment #12
c4rl CreditAttribution: c4rl commentedThe check on
$elements['#children'] === ''
is just bad design because it doesn't talk to the theme registry and instead assumes execution resulting in an empty string means there are sub-elements (true in come cases, but not all).The fact that our code and docs are un-grokkable likely indicates they are *both* wrong. drupal_render() implementation was conceived via Form API, and so there may be some conventions there we can refactor. I need to tinker with this a bit more, but it seems to me we need to establish some base principles that define a render array in D8.
Comment #13
thedavidmeister CreditAttribution: thedavidmeister commentedRelated #2015311: If #theme suggestions are set as an array for theme() but none are implemented #markup is treated incorrectly by drupal_render()
Comment #14
thedavidmeister CreditAttribution: thedavidmeister commentedso... I don't love the idea of having theme() returning mixed data types but this does seem like the simplest resolution of this bug with the smallest change to the API. It wouldn't be so bad if theme() became "private" to drupal_render().
I'm totally open to better ideas.
Comment #16
thedavidmeister CreditAttribution: thedavidmeister commentedComment #17
thedavidmeister CreditAttribution: thedavidmeister commentedthis one might actually pass tests...
Comment #18
thedavidmeister CreditAttribution: thedavidmeister commentedsimpler, might work.
Apologies for all the noise here!
Comment #20
thedavidmeister CreditAttribution: thedavidmeister commentedupdate for failing test, also moving the related issue into this one and closing that as a duplicate.
Comment #21
thedavidmeister CreditAttribution: thedavidmeister commentedHere's some tests demonstrating this issue. HEAD fails two of the four new tests in this patch on my local.
Comment #22
thedavidmeister CreditAttribution: thedavidmeister commentedThis combines #20 which the testbots like (yay!) with #21 which introduces new tests for this issue.
Comment #23
thedavidmeister CreditAttribution: thedavidmeister commentedIn IRC @Fabianx and @chx both said they'd prefer FALSE to NULL.
Comment #24
thedavidmeister CreditAttribution: thedavidmeister commentedThe inability to use #markup and #theme suggestions together is currently blocking #1898474: pager.inc - Convert theme_ functions to Twig.
Comment #25
Fabianx CreditAttribution: Fabianx commentedIt looks good to me and provides a needed next step to be able to combine #type and #theme (suggestions).
Comment #26
alexpottNeeds a reroll...
Comment #27
pwieck CreditAttribution: pwieck commentedWorking on reroll
Comment #28
pwieck CreditAttribution: pwieck commentedreroll to current head with only minor conflict fix in common_test.module
Comment #29
pwieck CreditAttribution: pwieck commentedstatus change
Comment #30
pwieck CreditAttribution: pwieck commented#28 green for the win.
Comment #31
thedavidmeister CreditAttribution: thedavidmeister commentedsetting back to RTBC as per #25.
Comment #32
alexpottNice tests!
Committed 431dc2e and pushed to 8.x. Thanks!
Think we need a change notice here as the behaviour of
drupal_render()
has slightly changed.Comment #33
alexpottOkay so this broke overlay... all content gets duplicated... see image for admin/content/node ...
So reverting... committed aca6007 and pushed to 8.x.
Comment #34
thedavidmeister CreditAttribution: thedavidmeister commentedThat's super weird. I will look into this more. Hopefully if I figure out what happened to overlay I can see if it's something that's likely to effect other parts of core.
Comment #35
scor CreditAttribution: scor commentedupdate tags (normalize to "Needs change notification")
Comment #36
thedavidmeister CreditAttribution: thedavidmeister commentedI figured out the issue.
If #theme and #render_children are both set then $theme_is_implemented will be assumed as FALSE, which leads to double rendering of children.
We should start by assuming that $theme_is_implemented = isset($elements['#theme']) and then re-assess after theme() has been called.
Patch coming soon.
Comment #37
thedavidmeister CreditAttribution: thedavidmeister commentedpatch attached that fixes the overlay doubling bug and includes an extra test to ensure that when #theme is set and #render_children is TRUE we dont... render the children. Maybe that variable should be called #children_rendered or something?
Comment #38
thedavidmeister CreditAttribution: thedavidmeister commentedor maybe #skip_theme
Comment #39
thedavidmeister CreditAttribution: thedavidmeister commentedComment #40
thedavidmeister CreditAttribution: thedavidmeister commentedthis needs work. I don't think what I did is quite right.
Comment #41
thedavidmeister CreditAttribution: thedavidmeister commentedactually, it is fine I think.
I was concerned that maybe I should wrap:
In an additional check for #children being an empty string. Essentially:
Should this render 'foo', 'bar' or 'foobar'? In the end, I felt that 'foobar' is simplest, seems to fit the current docs best and would be less surprising so I added a test for it :)
Comment #43
thedavidmeister CreditAttribution: thedavidmeister commentedIt's exactly the thinking in #42 that led to double overlay - glad I wrote a test for it :)
Comment #44
thedavidmeister CreditAttribution: thedavidmeister commentedconsolidated and extended the test coverage.
Comment #46
star-szrComment #47
steveoliver CreditAttribution: steveoliver commentedYes, renaming #render_children to #skip_theme would at least help this mess. Other than that, #44 looks good to me.
Comment #48
thedavidmeister CreditAttribution: thedavidmeister commentedI think actually renaming that variable should be a different issue as it could easily cause this bugfix to be blocked on bikeshedding.
Comment #49
steveoliver CreditAttribution: steveoliver commentedNo need to change this internal property name, really. Let's get this in.
Comment #50
alexpottNice sleuthing... I've manual tested overlay and we don't have the doubling up anymore...
Committed 0329b22 and pushed to 8.x. Thanks!
Comment #51
jenlamptonThink you meant to change this to fixed?
Comment #52
jhodgdonNo, it should be active for the change notice.
Comment #53
tim.plunkettWhich are all critical tasks.
Comment #54
tim.plunkettComment #55
ekl1773Summary:
Problem: If theme() returns an empty string for any reason, then drupal_render() ignores the theme function's decision not to render the children elements recursively renders the children anyway.
Solution: After clearing up some documentation confusion, the problem was solved by clarifying when to render children.
Before:
After:
Two commits
#33 was pushed to 8.x and broke the overlay- #36 found the fix: "If #theme and #render_children are both set then $theme_is_implemented will be assumed as FALSE, which leads to double rendering of children."
#50 patch was successfully committed to 8.x
This change affects: module developers. The behaviour of drupal_render() has slightly changed.
Comment #56
star-szr@ekl1773 - thanks for working on this! That looks a bit more like an issue summary to me. I think the change notice should be geared towards people affected by the change. Things like what comment the patch was committed in are IMO not relevant in that context.
In this case, it might be useful to use example code in the before/after. You could look at the tests in this patch to get an idea. You could show calling a theme function that is not implemented and add comments to indicate the behaviour before and after. Something like this (I'm not sure if this is accurate or not, and an example with children might be more instructive):
Before:
After:
Comment #57
thedavidmeister CreditAttribution: thedavidmeister commentedtheme(array('notimplemented'));
@Cottser, what you posted, without the array results in watchdog complaining about hooks not found.
Comment #58
thedavidmeister CreditAttribution: thedavidmeister commentedOk, so most of this issue was just figuring out how to fix a bug without breaking anything. Everything to do with the overlay being broken temporarily is now irrelevant - I simply didn't understand enough about the API at the time of filing that patch.
The summary of what has happened here:
Previously drupal_render() would render the children of a render element recursively and #markup if and only if #theme returned an empty string for any reason.
It is important that drupal_render() only attempts to handle child elements and #markup if theme() (called when #theme is set) never built markup via. a theme function or template.
Since it is entirely possible that a theme() function/template could render an empty string (this counts as theme() building markup), and this does not mean that the theme function/template was never called at all, there needed to be a way to differentiate between the lack of any theme implementation for an element and the implementation existing and being invoked but still returning an empty result.
The change has two parts:
- Now theme() returns FALSE when the theme hook/suggestion passed to theme() is not found in the registry and returns the return value of the theme function/template otherwise.
- drupal_render() will explicitly check for a FALSE return from theme() before rendering children or #markup, instead of assuming that an empty value automatically leads to invoking the "alternative" rendering process.
Incidentally (I don't know that this should be in the change notice), there's a very closely related bug still outstanding at #2061835: theme() doesn't enforce that what it returns is a string or FALSE.
Who does this effect? potentially nobody actually as the change is just designed to improve the internal logic of drupal_render(), but "themers" probably, the only way to be "stung" by this change is if you were abusing the API by making drupal_render() treat a render element's children differently by creating a theme() function that returns FALSE or an empty string instead of rendering children within the theme function.
Before:
After:
Comment #59
ekl1773Thank you @thedavidmeister, for all the information, and thanks @Cottser for the pointers. You're right, I was writing an issue summary instead of a change notice! I'll wrap all this together and ping you.
Comment #60
jessebeach CreditAttribution: jessebeach commentedCottser, I find this behavior confusing. Can you elaborate a bit more on why the return value would vary here because of value type provided in the
#theme
property?Comment #61
star-szr@jessebeach - I think this would do the same thing but I would definitely defer to @thedavidmeister on this topic :)
Comment #62
thedavidmeister CreditAttribution: thedavidmeister commented#61 is correct and probably clearer than my examples :)
The assumption in the examples is that theme_returns_empty() is a function that exists and returns an empty string, while 'theme_not_implemented' is not an implemented theme hook.
The data type does change how #theme behaves subtly, but is irrelevant to this change notice.
Comment #63
jessebeach CreditAttribution: jessebeach commentedOk, thanks for the further clarification. I tried to capture that extra input in the comments of the code example from #61.
Comment #64
ekl1773Created change record (thanks to @thedavidmeister, @jessebeach and @Cottser for helping to clarify).
https://drupal.org/node/2068529