As mentioned by bjaspan and others, we should really strive to keep our data structured as long possible. This will help us cleanly implement alternate output formats.
The attached patch is a superset of #350984: Kick comment rendering out of node module. This patch can't go in until that one does. Highlights:
- Menu callbacks may return a structured array that drupal_render() knows how to process.
- The node detail page returns a structured array
- The home page returns a structured array.
Patches like these have a tendency to rewrite half of Drupal if you are not careful. I think this patch is quite small and easily digested, once its dependency goes in. If we make it perfect, we have to change way too many lines of code. For example, the pager at the bottom of the homepage really wants to be a proper drupal_render() structure but that requires rewriting pager.inc and every call into it.
| Comment | File | Size | Author |
|---|---|---|---|
| #129 | drupal.hook-page-alter.patch | 39.73 KB | dmitrig01 |
| #126 | drupal.hook-page-alter.patch | 40.96 KB | sun |
| #124 | drupal.hook-page-alter.patch.patch | 41.1 KB | dmitrig01 |
| #123 | drupal.hook-page-alter.patch | 40.64 KB | sun |
| #119 | drupal_render_new.patch | 40.74 KB | webchick |
Comments
Comment #1
david straussSubscribing.
Comment #2
eaton commentedI need to get my D7 testing install back up to the latest codebase before testing extensively, but this approach is one that I've advocated for quite some time, and support wholeheartedly. We should NOT punt the work of different output formats on the theme system: it's the job of the routing system and this approach gives us loads of extra flexibility while keeping that path clean.
Comment #3
pwolanin commentedWhile I'm certinaly in agreement with the broad approach, I'm not sure I like the direction of the implementation here.
@eation - why should the theme layer NOT be the place to transform a given piece of structured data into the chosen output format?
Comment #4
eaton commentedFirst, the current patch doesn't prevent information from being piped through the theming system: it just ensures that the theming system isn't our only option for transforming and returning output in a variety of ways.
Our theming system is highly optimized for XHTML (possibly XML) output in which small pieces are overridden to control appearance while the majority of the output uses "default fallbacks." It is currently a bad, bad fit for wildly different output formats being used simultaneously on the same site, especially when one of the formats is something like JSON or serialized PHP.
There's been discussion elsewhere about grafting a new global (or pseudo-global) onto the theming system, a switch that would tell it what format to output in: JSON, Serialized PHP, XML, HTML, PDF, etc. I think this would be a mistake and would put too much responsibility on the templating system. We can use theming in some cases, but it shouldn't be required for everything, any more than everything in Drupal should be a node because nodes are flexible.
Comment #5
eaton commentedAlso, to clarify: I'm not opposed refactoring the theme system to expand its capabilities, if that makes sense in the future. I just think that this approach is a sensible, conservative one. Allowing us to build output as structured #renderables and return that is a real, measurable towards greater flexibility, even if every piece isn't yet in place.
Comment #6
sunYay! This makes http://drupal.org/project/comment_display obsolete.
Why PNW?
Comment #7
dmitrig01 commented@sun - CNW until #350984: Kick comment rendering out of node module is in.
I think this patch is good - it allows people to work separately on making output formats work and on making menu callbacks return drupal_renderable arrays. Therefore this patch prevents kittens from being killed.
We can move the drupal_render call in to the theme system later. We can move the output format rendering into another place later. We can convert menu callbacks to return arrays later. This just makes it possible to do those in separate patches, therefore not killing kittens.
Comment #8
dmitrig01 commentedActually this should be postponed until that patch gets in.
Comment #9
eaton commentedWell put, dmitri. Thanks for summarizing it better than I could.
Comment #10
pwolanin commented@dmitrig01 - I'm certainly in favor of incremental approaches - but I think we need a clear idea of where we want to end up.
@eaton - I would assert that any thorough approach to rendering a node fully as JSON or atom vs. xhtml is going to need a parallel function to each current theme function. The approach suggested by chx, which is better than what I had originally proposed, would be to populate (once per page load) a mapping of partial theme hooks to full theme hook. e.g. theme() would simply look in this mapping for 'page', and based on that tries to invoke (e.g.) theme_page() vs. theme_json_page(). Obviously we are special casing xhtml to be an empty prefix to avoid the work of re-writing all existing implementations.
The nice part about this is that it wold allow for very granular control - e.g. the blocks could be rendered xhtml inside a JSON wrapper (theme_json_page()) for use in remotely refreshing them block contents.
Comment #11
david strauss@pwolanin Completely agreed on composited theming. I had a long discussion on Skype with Moshe about the need for it for other reasons.
Comment #12
catchAnd it's on again.
Comment #13
drewish commentedsubscribing
Comment #14
dmitrig01 commentedRerolled, and it works for me.
Comment #15
dmitrig01 commentedBetter code style. Have no idea if tests actually pass.
Comment #16
dmitrig01 commentedOk, preview works and all node tests pass.
Comment #17
moshe weitzman commentedSame as before, but I went ahead and converted the other node listing pages - blog and taxonomy/term. Those needed changing anyway since node_view() not returns a renderable array instead of a string. I'm stopping here though, as this patch could easily get out of control.
The taxonomy term page has a very odd page flow involving multiple levels of theming. I have simplified that so it now matches the nice flow of blog and /node.
I added a theme_list() function which is a wrapper around theme('item_list') that works for renderable arrays. This was needed for blog pages, and will be useful elsewhere.
Lastly I added a drupal_alter('page', $return) to index.php so that modules can fiddle with the whole page before it gets rendered. Finally!
Comment #18
dmitrig01 commentedw00t at the drupal_alter('page')! However, I'd like to propose moving the if statement (or at least the elseif and else components) into common.inc, drupal_build_page(). That way, drupal_build_page can also do drupal_alter('page_$url') without busying index.php.
Comment #19
david straussThis looks kind of funny:
Comment #20
dmitrig01 commentedOk, removed. Also put the page rendering into a separate function, drupal_render_page, so we can add the extra drupal_render later.
Comment #21
dmitrig01 commentedAlso I've coded some of the conversion for forms - #353069: Make drupal_get_form to return unrendered forms
Comment #22
moshe weitzman commentedThanks dmitri. I fixed variable inconsistency in drupal_render_page() that was preventing pages rendering :)
I also cleaned up drupal_render_page() so it always has an array to run through drupal_render(). Further, we now only have one drupal_render() which does the page themeing as well as the contents theming. This is cleaner than rendering the content and then calling theme('page').
The admin pages are not printing for some reason so this isn't quite ready. I will work on it more later.
Comment #23
dmitrig01 commentedThanks for fixing the inconsitency. this patch fixes admin pages
Comment #24
pwolanin commenteda minor point - I really dislike the use of
$renderas a variable name - it's suggests an action, not data.Comment #25
dmitrig01 commentedOk, renamed $render to $page and removed it in a few places.
Comment #26
dmitrig01 commentedit's ready for review
Comment #27
moshe weitzman commentedRenamed a couple more $render. I ran quite a few tests and we are all green.
Comment #28
dmitrig01 commentedAfter talking to sun and chx, changed around a bit more stuff for #type => list. all the same besides that (blog tests pass). Also batch isn't ready for conversion yet
Comment #29
moshe weitzman commentedComment #30
moshe weitzman commentedEr, with a patch.
Comment #31
dmitrig01 commentedFixed failure
Comment #32
dmitrig01 commentedComment #33
drewish commentedHad a quick look at moshe's patch in #30 just to see where it was at. dmitrig01 posted an update but I don't have time to re-review so these may not be relevant.
theme_list()'s PHPDocs seem a little awkward. I'd rather have the required/optional bits up front:
The changes to the taxonomy module remove the n.sticky, n.title, and n.created fields from the queries. Don't those need to be there for the ORDER by to work on pgsql?
Comment #34
Frando commentedGreat patch! This will enable all kinds of functionality regarding different output formats. Great point is also that contrib can now simply conditionally redirect page rendering to a custom theme function, doing there whatever is suitable. Awesome.
+ // Move some variables to the top level for themer convenience and template cleanliness.
+ $names = array('show_blocks' => TRUE, 'show_messages' => TRUE);
Shouldn't we move the defaults to drupal_render_page or system_elements so that this can be altered in hook_alter_page?
Comment #35
dmitrig01 commentedGood idea, done
Comment #36
moshe weitzman commentedThe benchmarks show that we are within the margin of error comparing HEAD versus this patch.
HEAD 3.07 reqs/sec
Patch: 3.04 reqs/sec
Environment
- APC enabled (this patch does not add/remove files. Disabling this just masks other changes with slowness)
- Page, Block, CSS, Javascript cache disabled
- DB has 5000 nodes with taxo terms and aliases. 10000 comments. 5000 users.
[edit: removed wall of text, please find it attached later. chx.]
Comment #37
pwolanin commentedWhile I realize this could be done later, I think we should correct the basic flaws in drupal_render_page() now.
The most important problem is that we anticipate handling a variety of structured data that might already have a top-level
#themedefined. Hence, the setting of'#type' => 'page'is not generally going to work for the top-level array. Rather the page needs to be an element containing the passed in$contentarray.I would also suggest that it would be useful to set the #title and possibly some other properties before the drupal_alter() call.
This is what I had in http://drupal.org/node/134478:
As you can see above, doing this allows you to pass in params (like $show_blocks) - this codein the patch is rather ugly I think:
Comment #38
drewish commentedtested the patch on postgresql and the change to the taxonomy queries totally break it:
PDOException: SELECT DISTINCT(n.nid) FROM {node} n INNER JOIN {term_node} tn0 ON n.vid = tn0.vid WHERE n.status = 1 AND tn0.tid IN (?) ORDER BY n.sticky DESC, n.created DESC LIMIT 10 OFFSET 0 - Array ( [0] => 11 ) SQLSTATE[42P10]: Invalid column reference: 7 ERROR: for SELECT DISTINCT, ORDER BY expressions must appear in select list in taxonomy_select_nodes() (line 1259 of /Users/amorton/Sites/dh/modules/taxonomy/taxonomy.module).Comment #39
chx commentedThose benchmarks make a long issue even longer and unreadable. Please attach them instead.
Comment #40
Frando commentedI agree with pwolanin in #37. We shouldn't make the main content the root element of the page array. This is limiting in several ways, as we then cannot easily have more elements at the same level as the page's main content, for example blocks and regions. See the attached patch
The attaches patch does this:
I'm leaving at CNW because I haven't dealt yet with #38. Also, there are some notices on 404 pages, not yet sure why as I mostly just copy-pasted the code for block rendering from template_preprocess_page. Maybe the theme system isn't yet initialized at that point of time?
Comment #41
Frando commentedFixed the warnings in my last patch.
Setting to CNR, even though it needs work on postgres but this is not among the most important parts of the patch and the rest definitely needs review.
Comment #42
Frando commentedAnd now blocks are put into renderable arrays as well, making blocks and regions alterable in hook_page_alter. So blocks are now rendered during the drupal_render call in drupal_render_page as well. All block tests pass.
Comment #43
Frando commentedNew patch!
Most important changes, after discussion with moshe and dmitrig:
drupal_decorate_page(array('#show_blocks') => FALSE)), or to inject new content into regions, thus replacing drupal_set_content. (drupal_set_content() is still kept in the patch because it is used in theme maintaince mode, which we haven't touched at all yet).Comment #44
david straussI don't particularly like the model of drupal_decorate_page(). We don't usually handle decorators in Drupal using functions we call to make the change. Rather, we usually have hook_*_alter() hooks that get the data passed by reference. Why not hook_page_alter()?
Comment #45
Frando commentedWell, hook_page_alter gets called for each and every page view, while drupal_decorate_page() can be called by a menu callback function or whatever. So, if I want to disable blocks in my menu callback or want to add some content to some region, it's very simple with drupal_decorate_page(), while in hook_page_alter, I'd either have to check arg() or set some global in my menu callback, which is both kinda ugly IMO. drupal_decorate_page() really gives us lots of flexibility without the drawback of having to implement a hook_page_alter() which gets called on every page view.
So, drupal_decorate_page() has a quite some advantages as its much cheaper than hook_page_alter. And I cannot see any disadvantages about it.
We also have hook_page_alter(), of course, which used by block module in the latest patch to add the rendered blocks to the page array. drupal_decorate_page() instead is used by 404s to disable blocks and by block module to add region headers on admin/build/block, thus replacing drupal_set_content().
Comment #46
moshe weitzman commentedSpoke with David and we agreed that it would be more testable if menu callbacks would simply start off with a bare $page array and start decorating it themselves instead of calling drupal_decorate_page(). The attached patch implements this approach. Note that we don't need to check arg() or set a global as Frando postulated.
The bare page array is provided by a new drupal_get_page() function.
The attached patch works pretty well in my browser but fails miserably with unit tests. From what I can tell, the blocks are not showing up for some reason so simpletest cannot login and errors spew. Also busted is the block admin page, which does not show the empty regions (I never liked this feature much - but needs fixing nevertheless). Please help if you can fix these issues.
Comment #47
moshe weitzman commentedThis one passes virtually all tests. Too tired to fix the remaining exceptions. Help.
Comment #48
Frando commentedConstructing the pages in the menu callback with a bare page array is nice.
I'd still keep drupal_decorate_page(), though, for example to avoid something like that:
+ $item = menu_get_item();
+ if ($item['path'] == 'admin/build/block') {
+ $description = '
';
+ $page[$region]['blocks']['block_description'] = array('#markup' => $description);
+ }
What is the reasoning against drupal_decorate_page(), other than that so far we haven't done something like this in Drupal this way?
drupal_decorate_page() is a very neat little function IMO.. saves LOCs easily and can be called literally by anyone from anywhere. So it makes menu callbacks less special, which is a good thing IMO. And the function is just 4 lines.
You moved filling out empty regions into block.module. Why? I'd keep that in common.inc. Regions really are part of the base system, while block.module should just put its blocks into the regions. If we move into the direction of maybe making block.module optional in a follow up page, region handling should stay in common.inc IMO.
Otherwise the patch looks good.
Comment #49
moshe weitzman commentedI agree that regions "belongs" outside of block module so I moved that population of empty regions to template_preprocess_page(). That seems slightly cleaner to me than drupal_render_page(). It is the template system that really wants these empty variables.
I added #sorted = TRUE for the node listings since we have no need to re-sort them in php. That property should be added by #353632: Performance: Do not sort already rendered elements in drupal_render().
I put back the missing fields which were causing Postgres to puke - thanks Drewish (#33).
I am getting inconsistent test results between runs. I suspect my local environment is polluting the test somewhere. Hope others can run the test suite and fix failures/exceptions if needed.
I'll let David address the drupal_decorate_page() question.
Comment #50
moshe weitzman commentedThis one is passing all the tests. Reviews welcome.
Comment #51
dmitrig01 commentedSince i'm not really qualified to review this because i worked on the patch, i'm not going to review it, but here's my one-word response: "wow"
Comment #52
webchickHeh, nice humble tag. ;)
This looks really cool. I only had 20 minutes to look at this so far, but here's what stood out to me (mostly a "Coder module" pass at this point):
Seems odd that this accepts a string or an array, but dmitri told me that this is so that we can have backwards compatibility until all pages are converted in later patches, so works for me.
Um. What? :) Could we English-ify this a little?
a) What is the normal behaviour that is done if this isn't concatenated?
b) Why would I want to set this value?
c) The volume of comments here makes me feel like it's really important, so we should add a sentence at the top summarizing why it's important to have 5 lines describing it.
Please separate the values from their descriptions by colons, rather than periods for consistency with other places we list options like this.
Add a newline between } and /* please.
Need a space between if and (
Just curious, why is this $variables['block'] and not $variables['elements']['block'] like node and teaser above?
Please add a comma at the end of $weight.
It's rare, but pages do sometimes have multiple pagers on them. Should we make this an array with $element as the key?
I'm having some trouble here trying to figure out why sometimes things have # symbols before them and sometimes they don't. Could you explain this?
Would maybe $build['nodes'][$nid] be better, so node IDs could be parsed out more easily if required?
it's -> its
We'll also need some documentation .. somewhere... that describes what all the system properties of the $page object are. Maybe in the PHPDoc of drupal_get_page(). That might help me feel a bit less lost than I do now.
Also, now that tests are passing again, could we get a fresh set of benchmarks to see how we're fairing?
Comment #53
drewish commentedLooks like webchick beat me to this but we didn't overlap too much so I'll stop here and let these points get addressed.
In drupal_not_found() the "+ // Custom 404 handler." comment seems like it should be integrated into a paragraph with the next comments. Same goes for "+ // Custom 403 handler."
There's also trailing whitespace in drupal_not_found() that would be nice to remove.
You didn't change them but since we're in here we should document that drupal_access_denied() and drupal_not_found() could return strings or arrays depending on the error menu handler).
drupal_get_page()'s PHPDoc block starts with multiple sentences. It should start with a single sentence describing the function and then additional paragraphs. Please explain what decorating is, I don't think we can assume people will know. Some @see links to other functions like drupal_render_page() would be helpful.
Rather than:
Can't we just do
$page['content'] = array('#markup' => $return);?drupal_render_page()'s PHPDocs should describe the format of the page array. I think we're going to need some extensive documentation on this function. Need PHPDocs explaining what it returns.
Webchick had balked at:
and I'll agree it's pretty hard to parse. I'd suggest describing what the default behavior is first then describe why you might not want that and then how setting it to false will help. e.g.:
Comment #54
drewish commentedComment #55
moshe weitzman commentedI expect that this tag will be used by 20 or so patches a year so I am not trying to declare a winner :).
The attached patch addresses all the fine points made by drewish and webchick. See below ...
Comment #56
drewish commentedmoshe, i disagree on putting all the docs in to the page_example.module. that's the kiss of death for docs since they're not updated with the core patch. it's critical that the docs get attention from the people who developed the code rather than becoming an after thought. that was the whole reason for moving the hook documentation into core. to ensure that the docs get the same scrutiny as the code.
Comment #57
moshe weitzman commented@drewish - I said 'howto' docs should be there. By that, I mean lengthy comments which give background and so on. API docs should be in the code, as you say. I did the best I could expanding docs where you and webchick and others have requested. Please review the latest patch and suggest where improvements are needed.
Comment #58
drewish commentedUp in comment #33 I suggested that:
could be improved.
I hate to be a broken record but this will fail on pgsql because the ORDER BY terms aren't in the select list:
Same goes for:
I think:
is incorrect because we're using the contraction it has rather than the possessive. It would be best to just expand it to "it has".
Need to add PHPDocs for @params and @return to node_show().
The @param tags for node_build_multiple() use data types before the variable names. That's not the current style through core, the type is typically specified in the description. Personally I like the types but it's inconsistent so we should follow the rest of core. And I'm not sure if api.module would parse them correctly.
Comment #59
moshe weitzman commentedImplemented all suggestions. Thanks drewish.
Comment #60
moshe weitzman commentedoops - last patch was missing block.module. new one attached. i was toying with how much more is needed to make block module optional. apparently, only a little .profile change. that will be a follow-up patch.
Comment #61
drewish commentedup on #53 I'd asked about:
can we shorten that up?
if #items is really a mandatory parameter for theme_list() then we shouldn't set a default. that looks like hand holding. let it fail with some noise right away.
can we turn this into one paragraph?:
lets make this a proper sentence by adding a period.
this should either be a paragraph or separate comment blocks:
the code and comment for this seem like they could use some tweaking to make them read teh same way. perhaps transforming the tests?
hook_page_alter()'s PHPDocs could use a @see to drupal_render_page()... actually drupal_render_page() could also use one to hook_page_alter().
In taxonomy_term_page() we're adding the
Comment #62
moshe weitzman commentedMany thanks for these reviews ...
Implemented all suggestions except the one to make the English match the PHP. I think each language is expressing itself as clearly as possible and we do ourselves an injustice by unclarifying one to make them read the same.
Comment #63
pwolanin commentedI think
#concatenate_childrenshould be omitted from the patch. If nothing else, if this is the minimal, first, patch, it should not be needed anywhere.I defined a separate version of drupal_render in the page-randering patch to do the same thing (return an array containing all the child elements). However - the final output of this process will always be a concatenated string, so chx persuaded me that this was not the right approach.
Comment #64
pwolanin commentedtalking with Moshe I see I misunderstood a little what that flag is doing - it only affects a single level of the tree, it's not passed down recursively.
So, one could achieve the same effect by having regions be # properties of the page, but it's a toss-up which is less ugly.
One minor thought - if the meaning of the flag was reversed perhaps the logic could be simplified, e.g. something like:
Comment #65
Frando commentedWhy is #concatenate_children not the right approach? I can see it useful in other situations as well, to be precise whenever you know what children you expect (as we do here - it's the regions) and want to handle putting them into a template wherever you want by yourself. What's the reasoning against it?
I suggest to make $content an optional argument to drupal_get_page(), and assigning it to $page['content'] there if not empty. In 90% or the situations we will want to return the page array with just the content array and no additional parameters from the menu callback. In all these situations we could then simply write
return drupal_get_page($content);instead of
Saves some LOCs in many situations as soon as we start to convert more menu callbacks to return arrays.
I still somehow like drupal_decorate_page as in my patch in #43, but it doesn't matter a lot (we have the functionality there still)
Otherwise, the patch really looks great.
Comment #66
Frando commentedCross-posted with pwolanin, #concatenate_children seems to be adressed, great! Reversing the logic is fine with me.
Comment #67
drewish commentedI like Frando's suggestion in #65.
Comment #68
eaton commentedMy impression has always been that drupal_render() should spit out a fully rendered string. Inside the context of drupal_render(), template files control how the individual elements are output, and how they're wrapped in decorative HTML. Adding a new override flag to change drupal_render's behavior seems a little strange.
If you want to exercise greater control over what happens inside of it, you can call drupal_render on individual children, or implement theme functions and preprocessors that pre-render the chunks explicitly, then hand them off to a template. Am I missing something?
Other than that detail, wow, Freakin' sweet.
Comment #69
moshe weitzman commentedRerolled with Frando's fine suggestion to let drupal_get_page() set 'content' item.
I think #preserve_children is a bit less specific than #concatenate_children. Anyway, it is splitting hairs IMO. This is a tiny change and I think it is mostly drawing attention because of the gigantic comment that comes with it.
@Eaton - Please jump on IRC when you get a chance so we can discuss the tradeoffs here. I really like the elegance of drupal_render($page) that we do now in drupal_render_page(). Your proposal has us doing a separate drupal_render() for each region in drupal_render_page() and then passing those down to the template layer (which will have to move the variables to top level anyway). IMO, the current way is slightly more elegant. This is a matter of taste though.
Comment #70
Frando commentedI just ran benchmarks with #69, and performance seems to be unaffected by the patch, which is great.
HEAD on /node with 90 nodes (no caching):
11.72
11.63
11.68
HEAD + Patch #69 on /node with 90 nodes (no caching):
11.60
11.67
11.62
(Numbers are requests per second (mean) as reported by ab -n 200 -c 10)
Comment #71
Frando commentedPatch #69 was broken for menu callbacks returning strings. Fixed that in the attached patch.
I also moved strings returned by menu callbacks to $page['content']['main']. This makes it possible to add stuff in the content region above the main content (by setting a negative weight on the element added to $page['content']). This wouldn't be possible otherwise for menu callbacks returning strings (theme_markup adds all children to the end of the markup string). Also fixed the hook_page_alter example in system.api.inc so that it would actually print something.
Comment #72
Frando commentedI just had a lengthy chat with eaton about the hell that drupal_render() internals are, and while we both agree that a general cleanup of drupal_render() would be great, he also agrees that the #concatenate_children flag in this patch is the way to go for page rendering by now. We thought about renaming it to #preserve_seperate_keys and making it FALSE by default, which is OK with me as well (as is the current name).
Comment #73
sunI wanted to suggest a better property name than #concatenate_children and #preserve_separate_keys, since both are all Greek to me. However, I did not even completely understand the purpose of the property, so I asked Frando to explain it to me in IRC:
After reading this explanation a second time, I wonder whether it is a good idea to store two different data types in the same property (#children) depending on another flag. After all, our #properties can be compared with function arguments, and we usually try to avoid different data types for the same function argument.
Comment #74
pwolanin commentedI have been thinking about this more - and I really think
#concatenate_childrenshould be omitted. It introduces a variable return to drupal_render() which is just bad design. Also, and more importantly, we eventually want to be rendering the content array in the template to give themers the option to render different parts in a different order. Thus, we should move the call to render at least to the preprocess function.Comment #75
eaton commentedI agree, and that's exactly what I said we needed for this patch to be a killer. Frando, moshe and I spent about an hour or two hashing through the details, though, and we revealed a fundamental gotcha in drupal_render()'s internal workflow: the theme function for a given #element type receives its children pre-rendered, but the #theme function for an individual discrete element can't control the 'wrapper' markup that goes around the element. #355236 has been started to resolve this problem, and I'm whipping up some omnigraffle stuff in a bit.
A short-term compromise would be to use a drupal_render_children call or something like that rather than calling drupal_render() at the top level directly, until the problems #355236 deals with are resolved.
Comment #76
pwolanin commentedI'm working on re-rolling the patch per my comments and discussing w/ Moshe.
Comment #77
moshe weitzman commentedAttached patch is same as Frando's last one but adds two small changes:
This actually reduced LOC which is nice.
Comment #79
david straussNow that I'm back from vacation, here's why drupal_decorate_page() and all functions like it are bad:
* Functions that store things in static variables and regurgitate their static variables when you call them with magic arguments are just bad imitations of static classes. (I'm looking at you, drupal_add_js(), drupal_add_css(), drupal_set_message().)
* When you call functions with side-effects, it makes your code harder to cache and test. To cache or test code that calls side-effect functions, you have to unreliably probe for the side-effects. For example, if function P adds CSS file M.css, you know you should have M.css in the list of CSS files to be added to the final page. But if function Q also adds M.css to the page and is sometimes called before function P, you can't easily verify that function P is actually adding M.css. It's possible that your test will falsely pass or fail depending on whether function Q ran before P. This is a problem for good unit tests, which should be randomizable in order without affecting results. Finally, attempts to cache the portions of a page -- including (for example) the CSS files they require -- are thwarted by the inability to accurately determine the what CSS files a block (for example) decided to include. At best, you end up running a crude "diff" on the before and after states of the functions storing the side-effects. The "diff" approach doesn't necessarily even work if your side-effect functions are idempotent, like drupal_set_title().
* When you call functions with side-effects, it makes your code dirtier and slower to test because you have to ensure a starting clean environment and clean up any mess created by the side-effects. It's much easier to call a function that puts what it wants in its return value and verify that, for example, function P includes M.css in the $page array it returns.
* Side-effect functions are hard to control. If you run drupal_set_message() from a function returning a Form API array, the message may get set during form processing in addition to when the user first sees the form. By encapsulating things in return values like a $page array, we can better control the propagation of things like messages and titles. For forms, we *do not* want messages to display when a form is being loaded for validation purposes only. Similarly, when we use functions like drupal_set_title(), we currently have little control over their effect and have to resort to awkward mechanisms like calling node_page_view() if (and only if) you want to *also* set the page title.
For anything we'd like to do with drupal_decorate_page(), I'm sure we can find a superior architecture that avoids the problems above.
Comment #80
dmitrig01 commentedDavid - two comments
1. (minor) drupal_set_message actually uses $_SESSION
2. are you arguing for a global $page? that way, modules like drupal_add_js and such can alter it.
Comment #81
david strauss@dmitrig01: I'm not arguing for any approach other than avoiding a litany of side-effect functions.
Comment #82
dmitrig01 commentedok, then i'll begin arguing for global $page; :P
Comment #83
eaton commentedI'm a fan of a built-then-altered $page variable, as opposed to a global "add this to the current page". However, that means that every module will need access to some sort of get_current_page() function. Isn't that global state? I suppose it's still preferable to the tricky 'call it with no params and get the data back' model we have.
Comment #84
moshe weitzman commentedRerolled to fix a new unit test failure. The new User Cancel feature is the first test that uses the Batch API AFAICT. It turns out that the batch menu callback did not keep up with the rest of the patch. Fixed. Hooray for unit tests.
Please, we need some code reviews. Lets leave discussion about global $page and drupal_decorate_page($page) for another issue.
Comment #85
moshe weitzman commentedImplemented feedback from IRC patch review by chx.
Comment #86
eaton commentedJust spent some time taking this patch for a spin; I'm very impressed with the changes. There are a lot of subtle tweaks that were previously very difficult that can now be made simpler: modules that pull a small unrendered chunk of the normal $content region and put it into another area of the page (like a block or a header) when it's present, for example.
I stuck a simple hook_page_alter() implementation in a module that prints dsm($page), and surfed around a sample install: the modified structures are good. They give us a sound basis for future refinement (making more pages return structures rather than pre-rendered text, putting more page metadata in the $page variable for potential altering, and so on).
What rough edges there are in the details of the rendering for this patch need to be solved in drupal_render itself, IMO. This patch as it stands is a great improvement, the impact on developers writing module code is minimal, and it opens up new doors for future work. Big thumbs up.
I'm running the full suite of tests right now; pending the green light from simpletest, I give it a big thumbs up.
Comment #87
chx commentedAside from trivial coding changes, my very through review contained one more important change: we now use #prefix and #suffix as much as we can. Earlier benchmarks showed no speed up or decrease. And we have #353632: Performance: Do not sort already rendered elements in drupal_render() upcoming. Given my earlier review, opinion of the testingbot and Eaton's and the benchmark results, I am honored to greenlight this rather fine patch.
Comment #88
chx commentedPatch review (lightly edited IRC log) attached for posterity.
Comment #89
drewish commentedit's minor but hopefully one of the committers can remove all the trailing whitespace that the patch introduces... yuck. i'm sure that would knock a couple of KB off the patch.
Comment #90
drewish commentedApplied the patch then ran all the tests on pgsql and they look good. There was one failure (Database > SQL handling tests > First record inserted as expected) but I don't think it's related to this patch.
Comment #91
webchickComment #92
webchickNote that this is a functionality review of the patch, not a code review. I'll need to do one more of those before I'm comfortable confirming RTBC status.
The biggest thing I've been missing with this patch is an overall sense of what a $page object actually looks like. So I used Jeff Eaton's trick and created a module that implements a hook_page_alter() and sticks a textarea at the top of all pages with the contents of the $page array in it. This allowed me to click around and see how it changed. The module is attached to this issue for anyone else who is more "visually" oriented and would like to test.
Here's what it ends up looking like on most pages on a default install:
Things that doing this uncovered:
1. Node-related pages (empty front page, node page, front page with nodes in it) are missing the ['main'] index on $page['content']. It's just $page['content']['#markup']. I'm pretty sure this is a bug, since everywhere else it is not like this. At the very least, it's a DX problem because you need to deal with node pages and "other" pages differently, so I'd like to see this made consistent. Keep $page['content']['nodes'] though; that's really useful.
2. The array has a lot of very useful things in it, but also some notably missing things: hook_help() text, messages set by drupal_set_message(), breadcrumbs, and page titles. When I think "page" I think the entire contents of the browser window, so I would assume that I could alter these elements as well (for example, $page['content']['help'], $page['content']['messages'], $page['#title'], and $page['#breadcrumbs'] or $page['breadcrumbs']). It's confusing to me that I can alter things outside of the $content area (block regions, and blocks within those regions) but not things printed out before $content.
3. The side effect of placing this kind of power at developers' fingertips is that people are going to end up using it for absolutely everything: altering the contents of blocks (and creating new ones on-the-fly), adding or modifying links on nodes, completely swapping out a form on a given page, etc. In other words, stuff we have the hook system for. I see that the PHPDoc on hook_page_alter() alludes to when it's appropriate to use hooks and when it's appropriate to use this, but we should flesh this out a bit more so it's clear to new developers "If you want to do task X, this is the hook for you. If you want to do task Y, use this other hook." I think we should also mention that the entire page's HTML is rendered at the point this hook fires, so you gain more by altering things earlier in the cycle. It would also be nice to have a sample output of var_export($page) in the PHPDoc there so that I didn't have to write a module to see what this hook does. This should explain the philosophy behind the organization of indexes and what standards people should follow when adding in their own. In summary, more betterer comments on hook_page_alter(), please. ;)
4. I'm also wondering if we shouldn't store flags like #show_blocks and #show_messages within their own sub-index (like $page['#settings']). One thing that's nice about this array the way it is now that it's pretty clear which are "internal" properties (#attributes, #required, etc.) and which ones are meant to be monkeyed around with $page['content'], $page['left'], etc. I forsee us adding more of these little boolean properties in the future to turn on/off behaviours of a page, and it might be nice to collect them under one area. Makes the documentation easier to write ("see $page['#settings'] for a list of flags," vs. "#foo does this and #bar does that and...").
5. Minor, but what's the purpose of the #required flag on $page? Is this just automatically added to the array as a side-effect of drupal_render()?
Also, I renamed this issue both because I think it might help get some more people looking at it, and also because I could never remember what keywords to use to find it again. :P
Comment #93
webchickOh. And let's get rid of the trailing whitespace while we're at it, as noted in #89.
Comment #94
webchickOk. Now that I have had a cup of coffee, I realize that regarding #1, the pages that have $page['content']['main'] are in actuality legacy pages, which is why they are all consistent. So I should actually have been reviewing pages like node/* and taxonomy/term/*. Sheesh. :P Sorry. :P
I've attached a few var_export()s of various pages for comparison. I've also updated page_alter.module so it works in any page context, which is accomplished by doing Unholy Things (tm) -- creating a block on the fly and inserting it into the header region. ;)
Comment #95
Frando commentedno. 1:
well, on node pages, we return an array containing the individual nodes. So in hook_page_alter, I think you could just add stuff to $page['content'] with a low weight and it will be rendered above the nodes. Later on it then gets all rendered into #markup in $page['content']. We could modify drupal_get_page, though, to put the content returned by menu callbacks into $page['content']['main']. I don't think it's really required, but it would improve consistency between menu callbacks returning strings and those returning arrays a bit.
no. 2:
I think we should leave that up to an follow up issue. There's lot of stuff that could be moved from preprocess_page to drupal_render_page or page_alter, but there are certainly some items where this will lead to discussion, so I'd vote to first get in the basic stuff (which this patch is) and then move the rest around later.
no. 3:
Well, as a general rule of thumb I'd say "If it's possible to do what you want to achieve earlier in the page generation process, by using another hook, do it there. If it's not possible, use hook_page_alter".
no. 4:
I just fear that we're kinda getting into arbitrary decisions here. What should be moved into #settings? What should stay top-level? That would give us three realms to put "stuff" into (#property, children and #settings['property']). If we go this route, we really gotta make up some rules for it. Personally, I'd be more interested in that patch that was putting a leading underscore to internal FAPI attributes, but I have no idea if that one is still alive.
If we happen to add much more properties to the $page array (IMO, that would depend on the route we go for different output formats etc), I would be in favor of the #settings array, to keep things more seperated from "internal" FAPI properties.
no. 5:
I think this is just the default value. All elements with #type set and defaults merged in have it.
Comment #96
webchickAfter analyzing the various array structures, the biggest problem I have with this patch right now is the lack of consistency, which I think is going to cause confusion for new developers.
An example of the kind of consistency I think we should be shooting for is the way blocks are represented in the $page. All block regions have named indexes in the $page array, and the blocks within them are contained in a 'blocks' sub-element, sorted by weight. So if you want to target a given block on the page, you're always going after $page[{region name}]['blocks'][{module name}-{delta}]['#block']. This is consistent, and easily understandable. I can stand up in front of a room of new Drupal developers and tell them this and they will get it.
However, with $page['content'], it feels like your only recourse is to use something like page_alter.module to know what on earth is available to you:
- The node/taxonomy listing pages have a $page['content']['nodes'] property, but a single node page does not.
- If you're on a single node page, then properties that used to be at $page['content']['nodes'][{nid}]['body'] are now at $page['content']['body']. But there is nothing to distinguish this being a *node* body as opposed to just a generic "body" of a page.
- Or, is this lack of distinguishing by design? If it is, why don't the content listing pages and legacy pages also have a $page['content']['body'] property?
- There is no obvious way to, across any page in the whole system, go "You know what? Screw you and what you want, I want to override the entire contents of this page and you can't stop me!" I was excited earlier when I thought that all pages would end up with a $page['content']['#markup'] property, which would let me do things like this:
But it looks like I'll need to do a bunch of checking of whether or not property A exists in the $page, and if so, then override property B, but if property C is set, then override property D instead, etc. This is the kind of thing that's going to take me an hour and lots of flow-charting and pointing and gesturing to explain to a room full of new Drupal developers, and it's only going to get worse once this patch is committed and we start converting more pages over, each of which add their own funky properties.
We need consistency, consistency, consistency. :)
Comment #97
webchick@Frando:
Re: #2, Sure, I'm comfortable leaving the other stuff to a follow-up patch.
Re: #4, I have the sneaking suspicion that once this is all over, we'll end up with far more than two small #properties that are $page array-specific. ;) Just trying to prepare for the future in advance. #111079: Render API: Differentiate internal from user-settable renderable array attributes wouldn't actually help us much here, because things like #attributes and #required are not (normally) internal properties for things that get passed to drupal_render().
Comment #98
webchickComment #99
eaton commentedWebchick, I think it might be unrealistic to expect the stuff inside of $page['content'] to be consistent across all pages. The content itself will be different from one page to another, and the key is that they use the same representational structure that other drupal_render()able structures do.
If there are places where standards make sense (like $form['buttons']) we can implement them, but I don't think there is much to be gained from trying to plan out a "consistent" structure when we have never even had any structure at all up to this point.
Also, regarding the difference between #properties and sub-elements, that's a pretty well established issue in the drupal_render() family. #properties are settings and flags that are used to control/manage the rendering of an element itself, while children are the 'meat' of a container element. I'm nervous about putting big "bin" #properties on elements, as they lead to easy bloat and move us farther away from OOP-like child/property mappings.
Every renderable element implements custom #properties to do its work; I think the biggest danger is in littering the system with magical 'global' #properties that are undocumented but change the behavior of anything they're attached to...
Comment #100
eaton commentedThat said, I agree that putting things like the help messages, status messages, warnings, etc in the page array would be a big improvement. Right now they're treated very differently, though, and I'm concerned that this will turn into a boil-the-ocean patch rather than an "Enabling Improvement".
The Developer Experience is -- repeat -- no different than it is currently after this patch is installed. It's just another hook point, and another option for modules returning data. hook_page_alter(), even if we don't move everything to it in this version of the patch, is still a huge improvement over preg_replace_all(). ;-)
Comment #101
catchBit of profiling.
With 90 nodes on a front page, calls to drupal_render() go up from 690 to 792.
Benchmarks are fairly equivalent, few ticks slower with the patch but not significant.
HEAD:
/node
2.75 reqs/sec
/node/n
12.58
Patch:
/node
2.69 reqs/sec
/node/n
12.56
So a little slower, but not too much.
Comment #102
catchnode/n profile with the patch applied for comparison.
Comment #103
webchickSummary of a half hour or so discussion about the consistency concerns I have...
This was coming from a place where we have certain... how you say.... "ball of writhing, venomous snakes" places in Drupal where if modules aren't playing nicely together, the world becomes a painful, miserable, and awful place. Two such places that immediately spring to mind are hook_nodeapi (two modules adding a $node->parent property... everyone loses!) and .info files' "package" property (User interface vs. UI vs. Interface vs. ....).
However, Eaton argued convincingly that the situation with the $page object is more akin to how people choose to name their $form elements in a form definition function. You can name it $form['parent'] or $form['book_parent'] or $form['wookie'] or whatever you want; it doesn't ultimately matter. This is because each form is ultimately "owned" by a specific module, just as a given page on the site is. And, like forms, it would be the responsibility of other modules that want to alter it to play nicely with whatever elements the "mother" page was outputting.
My concerns with consistency can probably be summarized as:
"As we convert more pages over once this patch is committed, let's make a concerted effort to look for patterns that make sense to standardize on."
So, I'm comfortable with these concerns not holding this patch up now, as long as we agree to look for these patterns in future patches. :)
Since we're leaving messages/help/titles/etc. for a follow-up patch, and since we don't yet have enough data to know what's best to standardize on in terms of the page #properties, that basically leaves only the documentation improvements. I also want to make one last pass at the code again now that I understand better what the patch does.
Comment #104
webchickOh, and here's a summary of the changes introduced in this patch. It's not exhaustive, but should give people an idea:
Comment #105
moshe weitzman commentedComment #106
moshe weitzman commentedComment #107
webchick(summary of the review leading to patch above)
Now for the nitpick pass!
themeing -> theming. (I even grepped!)
missing an 'of'
Sun brought up that 'main' isn't a construct we ever use in Drupal. Eaton suggested 'default' instead, which implies "nothing has provided any renderable chunks here so you're just getting default output." Sounds good to me.
We provide a node_build_multiple() without a corresponding node_build(). This will be confusing for new people since all of the other X_Y_multiple() functions have a singular one as well. Moshe pointed out that the equivalent of a singular version is node_view(), so we should rename this to node_build() (extra side-bonus, no more confusion between node_show() and node_view(). Hooray!)
We also spent some time talking about implications on the theming side; for example, theme_taxonomy_term_page() was removed, and there are a few wrapper divs that are now moved beyond the reach of theme_X functions as a result of this patch. In future patches, we might want to explore alternative ways to make overriding these available to people who are scared of breaking into module development.
Comment #109
moshe weitzman commentedI had not saved all files when I made the last patch - thus the test failures. This one should be better.
All of webchicks comments are in this patch except changing $page['content']['main'] to $page['content']['default']. The rationale here does not make sense to me. When we name, we try to *describe* the object. We don't usually try to communicate how it was built. If you follow Eaton's logic, you end up with defaults like
variable_get('site_name', 'default'). "If you don't pass a site name, you get 'default'".Comment #110
starbow commentedSubscribing (with much excitement)
Comment #112
robloachHow does this compare to #134478: Refactor page (and node) rendering?
Comment #113
Frando commentedThis patch is a) about to be ready to go in comparison to #134478 and b) is a very important first step to allow rendering content into different output formats (by keeping output format independent array structures up to the very last point)
Comment #114
Frando commentedReroll after a failed hunk.
Only one little change to #109: If we set the #theme property on the page element, and leave #type empty, we can just use drupal_render($page) in drupal_render_page instead of theme('page'), which makes the theme function used overrideable in hook_page_alter(). This tiny changes would allow modules to use another theme function for page rendering, e.g. to print content into different output formats. And no drawbacks at all.
I really think that this one is ready to go in. A lot of great stuff can happen afterwards. Now we just need someone to finally RTBC it.
Comment #115
robloachIt looks like we're getting rid of theme_page. If we are, should we remove the function? Now that modules can use hook_page_alter() to change the theme, how do themes alter the page since theme('page') isn't called.
It also looks like
drupal_render_pageprints its contents within the function, but when it's called it's called withprint drupal_render_page(). It would make more sense to return from drupal_render_page() and print from there."Backward compatibility - allow menu callbacks to return strings."....... Not sure how I feel about that one. Would it be better to stick the ability to render strings inside the PHP Doc?
Could we get some tests?
Comment #116
david straussIt would make sense to give the theme the final change to page_alter(), though that clearly wouldn't be through the hook system.
Comment #117
Frando commentedNo, we're not at all getting rid of theme_page. theme_page still themes the page, after all. We just made it possible for contributed modules to switch to another theme function. By default and in core, the page is still themed by theme_page (preprocess_page and page.tpl.php, that is). And altering happens before the theming, so not sure what you mean there.
@David: Themes can of course still override theme_page through the regular theme override system. So the theme still has the "final control". No change here at all.
I changed drupal_render_page to return the rendered page instead of printing it directly. Makes sense.
Also removed the "backward compability" part of that comment - there certainly are situations where it's just fine for menu callbacks to return strings.
Tests - well, what should be tested? If something breaks during page rendering, pretty much all tests will break. And hook_page_alter should already be covered by block tests.
Comment #118
Frando commentedComment #119
webchickHere's a re-roll with new docs for hook_page_alter() that Eaton, Moshe, Crell, and I worked on tonight. No other changes.
Comment #120
moshe weitzman commentedThis now has approval of moshe, frando, webchick, eaton, and testbot. Onward!
Comment #121
chx commentedAnd mine too.
Comment #122
dmitrig01 commentedme too :-)
Comment #123
sunFixed a bunch of PHPDoc and coding-style issues.
Comment #124
dmitrig01 commentedMade a few small code changes, but i'd like to run this through the testing bot.
Comment #125
dmitrig01 commentedIt passed.
Comment #126
sunFixed indentation in improved PHPDoc blocks.
Comment #127
eaton commentedI'm biting my nails in anticipation.
Comment #128
dries commentedThis patch is RTBC but failed to apply. There is a chunk failing in taxonomy module -- needs a quick reroll! :)
Comment #129
dmitrig01 commentedThis should work.
Comment #130
dries commentedCommitted to CVS HEAD! Please update the upgrade instructions before marking it 'fixed'. Thanks for all the hard work and the precious reviews.
Comment #131
drewish commentedI really hope someone can write this up... I'd noticed that this broke the devel module's display redirection page function. I started to roll a patch and consulted the update docs but there weren't any.
Comment #132
moshe weitzman commentedAdded docs - http://drupal.org/node/224333#menu_callback_array. Folks are welcome to improve them.
Comment #133
mr.baileysI think this patch broke the Chameleon theme (by removing the
theme_blocks-function.) I've opened an issue for this (#374650: Chameleon broken by hook_page_alter).