Support for Drupal 7 is ending on 5 January 2025—it’s time to migrate to Drupal 10! Learn about the many benefits of Drupal 10 and find migration tools in our resource center.
Currently in HEAD, we have three types of "page" callbacks:
- Normal page callbacks return a string of html, or better, a renderable array, that gets passed back to index.php which then calls
drupal_render_page()
on it. - AJAX callbacks invoked via
$element['#ajax']['callback']
can return a string of html or an array of AJAX commands (which although an array is NOT the kind of renderable array that normal page callbacks return) or can issueajax_render()
themselves forcing anexit
. - AJAX callbacks invoked via
$element['#ajax']['path']
can't return anything, because doing so would triggerprint drupal_render_page()
anddrupal_page_footer()
, neither of which is desired in AJAX context. Instead, these kinds of callbacks MUST issueajax_render()
themselves forcing anexit
.
YUCK!!
My proposal coming in a follow-up comment.
Comment | File | Size | Author |
---|---|---|---|
#55 | drupal-http-headers-599804-55.patch | 1.43 KB | webchick |
#52 | menu_router.patch | 2.95 KB | catch |
#50 | menu_router.patch | 2.27 KB | catch |
#49 | delivery_callback.patch | 1.59 KB | catch |
#43 | drupal-http-headers-599804-43.patch | 1.43 KB | effulgentsia |
Comments
Comment #1
effulgentsia CreditAttribution: effulgentsia commentedHere's my proposed patch to deal with this. This patch introduces a separation between a "page callback" (whose job is to return an html string or a renderable array) and a "delivery callback" (whose job is to use the appropriate mechanism to send the result of a page callback to the browser). This allows all 3 types of page callbacks described at the top of this issue to be equivalent with respect to what they return: either an html string or a renderable array. Specifically, this patch:
delivery_callback
field to the{menu_router}
table.drupal_deliver()
anddrupal_deliver_page()
functions to common.inc and changes index.php to usedrupal_deliver()
instead of hard-coding a delivery mechanism.ajax_deliver()
to ajax.inc and sets up some, but not all, AJAX callback paths to use it. I setup a couple this way to demonstrate the code, but not all to both keep this patch smaller and to demonstrate that the code is "backwards compatible" (if an AJAX callback issues its ownajax_render()
forcing anexit
, then it doesn't matter what delivery callback is used, since we never get to that stage).ajax_form_callback()
to transparently return the return value of the specific callback it calls, creating a complete unification between the two kinds of AJAX callbacks with respect to return value.ajax_form_callback()
remains an awesome utility function for doing the FAPI cruft.poll_choices_js()
to demonstrate that a renderable array can be returned from an AJAX callback, just like one can be returned from a normal page callback.ajax_commands
), so that we retain the ability for an AJAX callback to return specific AJAX commands in situations where you want to do something more than just return HTML content. The use of this is demonstrated inbook_form_update()
.I kept the patch as small as possible to implement my recommendation and have a use case for each situation. If this patch lands, a follow up patch can deal with cleaning up other places to take advantage of the new capabilities.
Comment #2
effulgentsia CreditAttribution: effulgentsia commentedSome additional notes if you're interested in architectural implications (feel free to skip over reading this if you're not):
Comment #4
effulgentsia CreditAttribution: effulgentsia commentedYay, testbot!
Comment #5
mattyoung CreditAttribution: mattyoung commented.
Comment #6
rfaySubscribing
Comment #7
sunI'm sorry. I really really love this patch. But I also really doubt that we find a valid, sustainable, working, and approved solution within the next 5 days. :-/
Feel free to try. Pulling some people from IRC into this issue might help, too.
Comment #8
merlinofchaos CreditAttribution: merlinofchaos commentedOk, I was dubious when I started looking at this patch. After reading the code, I think I really like this. I have two issues.
1) I normally use URL switching mechanism to do javascript degradation. i.e, you visit panels/ajax/do-something or panels/nojs/do-something and my code checks the argument and I use a menu _load() function to do the test. In this system I believe I would have to have 2 different menu entries for this.
2) Similar, other systems like to use ?js=1 as a query parameter to say that this is an ajax call. In that system, you can't do it with menu entries.
Both of these could be solved with a menu_set_delivery_callback() function. We could also theoretically introduce a standard where a query variable could *automatically* change the delivery callback. In that case, I think the order of precedence would be:
a) Was a callback set with menu_set_delivery_callback() -- if so, use that.
b) Was a type requested with a query string? -- note, we'd obviously have to map identifiers to functions for safety here.
c) Was a delivery callback set from the menu?
d) Fallback to normal HTML delivery.
3) drupal_access_denied() and drupal_not_found() should be modified respect the delivery callback. This goes well with the comment you had to @todo about error handling in either case. For both cases, ajax_command_error(t('Page not found')) and ajax_command_error(t('Access denied')) are certainly acceptable.
We already have a pretty good fallback for "I have HTML, you want it as AJAX, package it up and send it". It does require that the client know what to do with it, but I think we're well prepared for that.
Comment #9
effulgentsia CreditAttribution: effulgentsia commentedThanks, merlinofchaos! I really appreciate you taking the time to probe it despite your initial reservations. This patch includes the ajax_command_alert() commands when a menu status constant is passed to ajax_deliver(). I used the same error message text as is used in the drupal_not_found(), drupal_access_denied() and drupal_site_offline() functions.
Regarding issues #1 and #2, my design goal in this patch was to make the least possible change to core. As sun says, only 5 days for this to land, so the less that's in it that's controversial, the better. That said, delivery callbacks are pluggable and chainable. So, you can implement a panels_deliver() callback that makes its own determination as to whether to route to drupal_deliver_page() or ajax_deliver() or something else, based on whatever run-time information you want to use. I'm all in favor of core having more built-in support for run-time changing of delivery callback and/or query-string conditionality of delivery callback, but what do you think of letting that be a follow up issue once this one lands? That way, if it takes more time to get everyone to agree on the details than is available before API freeze, it can get bumped to D8, but D7 would still have a way of accomplishing what's needed, just with modules having to implement their own custom delivery callback routers.
Regarding changing drupal_access_denied(), drupal_not_found() and drupal_site_offline() to be usable in contexts other than a drupal_deliver_page() delivery, I also agree that it would be nice, but also perhaps too ambitious for this patch? Regardless of whether that's done, what I would really like to see is a change to all page callbacks that call those functions to return the appropriate menu status constant instead, which IMO would be the preferable way of being delivery callback agnostic. That said, if you have an idea for how to change these functions in a way that won't derail the chances of this patch landing, please share it.
For all reviewers: regarding comment #7, please share anything you think in not valid, sustainable, or working in this patch. As can be seen from comment #8, this patch might have the flaw of not going as far with the idea as would be ideal if we had sufficient time to flush it out, but is there anything about it that would make D7 worse than not having it land?
Comment #10
effulgentsia CreditAttribution: effulgentsia commentedWhile researching something else entirely, I just happened to stumble on #145551: Enable loading and rendering into multiple formats (html, xml, json, atom, etc.), and found this neat gem in patch 91:
I'm still wary of changing the drupal_deliver() function in patch #9 to be any more sophisticated than what's in the patch, but I also continue to like merlinofchaos' idea to allow for request-time swapping of delivery callback (i.e., menu_set_delivery_callback()), query-string based intelligence, and now, $_SERVER['HTTP_X_REQUESTED_WITH'] based intelligence. I'm open to implementing any or all of these within this patch if it helps rather than hurts the chances of this patch landing within the next 5 days. Otherwise, if this patch lands more or less as-is, I'm interested to see something like ctools implement a more sophisticated delivery callback router.
Comment #11
effulgentsia CreditAttribution: effulgentsia commentedBy the way, one of the reasons I'm concerned about adding sophistication to drupal_deliver(), is that it might start looking to Dries like this patch is primarily a feature addition (which isn't allowed any more) rather than an API clean-up (which is this issue's primary intent, and still allowed for 5 more days). If Dries gives his blessing on a particular direction, I'll happily implement it.
Comment #12
effulgentsia CreditAttribution: effulgentsia commentedI got some great feedback from chx on IRC, incorporated in this patch.
@merlinofchaos: There's now a hook_page_delivery_callback_alter(). And the code example for it in system.api.php shows how you can achieve what you were wanting in #8. I'm not sure if this should remain the official code example, but I wanted to have it in this patch as a discussion point.
Comment #13
effulgentsia CreditAttribution: effulgentsia commentedA little cleanup, because I rolled patch 12 a little too tired. I'm liking this more and more with each pass.
Comment #14
effulgentsia CreditAttribution: effulgentsia commentedSorry, forgot one other thing. This one has it.
Comment #15
effulgentsia CreditAttribution: effulgentsia commentedAt chx's request, I'm adding a link here to #601570: hook_exit() and other cleanup needs to happen for AJAX requests too.
Comment #16
chx CreditAttribution: chx commentedThis looks really good now. My only problem is with
drupal_alter('menu_router_item', $router_item, $path);
this is not just an ordinary menu router item but the active menu handler item. So what aboutdrupal_alter('menu_active_handler', $router_item, $path);
?Comment #17
sunSince we introduced the AJAX framework in D7, I think this can be treated as a clean-up - hence, applying the critical tag.
Comment #18
effulgentsia CreditAttribution: effulgentsia commentedWith #16.
Comment #19
effulgentsia CreditAttribution: effulgentsia commentedDespite my initial reservations about scope-creep on this patch, I realized that merlinofchaos is right (this, of course, comes as no surprise) when he says in #8 that drupal_not_found(), drupal_site_offline(), and drupal_access_denied() need to respect delivery method. This patch fixes those functions. @merlinofchaos: please comment as to whether this patch now meets with your approval.
I decided against implementing a menu_set_delivery_callback() function that wraps a static variable, because I'm highly suspicious of propagating the equivalent of a global variable. I know we do stuff like that all over the place in core, but I hope we stop that practice soon. I prefer the alter() mechanism, and if a module wants to implement a static variable inside it, at least that stays out of core. I realize that creates an awkward implementation in drupal_not_found(), drupal_site_offline(), and drupal_access_denied(), and open to feedback on how to improve it, but I also hope that we don't let little details stop this issue from landing in D7.
Comment #20
effulgentsia CreditAttribution: effulgentsia commentedFixed me being stupid in #19.
Comment #21
effulgentsia CreditAttribution: effulgentsia commentedRe-rolled.
Comment #22
effulgentsia CreditAttribution: effulgentsia commentedAs a refresher to anyone newly looking at this issue:
The primary purpose of this patch is to remove the annoying WTF described at the top of this issue.
Some very nice side-effects of this patch are:
- It will make it possible for AJAX callbacks to not have to early exit. This will make it possible to re-use and unit-test these callbacks, which it is currently not possible to do.
- It will make it possible (with the aid of a contrib module) to deliver any drupal page as an AJAX response.
- It will make it possible for contrib modules to introduce new delivery callbacks, allowing any drupal page to be delivered as a response appropriate for the technology of choice (SWF, XML, etc.).
If you have the time, I encourage you to read the full comment trail. Or, if you prefer, simply read the patch. However, I decided to put together a textual explanation of what the patch does. I did this as an exercise for myself, so I could check whether everything in the patch makes sense. However, I'm also hoping it provides some help to anyone reviewing the patch.
Edit: notes were inlined here initially, but are now in an attached file in #24.
Comment #23
effulgentsia CreditAttribution: effulgentsia commentedAnd, yes, I'm not at all trying to hide the fact that I would really, really, really like to see this patch make it into D7. To that end, I'd like to point out that sun (#7), merlinofchaos (#8), and chx (#16) have all registered approval for it in principle. So far, only chx has signed off on the patch in its current state. Obviously, I'd like others to as well, or let me know what else needs to be done for it to be RTBC'able.
Comment #24
effulgentsia CreditAttribution: effulgentsia commentedRealized that comment #22 was too long with the notes inlined. Attaching here in a file instead.
Comment #25
Crell CreditAttribution: Crell commentedConceptually, it should be no surprise that I support this. :-) The specific implementation here is a bit ugly and does about a quarter of what it could potentially do, but as effulgentsia has noted we don't really have time for D7 to do all of the cool things that this could do. So I'm +1 in principle on this approach.
I've not gone over the code in minute detail, but some comments from just reading the patch:
- We shouldn't be hard coding the "html page" version of drupal_not_found() et al. Why do those have to be a special case instead of sub-call functions like everything else?
That line in menu_execute_active_handler() can easily be simplified by pre-choosing the delivery callback in menu rebuild. Just make that function name the default if one is not set, and then the DB has an accurate callback record for every router item.
OK, the additional alter hooks are just sexy. :-)
That's all I have time for right now, I'm afraid.
Comment #26
rfayI apologize that I haven't had time to do a real review of this due to "real life" issues using up all my time.
However, it looks to me like there is a consensus that this is a great thing for Drupal's architecture.
Let's go ahead and take the risk and get it in.
There are times that "doing the right thing" pays off enormously, so many times over, that it's worth doing even when we're very conservative, and in this case, even at the very end of the cycle.
My 2 cents.
Comment #27
effulgentsia CreditAttribution: effulgentsia commentedWith Crell's feedback (both from #25 and IRC) to make drupal_not_found() and friends not be partial to HTML page delivery.
Did not implement Crell's suggestion to make 'drupal_deliver_html_page' a default in the router table. We still need to have that be a default in code to handle the situation when no router item is returned. This way, that default only exists in one place.
Also followed sun's suggestion to put some of what's in the notes in #24 into the code comments. I think it's now possible to just read the patch and understand what's going on, but I've been staring at this for a while, and so might not be the best judge of that. The notes from #24 are still useful as background information for anyone who wants it.
Comment #29
effulgentsia CreditAttribution: effulgentsia commentedHmmm. Works on my machine without syntax error. Trying again, in case it was a testbot fluke.
Comment #30
sunThis is RTBC for me - aside from the following comment tweaks: (please mark as RTBC after fixing these)
(and elsewhere) PHPDoc summary shouldn't wrap at 80 chars. Just remove the newline.
(and elsewhere) There should be blank line between case statements of a switch (as long as you don't fall-through).
(and elsewhere)
Please re-phrase into 3rd person here. So instead of referring to "you", we refer to "page callback functions", or, "the/a module".
Also, we do not talk about the future, so let's drop that part about exception handling ;)
This should be formatted as PHPDoc list.
How can a page callback decide? From the previous paragraph, I understand that the page callback is invoked first, returns its results, and meah() determines the delivery callback. My missing link is how my page callback can define/override this delivery callback, because this paragraph states I can.
Outdated?
Now I think I understand - so the PHPDoc description above should just be appended by a
@see hook_page_delivery_callback_alter()
What's the result if the function doesn't exist?
If it's intentional to return NULL, let's append a comment at the end to clarify so (and perhaps why).
@param description should start with:
(optional) A boolean to indicate whether the content should be sent to the browser using the appropriate delivery callback (TRUE) or whether to return the result to the caller (FALSE).
Short inline comment about the purpose of this alter?
This comment should be above the type definition, not within the array.
Should also be shortened... probably all up to (and including) "In this case" can more or less go, because that's documented in ajax.inc. Here, we should explain renderable array internals/tricks only.
This review is powered by Dreditor.
Comment #31
sunBetter title?
Comment #32
effulgentsia CreditAttribution: effulgentsia commentedSetting RTBC as per #30.
Webchick/Dries: please see issue description and/or comment #22 for a high level "WHY?".
Comment #33
effulgentsia CreditAttribution: effulgentsia commentedDid not intend to change title in #32.
Comment #34
Dries CreditAttribution: Dries commentedI have to let this slip in because, well, this is exactly the architecture that I outlined 3-4 years ago!
Unfortunately, the patch no longer replies so it needs a reroll. I did a review so it should be RTBC after a reroll.
Comment #35
catchThis is hopefully a straight re-roll, setting CNR. Please do not commit until tests are green.
Comment #36
catchhttp://testing.drupal.org/pifr/file/1/15236
Comment #37
Dries CreditAttribution: Dries commentedAlright, committed! Great job all.
Comment #38
moshe weitzman CreditAttribution: moshe weitzman commentedNice patch!
We should emit the standard http header only for http delivery. This helps command line scripts a lot. 1 line patch attached.
Comment #39
moshe weitzman CreditAttribution: moshe weitzman commentedWhat do folks think about turning drupal_serve_page_from_cache() into a delivery callback? Thats what does the http headers and such for cached pages. Not sure how we go about terminating the request flow and immediately delivering the page. Still use exit?
Comment #41
moshe weitzman CreditAttribution: moshe weitzman commentedI can reproduce those test failures, but have no clue how I've triggerred that. Anyone hav a clue. Failure is on line 139 of openid.test?
Comment #42
effulgentsia CreditAttribution: effulgentsia commentedThe problem is that the
openid_test_yadis_xrds()
function in openid_test.module prints instead of returns, which is the current convention for any callback that needs to output something that it doesn't want routed through drupal_render_page(). As long as page callbacks issue print statements (or call functions that do), delivery callbacks can't call header(), since header() can't be called after print(): http://php.net/manual/en/function.header.php.Comment #43
effulgentsia CreditAttribution: effulgentsia commented@moshe: how about this?
Comment #44
moshe weitzman CreditAttribution: moshe weitzman commentedNice work, effulgentsia.
Comment #45
Dries CreditAttribution: Dries commentedMmm, that feels like a hack.
Comment #46
moshe weitzman CreditAttribution: moshe weitzman commentedWhats a hack? We used to unconditionally set a text/html http header very early in page processing. We did that for all requests including CLI requests and XML requests and so on. Thats hackish. This patch moves that to the html delivery callback and does so in a safe way. Thats a significant improvement, no?
Comment #47
katbailey CreditAttribution: katbailey commentedHey all,
I've added a documentation task which could really use the eyes of the people involved in this thread: #610068: Document AJAX no-js and use-ajax
thanks!
Comment #48
rfayAnd I hijacked katbailey's thread for a general to-do list and discussion of Ajax documentation. #610068: Document AJAX no-js and use-ajax.
Comment #49
catchFixing upgrade path, previous patch still required.
Comment #50
catchPlease ignore tiny kitten.
Comment #51
webchickI committed #49 to fix the upgrade path. Haven't had a chance to read the rest of this issue yet, sorry.
Comment #52
catchComment #54
webchickCatch moved his upgrade path fixing efforts to #613238: Add missing columns and tables required for upgrade. Sorry folks. Carry on!
Comment #55
webchickHere's a re-upload of #43 so testing bot stops complaining.
Comment #56
effulgentsia CreditAttribution: effulgentsia commentedBumping this to hopefully get it unstuck. Seems there's a disagreement between Dries and Moshe (#45 and #46) with respect to whether this is good or not. What can be done to move the conversation along?
Comment #57
sunI'm not sure why Dries feels uneasy about this fix.
The only thing that makes me feel a bit uneasy is the NULL condition. Otherwise, this looks ready to go.
Comment #58
moshe weitzman CreditAttribution: moshe weitzman commentedI asked Dries to take another look at this.
Comment #59
Dries CreditAttribution: Dries commentedCommitted to CVS HEAD.
Comment #61
moshe weitzman CreditAttribution: moshe weitzman commentedHasn't happenned yet, AFACT. Is there an issue for it? Lets unify, people. Anyone up for rolling the patch?
Comment #62
effulgentsia CreditAttribution: effulgentsia commentedSorry, Moshe, it slipped my radar. Here it is: #716602: Refactor ajax_render() and clean up 'ajax' element type. I hope it's not too late.
Comment #63
rfayYou may be interested in #838408: Remove hook_menu_active_handler_alter if you participated here, since this is where it was introduced.