CApplication::onBeforeOutput

I find myself in dire need of an onBeforeOutput event in my application.

I wrote an application component (a shopping cart) that needs to write data (current shopping cart contents) back to a cookie at the end of the request.

I hooked my application component into the CWebApplication::onEndRequest event - thinking that, at the end of the request, I would send the cart contents back as a cookie. I had not taken into account the fact that, once output has started, it’s too late to send cookies (or any other headers).

It seems like what is needed here is an event that lets you run some code at the end of the request, but before the result of the request is sent to the browser.

It occurs to me that CController::processOutput() may be an attempt to work around the same problem - the fact that certain components (such as CClientScript) need to perform some kind of post-processing, at the end of the request, before the output is sent the browser.

Perhaps it would be feasible to implement an event that CClientScript and other post-processors can hook into - providing a means for various components to set headers, send cookies, post-process the response, etc.

What do you think?

Here’s my solution for the time being.

I added the following methods to my CWebApplication extension:




  /**

   * Processes the current request.

   * Adds output buffering and invokes the onBeforeOutput event

   */

  public function processRequest()

  {

    ob_start();

    parent::processRequest();

    $output = ob_get_clean();

    

    $this->onBeforeOutput(

      $event = new COutputEvent($this, $output)

    );

    

    echo $event->output;

  }

  

  /**

   * Raised before the output of the request is sent to the browser

   */

  public function onBeforeOutput($event)

  {

    $this->raiseEvent('onBeforeOutput', $event);

  }



In the constructor of my shopping cart application component, I can now do something like this:




  /**

   * Initialize the shopping cart state

   */

  public function __construct()

  {

    $this->restoreCart();

    Yii::app()->attachEventHandler('onBeforeOutput', array($this,'storeCart'));

  }



Technically, I don’t need to alter the output in my case, I could have just buffered it in a local variable - but I decided to go ahead and implement it as an example and inspiration to perhaps add this feature to the framework.

I decided to use the existing COutputEvent class, which seemed appropriate, although this doesn’t have anything to do with COutputProcessor, which is what that event was designed for…

Just as a side note, perhaps a better architectural approach would be:

  1. A CHttpResponse class that can temporarily hold the response data, raise post-processing events, etc.

  2. Adjusting the controller workflow so that post-processing becomes a general application feature.

  3. Changing CClientScript so that it uses the onBeforeOutput event to alter the response output.

  4. Changing CCookieCollection so that it uses the onBeforeOutput event to set all of the collected cookies before sending the response.

This would effectively enable you to use the cookie collection from anywhere, at any time, without worrying about whether PHP has started sending the output.

Sounds good. I’m already waiting for the CHttpResponse component by the way :) But with this change the framework would enforce output buffering. I’m not sure if that will lead to problems, other than the fact that automatic output buffering isn’t enabled in a default php installation for a reason.

For example one may don’t want output-buffering when displaying a big list of data (so the user can see direct output). Also with all the changes to clientScript it would not be possible to disable this behavior.

Regarding your actual application, I’m curious what the code looks like. Normally you should be able to set cookies before render().

My shopping cart is an application component - so I add and remove items like this:




Yii::app()->shop->cart->add($product);



The problem is that many operations that change the contents of the cart can happen during the same request - so when do you generate and send the cookie? You don’t know when the last operation has happened.

So during init(), I hooked my shopping cart component into onEndRequest (now onBeforeOutput), so that it can write out it’s contents to a cookie as late as possible - or not at all, in case the cart component wasn’t used during this request.

So now I have buffering on permanently for every request, which I guess causes some unnecessary overhead…

Thinking out loud here: if there was a way to explicitly turn on output buffering, rather than having it always on, that might work - if during init() my application component could say Yii::app()->response->outputBuffering=true, that would probably work just as well, and it would only be on for requests that need it.

On the other hand, however - CClientScript (and other post-processing components) could be more smoothly integrated into the application workflow/lifecycle if output buffering was turned on by default.

Finally, here’s another important observation: Yii already uses output buffering, probably for all of your requests. Look at CController::renderPartial() - it always calls CBaseController::renderFile() with $return=true, causing CBaseController::renderInternal() to use output buffering functions.

So my thoughts are starting to converge here - that whole step, as far as I can figure, is there primarily to allow post-processing with CController::processOutput() so that CClientScript can work it’s magic.

I think it would be cleaner to have this output buffering done at a higher level, and making the post-processing hook generally available, not just to CClientScript - so that other application components can hook into an event, on the same terms as (a refactored version of) CClientScript.

Though my example with the list was a little wrong IF one uses the render() methods, I still have bad feeling about this.

  • Were does CClientScript comes into play when I call render() in order to save the output?

  • Will it affect debugging? (error messages/exceptions maybe "lost" in the output buffer?)

  • What if I stream a video or send a big file? There would be no output until the whole data got send.

  • Is there any other framework that uses global output buffering?

Other than that you can enable it globally via php.ini if really needed:

Also I’m pretty sure we could find another solution for your problem.

Clearly a change of this magnitude would need a lot of thought and testing.

I have implemented this, as shown in the code samples above - it’s been working well so far.

It does not seem to get in the way of error handling - the PHP standard error handler, when an error occurs, will flush all the output buffers and display the error message along with any generated content.

It got in the way of one action, where I needed to send an image response - I solved it simply by manually flushing the output buffers, e.g.:




    while (@ob_end_clean()) {}

    

    header('Content-type: image/jpeg');

    readfile($path);

    exit;



Frameworks like ASP.NET MVC solves this problem more elegantly by having multiple subclasses of the http response class.

CHttpResponse would be an (abstract) base class for any type of response, and the actual response type would be determined by the controller, which by default would probably use CHttpHtmlResponse, but with the option to switch to other types of response, e.g. CHttpFileResponse for a large, binary, unbuffered response.

It might look something like this:




class SampleController extends CController

{

  public actionPage()

  {

    $this->render('some_page');

  }

  

  public actionFile()

  {

    $this->setResponseType('file');

    readfile('large_file.zip');

  }

}



It would be a requirement, that if a controller needs to change the response type, it has to do so before outputting any content - attempting to change the response type after starting the output should result in an exception.

Various response types could implement various features specific/useful to each type of response - so the HTML response type, for example, would be responsible for buffering the output, as well as HTML-specific post-processing filters, e.g. CClientScript.

Another example could be a JSON (and/or AJAX) response type, which could (for example) abort the request if it was not submitted via AJAX.

For one, the example you mentioned, where a view generates a very large HTML response (for example an HTML report or a CSV export) currently will always be buffered due to the way that render() works - you could have an unbuffered response type for those situations, where you do not want the response buffered in memory at all, or when (for some reason) you do not want CClientScript to post-process the response.

I think this is a powerful concept with many useful applications?

I think I understand a little better now. A response object is definitely needed anyway. Then we could maybe also add core-functionality for client-side caching, templates for response types (like “jpg” => cache 1 hour client side) and so on. But speaking for myself here, I don’t know if several response classes would fit to Yii. Maybe this is something for Yii 2?

However I guess it will still take some time with the CHttpResponse (the orginal ticket is from march 2009).