This issue was the result of a 'hard problems' discussion at DrupalCon Amsterdam, see #2350943: [Meta] Untangle Drupal 8 page rendering for the meta issue.
Problem/Motivation
HtmlFragment was added to represent an HTML string and associated metadata, however, it did not take into account Drupal's render caching in the Render API, which was introduced in Drupal 7.
#post_render_cache (added in Drupal 8) implies that it must always be possible to cache HTML strings with placeholders, with the #post_render_cache callbacks run on the string retrieved from cache, before it's returned to the user. This is true except for the single exception of the page cache, which currently deals only with raw HTML strings.
HtmlFragment contains a fully rendered HTML string, as returned from drupal_render().
This makes it the final representation of that string, after all caching/#post_render_cache callbacks have run.
HtmlFragment as the final representation of an Html string + attached stuff was fine as an interface. However, being the final representation of a string, means that it's not possible to have an HtmlFragment inside any lower level than the entire page response.
For example, #2334219: Make blocks expose HtmlFragmentInterface directly would have made HtmlFragment be the return value for blocks. Blocks however, can be nested — the most obvious (but not only) example is mini panels. Having blocks return an HtmlFragment would make it impossible to implement caching for an entire mini-panel (unless we added #post_render_cache support to HtmlFragment which would require duplicating further functionality from render arrays).
HtmlPage, as the final representation of a page was also fine in theory. However, in practice this creates confusion with #2218271: Theme preprocess functions have to futz with $page_object = $variables['page']['#page'] in weird ways, where the page object is passed into template_preprocess_page() as $variables['page']['#page'].
Page templates are not fundamentally different from other templates in Drupal, so template_preprocess_page() should have the same ability, and restrictions, that other preprocess functions do (for 8.x this should ideally mean adding dynamic classes/attributes, and assets that specifically relate to the template, and not much else) — the reality isn't there yet, but it's slowly getting closer.
HtmlFragment to represent the concept of a page region (as opposed to a block), would have provided a structure object to interact with at that level, with the benefits of an interface that render arrays don't have. However, there are only a handful of modules that ever deal with regions, so the DX benefits would not trickle down in any way to most contributed and custom modules.
Additionally, there was no goal to replace render arrays with HtmlFragments, so even with a clearer DX (but, again, only for the very narrow case of building pages, not other templates), they represent a duplication and additional layer of abstraction on top of render arrays — adding to the complexity of the overall system rather than reducing it.
Additionally, arguments such as the page array being 'huge', while true for Drupal 7, are no longer the case for Drupal 8. Render arrays returned from blocks are very small, structured pseudo-objects with #pre_render equivalent to a build() method (which is in fact how blocks are implemented). This is a result of the move to render caching of both entity and block views, which represent most HTML on the page.
After talking through all of these various issues, the conclusion was to remove the concept.
To improve the DX of render arrays at all levels, we will continue to add interfaces such as:
https://api.drupal.org/api/drupal/core!lib!Drupal!Core!Cache!CacheableIn...
This takes the functionality of providing #keys for render arrays, which is often the most confusing part, and puts it into a nice interface with methods that can later be used to extract a valid render array.
The advantage of this bottom-up approach is that the system as a whole is only dealing with a single class of object (the render array), not a combination of both HtmlFragment and render arrays at different levels.
Proposed resolution
As per #2350943: [Meta] Untangle Drupal 8 page rendering, we decided to remove HtmlFragment & HtmlPage.

The patch in #78 was profiled, see #81.
Remaining tasks
Get child issues committed, to make the patch on this issue more reviewable:
- #2357937: Remove {{ feed_icons }} from page template (page.html.twig)
- #2361681: drupal_render(): invert second argument ($is_recursive_call -> $is_root_call) => more strict, better DX/TX
- #2239003: Remove the '_http_statuscode' request attribute
- #2361693: AjaxEnhancer is dead code, remove it
- #2362517: Improve default 403/404 exception HTML subscriber: don't duplicate the page render pipeline, use the routing system — add system.403 and system.404 routes
- #2363025: Push usage of drupal_set_page_content() higher: out of blocks and page display variants
- #2363139: _content controllers may only return render arrays, not strings
- #2364127: Merge AjaxResponseRenderer into AjaxController
- #2364189: One service per format + no hardcoded format to service mapping, but tagged services
- #2366147: Introduce a page display selection event
After that:
- Build consensus for this patch
- Commit
User interface changes
None.
API changes
- Removed
_drupal_add_feed(). (Split off to #2357937: Remove {{ feed_icons }} from page template (page.html.twig).) - Removed
drupal_get_feeds(). (Split off to #2357937: Remove {{ feed_icons }} from page template (page.html.twig).) - Removed
drupal_set_page_content(). [PAGE-VARIANTS] - Removed
drupal_prepare_page(). [MAIN-CONTENT-CONTROLLERS] - Changed
drupal_render()'s signature. [DRUPAL_RENDER] - Removed
\Drupal\Core\Ajax\AjaxResponseRenderer, is now part of\Drupal\Core\Controller\AjaxController, shared functionality lives in\Drupal\Core\Controller\MainContentControllerBase[MAIN-CONTENT-CONTROLLERS] - Added
\Drupal\Core\Block\MainContentBlockPluginInterface(for blocks rendering the main content). [PAGE-VARIANTS] - Removed
\Drupal\Core\Controller\HtmlControllerBase[MAIN-CONTENT-CONTROLLERS] - Removed
\Drupal\Core\Controller\HtmlPageController(superseded by\Drupal\Core\Controller\HtmlController) [MAIN-CONTENT-CONTROLLERS] - Added
\Drupal\Core\Controller\MainContentControllerBaseand\Drupal\Core\Controller\MainContentControllerInterface(interface and base class, respectively, for\Drupal\Core\Controller\HtmlController,\Drupal\Core\Controller\AjaxController,\Drupal\Core\Controller\DialogControllerand\Drupal\Core\Controller\ModalController) [MAIN-CONTENT-CONTROLLERS] - Added
\Drupal\Core\Display\Annotation\PageDisplayVariant: annotation for the next item. [PAGE-VARIANTS] - Added
\Drupal\Core\Display\PageVariantInterface, subclass of\Drupal\Core\Display\VariantInterface, specifically for "page variants" (it adds asetMainContent()method). [PAGE-VARIANTS] - Removed
Drupal\Core\EventSubscriber\HtmlViewSubscriber. [MAIN-CONTENT-CONTROLLERS] - Removed
\Drupal\Core\Page\DefaultHtmlFragmentRenderer. [MAIN-CONTENT-CONTROLLERS] - Removed
\Drupal\Core\Page\DefaultHtmlPageRenderer. [MAIN-CONTENT-CONTROLLERS] - Removed
\Drupal\Core\Page\FeedLinkElement(Split off to #2357937: Remove {{ feed_icons }} from page template (page.html.twig).) - Removed
\Drupal\Core\Page\HeadElement. [MAIN-CONTENT-CONTROLLERS] - Removed
\Drupal\Core\Page\HtmlFragment. [MAIN-CONTENT-CONTROLLERS] - Removed
\Drupal\Core\Page\HtmlFragmentInterface. [MAIN-CONTENT-CONTROLLERS] - Removed
\Drupal\Core\Page\HtmlFragmentRendererInterface. [MAIN-CONTENT-CONTROLLERS] - Removed
\Drupal\Core\Page\HtmlPage. [MAIN-CONTENT-CONTROLLERS] - Removed
\Drupal\Core\Page\HtmlPageRendererInterface. [MAIN-CONTENT-CONTROLLERS] - Removed
\Drupal\Core\Page\LinkElement. [MAIN-CONTENT-CONTROLLERS] - Removed
\Drupal\Core\Page\MetaElement. [MAIN-CONTENT-CONTROLLERS] - Removed
\Drupal\Core\Page\RenderHtmlRenderer. [MAIN-CONTENT-CONTROLLERS] - Removed
\Drupal\Core\Page\RenderHtmlRendererInterface. [MAIN-CONTENT-CONTROLLERS] - Added
\Drupal\Core\Render\BareHtmlPageRenderer, successor of\Drupal\Core\Page\DefaultHtmlPageRenderer. [HTML-RENDERING] - Added
\Drupal\Core\Render\BareHtmlPageRendererInterface, see previous item. [HTML-RENDERING] - Changed
\Drupal\block\Plugin\DisplayVariant\FullPageVariant: renamed toBlockPageVariantand now implements\Drupal\Core\Display\PageVariantInterface. [PAGE-VARIANTS] - Added
\Drupal\block\Plugin\DisplayVariant\DemoBlockPageVariant. [PAGE-VARIANTS] - Added
\Drupal\system\Event\PageDisplayVariantSelectionEvent. [PAGE-VARIANTS] - Added
\Drupal\system\Event\SystemEvents: to add the "page display variant selection" event. [PAGE-VARIANTS] - Added
\Drupal\block\Plugin\DisplayVariant\SimplePageVariant: to encapsulate the default behavior ofdrupal_prepare_page(), i.e. to be used when Block module is not installed, andBlockPageVariantcannot be used. [PAGE-VARIANTS] + [MAIN-CONTENT-CONTROLLERS] - Changed
html.html.twig. [PAGE-VS-HTML]
| Comment | File | Size | Author |
|---|---|---|---|
| #96 | untangle_page_rendering-2352155-96_review-do-not-test.patch | 141.36 KB | wim leers |
| #96 | untangle_page_rendering-2352155-96.patch | 192.28 KB | wim leers |
| #67 | d8_render_pipeline.png | 321.45 KB | wim leers |
| #67 | d8_render_pipeline.graffle.zip | 12.46 KB | wim leers |
| #67 | d8_render_pipeline.svg_.txt | 81.8 KB | wim leers |
Comments
Comment #1
catchComment #2
wim leersAdding formatting and commas.
Comment #3
catchComment #4
wim leersAdding related issues.
Comment #5
Anonymous (not verified) commentedwow. awesome work, solutions derived from a thorough understanding of Drupal's actual challenges for the win.
Comment #6
wim leersI'm working on this one today. Hope to post a patch by the end of the day.
Comment #7
jibranI don't know but this seems related.
Comment #8
wim leersOpened a small issue to help pave the way for this one: #2357937: Remove {{ feed_icons }} from page template (page.html.twig).
Comment #9
wim leersAnother, bigger issue to help pave the way: #2359901: Discourage $main_content['#title'] in favor of route titles and title callbacks.
Comment #10
wim leers#2359901: Discourage $main_content['#title'] in favor of route titles and title callbacks is no longer a blocker. I found a work-around.
Tomorrow, I'll be posting a patch at last. Most encountered problems have been fixed; I went from >5000 failed tests to now <30. Expect a very big update tomorrow.
The reason I've been working in relative isolation (I did have frequent check-ins with catch) is that fixing one thing uncovered another thing needing to be fixed, and that thing in turn required another thing to be fixed, etc. It's all still so interwoven, due to the many loose ends that haven't been fixed (both in relation to
HtmlPage/HtmlFragmentand relatively independent of them). I felt it was very important to formulate a complete, cohesive answer, because that's a big part of the current entangledness in HEAD: many things that make sense in isolation, but not together.Since this is the closing stone for #2350943: [Meta] Untangle Drupal 8 page rendering (all other child issues have been resolved), and since it aims to close so many issues, I felt it was important that this issue provided complete closure for this problem space: with the patch for this issue committed (even if that needs to happen in parts/child issues) should allow us to look at Drupal 8 page rendering and say and preferably even make developers think and say, after reading a single page of docs .
Comment #11
giorgio79 commentedWish I could hit a like on Drupal posts, but since I cannot, let me just say Thank you Wim!
Comment #12
fabianx commentedIndeed, thank you so much, Wim!
The issue summary looks great, the whole problem space reminds me of this xkcd: http://xkcd.com/927/
I am looking forward to the patch.
Comment #13
almaudoh commentedWim++
Comment #14
wim leersSpeaking at a high-level: as I said in #10: I felt it was very important to formulate a complete, cohesive answer, because that's a big part of the current entangledness in HEAD: many things that make sense in isolation, but not together.
In doing so, I made sure to keep in mind (roughly in order of priority) while working on this patch:
Larry (Crell) and others who worked on
HtmlPage/HtmlFragment, I'd like to say again that I feel very sorry that we jointly had to conclude at DrupalCon Amsterdam that we'd have to removeHtmlPageandHtmlFragment. But I think and hope that you will see that your work was not in vain — in many places, theHtmlPage/HtmlFragmentpatches have actually paved the way for this one. And I hope you'll agree that some of the things you were working towards — like SCOTCH — are now actually closer within reach than ever before!While working on this and reading the many existing issues around page rendering, I stumbled upon #1594870-24: Decide what KernelEvents::VIEW subscribers should be used for, document it somewhere, and make ViewSubscriber comply and #1594870-26: Decide what KernelEvents::VIEW subscribers should be used for, document it somewhere, and make ViewSubscriber comply, written by effulgentsia a full 1.5 years ago… and it describes rather closely what this patch now effectively does.
The patch
Diffstat:
125 files changed, 2207 insertions(+), 3144 deletions(-). 1000 LoC less, with all outstanding loose ends fixed in page rendering. The difference would have been several hundred LoC greater still if this patch didn't add hundreds of lines of docs.This is a ~300 KB patch. Unless people think otherwise, I will begin splitting off parts of it that can be split off into child issues, to simplify the review process.
The sections below the "API changes" section will list changes per topic. Some of those topics can be moved into child issues. Many of them are too intertwined, though. But the per-topic descriptions of what's changed should help you understand this huge patch.
Flowchart
I created a flowchart to help make this patch more digestable, and also to just document the hell out of the (proposed) Drupal 8 page rendering pipeline, so that anybody can understand how it works — it literally took me days to understand the rendering pipeline in HEAD.
I want the understanding process to take an hour at most with this proposed patch.
Changes
In the order you encounter them in the patch:
_drupal_add_feed(). (Split off to #2357937: Remove {{ feed_icons }} from page template (page.html.twig).)drupal_get_feeds(). (Split off to #2357937: Remove {{ feed_icons }} from page template (page.html.twig).)drupal_set_page_content(). [PAGE-VARIANTS]drupal_prepare_page(). [MAIN-CONTENT-CONTROLLERS]drupal_render()'s signature. [DRUPAL_RENDER]\Drupal\Core\Ajax\AjaxResponseRenderer, is now part of\Drupal\Core\Controller\AjaxController, shared functionality lives in\Drupal\Core\Controller\MainContentControllerBase[MAIN-CONTENT-CONTROLLERS]\Drupal\Core\Block\MainContentBlockPluginInterface(for blocks rendering the main content). [PAGE-VARIANTS]\Drupal\Core\Controller\ModalController(thin subclass of\Drupal\Core\Controller\DialogController, to comply with\Drupal\Core\Controller\MainContentControllerInterface) [MAIN-CONTENT-CONTROLLERS]\Drupal\Core\Controller\HtmlControllerBase[MAIN-CONTENT-CONTROLLERS]\Drupal\Core\Controller\HtmlPageController(superseded by\Drupal\Core\Controller\HtmlController) [MAIN-CONTENT-CONTROLLERS]\Drupal\Core\Controller\MainContentControllerBaseand\Drupal\Core\Controller\MainContentControllerInterface(interface and base class, respectively, for\Drupal\Core\Controller\HtmlController,\Drupal\Core\Controller\AjaxController,\Drupal\Core\Controller\DialogControllerand\Drupal\Core\Controller\ModalController) [MAIN-CONTENT-CONTROLLERS]\Drupal\Core\Display\Annotation\PageDisplayVariant: annotation for the next item. [PAGE-VARIANTS]\Drupal\Core\Display\PageVariantInterface, subclass of\Drupal\Core\Display\VariantInterface, specifically for "page variants" (it adds asetMainContent()method). [PAGE-VARIANTS]\Drupal\Core\EventSubscriber\ContentControllerSubscriber, to have full documentation rather than a misleading one-liner, and to use the result of the compiler pass rather than a hardcoded list. [MAIN-CONTENT-CONTROLLERS]\Drupal\Core\EventSubscriber\DefaultExceptionHtmlSubscriberto extend\Drupal\Core\EventSubscriber\DefaultExceptionHtmlSubscriber, since the latter now behaves more like the former than before. [403-404]\Drupal\Core\EventSubscriber\DefaultExceptionHtmlSubscriberto work more like\Drupal\Core\EventSubscriber\CustomExceptionHtmlSubscriber: no more hard-coded responses. [403-404] + [HTML-RENDERING]\Drupal\Core\EventSubscriber\FinishResponseSubscriber:Statusheader handling. [STATUS-HEADER]Drupal\Core\EventSubscriber\HtmlViewSubscriber. [MAIN-CONTENT-CONTROLLERS]\Drupal\Core\EventSubscriber\MaintenanceModeSubscriberto use\Drupal\Core\Render\BareHtmlPageRendererinstead of\Drupal\Core\Page\DefaultHtmlPageRenderer. [HTML-RENDERING]\Drupal\Core\EventSubscriber\ViewSubscriber: it needs to do less work now. [MAIN-CONTENT-CONTROLLERS]\Drupal\Core\Page\DefaultHtmlFragmentRenderer. [MAIN-CONTENT-CONTROLLERS]\Drupal\Core\Page\DefaultHtmlPageRenderer. [MAIN-CONTENT-CONTROLLERS]\Drupal\Core\Page\FeedLinkElement(Split off to #2357937: Remove {{ feed_icons }} from page template (page.html.twig).)\Drupal\Core\Page\HeadElement. [MAIN-CONTENT-CONTROLLERS]\Drupal\Core\Page\HtmlFragment. [MAIN-CONTENT-CONTROLLERS]\Drupal\Core\Page\HtmlFragmentInterface. [MAIN-CONTENT-CONTROLLERS]\Drupal\Core\Page\HtmlFragmentRendererInterface. [MAIN-CONTENT-CONTROLLERS]\Drupal\Core\Page\HtmlPage. [MAIN-CONTENT-CONTROLLERS]\Drupal\Core\Page\HtmlPageRendererInterface. [MAIN-CONTENT-CONTROLLERS]\Drupal\Core\Page\LinkElement. [MAIN-CONTENT-CONTROLLERS]\Drupal\Core\Page\MetaElement. [MAIN-CONTENT-CONTROLLERS]\Drupal\Core\Page\RenderHtmlRenderer. [MAIN-CONTENT-CONTROLLERS]\Drupal\Core\Page\RenderHtmlRendererInterface. [MAIN-CONTENT-CONTROLLERS]\Drupal\Core\Render\BareHtmlPageRenderer, successor of\Drupal\Core\Page\DefaultHtmlPageRenderer. [HTML-RENDERING]\Drupal\Core\Render\BareHtmlPageRendererInterface, see previous item. [HTML-RENDERING]\Drupal\Core\Render\Element\Html: added a#pre_rendercallback to assign meta tags. [MAIN-CONTENT-CONTROLLERS]\Drupal\Core\Render\MainContentControllerPass. [MAIN-CONTENT-CONTROLLERS]\Drupal\Core\Routing\Enhancer\AjaxEnhancer, already dead code in HEAD, now definitely. [MAIN-CONTENT-CONTROLLERS]block.module: introduced ahook_page_top()implementation for the "backlink" on the demo page. [PAGE-VARIANTS]\Drupal\block\EventSubscriber\BlockPageDisplayVariantSubscriber: selects the block page display variant. [PAGE-VARIANTS] + [MAIN-CONTENT-CONTROLLERS]\Drupal\block\Plugin\DisplayVariant\FullPageVariant: renamed toBlockPageVariantand now implements\Drupal\Core\Display\PageVariantInterface. [PAGE-VARIANTS]\Drupal\block\Plugin\DisplayVariant\DemoBlockPageVariant. [PAGE-VARIANTS]ckeditor.module: theformat_tagssetting for each text format must be pre-calculated. Otherwise a "root" call ofdrupal_render()could happen within a root call, which now triggers an exception. [DRUPAL_RENDER]\Drupal\ckeditor\Plugin\CKEditorPlugin\Internal: see above. [DRUPAL_RENDER]\Drupal\comment\CommentPostRenderCache: bugfix. Fixes #2238835: #post_render_cache + #attached. [DRUPAL_RENDER]\Drupal\views\Plugin\views\style\Rss+\Drupal\node\Plugin\views\row\Rss: "final" rendering because rendering a feed, not HTML. [DRUPAL_RENDER]filter.module:check_markup()does "final" rendering. [DRUPAL_RENDER]node.module:drupal_render()no longer does "final" rendering by default. [DRUPAL_RENDER]\Drupal\rest\Plugin\views\display\RestExport: "final" rendering because rendering REST export, not HTML. [DRUPAL_RENDER]\Drupal\system\Controller\BatchControllerto use\Drupal\Core\Render\BareHtmlPageRendererinstead of\Drupal\Core\Page\DefaultHtmlPageRenderer. [HTML-RENDERING]\Drupal\system\Controller\DbUpdateControllerto use\Drupal\Core\Render\BareHtmlPageRendererinstead of\Drupal\Core\Page\DefaultHtmlPageRenderer. [HTML-RENDERING]\Drupal\system\Controller\Http4xxController: for thesystem.403andsystem.404routes. [403-404] + [HTML-RENDERING]\Drupal\system\Event\PageDisplayVariantSelectionEvent. [PAGE-VARIANTS]\Drupal\system\Event\SystemEvents: to add the "page display variant selection" event. [PAGE-VARIANTS]\Drupal\system\Plugin\Block\SystemMainBlock: to replace the call todrupal_set_page_content()with an implementation ofMainContentBlockPluginInterface. [PAGE-VARIANTS]\Drupal\block\Plugin\DisplayVariant\SimplePageVariant: to encapsulate the default behavior ofdrupal_prepare_page(), i.e. to be used when Block module is not installed, andBlockPageVariantcannot be used. [PAGE-VARIANTS] + [MAIN-CONTENT-CONTROLLERS]\Drupal\system\Tests\Common\AddFeedTestbecauseFeedLinkElementandHtmlPageare no more. [MAIN-CONTENT-CONTROLLERS]\Drupal\system\Tests\Routing\ExceptionHandlingTest: was testing in a too-artificial manner, and hence was actually testing the wrong thing. Actual page responses are alwaysprepare()d, which sets the content type automatically, the tests had to be adjusted to reflect that. [403-404]\Drupal\system\Tests\System\MainContentFallbackTest: obsolete, now that we haveSimplePageVariant. [PAGE-VARIANTS]system.routing.yml: to addsystem.403andsystem.404, but also to fixsystem.batch_page.normal: it's only ever intended to return HTML, but because it didn't set a_format, the wrong format could be negotiated, breaking things horribly. [403-404]html.html.twig. [PAGE-VS-HTML]page.html.twig. (Split off to #2357937: Remove {{ feed_icons }} from page template (page.html.twig).)\Drupal\system_module_test\EventSubscriber\HtmlPageSubscriber: was overriding assigned meta tags, this now happens in\Drupal\Core\Render\Element\Html::preRenderHtml()rather than onHtmlPage, hence needed to be changed into#pre_rendercallbacks. [MAIN-CONTENT-CONTROLLERS]\Drupal\views\EventSubscriber\RouteSubscriber: removedStatusheader overriding, no longer needs an event subscriber now. [STATUS-HEADER]\Drupal\views\Plugin\views\area\HTTPStatusCode: no longer needs to set a global to override theStatusheader. [STATUS-HEADER]\Drupal\views\Plugin\views\display\Feed+\Drupal\views\Plugin\views\row\RssFields+\Drupal\views\Plugin\views\style\Rss: "final" rendering because rendering feed, not HTML. [DRUPAL_RENDER]views.module: handling contextual links for "page" displays needs to happen differently now that the globalHtmlPageobject is gone. [MAIN-CONTENT-CONTROLLERS]\Drupal\Tests\Core\Ajax\AjaxResponseRendererTestto\Drupal\Tests\Core\Controller\AjaxControllerTest, sinceAjaxResponseRendereris now part ofAjaxController. [MAIN-CONTENT-CONTROLLERS]bartik.theme. [PAGE-VS-HTML]seven.theme. [PAGE-VS-HTML]"main content" controllers (+ actually remove
HtmlFragment&HtmlPage)HEAD has
controller.page,controller.ajaxandcontroller.dialog. All three are responsible for rendering "main content" (as received from_contentcontrollers) in the respective formats they support: the first is for thehtmlformat, the second fordrupal_ajaxand the third for bothdrupal_dialoganddrupal_modal. That's not very consistent. That's definitely not very discoverable. On the surface, it looks like any other controller, just… a little bit more special.To clarify that, I introduced
MainContentControllerInterfaceand made each of them implement that. And in fact, I noticed that they all had one method that was identical across all of them (getContentResult()). And after updating each to use the newly introduced interface, even more similarities appeared. So I introduced a common base class:MainContentControllerBase.The old controllers were actually implicitly coupled to
ContentControllerSubscriber, but only by convention, not by any clearer structure. They were literally hardcoded.To improve that situation — because it's possible somebody might want to add a new format to render the main content into, but also for increased understandability — I've opted to introduce a
main_content_controllers_controllerservice tag, along with aformatattribute. This allowed me to add aMainContentControllerPasscompiler pass that creates a key-value map of formats to corresponding controllers (stored in the%main_content_controllers%container parameter), and haveContentControllerSubscriberuse that instead of a hardcoded list.End result:
MainContentControllerInterface) and base class (MainContentControllerBase), which makes the various "main content" controllers much easier to understandcontroller.page->main_content_controller.htmlcontroller.ajax->main_content_controller.ajaxcontroller.dialog->main_content_controller.dialog+main_content_controller.modal\Drupal\Core\Ajax\AjaxResponseRenderer+\Drupal\Core\Controller\AjaxController=\Drupal\Core\Controller\AjaxController(main_content_controller.ajax)\Drupal\Core\Controller\HtmlPageController+\Drupal\Core\EventSubscriber\HtmlViewSubscriber+\Drupal\Core\Page\RenderHtmlRenderer+\Drupal\Core\Page\DefaultHtmlFragmentRenderer+\Drupal\Core\Page\DefaultHtmlPageRenderer=\Drupal\Core\Controller\HtmlController(main_content_controller.html)HtmlFragment,HtmlPageand related classes (likeHeadElementetc.)main_content_controllerservice tag plus aformatattribute, which then allowsContentControllerSubscriberto use the services tagged as such when determining the controller to useContentControllerSubscriberthat allows you to understand the system, rather than a very vague single line which doesn't help you to understand the system at all.This fixes:
This at least somewhat helps:
Page variants: now a first-class citizen
This builds upon [MAIN-CONTENT-CONTROLLERS].
Page variants are about rendering the main content in a certain way. Without Block module, a page will just show the main content, and that's it. With Block module, the main content will be decorated by blocks, and the main content itself will also be rendered in a block (hence allowing you to put the main content anywhere you want).
Page variants are the abstraction necessary to let Page Manager, Panels, and so on be cleanly implemented in Drupal 8 contrib.
Conceptually speaking, page display variants or page variants in short, are the successors to
hook_page_build()as they worked in Drupal 7. For lack of a better word,hook_page_build()was already castrated in #2350949: Add hook_page_attachments(_alter)() and deprecate hook_page_build/alter(), because it allowed any module to make any kind of manipulation. This caused a lot of problems in Drupal 7.Page variants don't suffer from that problem: only a single page variant can be selected, at which point the selected page variant and only that one can determine what the body content will be (how to fill the regions inside
page.html.twig).Clearly, page variants only apply when rendering HTML (i.e. when the request format is
html). On top of that, it's only applicable when rendering "main content" (a_contentcontroller).Combine the two and the only logical place for this to live is in
\Drupal\Core\Controller\HtmlController. There, aSystemEvents::SELECT_PAGE_DISPLAY_VARIANTevent is dispatched that allows any event subscriber (and hence any module) to select the page variant to be used for the current route.\Drupal\Core\Controller\HtmlControllerthen uses the selected page display variant plugin to build the page to be rendered.Display variants are a generic, abstract concept: they're intended to represent different variations of displaying things. For displaying variations of pages, which always have some "main content", it needs to be possible to somehow send that main content to the variant.
In HEAD, that happens through a global static:
drupal_set_page_content(). We don't want that to continue (especially because this was nigh impossible to understand), and therefore we want to be able to send the main content to a variant directly. For that purpose, I introduced\Drupal\Core\Display\PageVariantInterface, which subclasses the generic variant interface. Its sole addition: asetMainContent()method.Now the code invoking a page display variant can send the main content and let the variant deal with it further!
In HEAD, we already have the
SystemMainBlock, which is rendering the main content. Until now, that was usingdrupal_set_page_content(), i.e. that global static again. The Block module's page display variant already receives the main content cleanly (as per the above), so now we need to pass it cleanly to the block(s) rendering the main content as well. For that, a similar interface was introduced:\Drupal\Core\Block\MainContentBlockPluginInterface, a subclass ofBlockPluginInterface, with again a sole addition: asetMainContent()method.So now
HtmlControllercan always callsetMainContent()on the page display variant (since page display variants must implementPageVariantInterface), which then in turn has the responsibility of rendering the main content, and in the case of blocks, we detect which block renders the main content by checking if it implementsMainContentBlockPluginInterface, and if it does, we again callsetMainContent(). Et voilà!This fixes:
Related:
HTML rendering: consolidated the many HTML renderers
Removed
html_view_subscriber(\Drupal\Core\EventSubscriber\HtmlViewSubscriber),render_html_renderer(\Drupal\Core\Page\RenderHtmlRenderer),html_fragment_renderer(\Drupal\Core\Page\DefaultHtmlFragmentRenderer) andhtml_page_renderer(\Drupal\Core\Page\DefaultHtmlPageRenderer).Instead, there now are only
\Drupal\Core\Controller\HtmlController(used for_contentcontrollers) and\Drupal\Core\Render\BareHtmlPageRenderer(used for "one-off pages").Rather than using
DefaultHtmlPageRenderer::renderPage(), which was a static "temporary shim" method for the one-off pages where it's impossible to use a "main content controller", such as the installer or error pages, we now havebare_html_page_renderer(BareHtmlPageRenderer), where the "bare" indicates that thehook_page_(attachments|attachments_alter|top|bottom)()aren't invoked, nor is a page display variant selected (to allow e.g. blocks to appear).This fixes:
drupal_render(): more strict, better DX
Any remaining
drupal_render()calls that were called with$is_recursive = FALSE, where they really shouldn't have been, have been fixed.While fixing some test failures, I then noticed that
#post_render_cachecallbacks and#attachedassets were no longer being applied for the main content. Turns out the root cause was once again "wrong"drupal_render()calls; these caused the stack (that is used internally, see #2273277: Figure out a solution for the problematic interaction between the render system and the theme system when using #pre_render) not to be empty at the end of a "root" (non-recursive)drupal_render()call, which effectively means that some of the bubbleable metadata is being left behind in the stack, and is missing from the final result for the "root" call.I first embarked on a mission to fix all
drupal_render()calls to have$is_recursive = FALSE… but since this is the majority of the calls, it really is:drupal_render()without specifying$is_recursive = FALSE, that that is wrong.So instead of "fixing" all existing
drupal_render()calls, I instead fixeddrupal_render()itself: the signature changed fromto
i.e. I inverted the second argument's meaning. Now the default is once again
drupal_render($build), and only when rendering the final markup (i.e. just before generating aResponse). The dozen or so cases in Drupal core that need to do this were updated to do this.To prevent this problem in the future, even though the above already diminished the chance of this happening again greatly,
drupal_render()now throws an exception whenever a root call to it does not end with an empty stack.(Files with
$is_root_call = TRUEcalls fordrupal_render(): whenever rendering the "final" markup, which happens inauthorize.php,update.php,install.php, exception handling, maintenance mode and when rendering render arrays for non-HTML responses, such as feeds.)As part of this work, it turns out we fixed:
Default 403/404
Changed
\Drupal\Core\EventSubscriber\DefaultExceptionHtmlSubscribernow works like\Drupal\Core\EventSubscriber\CustomExceptionHtmlSubscriberalready does in HEAD: rather than hardcoding responses, it now performs a sub-request to newly addedsystem.403andsystem.404routes.Hence
\Drupal\Core\EventSubscriber\CustomExceptionHtmlSubscribernow is almost empty; it only needs to subclass\Drupal\Core\EventSubscriber\DefaultExceptionHtmlSubscriber.This brings more consistency to both exception handling and HTML page rendering: that then leaves only a handful of routes/responses operating in limited environments (the complete list:
install.php/update.php/authorize.php/PHP code exceptions (DefaultExceptionSubscriber)/maintenance mode (MaintenanceModeSubscriber)), which is conceptually simpler to understand.This ties back to [HTML-RENDERING].
Letting main content set the status header
In all of Drupal core, there's only a single case where the main content (
_content) wants to set the HTTPStatusheader (Status: 200by default).In HEAD, Views'
\Drupal\views\EventSubscriber\RouteSubscriberhas an event listener forKernelEvents::VIEWthat runs very late, to modify theHtmlPageobject's HTTP status code just before it's converted into a response. This is a global in disguise.But to determine which status to set, some other Views code sets a request attribute (another global in disguise).
So not only does HEAD actually use two globals, this issue of course aims to remove
HtmlPage, so this pattern cannot continue.Drupal already uses
['#attached']['http_header'], which maps to_drupal_add_http_header(), which is retrieved viadrupal_get_http_header()(and yes, this is also a global, but it's not a new one), and\Drupal\Core\EventSubscriber\FinishResponseSubscriberapplies those headers to the SymfonyResponseobject. The only tricky bit is that Symfony apparently special-cases theStatusheader, so a tiny addition was made:Hence now the main content can simply do e.g.:
to render content, but mark it as a 404 to prevent search engines from indexing the response.
'page' vs. 'html'
Part one: the templates
page.html.twighas variables for every region of this form:page.REGION_NAME, i.e.:page.header,page.help,page.content,page.sidebar_first, and so on.Confusingly,
html.html.twighas very similar variables:page.head,page.styles,page.scripts,page.content, but then alsopage_topandpage_bottom(note the underscore instead of period). This is an artefact that AFAICT has two causes:HtmlPageobject enforced this, becausepage.scriptsautomatically mapped to$page->getScripts(),page.contentto$page->getContent(), and so on.So,
page.SOMETHINGinpage.html.twigis operating on a very different "page" thanhtml.html.twig. The latter is operating on theHtmlPageobject, the former is operating on a$pagerender array.But it seems that
html.html.twigis — somehow — rendering whatever is in the$pagerender array again!To be clear, the relation of
page.html.twigtohtml.html.twigis thatpage.html.twigmaps exactly tohtml.html.twig'spage.contentvariable. That's right — if you're confused, you're not alone. It took me quite some frustration to figure that out.That's why I changed
html.html.twigfrom:to:
This makes things much clearer IMO:
html.page== the renderedpage.html.twightml.page_top== the results ofhook_page_top()html.page_bottom== the results ofhook_page_bottom()This also makes it super clear where the results of
hook_page_top()/hook_page_bottom()will end up, and it makes it super explicit that these are NOT page regions.Part two: consequences of removing
HtmlPagefor 'page' template preprocessingGoing back to
$variables['html_attributes']and$variables['attributes']inhook_preprocess_html(), rather thanIt's also no longer possible to specify attributes for
#type => 'page'(and hence also not inhook_preprocess_page()) which then automatically become attributes on the<body>tag. The<body>tag is not part of#type => 'page'/page.html.twigand hence it should not be possible to set<body>attributes from there.This only worked thanks to hacks in the old system. If you want to set
<body>attributes, implementhook_preprocess_html()and set$variables['attributes'].Hence this fixes #2218271: Theme preprocess functions have to futz with $page_object = $variables['page']['#page'] in weird ways.
Attachment handling
Only a single call to
drupal_process_attached()remaining when rendering HTML pages (we don't render HTML pages when rendering something for e.g. AJAX responses, so those still have their own calls).And best of all: it's called in the same place and just before calling
drupal_get_(css|js)()! This means that the putting of data into a global static is now in the same place as where we're gathering the data from a global static, hence it will be finally possible with a simple follow-up patch to get rid of those global statics!In
template_preprocess_html():Comment #15
dawehnerGreat work in genera ... here is some quick feedback.
I'm sorry but please split things up into reviewable and discussed pieces. No wonder you did not wanted help, one patch to rule them all IS NOT the fastest way, especially not when it comes down to spreading the knowledge around.
First general feedback: I think it is wrong that HtmlController::handle does all that ... Didn't we talked about converting stuff on kernel::view
and not directly as part of the _controller callback? ... I still consider it as a requirement for D9 to get rid of _content, _form and what not. Your current architecture does not really allows that.
How?
Why the hack that?
One thing to rule them all is not necesarrily a good architecture. Are we really sure we want to have one really big object?
Comment #16
fabianx commentedThanks Wim.
The proposal looks great to me, but I agree with dawehner we should split things up into little manageable patches.
- E.g. there is an issue open for changing signature of drupal_render() back again, this can go in independently.
- The feed icons mini issue can go independently.
- For the page vs. html issue, we will need themer feedback, too - generally I tend to agree as themers will copy html.html.twig anyway as all classes are added in the templates directly per convention.
- Putting a lot of things like headers to #attached makes a lot of sense.
So lets discuss this proposal some, then come up with manageable child issues of this task and split those out?
Comment #17
wim leersOf course. I agree. I'll repeat what I already said:
I understand it's a lot of digest, but please first look at a patch before making claims about it. This is wrong.
HtmlController(281 LoC) is only a bit bigger than just theHtmlPagevalue object on its own (234 LoC).I say "A + B + C + D + … = the new thing", but what that really means is bits of B and C and all the rest no longer being necessary at all. By removing the intermediate value objects, and just use
drupal_render(), we don't have to convert from one intermediate form to another to yet another anymore.Well, I kept
ContentControllerSubscriberas-is mostly. If other people agree with you on that, I think we could makeContentControllerSubscriberreact toKernelEvents::VIEWinstead, detect if it's a render array, and if so, apply pretty much exactly the same logic there.I mentioned #1594870-24: Decide what KernelEvents::VIEW subscribers should be used for, document it somewhere, and make ViewSubscriber comply in my comment, which I recommend you to read. It provides a rationale — written 1.5 years ago — for precisely the way it's implemented right now: render arrays (i.e. rendered content) should use
_contentroutes, which are enhanced to_controllerroutes. Whereas data (and corresponding domain objects) would indeed useKernelEvents::VIEWto render that data in a certain way.But as I said, if consensus says we should always use
KernelEvents::VIEW, then that's fine, and I'll change the patch to use that instead.Comment #18
fabianx commented#16: \Drupal\Core\Controller\HtmlController
The class is simple enough that I think it is okay to just have one class to delegate the task of building a page to an event, collect page assets and render page_top and page_bottom.
Further feedback for untangling drupal_render():
- The render_stack should in the ideal case be setup before the first controller call is made and injected or gotten via the container and be made a first class citizen (this can be follow up).
- Then every drupal_render() call is a non-root call, just in different stages of the page and in varying recursive levels, where the root is the render stack itself.
This allows to just do:
for the final X-Drupal-Cache-Tags and the render cache starting at recursion level 1 when initialized / constructed.
Things like: drupal_render($page['top']); will then just naturally work - without any $is_root call needed, but as said I am okay with intermediate step.
Comment #19
wim leersHere are the first child issues:
drupal_render()topic in #14: #2361681: drupal_render(): invert second argument ($is_recursive_call -> $is_root_call) => more strict, better DX/TXComment #20
wim leersComment #21
moshe weitzman commentedI read through WIm's comment carefully and this is a huge improvement IMO.
I'm going to apply the patch and step through the flow in a debugger and see if I can provide more detailed feedback. Stay tuned!
Comment #22
yched commentedLargely above my head, I won't interfere with the discussions here - just : that diagram in #14 is insanely helpful, we should really make sure it ends up in a prominent place in the docs.
Also : impressive work, Wim :-°
Comment #23
wim leers#21 & #22: Thank you :)
I spent several hours today on creating that diagram. It's very much intended to become part of the official documentation! It's the thing I (always) wish(ed) already existed. Hence I've already included a SVG as well. If the diagram needs to be updated because we decide to change some things here, then I'll update it. Hence I've also provided the original format (in OmniGraffle), so that anybody can get the original (from which I generated the PDF, PNG and SVG).
Comment #24
moshe weitzman commentedI stepped through in a debugger and it is pretty straightforward. There's a little dance with the drupal_render() calls in \Drupal\Core\Controller\HtmlController::prepareMainContent but it is well contained. I don't have any issues with the patch.
Comment #25
fabianx commentedI agree that the diagram is awesome, the flow is really simple and clean.
It also makes things like using #pre_render pattern for main request content much simpler.
Comment #26
yched commentedHem, said I wouldn't interfere but ... :-)
Sticking to the flowchart, the steps mentioned in the "_content / HtmlController::handle()" swimline seem oddly named :
Step 1, "getMainContent()" is about retrieving the main content, fine.
Step 2. "prepareMainContent()" is about preparing a full-fledged #type = 'page' render array
Step 3. "renderMainContent()" is about wrapping in a #type = 'html' and rendering it
So it looks like the progression is not so much "get / prepare / render the main content" but rather "get the main content / assemble the page content / render the html content" ?
Comment #27
wim leers#24 & #25: lovely! :)
#26: :) I welcome your interference!
I'm replying mainly with an updated flowchart. See, the thing is that this is not specific to building a HTML response; the controllers building AJAX/Drupal Dialog/Drupal Modal Dialog responses go through the exact same three steps.
<body>, which is represented bypage.html.twig. Hence the HTML controller (HtmlController) "prepares" the main content it received by transforming it into#type => 'page'. The final rendering of the HTML document requires#type => 'page'to be wrapped in#type => 'html', since#type => 'html'is how Drupal renders (usingdrupal_render()) an HTML document: by filling the placeholders inhtml.html.twig.AjaxController), no preparation is necessary.prepareMainContent()is basically a no-op, we go immediately torenderMainContent().DialogController&ModalController) we again have a preparation step.But, the dialog should show this much more clearly than I can put into words. I'm only putting it in the IS, not here, because otherwise this issue page will become super slow to load.
Comment #28
yched commented@Wim: Thanks for the updated chart. Seeing ajax / dialog alongside is even better :-)
Got that - but still we are using the same terminology ("Main Content" / $main_content) for related-but-importantly-different things across the various steps of the call stack.
MainContentControllerBase::handle() basically does (simplified, comments are mine) :
In short, this introduces a terminology that looks "official" and specific ("*main* content", instead of just "content"), but uses that terminology indifferently for all the (importantly) different states the "content" goes through.
Trying to take step back :
- From its phpdoc, MainContentControllerInterface is "The interface for "main content" (_content) controllers".
- So "MainContent" == _content (as opposed to "the
<body>for HTML pages") - right ?- So we need another terminology for "the final content wrapping / built from (and in some cases in fact equal to) that main content" ?
Why not just "content" ? It seems intuitive that a "main content" is called "main" because it's a subpart of a larger "content" ?
For HtmlController, that "content" is a #type 'html', fair too.
Then MainContentControllerBase::handle() does (simplified again) :
?
Comment #29
wim leers#28: All of that makes a lot of sense.
I'm totally fine with that :)
Thanks for interfering :D
#2357937: Remove {{ feed_icons }} from page template (page.html.twig), #2361681: drupal_render(): invert second argument ($is_recursive_call -> $is_root_call) => more strict, better DX/TX and #2239003: Remove the '_http_statuscode' request attribute are RTBC. The 4th child (#2361693: AjaxEnhancer is dead code, remove it) was trivial and is already committed.
Once either of those children are committed, I'll reroll this patch.
The next child issue, just created it: #2362517: Improve default 403/404 exception HTML subscriber: don't duplicate the page render pipeline, use the routing system — add system.403 and system.404 routes.
Comment #30
fabianx commentedI agree with the new naming in #29.
I still think we need to start the render chain / setup the stack before getting the main content, but that can be followup and works nicely with this proposal.
Thanks for the new child issue and thanks so much for breaking this up into manageable chunks!
Comment #31
wim leers#2239003: Remove the '_http_statuscode' request attribute, #2357937: Remove {{ feed_icons }} from page template (page.html.twig) and #2361681: drupal_render(): invert second argument ($is_recursive_call -> $is_root_call) => more strict, better DX/TX got committed, and #2362517: Improve default 403/404 exception HTML subscriber: don't duplicate the page render pipeline, use the routing system — add system.403 and system.404 routes is now also RTBC. We're making good progress :)
Next child issue: #2363025: Push usage of drupal_set_page_content() higher: out of blocks and page display variants.
Currently rerolling this patch now that three (small) patches have been committed, and incorporating #29.
Comment #32
catchComment #33
wim leersThe rerolled patch goes from 293 KB to 244 KB :) Hurray! First, here's a straight reroll. I'm providing only an interdiff for the things that weren't fixed during the merge process. The interdiff shows a crucial part of
HtmlControllergetting a much, much clearer comment.Also attached: a review-only (do-not-test) patch, which was created with the
-Dflag to list file deletions, but not list the contents. For some reason, testbot can't deal with this, hence marked as "do-not-test". That should make reviews already somewhat feasible, but most people probably want to wait until more child issues have been committed.In my next comment:
Comment #34
wim leersAnd here it is, #29 implemented.
Comment #35
wim leersAnother child issue added: #2363139: _content controllers may only return render arrays, not strings.
Comment #36
yched commentedYay !
No biggie, just curious :
Is there a reason "wrap in #type 'html' " and "run hook_page_(top|bottom)()" are done in step 3 rather than in step 2 ?
Also - those three methods are a handy decomposition of what happens in MainContentControllerBase::handle(), that provides clean override mounting points for the concrete subclasses. But they are only that - helpers for handle() that are never called from the outside, and thus have no real reason to be public methods promoted to an interface ?
In practice, MainContentControllerInterface is then just ... handle($request, $route_match, $controller_definition) ?
Which in turn makes it quite unspecific to "_content / MainContent" (I was actually surprised that there isn't an existing, generic interface for that definition of handle()) ?
The real structuring concept for "_content / MainContent" seems to be MainContentControllerBase, rather than the interface ?
Comment #37
wim leersAn excellent question.
This is one of the things I'm not entirely sure about what's the best way — what matters most is that we have formalized steps to take "the main content" and turn into a
Responsefor the negotiatedformat(Content-Type). It's one of the things that needs to be discussed here :)A few thoughts:
MainContentControllerBase.ContentControllerSubscriber::onRequestDerivceController()just generate a closure that does these three steps? Then the interface really is just about formalizing those 3 steps. But then the resulting code aren't really controllers.::handle().Hopefully that made sense. I hope you have some good ideas on how to combine those needs in a better way! :)
Comment #38
yched commented@Wim: Yeah, those objectives sound valid.
I'd think, If the MainContentControllerInterface only had handle() :
- other "_content controllers" would still be strongly encouraged to use MCCBase
(e.g. because we'd say so in the doc of the interface, and also because in order to be a "_content controller" you need the non-fully-trivial code in MCCBase::getMainContent())
- MCCBase then strongly encourages you to follow its internal code flow of (protected) getMainContent() / prepareContent() / renderContentIntoResponse(),
- but you can still override handle() if you want to bend that flow a bit ?
Point is, looking at an interface gives you a notion of what the outside world is going to do with the objects. So you tend to understand the various methods listed there as "independant tasks called independantly". Seeing 4 methods, 3 of which are in fact only intended to be called by the 4th one, is a bit misleading about the actual code flow.
Anyway - at this point, this does feel like bikeshedding, so I'll leave, er, others more familiar with the problem space :-) comment on the actual code...
Comment #39
wim leersAnother child issue added: #2364127: Merge AjaxResponseRenderer into AjaxController. As you can probably already tell by its title, it's getting more difficult to split this patch up further. I have ideas for 1 or 2 more child issues, at least one of which will be more clear than #2364127.
Rerolling this patch, because #2362517: Improve default 403/404 exception HTML subscriber: don't duplicate the page render pipeline, use the routing system — add system.403 and system.404 routes got committed today!
Comment #40
wim leersAnother 16 KB of changes removed from this patch, thanks to #2362517: Improve default 403/404 exception HTML subscriber: don't duplicate the page render pipeline, use the routing system — add system.403 and system.404 routes having landed.
Comment #41
wim leersComment #42
wim leersAnd another child: #2364189: One service per format + no hardcoded format to service mapping, but tagged services.
Comment #43
wim leersThere's only one more thing I can split off into a separate issue, but that's blocked on #2363025: Push usage of drupal_set_page_content() higher: out of blocks and page display variants.
If all four uncommitted patches would be committed, then we'd already be down to a ~110 KB patch to be reviewed. Together with that final child issue that I just mentioned, it should drop below the 100 KB barrier. Then this should be a fairly reviewable, well-focused patch.
Comment #44
wim leers#2363139: _content controllers may only return render arrays, not strings landed; straight reroll.
Comment #45
effulgentsia commentedHere's my suggestion for how to resolve #36-#38:
- Create a new sub-namespace:
Drupal\Core\Render\Response(i.e., the part of the Render system that deals with rendering a render array to a Response).- Move
MainContentControllerInterfacetoDrupal\Core\Render\Response\ResponseRendererInterfaceand(Html|Ajax|Dialog|Modal)ControllertoDrupal\Core\Render\Response\*ResponseRenderer.- The above interface should have only two methods:
prepare()andrenderResponse().- Rename the service tag from
main_content_controllertorender.response_renderer.- Move the implementation of
MainContentControllerBase::getMainContent()andMainContentControllerBase::handle()intoContentControllerSubscriber, and removeMainContentControllerBase. #37 suggests doing that with a closure, but they could also be methods (e.g.,$request->attributes->set('_controller', array($this, 'handle'))). A closure has the benefit of not needing the methods to be public, so another option is a thin closure around a protected method. I'll leave that choice to you, Wim.The benefit of above is that per #15, if we want to, we'll be able to in the future (maybe even in 8.0 or 8.1) change the implementation from a controller wrapper to a
KernelEvents::Viewlistener without that affecting any interfaces or the response renderer implementations.An additional suggestion:
That's an ugly return value. How about making the return value just a render array? For HtmlResponseRenderer, the title can be on the page element's #title and $custom is unused. For DialogResponseRenderer, how about introducing a
'#type' => 'dialog'type that can wrap the content element and have a #title, #target, and #options?Comment #46
wim leersWow. Awesome! Thanks for your wonderful, clarity-bringing comment :) I agree with your comment, as do dawehner and Fabianx (I talked to them in IRC).
The only thing that that doesn't handle, is when you want to override the
handle()method for a certain format. But that should be extremely rare anyway, and can be addressed by subclassingContentControllerSubscriber, only making it work for your format, and then overridinghandle().I'm currently implementing this. I've got the biggest changes already implemented (including running at
::VIEWinstead of at::REQUEST), now just doing the renames. Now that the code is running at::VIEW, the change from "controller" to "renderer" in your suggested naming scheme also makes a ton of sense. And with all this done, #2092647: Consolidate _form, _controller, _entity_form, etc. into just _controller? becomes trivial! :)In that area (naming scheme), I'm not entirely convinced about
The reason being: this name suggests it's something strongly related to
AjaxResponse, but it's not; it's completely independent.The part that's missing in your suggested naming scheme for me — on second thought, because I initially was very happy with it — is the "main content" part.
What about
MainContentRendererInterfaceand\Drupal\Core\MainContent\(Html|Ajax|Dialog|Modal)Renderer? I think that more accurately captures what this is about. The service tag would then becomemain_content_renderer.RE: additional suggestion: I totally agree it's ugly. But
'#type' => 'dialog'is not an option; we don't render the entire dialog usingdrupal_render(), we render it into an AJAX command. Introducing'#type' => 'dialog'would therefore be misleading.We could just piggyback everything onto the main content render array, since that piggy backing is very much self-contained. It's choosing between piggybacking (which is ugly) and an ugly triple of return values (which is also ugly). The latter is more explicit though, so that's why I went with that.
If people prefer the piggybacking, then I'll update the patch to do that instead.
Comment #47
moshe weitzman commentedI have done stuff like triple return values in the past and not felt bad about it. I think it is clear and useful in situations like these.
Comment #48
wim leersI meant to say
\Drupal\Core\Render\MainContent\*.Comment #49
almaudoh commented#46 + #45 sounds good
Comment #50
yched commentedI like the new proposed organization too, but - terminology again :
Drupal\Core\Render\MainContent\*Renderer makes those a little disconnected from "this is what is fired in ContentControllerSubscriber::VIEW to produce a Response" ?
"MainContent" is a bit vague too.
- In the previous proposal, it was always "MainContentController(Interface)", which grounded it with some more specific context "Controllers for routes that massage the output of a 'main content' callback".
- Does "Content" in ContentControllerSubscriber correspond to "MainContent" in Drupal\Core\Render\MainContent\*Renderer ?
Is there a way we could unify ?
Comment #51
wim leersI had a long but productive call with effulgentsia to resolve #45/#46 — we agreed on what I proposed in #46, but with the service tag being namespaced, so:
render.main_content_renderer.We also agreed that it makes sense to convert from what all preceding patches did (generating a
Responsefrom the_controllerset during::REQUEST) to letting_contentcontrollers return render arrays and then during::VIEWconverting them into aResponse. dawehner has pointed out repeatedly that this is how it was intended to work, that that was said during DC Amsterdam (which I forgot — my apologies!), and I realized that that is in fact how HEAD already works. So it's not an actual change relative to HEAD.I'm currently still making the necessary changes, but that will take a while longer; especially because I will also need to update the diagram and #2363025: Push usage of drupal_set_page_content() higher: out of blocks and page display variants has landed in the mean time.
I will probably not be able to still post a patch today, but definitely expect one tomorrow! :)
Yes. This has bothered me also. IMO: rename
ContentControllerSubscribertoMainContentRendererSubscriber. dawehner, Fabianx, effulgentsia: what do you think?Comment #52
larowlanInstead of a triple value return, let's create a value object like we did in #2242749: Port Form API security fix SA-CORE-2014-002 to Drupal 8
Comment #53
effulgentsia commentedGood idea, but how about
MainContentViewSubscriber(since it'll be subscribed to the::VIEWevent)? Note, we can then rename (in a separate issue) our existingViewSubscriberto something likeDataViewSubscriber(since it's about returning a plain data object, like for an autocomplete response, as opposed to a "main content" render array).Yep, but per #51, it's specifically the "controller" part of that terminology that we want to remove now, in order to change the implementation from a controller wrapper to a view listener. Good call on retaining the "MainContent" part though.
+1, though that starts looking a bit like HtmlFragment again, just with a much scaled down responsibility :)
Comment #54
effulgentsia commentedSo, just to be clear, let's NOT use that name. Not because it's a bad name, but because it's too good a name to squat for our specific use case. A properly generic
HtmlFragmentAPI would be great to continue exploring in contrib and maybe fold back into core for 8.1, 8.2, or 9.0. For our use case, perhaps the value object could be\Drupal\Core\Render\MainContent\PreparedContent? With methods likegetTitle()andgetContent()? And then perhaps aPreparedDialogContentcould extend that withgetTarget()andgetOptions()?Comment #55
larowlan#53 and #54 yep prepared content sounds good +1
Comment #56
fabianx commented+1 on all MainContentViewSubscriber, also +1 on PreparedContent.
Comment #57
wim leers+1 for
MainContentViewSubscriber.I think a value object with such a limited lifetime a waste of CPU cycles and not very helpful at all, since only <1% of developers will ever write their own main content renderer. The code literally looks like this without the value object:
Introducing a value object in between there does not make sense to me. Especially not if we're going to add methods for each property. If we'd be using value objects like C-structs, then I'd understand it more. But in Drupal 8, we prefer to always have methods for some reason.
Hence We'd change that code to this:
And if passing in
$prepared_contentwholesale, then we'd still have to do that unpacking in the main content renderer. I just don't see the value.Actually, with all those changes, we're down from 4 methods (
getMainContent(),prepareContent(),renderContentIntoResponse()andhandle()to use the first 3) to 2 (prepare(),renderResponse()). Andprepare()is arguably mostly/only there for thehtmlcase. Theajaxcase doesn't need it at all. Thedialogandmodalcases use it, but could easily live without it. So perhaps it's better to dropprepare()and keep onlyrenderResponse(). Thehtmlcase (HtmlRenderer) can just have a protectedprepare()method. KISS.Talked to dawehner about this in IRC, he agreed. That makes
MainContentRendererInterfacevery clear, very focused, with only:And it lets us simply avoid the whole
PreparedContentdiscussion. :)#2364127: Merge AjaxResponseRenderer into AjaxController apparently landed half an hour ago. Together with #2363025: Push usage of drupal_set_page_content() higher: out of blocks and page display variants having landed a few days ago, that should bring this patch down to 175 KB total, 120 KB to be reviewed (i.e. with
git diff -D).That leaves just #2364189: One service per format + no hardcoded format to service mapping, but tagged services, but that's very closely related to the discussion we're now having here (which is why it's marked as postponed: I postponed it yesterday precisely because we're having this discussion here).
I think it might be wiser to do the remainder of the work in this issue, because everything that remains is truly focused on the render pipeline, without tangents. We'll see what works best. But since we're already having this discussion here, that just feels more appropriate for me now.
Comment #58
effulgentsia commented+1 to dropping prepare() (as a public API) and closing #2364189: One service per format + no hardcoded format to service mapping, but tagged services in favor of doing it as part of this larger issue.
Comment #59
wim leersAddressed everything from #36–#58 as well as #15 — thanks for this excellent discussion, all! :)
This now really paves the way for #2092647: Consolidate _form, _controller, _entity_form, etc. into just _controller?. (Because the main content renderers now work during
KernelEvents::VIEWrather than as a_controllerduringKernelEvents::REQUEST.)The interdiff is quite large, because there's lots of moving around of things. It also means that the patch size hasn't decreased as much as predicted with those two additional child issues having landed.
Highlights:
MainContentControllerBaseis gone.ContentControllerSubscriberwas kept, but only for theKernelEvents::REQUESTevents of 1) negotiating a format (which will go away once #2331919: Implement StackNegotiation lands) and 2) simply assigning_contentto_controllerunconditionally (which will go away once #2092647: Consolidate _form, _controller, _entity_form, etc. into just _controller? lands). This allowsMainContentViewSubscriberto stay true to its name: be aKernelEvents::VIEWsubscriber, and nothing else. It also means we'll be able to simply deleteContentControllerSubscriberonce we've cleaned up our cruft.application/hal+json) to a route that either A) doesn't have a REST module route, B) doesn't havesupported_formatsset or C) doesn't havesupport_authset, all tests in HEAD expect a 404. But this is wrong; in all cases in HEAD, there is the canonical (non-REST module) route, which doesn't specify any formats! HenceAcceptHeaderMatcheractually considers them a (low-quality/non-preferred) match! With this interdiff, when REST module is not set up (or incorrectly) for an entity,MainContentViewSubscriberwill correctly get to handle the request, but will be forced to bail with a 406 since it doesn't know how to render a render array into a HAL+JSON response.Once again, the diagram has been updated. However, since we're now effectively depending on two rather than one Symfony events, I felt it became more important to show the timeline of the different Symfony events. More generally, I felt it was important to visualize which bits of request handling and rendering are pure Symfony and which bits are Drupally. For the Symfony part, I'm leaning heavily on http://symfony.com/doc/current/components/http_kernel/introduction.html, and using exactly those headings to indicate the different steps of
HttpKernel::handle()In doing so, I also realized that it would be valuable to truly capture the entire request handling flow. Hence I included
index.phpas well. So the diagram now showsindex.php, Symfony'sHttpKernel, the controller in Drupal,MainContentViewSubscriberand the various main content renderers. I hope you like it :) (And if you have ideas on how to make the diagram significantly clearer, let me know!)An unfortunate side-effect is that the diagram has gotten quite a bit bigger (30% wider, 10% taller). When printed on A4 paper with a standard (non-marginless) printer, you can still read it just fine, albeit the font size is very small.
Comment #60
fabianx commentedThe diagram is great!
Once we have this implemented, please publish a blog post on it as the request timeline never has been clearer!
It also makes all a lot of sense.
Comment #61
wim leers#60: Glad you like it! :) And absolutely, I will write a blog post about this!
Added a new child issue: #2366147: Introduce a page display selection event , which I can do now that #2363025: Push usage of drupal_set_page_content() higher: out of blocks and page display variants has landed. That should take another 20KB out of this patch.
Comment #62
fabianx commentedNot reviewed full patch, yet, but this stood out particularly:
IIRC there was a good reason we moved that to an object and rendered it lazily, but I don't remember.
Searching the git log for RenderWrapper() should give back something ...
Oh, yes it was the removal of the hook_template_process() layer.
You remove the ability to add/change css/js via preprocess_html() this way.
So can we keep that in a value object still?
Also as spoken on IRC would like the bare_html_renderer to perhaps be factored out as its a good and on-its-own pre-step - that is independently useful.
Comment #63
dawehnerGreat diagramm and great general flow. Its great to see the separation of concerns for the different main content renderers.
Kinda nitpick: I'm curious why you decided to use an event to determine the page display variant and not
using a chain of responsibility pattern, so a tagged services approach. The first one who says yes, wins.
But yeah this is a small detail.
+1 for having a proper service here, this helps a lot, especially in terms of brittleness.
Just curious: Can we make that kind of stuff easier accessible?
Maybe we should consider linking the flow diagram for here or somewhere else? I think this would be SUPER helpful to understand things.
Do those renderer have nothing like a common interface? MainContentRendererInterface seems to be one.
Is there any way how we can be a little bit more specific her? An array of domain objects for example could be a usecase someone has.
Nitpick: you can just use
@inheritdochere.Just curious, why do we have a priority set here? Maybe 30 is just coming from
KernelEvents::REQUESTearlier?This is beatiful and well be even less, once we move these classes into the templates.
This is kinda confusing. So
drupal_render_root()is actually not the last call :) Maybe we should explain why its okay to "just" calldrupal_render()for$htmlIt would be great if you could add some documentation that these renderes should rely on as less as possible states, like having routing system being executed. We had some crazy bugs in the installer at some point for example.
thiswill => this will
Given how similar their implementations are, I wonder, whether the callers should instead distinct the behaviour between install/maintenance, so we end up with just a single
renderBarPage($content, $title, $page_additions, $attributes, $page_theme_property)Interesting idea ...
nitpick: let's use the interface here
Maybe I'm stupid but I don't see why these events should be part of system module.
Did you considered to expand the event to have the route match available? It would be just much nicer DX if you would not need to inject it.
So there seems to be just a single usecase for it, ... did you considered to instead call a method on the main content renderers injecting in there or alternating the arguments of the service. Using the parameter is just one small level of additional indirection.
Just curious, this is just actually for a specific page variant. Did you considered to be able to move this logic into the block admin demo page variant?
That renaming seems a little bit out of scope but well whatever you want.
So good!
Nor, why th simple page should not be part of core.
Ah this is the reason why these are static methods ... I think you can also move it to the service, make it a public method, but not be on the interface ... we are testing core code here so its fine to assume the actual congrete implementation.
nice!
Oh this is maybe a good place for this diagramm.
This is indeed also better to understand.
Oh, this is a bit confusing, can you maybe add it to the IS why we need to change it?
Comment #64
wim leers#62
It was related to
drupal_render()putting the metadata for assets in global statics by callingdrupal_process_attached(). As of #2273277: Figure out a solution for the problematic interaction between the render system and the theme system when using #pre_render,drupal_render()doesn't calldrupal_process_attached()anymore. It just bubbles up the attached assets from children up to the root of the render array. The root of the render array now contains what those global statics used to contain. So now that complexity/cruft is gone, and we don't need to deal with it anymore.The code cited in #62 omits the few lines that come before it, and that helps explain why the code works this way:
We basically have all the required attachments in
#attached, then put them in a global statics because that's what we have in HEAD, and then immediately pull them out of the statics. Because the code is already next to each other, it will now be trivial to finally get rid of the global statics :)Also see the "attachment handling" section in #14.
This is true. But this is by design.
The
html.html.twigtemplate is responsible for rendering the final HTML document. Including<HEAD>. Hence also including<SCRIPT>tags to load JS assets, analogously for loading CSS assets. Therefore, if we're going to allowHOOK_preprocess_html()to attach additional assets… how are we ever supposed to be able to render the HTML to load JS assets etc (<SCRIPT>tags etc)?This is essentially a slight change back to how it worked before: you cannot attach things to
html.html.twig, but you can attach them topage.html.twig(just like before). This also makes conceptual sense: you can attach assets to the page (page.html.twig~=<BODY>), which are then bubbled up to the level of the HTML document (html.html.twig).And finally, this is also consistent with the concept of
drupal_render(), which is rendering recursively. This patch makes'#type' => 'html'a render element like any other again, no more special casing with a special pre-builtHtmlPageobject being inserted and the nasty TX consequences of that. Basically, we need to populate{{ html.scripts }}inhtml.html.twig. The way Drupal/drupal_render()allows you to populate that variable, is through the preprocess function associated with that template. Hencetemplate_preprocess_html()MUST be able to calculate$html['scripts']. Hence by that time, all attachments be known. Hence, we cannot allow additional assets to be added inhook_preprocess_html(). Q.E.D.One last thing: if a theme wants to attach assets to all pages, a theme should just specify assets to be loaded. If a module or theme wants to attach assets conditionally, use
hook_page_attachments()/hook_page_attachments_alter(). There really is no need forhook_preprocess_html()AFAICT.I hope that made sense (and that I didn't make any mistakes, all that was from memory).
#63
WOW!!! Thank you for such a complete review! I will probably not be able to finish addressing those remarks today, but I'm so looking forward to addressing your feedback :) Thanks!
Comment #65
wim leersDiagram updated based on feedback from msonnabaum in chat. Now consumes less vertical space, by reducing the height of events 2/3/4 in
HttpKernel::handle(), since they're not important to understand the whole, only step 5 is (and step 2 is even confusing, since it might be mistaken for "apply routing to determine which controller to use"). A few arrows are made clearer.Comment #66
wim leersRE: nitpick about page display variant selection event being an event: doesn't work because
SimplePageVariantby definition must alway say yes because it's the fallback.BlockPageVariantwill also always say yes. It'll be up to Page Manager/Panels/… to have whatever precise/subtle conditions they want to use to say "yes", hence events feel more appropriate.$variables['html']['page']['#title']to$variables['title'], so that people can use{{ title }}inhtml.html.twigto print the plain title? Not yet added here, but +1 to that.Unfortunately not AFAICT. A render array doesn't have to have a
#type. A render array might also only contain render child arrays, e.g.['a' => ['#type' => 'something'], 'b' => ['#type' => 'something else']]— which means that we'd have to recurse into the array to find any render array-like characteristics. Finally, the empty array also is a valid render array.This is unfortunate, but it's not that important in the grand scheme of things. I think it makes more sense to wrap arrays of domain object in a meaningful value object than it makes sense to wrap render arrays in a value object. Because then you can indicate for those domain objects what type of collection it is: an ordered list, unordered list, set, stack, linked list, and whatnot, which then allows multiple view subscribers to render that same collection in very different, more meaningful ways. Render arrays are what they are, but for domain objects, we can store them in a richer, more meaningful collection than "PHP array".
HtmlRenderer::renderResponse()(the current comment still refers to::renderPage(), which is wrong). I now clarified the comment that the exact same pattern is used in that other place, with a detailed explanation. DRY: I don't want to write a near-exact duplicate comment.BareHtmlPageRendereris so nice/easy to understand (as you said in point 8). IIRC this is also why I did 26: those(install|maintenance)-backgroundclasses were added by Seven in preprocess functions because those provided by core were inconsistent/broken. This guarantees that the(install|maintenance)-pageclasses are always set, without fail. Hence I was also able to do the small clean-up in #26.If you're not convinced by this argument, and others also don't, then I'm fine with doing what you described. But I remember trying exactly that first and running into problems, or at least brittleness.
\Drupal\system\Event\SystemEvents->\Drupal\Core\Render\RenderEvents\Drupal\system\Event\PageDisplayVariantSelectionEvent->\Drupal\Core\Render\PageDisplayVariantSelectionEventhook_page_top()maps to{{ html.page_top }}. Page display variants map to{{ html.page }}. Therefore page display variants can't set things in{{ html.page_top }}or{{ html.page_bottom }}. This is not new, this is already the case in HEAD. But withhook_page_build(), you were able to treat them as the same, even though they are treated very differently by Drupal.page_topandpage_bottomare NOT actual regions! (This is also why they don't show up in the Block UI.)\Drupal\Core\Render\Plugin\DisplayVariant\SimplePageVariant.@internal. Now using an injected module handler rather than resorting to\Drupal::moduleHandler().Comment #67
wim leersAnother diagram update. This time mostly based on feedback from effulgentsia, but also some from Gábor Hojtsy.
He roughly made two suggestions:
Comment #68
cosmicdreams commentedOMG I can't believe I'm saying this but, if you find yourself with extra time on your hands it might also make sense to break out the different workflows into different pages. For example, it would would great to show a Symfony developer:
red = Here's the workflow if you want to handle it yourself (just to show that their expectations aren't violated).
blue = Here's an example of the workflow if you want to work with Drupal a little
green = Here's how Drupal typically responds to requests.
Love the combined diagram, awesome to see it.
P.S: It needs a legend.
Comment #69
wim leers#68:
Comment #70
catchI've been discussing this in depth with Wim both before and after this issue was opened.
Looking through the patch it's better than I expected in terms of the changes necessary, and the diagram is fantastic for understanding the overall flow. Gets us extremely close to doing things which have been a goal of the entire release, like removing the drupal_get_*() functions and only 1600 new lines of code makes this very reviewable.
The main wart is handled by #2359901: Discourage $main_content['#title'] in favor of route titles and title callbacks, happy to leave that problem in that issue.
Very minor nit.
This doesn't actually throw an exception as stated in the @throws.
Comment #71
cosmicdreams commented#69.1: Yes one diagram per workflow.
#69.2: Legend could describe the separate workflows with a short description. No need of a legend that describes the parts, just what they represent together.
Comment #72
joelpittetRe #62/#64
I'm fine with preprocess_html not being able to do attached as d7/d6 didn't have this feature. The "process" layer did do this as a "flattening process" mostly and that's why we removed the process layer (confusion on what it did, and early rendering/flattening, templates didn't have access to structured data) RenderWrapper was a stop gap to remove the process layer, glad it's finally gone and don't want it back because it was not a particularly intuitive DX, being a generic "call this callback function when printed with args held inside" object (AND because my poor class naming skills).
Also, there is enough context available in other preprocess functions further down to do most any conditional asset attachment necessary. page is high enough, IMO.
I'll do my best to have a review of this at BADCamp.
Comment #73
joelpittetThe reason these were prefixed with 'page.' was because it was coming in as an object and using Twig's magic method detection for page->getContent() and page->getScripts(). We don't need the 'html.' and this can now be turned back to what we had in D7 and simpler unless there is another reason for the variable changes?
I suggesting the following to revert back before #2272279-38: Kill RenderWrapper class
{{ scripts_bottom }}is the only new one and I'm glad it's getting added.Comment #74
dawehnerPair review of @joelpittet and @dawehner, continuued later
HAHA, this is a nice one! We do need css settings, absolutely!
Can we add a follow up here to not rely on this really implicit behaviour?
We (joelpittet and dawehner) don't understand 'render element'. it would be cool if someone could enlighten us
I literally hate that because it requires #2331919: Implement StackNegotiation to be rerolled
I try to understand why we don't throw an exception in that case and provide a nicer page ... maybe we could render it nicely as json for example.
Comment #75
joelpittet.
The classes we'd like those on the templates.
After that there is literally not really a big difference.
Both @tim and @dawehner thinks that the concept of a bare html page should be not be connected with maintenancen or install itself.
It is super confusing / a bug that we don't add it using hook_page_attachments, because you cannot alter them using hook_page_attachments_alter if you do it in #pre_render
follow up?
The order of the methods in this class is kinda weird. The main thing you do is renderResponse ... but its not the first thing. If you follow the code then you have to jump over. It should be renderResponse ... prepare ... invokePageAttachmentHooks
This may be an issue for contitional titles like the ones which come from the front page view.
Why do we not just use $main_content += ['#title' => NULL];
#markup is always defined to be safe anyway. ... so we could drop the full file.
Is this the responsibility of the PageVariant for things like panels and ds?
This may be out of scope?
Did we considered to override isPropagationStopped()? If you want to override choose a higher priority. We could stop propagation if a page variant got set. This potentially allows to speed things up a bit.
Cheater!
Can we open a follow up to swap that either to a template or a inline_template?
We were wondering whether the block admin demo could be implemented using a type page?
We wonder whether we can keep the test to ensure that the main content is rendered even if block module is uninstalled ...
Ideally we should use
hook_page_attachments_alter()Someone should read the documentation :p
The selector is too specific.
We should file a follow up to not rely on the incoming path but actually the system path.
Comment #76
joelpittetI guess we should status this issue too (from @YesCT) ;-)
Comment #77
wim leersThanks for all the reviews!
First, a straight reroll to chase HEAD, since the following changes in HEAD broke this patch:
Next: addressing all the reviews. (Only setting to NR so testbot verifies it still passes.)
Comment #78
wim leers#70: Fixed; that was a wrong/leftover
@throws.#72: yep, agreed, thanks for the extra context :)
#73: Correct, the
HtmlPage $pageobject was the reason for the Twig template variables being prefixed withpage.. I'm fine with what you propose, with only one difference: I don't want{{ content }}, but{{ page }}, because that accurately and clearly reflects that{{ page }}corresponds topage.html.twig.There is one subtle "regression" in your proposal: the hierarchy is less clear. For example:
page.html.twigrenders the regions, and therefore contains the regions, and this is reflected in the Twig variables:{{ page.content }},{{ page.sidebar_first }}, etc.This logic/reasoning also was present until you asked me to remove it:
html.html.twigrenders the page, page top and page bottom, and therefore contains them, but is now no longer reflected in the Twig variables ({{ html.page }}->{{ page }}).#74:
render_element, otherwise we usevariables. The exact "how and why" I don't know, but the key information is at:and the docs for
#render_children:My own answer: because then machines could be programmed to know that Drupal sites return a JSON response with the available formats/MIME types so they can repeat the request with a different
Acceptheader, silly!Note: I went with the current code because that was suggested on StackOverflow (http://stackoverflow.com/questions/4422980/how-to-properly-send-406-stat...).
Done!
#75:
And I still can't.
Because those classes that you'd like to see in the template… they can't live in the template, because they don't apply to the
maintenance-page.html.twigtemplate! They are meant to become classes on the<body>tag. And the<body>tag is owned byhtml.html.twig, notmaintenance-page.html.twig. Just likepage.html.twigends up in{{ page }}inhtml.html.twig,maintenance-page.html.twigandinstall-page.html.twigdo, too.Hence this reroll doesn't change that at all.
Also: this is still a big step forward in clarity/simplicity for rendering maintenance and install pages, so I don't believe we should hold this up over that.
\Drupal\Core\Render\Element\Html::preRenderHtml()altogether and just move that intosystem_page_attachments(). That's more consistent.@todo.In other words: we're about to call the page display variant's
build()method. We need to render the main content already, because the main content may set a title. But if we pass in the original$main_content, the one on which we invokeddrupal_render(), we'd be making it possible for the page display variant to:By enforcing the main content to be rendered already, we prevent those problems. But since we're working in a world of render arrays, we pass around that rendered HTML as
#markup. Of course, that#markuphas associated metadata that needs to be bubbled. Hence#attached,#cacheand#post_render_cache.isPropagationStopped(), so I don't think this one should either. Plus, if we'd override it, that'd prevent event subscribers from being able to usestopPropagation()and have that take effect.drupal_set_page_content(). Restored the test!Comment #79
wim leersUgh, missed this during my own review. Will fix that in the next reroll.
Comment #80
joelpittet@Wim Leers re #73 and #78
{{ content }}to{{ page }}That's cool with me thanks for the change.Sorry I didn't realize that the keys for
page_bottomandpage_topwere nested in 'html' key and you had to pull them up out of that, was aiming for a smaller patch size with that suggestion. I'll dig into that a bit later. It appears as that shouldn't be the case but... huh.Comment #81
wim leers#80: that's because
{{ page_top }}and{{ page_bottom }}aren't actually page regions. They're really{{ page_prefix }}and{{ page_suffix }}. They don't live insidepage.html.twig, they're the prefix and suffix for that very template. This is also why we have a separatehook_page_top()andhook_page_bottom()hook: they populate these "pseudo regions".I really wanted to do benchmarking + profiling of this patch — after all, we don't want to regress performance-wise while improving the DX. Since we shift around quite a few things, we must *definitely* test this.
I expected the numbers to stay pretty much identical, since the same amount of work needs to be done. A few layers of abstraction and passing around have been removed, but at the same time another one is introduced (the page display variant selection event). So: should remain pretty much identical perf-wise.
My expectation was confirmed:
ab -c 10 -n 100 http://tres/)Comment #82
joelpittetNice profiling! Yeah I must be missing something because those variables are not in page.html.twig but html.html.twig, that's where I'm lost.
Comment #83
dawehnerRe #74:
#2 Well ... type HTML should rather get the CSS/JS explicit as variable passed in, but sure this is so out of scope here.
It is just one thing we could do in the future.
#3: Ah, I can imagine how people thought this might be a sane idea but yeah I don't think that additional level of abstraction is worth in general.
We just stumbled upon it, while reading the code, so we tried to understand it. This was not a critique against the code at all.
#5:
Ah, this is a good point, now I am though confused why you changed it. I will reread your and my answer in another timezone ..., sorry.
#75:
#2:
Well, the distinction could be made by the caller itself, well I don't want to block this really important patch for that,
so let's file a follow up and try to come up with a simpler solution in the future.
#6:
Still think that the other issue just don't care about the crazy dynamicness of Drupal. Drupal is based around features, not around clean architecture.
#8:
I assume that this was always the case ... but yeah this is the way to work around double escaping everywhere :)
#9:
+1 for follow ups
#10:
Well, its not only the optimization, it is about avoiding the logic. Priority should be the way to go, not anti-priority.
The normal request flow also uses that kind of propagation logic, so the render flow should work similar.
I will RTBC this patch, in case we agree to discuss that and maybe still be able to change that behaviour later.
#13
Thank you.
#14
High five!
#15:
Another plus point, we decreased the patch size with this.
#17:
Oh well, we just did not read the documentation together ... I think we maybe should have a dedicated "review" follow up to read the documentation.
#19:
Valid response.
#81:
So the previous level of abstractions HTmlFragment/Page has never been a performance issue, right?
Okay, feel free to be killed by mark. There is no point in running AB here ... you want to test drupal not apache. You could use just a timer in
your index.php and be done.
Comment #84
fabianx commentedThe profiling is fine. I disagree that ab is not giving a good idea of an direction of where performance is headed, it is not precise, but it gives a general idea and here it was not used as the single thing.
In most cases ab benchmarks did match up with xhprof-kit / XHProf Lib analysis very much.
Of course we can say AB, XHProf, etc. all screw the statistics with their overhead, but in the end this is not about being statistically correct, but to ensure we don't have a huge performance regression or even some improvement. And that do those tools ... Just my 2c.
To the patch: RTBC + 1
Comment #85
znerol commentedWhen profiling with
ab, then use-k(keep-alive = no connect overhead) and-c1(no concurrency). Only use-c > 1when load testing servers (i.e. verifyingMaxClientssettings, etc.).Comment #86
wim leers#83:
Content-Typeheader correctly. Basically, the HTTP spec doesn't have a strong opinion about this, it's kind of unknown territory and hence unspecified behavior. Returning a JSON response is more structured and more machine-friendly, so I went with that.*.routing.ymlfile, only to override it later. I'd be fine with rescoping that patch to only do that sort of clean-up.#85: I was repeating the same practice we used in the past, but thanks for that. I will do testing using
-k -c 1from now on.Comment #87
catch#81.A yes there's no performance issue in terms of where HtmlFragment/HtmlPage are used currently.
However HtmlFragment being the final representation of string plus assets with no support for nesting/post_render_cache means it is not applicable anywhere outside of the very top level of page rendering - see the discussion in #2334219: Make blocks expose HtmlFragmentInterface directly which was part of prompted this one. i.e. if HtmlFragment was applied anywhere without adding at least some of the features of render arrays in terms of support for nested elements, there would have been a serious and unresolvable cacheability issue. No more ability to cache a mini-panel at the block level, for example.
The remaining argument for having it was 'DX', but there's two issues with that - 1. The implementation was incomplete and added complexity/confusion rather than removing it, see for example #2218271: Theme preprocess functions have to futz with $page_object = $variables['page']['#page'] in weird ways 2. Very few modules interact with the top level of page rendering and would be able to use the API, the rest all deal with render arrays everywhere anyway. The eventual answer to that will probably be #1843798: [meta] Refactor Render API to be OO instead, and we can make incremental, bottom-up steps towards that during the 8.x cycle, and already have done to an extent with things like CacheableInterface.
Comment #88
wim leersOh and one possible reason we aren't seeing a slight performance improvement (that isn't a margin of error kind of improvement, which is how the "improvements" in #81 should be seen) is that we introduce one new event: the page display variant selection event. That wasn't happening in HEAD, and event dispatching is not cheap, so it could be costing us a net improvement. But it fixes one of the big remaining questions, and since this issue aims to leave us with an understandable architecture, it was important for it to be done as part of this issue.
So, in summary: this patch presents a status quo performance-wise, but cleans up the DX, removes loose ends (page display selection a.o.) and paves the way for cleaning up more loose ends (removing
drupal_get_js()and friends).Comment #89
catchI'll probably commit this later today or early tomorrow, since discussion here has slowed down and there's wide agreement on RTBC.
Gave it one more look through and had the following minor nitpicks:
And shouldn't this implement MainContentRendererInterface?
And here too.
Why static:: here? Can't see a good reason, need a comment if there actually is one.
Comment #90
wim leersAjaxRenderer.$this->.I'll provide a reroll.
Comment #91
fabianx commentedThis does not inhibit RTBC status, but I was very confused when I read that ...
as the function is just public and not public static ...
I wonder how that does even work ...
Comment #92
dawehnerAccording to http://3v4l.org/8ArS9 this seems to be "just" a strict error, but sure fatals, in case
$thisis called.Comment #93
wim leersReroll, as promised.
#91: that's what #89.3 gets at, and it's fixed in this reroll. Regarding why that even works: PHP works in mysterious ways :)
Comment #94
catchYes PHP lets you do that, just about, I've always assumed it's because the same syntax is used for parent:: which works for both static and non-static methods (and with no other option). Which isn't a good argument but seems plausible.
Comment #95
catchInterdiff is missing
Comment #96
wim leersArghhh! So stupid! A final reroll for a single line change…
Comment #98
catchOK that's all the nitpicks I had, and the rest has been reviewed extensively by several people now. Committed/pushed to 8.x, thanks!
See you in the follow-ups :)
Comment #99
fabianx commentedYeah!
Comment #100
wim leersHurray! :)
Now that this is committed, catch and I have teamed up and updated all issues that needed to be:
Comment #101
almaudoh commentedGreat job, guys! You rock!!
Comment #102
amateescu commentedFound two small bugs coming from this patch:
#2375923: favicon missing
#2376147: Installer is missing all of the global Seven theme stylesheets