RendererInterface::render() defines #pre_render and #post_render standard attributes for render elements.

Using either/both of these is rather clunky as you need to know the functions that might want to modify this element ahead of time and alterations to the element are bound to the definition of the element.

Honestly, I'm surprised that we're not using hooks for this already so I think I'm missing something or this was just an oversight at some point.

The proposal is that instead of needing to do this:

array(
  '#theme' => 'foo',
  '#pre_render' => array(
    'exact_name_of_callback',
    'another_explicit_callback',
  ),
);

You could just do this:

array(
  '#theme' => 'foo',
  '#alters' => array('bar'),
);

And inside \Drupal\Core\Render\Renderer::render() we could do something like this:

// Assume an alter on #type if it is set.
if (isset($elements['#type']) {
  $elements['#alters'][] = $elements['#type'];
}

foreach ($elements['#alters'] as $alter) {
  Drupal::moduleHandler()->alter('pre_render_' . $alter, $elements);
}

And the callbacks could define themselves as hook_pre_render_bar_alter(). All pretty standard Drupal-y stuff, really.

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

thedavidmeister’s picture

Issue summary: View changes

Updated issue summary.

thedavidmeister’s picture

Issue summary: View changes

Updated issue summary.

thedavidmeister’s picture

#2025259: drupal_render() should preserve the original render element in #original so pre and post render functions can access "raw" input could be useful to get in as it would be more useful to a dynamic hook setup than a pre-defined list of callbacks.

Fabianx’s picture

+1 for this idea. This is consistent how other things like cache_tags or db query alters work now, which is very flexible.

thedavidmeister’s picture

Title: Introduce #pre_alters/#post_alters for drupal_render(), deprecate #pre_render/#post_render » Introduce #pre_alters/#post_alters and #render for drupal_render(), deprecate #pre_render/#post_render

I had a bit more of a think. It might be worth introducing #render in this issue too so that we can stop using alters/pre_render to flatten things into #markup (which always feels like a hack to me).

Alters would just be responsible for sanitising and other processing, #render would simply concatenate strings.

I think alters + #render + recursive drupal_render() would be generally preferable to the #pre_render -> drupal_render() recursive -> #markup workflow that we have now, without being too radical a change conceptually.

thedavidmeister’s picture

Status: Needs review » Active
FileSize
3.28 KB

Patch as a proof-of-concept for pre/post #alters + #render for Drupal render with the intention that these could eventually replace #pre_render and #post_render. Unfortunately, removing the #pre/post callbacks would be a big API break and so wouldn't be feasible for D8 - but we could theoretically add in the new alters I think as their addition doesn't break any backwards compatibility.

So, aside from just saying "hooks are good, callbacks are bad", I wanted to elaborate on a bit of the reasoning behind what's in this patch and what it does:

- #alters is a new standard attribute used by drupal_render(). It is an array of strings that are used to construct the alter hooks that will be called for *both* pre and post processing. This means that for as long as we have both a pre and post render phase, each value in the array will trigger *two* alters, one before the element is rendered into a string and one afterwards. This enforces a consistent API for modules to hook into for each element.

- defining #type for a render element automatically sets an alter with the same name as #type. This gives developers a nice carrot to use #type consistently and correctly in their code and (I believe) is a decent step towards a "render components library" that has been identified as a goal for Drupal elsewere.

- #render is a new standard attribute used by drupal_render(). It can be a string or an array of callbacks that should render the element but not its children - in fact it must assume and respect that children may have been rendered into #children. This works pretty much identically to #theme_wrappers but without the performance overhead and Nth degree of flexibility of the theme system and will therefore need something analagous to the patch over at #2042285: #theme_wrappers should be able to have a unique set of variables per wrapper hook. These callbacks are really intended to render:
-- Elements that are text strings or contain inline markup only
-- Elements that are a single tag with no known use-case to provide templates for
-- Maybe elements with a single tag and a single wrapper tag
There are lots of reasons why this "atomic" markup should be only "alter data, concatenate string" and not full Twig templates, but there are lots of reasons to use Twig templates for anything bigger/more complex.

An example is that something like this:

array(
  '#type' => 'more_link',
);

Could fetch defaults that look like:

array(
  '#alters' => array('link', 'container'),
  '#render' => array('link', 'container'),
  'more' => t('More'),
  ...
  ...
);

To build an extensible, consistent link wrapped in a div with the minimal performance overhead we can achieve using standard Drupal methods (without hacks like pre-rendering #markup).

What all this means is that we can take our existing "atomic" #type elements like #type 'link' and stop using hacks to make them work. Specifically, we could replace this:

- #type 'link' loads a default #pre_render
- The #pre_render both processes the $elements variable to do sanitisation and other "preparation" and then passes it to l() and then saves the return of l() as a string in #markup
- The theme system is skipped, there is no chance for modules to react or do further processing on the link (well, there is, but only because we recently added drupal_alter() to l(), but this isn't anything that drupal_render() is doing for us) without doing things in hook_element_info_alter() - but even this isn't foolproof as it only changes the defaults, an element that sets #pre_render and #type 'link' will not be processed by your module that wants to process link elements.
- The link's markup is *concatenated before* the rendered children of this render element so child elements don't even appear within the link (if you want to put content in your link you need to use the "special" #title attribute)

With this:

- #type 'link' sets up hook_pre_render_link_alter() and has no need for any #pre_render callbacks
- Any module implementing the hook can prepare the variables ready to be rendered
- The theme system is skipped but there is no need for modules to do any further processing as this is a single html tag built from a simple, alterable data structure, the link #render callback is called which simply returns <a> with sanitised/processed attributes and pre-rendered #children *inside* the tag. There is no need for a special #title attribute as we can just use element children as we would for anything else.

#type 'link' could potentially even use the same #render function as #type 'html_tag' with #alters looking like array('link', 'html_tag').

thedavidmeister’s picture

Status: Active » Needs review
Fabianx’s picture

Status: Active » Needs review

Looks nice!

thedavidmeister’s picture

Status: Needs review » Needs work

docs do need improving though

thedavidmeister’s picture

Status: Needs work » Needs review
FileSize
8.8 KB

- Improved docs here
- Moved the #render into the same logic block as #markup to avoid potential weird behaviour with #render_children
- Cast #alters and #render to an array as we iterate over them to mop up potential issues with anyone providing a string by mistake
- Check that #type is not already one of the values in #alters before adding it so that we don't accidentally call a single alter hook twice

thedavidmeister’s picture

Status: Needs review » Needs work

still probably needs:

- some basic tests
- something like #2042285: #theme_wrappers should be able to have a unique set of variables per wrapper hook for #render

thedavidmeister’s picture

Issue summary: View changes

Updated issue summary.

tim.plunkett’s picture

Version: 8.x-dev » 9.x-dev

Um, #pre_render is also used by Form API. See stuff like filter_form_access_denied().
I don't see any reason to rename this or change its functionality.

thedavidmeister’s picture

Title: Introduce #pre_alters/#post_alters and #render for drupal_render(), deprecate #pre_render/#post_render » Introduce #pre_alters/#post_alters and #render for drupal_render()
Status: Needs work » Needs review
FileSize
15.1 KB

Sure, I updated the patch:

- Removed any mention of #pre_render or #post_render being deprecated
- Added test coverage
- Added the ability for #render callbacks to clarify which attributes are "owned" by them

Is this definitely a D9 thing? The patch doesn't break any backwards compatibility for the API.

Fabianx’s picture

Version: 9.x-dev » 8.x-dev

Back to 8.x-dev, this might be an important step for clearer definition in our theme system.

And to #10:

I took a look at https://api.drupal.org/api/drupal/core%21modules%21filter%21filter.modul... and https://api.drupal.org/api/drupal/core%21modules%21filter%21filter.modul....

The proposed API makes more sense here as well:

Lets assume I want to mark all disabled form element texts with a red color.

I would need to:

* Implement an alter on the element_process() or in an after_build
* Check the #pre_render value for the filter_form_access_denied key
* Add an #attributes (not sure if possible) or my own #pre_render function or a #theme_wrapper.

New API:

I would simply implement mymodule_pre_render_filter_form_access_denied_alter() and add my attributes and I could even change the default text and e.g. replace it with an image or such.

I personally find the 2nd way easier and more Drupal'y.

Fabianx’s picture

+++ b/core/modules/system/lib/Drupal/system/Tests/Common/RenderTest.phpundefined
@@ -73,6 +73,111 @@ function testDrupalRenderBasics() {
+          '#render' => array(
...
+          '#common_test_render_div_attributes' => array('foo' => 'foo'),
+          '#common_test_render_div2_attributes' => array('bar' => 'bar'),

I love the override functionality, but I dislike the long variable names.

I have a counter proposal:

What about:

array(
'#render' => array(
  'common_test_render_div' => array(
    '#attributes' => array('foo' => 'foo'),
  ),
  'common_test_render_div2' => array(
    '#attributes' => array('bar' => 'bar'),
  ),
),
);

I see that it can be confusing at first, but overall this makes in my opinion much more sense to have the overrides near the render functions and not extra.

Fabianx’s picture

Status: Needs review » Needs work

CNW per #13.

tim.plunkett’s picture

Issue tags: +needs profiling

It's a feature request and seemed like an API change. Now as an addition it seems like a perf hit.

thedavidmeister’s picture

#13 - yeah, ok. Makes sense. I'll try a re-roll at the #theme_wrappers issue first then port back here.

#15 - Automatically including #type in #alters could be a modest performance hit, I agree. Happy to re-roll without that functionality (although I think it would be great for learnability/usability of the system) and require #alters to be set explicitly in the element or as a default from #type. Also happy to look into setting up some test scenarios for profiling but I think including #alters in a #type definition returned by hook_element_info() would lead to either comparable performance or a performance improvement once some #theme hooks have actually been converted to #render functions (some things on my hit-list would be 'link', 'image', 'html_tag', 'container'). Two reasons for my belief:

1. When we remove a #theme implementation and convert it to #type -> #pre_render -> #markup style rendering we've been inventing new alter hooks in the #pre_render functions to maintain a degree of the flexibility we lose by removing the theme layer. Adding #alters as in #11 simply gives us a formal place to put the alter hooks, they're not intended to be something that we wouldn't be wanting to introduce anyway, see #2030243: Remove theme_file_upload_help() from the theme system for an example of a patch with a new alter that wouldn't be required if it fit into an #alters/#render setup or some other equivalent paradigm. It looks like we're introducing new things with #alters but we're really just moving them somewhere central and giving them a consistent naming convention for every element that would actually utilise them.

2. Using a system that formally accommodates extensible rendering outside the theme system will be a performance improvement for each conversion of #theme to #render that is completed - our profiling when we swapped out theme() for alter() in #1922454: Gut l(), fix theme('link'), have a working framework for consistently generated links inside and outside of Twig showed links being rendered 4-5% faster overall with the alter() implementation for theme functions and something like 10-15% improvement over twig templates. Improving performance for rendering tiny, "atomic" chunks of HTML without resorting to #pre_render -> #markup style hacks is one of the key goals of this issue.

thedavidmeister’s picture

Looking at #2047263: Provide inline_template tag within Twig that inlines a template for usage with a render array, and talking to @Fabianx in IRC, it seems like the #type -> #pre_render -> #markup rendering flow we currently use makes life very difficult for supporting "drillability" in Twig templates, and this issue would be one way to clear that road block.

A suggestion also came up in the discussion that #alters could be expected to build/modify a #variables attribute inside the render element being processed in drupal_render() and this is what is expected to represent the "ready to render" form of the element, whether by #render or Twig. Kind of like the "inverse" of #2025259: drupal_render() should preserve the original render element in #original so pre and post render functions can access "raw" input - instead of taking a snapshot of the element before modifying it directly, making a new "bucket" to throw all our modifications into and not touching the original element at all.

I'll see if I can make a patch incorporating the most recent discussion points in the next few days.

thedavidmeister’s picture

Title: Introduce #pre_alters/#post_alters and #render for drupal_render() » Introduce #alters and #render for drupal_render()
thedavidmeister’s picture

Title: Introduce #alters and #render for drupal_render() » Introduce #alters for drupal_render()
thedavidmeister’s picture

Issue tags: -needs profiling
FileSize
5.08 KB

Ok.

Here's a patch that doesn't do anything with #render, just #alters. Patch also removed the automatic setting of #alters based on #type.

I removed the profiling tag as when #alters is not set, we only add 2 isset() calls, which is very lightweight.

thedavidmeister’s picture

Status: Needs work » Needs review
thedavidmeister’s picture

Issue summary: View changes

Updated issue summary.

sun’s picture

jibran’s picture

20: 2044105-20.patch queued for re-testing.

Status: Needs review » Needs work

The last submitted patch, 20: 2044105-20.patch, failed testing.

joelpittet’s picture

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

Not totally seeing how this helps but regardless we are in RC and this didn't make the cut so I'm bumping to the next feature release if this needs a pickup.

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.

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.

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.

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.

markhalliwell’s picture

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.

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.

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.

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.

andypost’s picture

There's no drupal_render() anymore, probably could be closed

star-szr’s picture

Title: Introduce #alters for drupal_render() » Introduce #alters for \Drupal\Core\Render\Renderer::render()
Issue summary: View changes

If we close this, it should be because we agree that we don't want to pursue this.

The code from drupal_render() still exists, just under a different name. Updating issue summary and title slightly to attempt to reflect this better.

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.

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.

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.