Commit 3e18479d by Qiang Xue

Fixes issue #545: PageCache doesn't work.

parent f18d6df6
......@@ -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.
......
......@@ -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)
{
}
......
......@@ -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);
}
/**
......
......@@ -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) {
......
......@@ -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);
}
}
......@@ -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));
}
}
......@@ -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;
}
......
......@@ -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();
}
}
......@@ -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}");
}
}
}
......@@ -5,7 +5,9 @@
* @license http://www.yiiframework.com/license/
*/
namespace yii\base;
namespace yii\web;
use yii\base\Event;
/**
* ResponseEvent represents the event data for the [[Application::EVENT_RESPONSE]] event.
......@@ -21,9 +23,6 @@ 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;
......
......@@ -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 <qiang.xue@gmail.com>
* @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);
}
......@@ -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();
}
/**
......
......@@ -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 = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n";
$this->response->data = $data;
$this->formatter->format($this->response);
$this->assertEquals($head . $xml, $this->response->content);
}
$xml = $head . "<response></response>\n";
$this->assertEquals($xml, $this->formatter->format($this->response, null));
$xml = $head . "<response>1</response>\n";
$this->assertEquals($xml, $this->formatter->format($this->response, 1));
$xml = $head . "<response>abc</response>\n";
$this->assertEquals($xml, $this->formatter->format($this->response, 'abc'));
$xml = $head . "<response>1</response>\n";
$this->assertEquals($xml, $this->formatter->format($this->response, true));
public function formatScalarDataProvider()
{
return array(
array(null, "<response></response>\n"),
array(1, "<response>1</response>\n"),
array('abc', "<response>abc</response>\n"),
array(true, "<response>1</response>\n"),
array("<>", "<response>&lt;&gt;</response>\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 = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n";
$this->response->data = $data;
$this->formatter->format($this->response);
$this->assertEquals($head . $xml, $this->response->content);
}
$xml = $head . "<response/>\n";
$this->assertEquals($xml, $this->formatter->format($this->response, array()));
$xml = $head . "<response><item>1</item><item>abc</item></response>\n";
$this->assertEquals($xml, $this->formatter->format($this->response, array(1, 'abc')));
$xml = $head . "<response><a>1</a><b>abc</b></response>\n";
$this->assertEquals($xml, $this->formatter->format($this->response, array(
public function formatArrayDataProvider()
{
return array(
array(array(), "<response/>\n"),
array(array(1, 'abc'), "<response><item>1</item><item>abc</item></response>\n"),
array(array(
'a' => 1,
'b' => 'abc',
)));
$xml = $head . "<response><item>1</item><item>abc</item><item><item>2</item><item>def</item></item><item>1</item></response>\n";
$this->assertEquals($xml, $this->formatter->format($this->response, array(
), "<response><a>1</a><b>abc</b></response>\n"),
array(array(
1,
'abc',
array(2, 'def'),
true,
)));
$xml = $head . "<response><a>1</a><b>abc</b><c><item>2</item><item>def</item></c><item>1</item></response>\n";
$this->assertEquals($xml, $this->formatter->format($this->response, array(
), "<response><item>1</item><item>abc</item><item><item>2</item><item>def</item></item><item>1</item></response>\n"),
array(array(
'a' => 1,
'b' => 'abc',
'c' => array(2, 'def'),
'c' => array(2, '<>'),
true,
)));
), "<response><a>1</a><b>abc</b><c><item>2</item><item>&lt;&gt;</item></c><item>1</item></response>\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 = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n";
$this->response->data = $data;
$this->formatter->format($this->response);
$this->assertEquals($head . $xml, $this->response->content);
}
$xml = $head . "<response><Post><id>123</id><title>abc</title></Post></response>\n";
$this->assertEquals($xml, $this->formatter->format($this->response, new Post(123, 'abc')));
$xml = $head . "<response><Post><id>123</id><title>abc</title></Post><Post><id>456</id><title>def</title></Post></response>\n";
$this->assertEquals($xml, $this->formatter->format($this->response, array(
public function formatObjectDataProvider()
{
return array(
array(new Post(123, 'abc'), "<response><Post><id>123</id><title>abc</title></Post></response>\n"),
array(array(
new Post(123, 'abc'),
new Post(456, 'def'),
)));
$xml = $head . "<response><Post><id>123</id><title>abc</title></Post><a><Post><id>456</id><title>def</title></Post></a></response>\n";
$this->assertEquals($xml, $this->formatter->format($this->response, array(
new Post(123, 'abc'),
), "<response><Post><id>123</id><title>abc</title></Post><Post><id>456</id><title>def</title></Post></response>\n"),
array(array(
new Post(123, '<>'),
'a' => new Post(456, 'def'),
)));
), "<response><Post><id>123</id><title>&lt;&gt;</title></Post><a><Post><id>456</id><title>def</title></Post></a></response>\n"),
);
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment