From 3e18479d1f488cd59125302d801043d11815825e Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Mon, 17 Jun 2013 00:48:05 -0400 Subject: [PATCH] Fixes issue #545: PageCache doesn't work. --- framework/yii/base/ActionEvent.php | 4 + framework/yii/base/ActionFilter.php | 5 +- framework/yii/base/Controller.php | 11 +- framework/yii/base/ErrorHandler.php | 15 +-- framework/yii/base/Module.php | 7 +- framework/yii/base/Response.php | 9 -- framework/yii/base/ResponseEvent.php | 40 ------ framework/yii/web/Application.php | 2 +- framework/yii/web/PageCache.php | 14 +- framework/yii/web/Response.php | 144 ++++++++++++--------- framework/yii/web/ResponseEvent.php | 39 ++++++ framework/yii/web/ResponseFormatter.php | 10 +- framework/yii/web/XmlResponseFormatter.php | 12 +- .../framework/web/XmlResponseFormatterTest.php | 131 +++++++++++-------- 14 files changed, 243 insertions(+), 200 deletions(-) delete mode 100644 framework/yii/base/ResponseEvent.php create mode 100644 framework/yii/web/ResponseEvent.php diff --git a/framework/yii/base/ActionEvent.php b/framework/yii/base/ActionEvent.php index 9507b12..9b6c2f0 100644 --- a/framework/yii/base/ActionEvent.php +++ b/framework/yii/base/ActionEvent.php @@ -22,6 +22,10 @@ class ActionEvent extends Event */ public $action; /** + * @var mixed the action result. Event handlers may modify this property to change the action result. + */ + public $result; + /** * @var boolean whether to continue running the action. Event handlers of * [[Controller::EVENT_BEFORE_ACTION]] may set this property to decide whether * to continue running the current action. diff --git a/framework/yii/base/ActionFilter.php b/framework/yii/base/ActionFilter.php index 1e957d5..bc3b60c 100644 --- a/framework/yii/base/ActionFilter.php +++ b/framework/yii/base/ActionFilter.php @@ -57,7 +57,7 @@ class ActionFilter extends Behavior public function afterFilter($event) { if ($this->isActive($event->action)) { - $this->afterAction($event->action); + $this->afterAction($event->action, $event->result); } } @@ -76,8 +76,9 @@ class ActionFilter extends Behavior * This method is invoked right after an action is executed. * You may override this method to do some postprocessing for the action. * @param Action $action the action just executed. + * @param mixed $result the action execution result */ - public function afterAction($action) + public function afterAction($action, &$result) { } diff --git a/framework/yii/base/Controller.php b/framework/yii/base/Controller.php index b6275a8..25123fd 100644 --- a/framework/yii/base/Controller.php +++ b/framework/yii/base/Controller.php @@ -114,9 +114,9 @@ class Controller extends Component if ($this->module->beforeAction($action)) { if ($this->beforeAction($action)) { $result = $action->runWithParams($params); - $this->afterAction($action); + $this->afterAction($action, $result); } - $this->module->afterAction($action); + $this->module->afterAction($action, $result); } $this->action = $oldAction; return $result; @@ -208,10 +208,13 @@ class Controller extends Component * This method is invoked right after an action is executed. * You may override this method to do some postprocessing for the action. * @param Action $action the action just executed. + * @param mixed $result the action return result. */ - public function afterAction($action) + public function afterAction($action, &$result) { - $this->trigger(self::EVENT_AFTER_ACTION, new ActionEvent($action)); + $event = new ActionEvent($action); + $event->result = &$result; + $this->trigger(self::EVENT_AFTER_ACTION, $event); } /** diff --git a/framework/yii/base/ErrorHandler.php b/framework/yii/base/ErrorHandler.php index 338a392..6fb1ee2 100644 --- a/framework/yii/base/ErrorHandler.php +++ b/framework/yii/base/ErrorHandler.php @@ -97,12 +97,12 @@ class ErrorHandler extends Component if ($result instanceof Response) { $response = $result; } else { - $response->setContent($result); + $response->data = $result; } } elseif ($response->format === \yii\web\Response::FORMAT_HTML) { if (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] === 'XMLHttpRequest') { // AJAX request - $response->setContent(Yii::$app->renderException($exception)); + $response->data = Yii::$app->renderException($exception); } else { // if there is an error during error rendering it's useful to // display PHP error in debug mode instead of a blank screen @@ -110,22 +110,21 @@ class ErrorHandler extends Component ini_set('display_errors', 1); } $file = $useErrorView ? $this->errorView : $this->exceptionView; - $response->setContent($this->renderFile($file, array( + $response->data = $this->renderFile($file, array( 'exception' => $exception, - ))); + )); } } else { - if ($exception instanceof Exception) { - $content = $exception->toArray(); + if ($exception instanceof Arrayable) { + $response->data = $exception; } else { - $content = array( + $response->data = array( 'type' => get_class($exception), 'name' => 'Exception', 'message' => $exception->getMessage(), 'code' => $exception->getCode(), ); } - $response->setContent($content); } if ($exception instanceof HttpException) { diff --git a/framework/yii/base/Module.php b/framework/yii/base/Module.php index 6603b28..53042ae 100644 --- a/framework/yii/base/Module.php +++ b/framework/yii/base/Module.php @@ -659,9 +659,12 @@ abstract class Module extends Component * This method is invoked right after an action is executed. * You may override this method to do some postprocessing for the action. * @param Action $action the action just executed. + * @param mixed $result the action return result. */ - public function afterAction($action) + public function afterAction($action, &$result) { - $this->trigger(self::EVENT_AFTER_ACTION, new ActionEvent($action)); + $event = new ActionEvent($action); + $event->result = &$result; + $this->trigger(self::EVENT_AFTER_ACTION, $event); } } diff --git a/framework/yii/base/Response.php b/framework/yii/base/Response.php index 886774f..467de9e 100644 --- a/framework/yii/base/Response.php +++ b/framework/yii/base/Response.php @@ -14,12 +14,6 @@ namespace yii\base; class Response extends Component { /** - * @event ResponseEvent an event that is triggered by [[send()]] before it sends the response to client. - * You may respond to this event to modify the response before it is sent out. - */ - const EVENT_SEND = 'send'; - - /** * @var integer the exit status. Exit statuses should be in the range 0 to 254. * The status 0 means the program terminates successfully. */ @@ -27,11 +21,8 @@ class Response extends Component /** * Sends the response to client. - * This method will trigger the [[EVENT_SEND]] event. Please make sure you call - * the parent implementation first if you override this method. */ public function send() { - $this->trigger(self::EVENT_SEND, new ResponseEvent($this)); } } diff --git a/framework/yii/base/ResponseEvent.php b/framework/yii/base/ResponseEvent.php deleted file mode 100644 index 1521a97..0000000 --- a/framework/yii/base/ResponseEvent.php +++ /dev/null @@ -1,40 +0,0 @@ - - * @since 2.0 - */ -class ResponseEvent extends Event -{ - /** - * @var Response the response object associated with this event. - * You may modify the content in this response or replace it - * with a new response object. The updated or new response will - * be used as the final out. - */ - public $response; - - /** - * Constructor. - * @param Response $response the response object associated with this event. - * @param array $config the configuration array for initializing the newly created object. - */ - public function __construct($response, $config = array()) - { - $this->response = $response; - parent::__construct($config); - } -} diff --git a/framework/yii/web/Application.php b/framework/yii/web/Application.php index 6536db7..242a0db 100644 --- a/framework/yii/web/Application.php +++ b/framework/yii/web/Application.php @@ -71,7 +71,7 @@ class Application extends \yii\base\Application } else { $response = $this->getResponse(); if ($result !== null) { - $response->setContent($result); + $response->data = $result; } return $response; } diff --git a/framework/yii/web/PageCache.php b/framework/yii/web/PageCache.php index 5f97338..d28203e 100644 --- a/framework/yii/web/PageCache.php +++ b/framework/yii/web/PageCache.php @@ -94,16 +94,26 @@ class PageCache extends ActionFilter $properties[$name] = $this->$name; } $id = $this->varyByRoute ? $action->getUniqueId() : __CLASS__; - return $this->view->beginCache($id, $properties); + ob_start(); + ob_implicit_flush(false); + if ($this->view->beginCache($id, $properties)) { + return true; + } else { + Yii::$app->getResponse()->content = ob_get_clean(); + return false; + } } /** * This method is invoked right after an action is executed. * You may override this method to do some postprocessing for the action. * @param Action $action the action just executed. + * @param mixed $result the action execution result */ - public function afterAction($action) + public function afterAction($action, &$result) { + echo $result; $this->view->endCache(); + $result = ob_get_clean(); } } diff --git a/framework/yii/web/Response.php b/framework/yii/web/Response.php index 4783ffd..e087a13 100644 --- a/framework/yii/web/Response.php +++ b/framework/yii/web/Response.php @@ -23,6 +23,20 @@ use yii\helpers\StringHelper; */ class Response extends \yii\base\Response { + /** + * @event ResponseEvent an event that is triggered at the beginning of [[send()]]. + */ + const EVENT_BEFORE_SEND = 'beforeSend'; + /** + * @event ResponseEvent an event that is triggered at the end of [[send()]]. + */ + const EVENT_AFTER_SEND = 'afterSend'; + /** + * @event ResponseEvent an event that is triggered right after [[prepare()]] is called in [[send()]]. + * You may respond to this event to filter the response content before it is sent to the client. + */ + const EVENT_PREPARE = 'prepare'; + const FORMAT_RAW = 'raw'; const FORMAT_HTML = 'html'; const FORMAT_JSON = 'json'; @@ -30,16 +44,46 @@ class Response extends \yii\base\Response const FORMAT_XML = 'xml'; /** - * @var string the response format. + * @var string the response format. This determines how to convert [[data]] into [[content]] + * when the latter is not set. By default, the following formats are supported: + * + * - [[FORMAT_RAW]]: the data will be treated as the response content without any conversion. + * No extra HTTP header will be added. + * - [[FORMAT_HTML]]: the data will be treated as the response content without any conversion. + * The "Content-Type" header will set as "text/html" if it is not set previously. + * - [[FORMAT_JSON]]: the data will be converted into JSON format, and the "Content-Type" + * header will be set as "application/json". + * - [[FORMAT_JSONP]]: the data will be converted into JSONP format, and the "Content-Type" + * header will be set as "text/javascript". Note that in this case `$data` must be an array + * with "data" and "callback" elements. The former refers to the actual data to be sent, + * while the latter refers to the name of the JavaScript callback. + * - [[FORMAT_XML]]: the data will be converted into XML format. Please refer to [[XmlResponseFormatter]] + * for more details. + * + * You may customize the formatting process or support additional formats by configuring [[formatters]]. + * @see formatters */ public $format = self::FORMAT_HTML; /** * @var array the formatters for converting data into the response content of the specified [[format]]. * The array keys are the format names, and the array values are the corresponding configurations * for creating the formatter objects. + * @see format */ public $formatters; /** + * @var mixed the original response data. When this is not null, it will be converted into [[content]] + * according to [[format]] when the response is being sent out. + * @see content + */ + public $data; + /** + * @var string the response content. When [[data]] is not null, it will be converted into [[content]] + * according to [[format]] when the response is being sent out. + * @see data + */ + public $content; + /** * @var string the charset of the text response. If not set, it will use * the value of [[Application::charset]]. */ @@ -203,9 +247,12 @@ class Response extends \yii\base\Response */ public function send() { - parent::send(); + $this->trigger(self::EVENT_BEFORE_SEND, new ResponseEvent($this)); + $this->prepare(); + $this->trigger(self::EVENT_PREPARE, new ResponseEvent($this)); $this->sendHeaders(); $this->sendContent(); + $this->trigger(self::EVENT_AFTER_SEND, new ResponseEvent($this)); $this->clear(); } @@ -217,7 +264,8 @@ class Response extends \yii\base\Response $this->_headers = null; $this->_cookies = null; $this->_statusCode = null; - $this->_content = null; + $this->data = null; + $this->content = null; $this->statusText = null; } @@ -271,7 +319,7 @@ class Response extends \yii\base\Response */ protected function sendContent() { - echo $this->getContent(); + echo $this->content; } /** @@ -322,12 +370,13 @@ class Response extends \yii\base\Response if ($begin !=0 || $end != $contentLength - 1) { $this->setStatusCode(206); $headers->set('Content-Range', "bytes $begin-$end/$contentLength"); - $this->setContent(StringHelper::substr($content, $begin, $end - $begin + 1), self::FORMAT_RAW); + $this->content = StringHelper::substr($content, $begin, $end - $begin + 1); } else { $this->setStatusCode(200); - $this->setContent($content, self::FORMAT_RAW); + $this->content = $content; } + $this->format = self::FORMAT_RAW; $this->send(); } @@ -368,7 +417,8 @@ class Response extends \yii\base\Response ->setDefault('Content-Transfer-Encoding', 'binary') ->setDefault('Content-Length', $length) ->setDefault('Content-Disposition', "attachment; filename=\"$attachmentName\""); - + $this->format = self::FORMAT_RAW; + $this->data = $this->content = null; $this->send(); fseek($handle, $begin); @@ -647,88 +697,56 @@ class Response extends \yii\base\Response return in_array($this->getStatusCode(), array(201, 204, 304)); } - private $_content; - - /** - * @return string the content of this response - */ - public function getContent() - { - return $this->_content; - } - /** - * Sets the content of this response. - * The existing content will be overwritten. - * Depending on the value of [[format]], the data will be properly formatted. - * @param mixed $data the data that needs to be converted into the response content. - * @param string $format the format of the response. The [[formatters]] property specifies - * the supported formats and the corresponding formatters. Additionally, the following formats are - * supported if they are not found in [[formatters]]: - * - * - [[FORMAT_RAW]]: the data will be treated as the response content without any conversion. - * No extra HTTP header will be added. - * - [[FORMAT_HTML]]: the data will be treated as the response content without any conversion. - * The "Content-Type" header will set as "text/html" if it is not set previously. - * - [[FORMAT_JSON]]: the data will be converted into JSON format, and the "Content-Type" - * header will be set as "application/json". - * - [[FORMAT_JSONP]]: the data will be converted into JSONP format, and the "Content-Type" - * header will be set as "text/javascript". Note that in this case `$data` must be an array - * with "data" and "callback" elements. The former refers to the actual data to be sent, - * while the latter refers to the name of the JavaScript callback. - * - [[FORMAT_XML]]: the data will be converted into XML format. Please refer to [[XmlResponseFormatter]] - * for more details. - */ - public function setContent($data, $format = null) - { - if ($format !== null) { - $this->format = $format; - } - $this->_content = $this->formatContent($data, $format); - } - - /** - * Formats the given data as the specified format. - * @param mixed $data the data to be formatted. - * @param string $format the format to use. - * @return string the formatting result. + * Prepares for sending the response. + * The default implementation will convert [[data]] into [[content]] and set headers accordingly. * @throws InvalidParamException if `$format` is not supported * @throws InvalidConfigException if the formatter for the specified format is invalid */ - protected function formatContent($data, $format) + protected function prepare() { - if (isset($this->formatters[$format])) { - $formatter = $this->formatters[$format]; + if ($this->data === null) { + return; + } + + if (isset($this->formatters[$this->format])) { + $formatter = $this->formatters[$this->format]; if (!is_object($formatter)) { $formatter = Yii::createObject($formatter); } if ($formatter instanceof ResponseFormatter) { - return $formatter->format($this, $data); + $formatter->format($this); + return; } else { - throw new InvalidConfigException("The '$format' response formatter is invalid. It must implement the ResponseFormatter interface."); + throw new InvalidConfigException("The '{$this->format}' response formatter is invalid. It must implement the ResponseFormatter interface."); } } switch ($this->format) { case self::FORMAT_RAW: - return $data; + $this->content = $this->data; + break; case self::FORMAT_HTML: $this->getHeaders()->setDefault('Content-Type', 'text/html; charset=' . $this->charset); - return $data; + $this->content = $this->data; + break; case self::FORMAT_JSON: $this->getHeaders()->set('Content-Type', 'application/json'); - return Json::encode($data); + $this->content = Json::encode($this->data); + break; case self::FORMAT_JSONP: $this->getHeaders()->set('Content-Type', 'text/javascript; charset=' . $this->charset); - if (is_array($data) && isset($data['data'], $data['callback'])) { - return sprintf('%s(%s);', $data['callback'], Json::encode($data['data'])); + if (is_array($this->data) && isset($this->data['data'], $this->data['callback'])) { + $this->content = sprintf('%s(%s);', $this->data['callback'], Json::encode($this->data['data'])); + break; } else { throw new InvalidParamException("The 'jsonp' response requires that the data be an array consisting of both 'data' and 'callback' elements."); } case self::FORMAT_XML: - return Yii::createObject('yii\web\XmlResponseFormatter')->format($this, $data); + $this->content = Yii::createObject(XmlResponseFormatter::className())->format($this); + break; default: - throw new InvalidConfigException("Unsupported response format: $format"); + throw new InvalidConfigException("Unsupported response format: {$this->format}"); } } } diff --git a/framework/yii/web/ResponseEvent.php b/framework/yii/web/ResponseEvent.php new file mode 100644 index 0000000..e5d4210 --- /dev/null +++ b/framework/yii/web/ResponseEvent.php @@ -0,0 +1,39 @@ + + * @since 2.0 + */ +class ResponseEvent extends Event +{ + /** + * @var Response the response object associated with this event. + */ + public $response; + + /** + * Constructor. + * @param Response $response the response object associated with this event. + * @param array $config the configuration array for initializing the newly created object. + */ + public function __construct($response, $config = array()) + { + $this->response = $response; + parent::__construct($config); + } +} diff --git a/framework/yii/web/ResponseFormatter.php b/framework/yii/web/ResponseFormatter.php index d36b8f0..dc7c979 100644 --- a/framework/yii/web/ResponseFormatter.php +++ b/framework/yii/web/ResponseFormatter.php @@ -8,7 +8,7 @@ namespace yii\web; /** - * ResponseFormatter specifies the interface needed to format data for a Web response object. + * ResponseFormatter specifies the interface needed to format a response before it is sent out. * * @author Qiang Xue * @since 2.0 @@ -16,10 +16,8 @@ namespace yii\web; interface ResponseFormatter { /** - * Formats the given data for the response. - * @param Response $response the response object that will accept the formatted result - * @param mixed $data the data to be formatted - * @return string the formatted result + * Formats the specified response. + * @param Response $response the response to be formatted. */ - public function format($response, $data); + public function format($response); } diff --git a/framework/yii/web/XmlResponseFormatter.php b/framework/yii/web/XmlResponseFormatter.php index 32b7213..92af48d 100644 --- a/framework/yii/web/XmlResponseFormatter.php +++ b/framework/yii/web/XmlResponseFormatter.php @@ -45,19 +45,17 @@ class XmlResponseFormatter extends Component implements ResponseFormatter public $itemTag = 'item'; /** - * Formats the given data for the response. - * @param Response $response the response object that will accept the formatted result - * @param mixed $data the data to be formatted - * @return string the formatted result + * Formats the specified response. + * @param Response $response the response to be formatted. */ - public function format($response, $data) + public function format($response) { $response->getHeaders()->set('Content-Type', $this->contentType); $dom = new DOMDocument($this->version, $this->encoding === null ? $response->charset : $this->encoding); $root = new DOMElement($this->rootTag); $dom->appendChild($root); - $this->buildXml($root, $data); - return $dom->saveXML(); + $this->buildXml($root, $response->data); + $response->content = $dom->saveXML(); } /** diff --git a/tests/unit/framework/web/XmlResponseFormatterTest.php b/tests/unit/framework/web/XmlResponseFormatterTest.php index f39d474..590caef 100644 --- a/tests/unit/framework/web/XmlResponseFormatterTest.php +++ b/tests/unit/framework/web/XmlResponseFormatterTest.php @@ -45,73 +45,92 @@ class XmlResponseFormatterTest extends \yiiunit\TestCase $this->formatter = new XmlResponseFormatter; } - public function testFormatScalars() + /** + * @param mixed $data the data to be formatted + * @param string $xml the expected XML body + * @dataProvider formatScalarDataProvider + */ + public function testFormatScalar($data, $xml) { $head = "\n"; - - $xml = $head . "\n"; - $this->assertEquals($xml, $this->formatter->format($this->response, null)); - - $xml = $head . "1\n"; - $this->assertEquals($xml, $this->formatter->format($this->response, 1)); - - $xml = $head . "abc\n"; - $this->assertEquals($xml, $this->formatter->format($this->response, 'abc')); - - $xml = $head . "1\n"; - $this->assertEquals($xml, $this->formatter->format($this->response, true)); + $this->response->data = $data; + $this->formatter->format($this->response); + $this->assertEquals($head . $xml, $this->response->content); + } + + public function formatScalarDataProvider() + { + return array( + array(null, "\n"), + array(1, "1\n"), + array('abc', "abc\n"), + array(true, "1\n"), + array("<>", "<>\n"), + ); } - public function testFormatArrays() + /** + * @param mixed $data the data to be formatted + * @param string $xml the expected XML body + * @dataProvider formatArrayDataProvider + */ + public function testFormatArrays($data, $xml) { $head = "\n"; + $this->response->data = $data; + $this->formatter->format($this->response); + $this->assertEquals($head . $xml, $this->response->content); + } - $xml = $head . "\n"; - $this->assertEquals($xml, $this->formatter->format($this->response, array())); - - $xml = $head . "1abc\n"; - $this->assertEquals($xml, $this->formatter->format($this->response, array(1, 'abc'))); - - $xml = $head . "1abc\n"; - $this->assertEquals($xml, $this->formatter->format($this->response, array( - 'a' => 1, - 'b' => 'abc', - ))); - - $xml = $head . "1abc2def1\n"; - $this->assertEquals($xml, $this->formatter->format($this->response, array( - 1, - 'abc', - array(2, 'def'), - true, - ))); - - $xml = $head . "1abc2def1\n"; - $this->assertEquals($xml, $this->formatter->format($this->response, array( - 'a' => 1, - 'b' => 'abc', - 'c' => array(2, 'def'), - true, - ))); + public function formatArrayDataProvider() + { + return array( + array(array(), "\n"), + array(array(1, 'abc'), "1abc\n"), + array(array( + 'a' => 1, + 'b' => 'abc', + ), "1abc\n"), + array(array( + 1, + 'abc', + array(2, 'def'), + true, + ), "1abc2def1\n"), + array(array( + 'a' => 1, + 'b' => 'abc', + 'c' => array(2, '<>'), + true, + ), "1abc2<>1\n"), + ); } - public function testFormatObjects() + /** + * @param mixed $data the data to be formatted + * @param string $xml the expected XML body + * @dataProvider formatObjectDataProvider + */ + public function testFormatObjects($data, $xml) { $head = "\n"; + $this->response->data = $data; + $this->formatter->format($this->response); + $this->assertEquals($head . $xml, $this->response->content); + } - $xml = $head . "123abc\n"; - $this->assertEquals($xml, $this->formatter->format($this->response, new Post(123, 'abc'))); - - $xml = $head . "123abc456def\n"; - $this->assertEquals($xml, $this->formatter->format($this->response, array( - new Post(123, 'abc'), - new Post(456, 'def'), - ))); - - $xml = $head . "123abc456def\n"; - $this->assertEquals($xml, $this->formatter->format($this->response, array( - new Post(123, 'abc'), - 'a' => new Post(456, 'def'), - ))); + public function formatObjectDataProvider() + { + return array( + array(new Post(123, 'abc'), "123abc\n"), + array(array( + new Post(123, 'abc'), + new Post(456, 'def'), + ), "123abc456def\n"), + array(array( + new Post(123, '<>'), + 'a' => new Post(456, 'def'), + ), "123<>456def\n"), + ); } }