From a951e1c8e1966d1f195c27e58e2c22bc21fbeb63 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Fri, 14 Jun 2013 16:26:33 -0400 Subject: [PATCH] Replaced Jsonable with Arrayable. Added support for different response formats. Support for error response in different formats. --- framework/yii/YiiBase.php | 26 +++++++++++++ framework/yii/base/Application.php | 27 +++++-------- framework/yii/base/Arrayable.php | 23 +++++++++++ framework/yii/base/ErrorHandler.php | 27 ++++++++----- framework/yii/base/Exception.php | 16 +++++++- framework/yii/base/Jsonable.php | 22 ----------- framework/yii/base/Model.php | 12 +++--- framework/yii/base/Object.php | 14 +++---- framework/yii/base/Response.php | 12 ++++++ framework/yii/helpers/base/Json.php | 5 +-- framework/yii/web/Application.php | 4 +- framework/yii/web/HttpException.php | 11 ++++++ framework/yii/web/Response.php | 77 +++++++++++++++++++++++++++++++------ 13 files changed, 199 insertions(+), 77 deletions(-) create mode 100644 framework/yii/base/Arrayable.php delete mode 100644 framework/yii/base/Jsonable.php diff --git a/framework/yii/YiiBase.php b/framework/yii/YiiBase.php index ee75822..61208dc 100644 --- a/framework/yii/YiiBase.php +++ b/framework/yii/YiiBase.php @@ -6,6 +6,7 @@ */ namespace yii; +use yii\base\Arrayable; use yii\base\Exception; use yii\base\InvalidConfigException; use yii\base\InvalidParamException; @@ -636,6 +637,31 @@ class YiiBase { return get_object_vars($object); } + + /** + * Converts the object into an array. + * @param object|array $object the object to be converted into an array + * @param boolean $recursive whether to recursively converts properties which are objects into arrays. + * @return array the array representation of the object + */ + public static function toArray($object, $recursive = true) + { + if ($object instanceof Arrayable) { + $object = $object->toArray(); + if (!$recursive) { + return $object; + } + } + $result = array(); + foreach ($object as $key => $value) { + if ($recursive && (is_array($value) || is_object($value))) { + $result[$key] = static::toArray($value, true); + } else { + $result[$key] = $value; + } + } + return $result; + } } YiiBase::$aliases = array( diff --git a/framework/yii/base/Application.php b/framework/yii/base/Application.php index 0f78cb6..b1a7750 100644 --- a/framework/yii/base/Application.php +++ b/framework/yii/base/Application.php @@ -19,10 +19,6 @@ use yii\web\HttpException; abstract class Application extends Module { /** - * @event ResponseEvent an event that is triggered after [[handle()]] returns a response. - */ - const EVENT_RESPONSE = 'response'; - /** * @var string the application name. */ public $name = 'My Application'; @@ -144,11 +140,7 @@ abstract class Application extends Module public function run() { $response = $this->handle($this->getRequest()); - - $event = new ResponseEvent($response); - $this->trigger(self::EVENT_RESPONSE, $event); - $event->response->send(); - + $response->send(); return $response->exitStatus; } @@ -422,18 +414,19 @@ abstract class Application extends Module */ public function handleFatalError() { + // load ErrorException manually here because autoloading them will not work + // when error occurs while autoloading a class + if (!class_exists('\\yii\\base\\Exception', false)) { + require_once(__DIR__ . '/Exception.php'); + } + if (!class_exists('\\yii\\base\\ErrorException', false)) { + require_once(__DIR__ . '/ErrorException.php'); + } + $error = error_get_last(); if (ErrorException::isFatalError($error)) { unset($this->_memoryReserve); - // load ErrorException manually here because autoloading them will not work - // when error occurs while autoloading a class - if (!class_exists('\\yii\\base\\Exception', false)) { - require_once(__DIR__ . '/Exception.php'); - } - if (!class_exists('\\yii\\base\\ErrorException', false)) { - require_once(__DIR__ . '/ErrorException.php'); - } $exception = new ErrorException($error['message'], $error['type'], $error['type'], $error['file'], $error['line']); // use error_log because it's too late to use Yii log error_log($exception); diff --git a/framework/yii/base/Arrayable.php b/framework/yii/base/Arrayable.php new file mode 100644 index 0000000..1822e51 --- /dev/null +++ b/framework/yii/base/Arrayable.php @@ -0,0 +1,23 @@ + + * @since 2.0 + */ +interface Arrayable +{ + /** + * Converts the object into an array. + * @return array the array representation of this object + */ + public function toArray(); +} diff --git a/framework/yii/base/ErrorHandler.php b/framework/yii/base/ErrorHandler.php index e9ca91e..1e6671b 100644 --- a/framework/yii/base/ErrorHandler.php +++ b/framework/yii/base/ErrorHandler.php @@ -9,7 +9,6 @@ namespace yii\base; use Yii; use yii\web\HttpException; -use yii\web\Response; /** * ErrorHandler handles uncaught PHP errors and exceptions. @@ -90,31 +89,41 @@ class ErrorHandler extends Component $useErrorView = !YII_DEBUG || $exception instanceof UserException; + $response = Yii::$app->getResponse(); if ($useErrorView && $this->errorAction !== null) { $result = Yii::$app->runAction($this->errorAction); if ($result instanceof Response) { $response = $result; } else { - $response = new Response; - $response->content = $result; + $response->setContent($result); } - } else { - $response = new Response; + } elseif ($response->format === \yii\web\Response::FORMAT_HTML) { if (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] === 'XMLHttpRequest') { // AJAX request - $response->content = Yii::$app->renderException($exception); + $response->setContent(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 if (YII_DEBUG) { ini_set('display_errors', 1); } - $file = $useErrorView ? $this->errorView : $this->exceptionView; - $response->content = $this->renderFile($file, array( + $response->setContent($this->renderFile($file, array( 'exception' => $exception, - )); + ))); + } + } else { + if ($exception instanceof Exception) { + $content = $exception->toArray(); + } else { + $content = 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/Exception.php b/framework/yii/base/Exception.php index 956f17b..fe497a7 100644 --- a/framework/yii/base/Exception.php +++ b/framework/yii/base/Exception.php @@ -13,7 +13,7 @@ namespace yii\base; * @author Qiang Xue * @since 2.0 */ -class Exception extends \Exception +class Exception extends \Exception implements Arrayable { /** * @return string the user-friendly name of this exception @@ -22,4 +22,18 @@ class Exception extends \Exception { return \Yii::t('yii', 'Exception'); } + + /** + * Returns the array representation of this object. + * @return array the array representation of this object. + */ + public function toArray() + { + return array( + 'type' => get_class($this), + 'name' => $this->getName(), + 'message' => $this->getMessage(), + 'code' => $this->getCode(), + ); + } } diff --git a/framework/yii/base/Jsonable.php b/framework/yii/base/Jsonable.php deleted file mode 100644 index e9425a6..0000000 --- a/framework/yii/base/Jsonable.php +++ /dev/null @@ -1,22 +0,0 @@ - - * @since 2.0 - */ -interface Jsonable -{ - /** - * @return string the JSON representation of this object - */ - public function toJson(); -} diff --git a/framework/yii/base/Model.php b/framework/yii/base/Model.php index b9b7d2d..9a3e08d 100644 --- a/framework/yii/base/Model.php +++ b/framework/yii/base/Model.php @@ -7,10 +7,10 @@ namespace yii\base; +use Yii; use ArrayObject; use ArrayIterator; use yii\helpers\Inflector; -use yii\helpers\Json; use yii\validators\RequiredValidator; use yii\validators\Validator; @@ -42,7 +42,7 @@ use yii\validators\Validator; * @author Qiang Xue * @since 2.0 */ -class Model extends Component implements \IteratorAggregate, \ArrayAccess, Jsonable +class Model extends Component implements \IteratorAggregate, \ArrayAccess { /** * @event ModelEvent an event raised at the beginning of [[validate()]]. You may set @@ -639,13 +639,13 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess, Jsona } /** - * Returns the JSON representation of this object. + * Converts the object into an array. * The default implementation will return [[attributes]]. - * @return string the JSON representation of this object. + * @return array the array representation of the object */ - public function toJson() + public function toArray() { - return Json::encode($this->getAttributes()); + return $this->getAttributes(); } /** diff --git a/framework/yii/base/Object.php b/framework/yii/base/Object.php index 79c1f7b..946be85 100644 --- a/framework/yii/base/Object.php +++ b/framework/yii/base/Object.php @@ -8,14 +8,13 @@ namespace yii\base; use Yii; -use yii\helpers\Json; /** * @include @yii/base/Object.md * @author Qiang Xue * @since 2.0 */ -class Object implements Jsonable +class Object implements Arrayable { /** * @return string the fully qualified name of this class. @@ -221,12 +220,13 @@ class Object implements Jsonable } /** - * Returns the JSON representation of this object. - * The default implementation will return all public member variables. - * @return string the JSON representation of this object. + * Converts the object into an array. + * The default implementation will return all public property values as an array. + * However, if the object is traversable, it will return the data obtained by the data iteration. + * @return array the array representation of the object */ - public function toJson() + public function toArray() { - return Json::encode(Yii::getObjectVars($this)); + return Yii::toArray($this, false); } } diff --git a/framework/yii/base/Response.php b/framework/yii/base/Response.php index 83fef93..886774f 100644 --- a/framework/yii/base/Response.php +++ b/framework/yii/base/Response.php @@ -14,12 +14,24 @@ 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. */ public $exitStatus = 0; + /** + * 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/helpers/base/Json.php b/framework/yii/helpers/base/Json.php index 41d5bf0..2d95017 100644 --- a/framework/yii/helpers/base/Json.php +++ b/framework/yii/helpers/base/Json.php @@ -8,7 +8,7 @@ namespace yii\helpers\base; use yii\base\InvalidParamException; -use yii\base\Jsonable; +use yii\base\Arrayable; use yii\web\JsExpression; /** @@ -91,9 +91,8 @@ class Json $token = '!{[' . count($expressions) . ']}!'; $expressions['"' . $token . '"'] = $data->expression; return $token; - } elseif ($data instanceof Jsonable) { - return $data->toJson(); } else { + $data = $data instanceof Arrayable ? $data->toArray() : get_object_vars($data); $result = array(); foreach ($data as $key => $value) { if (is_array($value) || is_object($value)) { diff --git a/framework/yii/web/Application.php b/framework/yii/web/Application.php index 54df2a7..aa03a46 100644 --- a/framework/yii/web/Application.php +++ b/framework/yii/web/Application.php @@ -71,7 +71,9 @@ class Application extends \yii\base\Application return $result; } else { $response = $this->getResponse(); - $response->content = $result; + if ($result !== null) { + $response->setContent($result); + } return $response; } } catch (InvalidRouteException $e) { diff --git a/framework/yii/web/HttpException.php b/framework/yii/web/HttpException.php index 384a5b4..c0a23db 100644 --- a/framework/yii/web/HttpException.php +++ b/framework/yii/web/HttpException.php @@ -51,4 +51,15 @@ class HttpException extends UserException return 'Error'; } } + + /** + * Returns the array representation of this object. + * @return array the array representation of this object. + */ + public function toArray() + { + $array = parent::toArray(); + $array['status'] = $this->statusCode; + return $array; + } } diff --git a/framework/yii/web/Response.php b/framework/yii/web/Response.php index 696aa1b..32a0f85 100644 --- a/framework/yii/web/Response.php +++ b/framework/yii/web/Response.php @@ -8,6 +8,7 @@ namespace yii\web; use Yii; +use yii\base\InvalidConfigException; use yii\web\HttpException; use yii\base\InvalidParamException; use yii\helpers\FileHelper; @@ -23,6 +24,21 @@ use yii\helpers\StringHelper; */ class Response extends \yii\base\Response { + const FORMAT_RAW = 'raw'; + const FORMAT_HTML = 'html'; + const FORMAT_JSON = 'json'; + const FORMAT_JSONP = 'jsonp'; + const FORMAT_XML = 'xml'; + + /** + * @var string the response format. + */ + public $format = self::FORMAT_HTML; + /** + * @var string the charset of the text response. If not set, it will use + * the value of [[Application::charset]]. + */ + public $charset; /** * @var integer the HTTP status code that should be used when redirecting in AJAX mode. * This is used by [[redirect()]]. A 2xx code should normally be used for this purpose @@ -33,10 +49,6 @@ class Response extends \yii\base\Response /** * @var string */ - public $content; - /** - * @var string - */ public $statusText; /** * @var string the version of the HTTP protocol to use. If not set, it will be determined via `$_SERVER['SERVER_PROTOCOL']`, @@ -133,6 +145,9 @@ class Response extends \yii\base\Response $this->version = '1.1'; } } + if ($this->charset === null) { + $this->charset = Yii::$app->charset; + } } /** @@ -172,7 +187,7 @@ class Response extends \yii\base\Response public function renderJson($data) { $this->getHeaders()->set('Content-Type', 'application/json'); - $this->content = Json::encode($data); + $this->setContent(Json::encode($data)); $this->send(); } @@ -180,16 +195,16 @@ class Response extends \yii\base\Response { $this->getHeaders()->set('Content-Type', 'text/javascript'); $data = Json::encode($data); - $this->content = "$callbackName($data);"; + $this->setContent("$callbackName($data);"); $this->send(); } /** * Sends the response to the client. - * @return boolean true if the response was sent */ public function send() { + parent::send(); $this->sendHeaders(); $this->sendContent(); $this->clear(); @@ -199,8 +214,8 @@ class Response extends \yii\base\Response { $this->_headers = null; $this->_statusCode = null; + $this->_content = null; $this->statusText = null; - $this->content = null; } /** @@ -253,7 +268,7 @@ class Response extends \yii\base\Response */ protected function sendContent() { - echo $this->content; + echo $this->getContent(); } /** @@ -304,10 +319,10 @@ class Response extends \yii\base\Response if ($begin !=0 || $end != $contentLength - 1) { $this->setStatusCode(206); $headers->set('Content-Range', "bytes $begin-$end/$contentLength"); - $this->content = StringHelper::substr($content, $begin, $end - $begin + 1); + $this->setContent(StringHelper::substr($content, $begin, $end - $begin + 1), self::FORMAT_RAW); } else { $this->setStatusCode(200); - $this->content = $content; + $this->setContent($content, self::FORMAT_RAW); } $this->send(); @@ -628,4 +643,44 @@ class Response extends \yii\base\Response { return in_array($this->getStatusCode(), array(201, 204, 304)); } + + private $_content; + + public function getContent() + { + return $this->_content; + } + + public function setContent($value, $format = null) + { + if ($format !== null) { + $this->format = $format; + } + $this->_content = $this->formatContent($value, $format); + } + + protected function formatContent($data, $format) + { + switch ($this->format) { + case self::FORMAT_RAW: + return $data; + case self::FORMAT_HTML: + $this->getHeaders()->setDefault('Content-Type', 'text/html; charset=' . $this->charset); + return $data; + case self::FORMAT_JSON: + $this->getHeaders()->set('Content-Type', 'application/json'); + return Json::encode($data); + case self::FORMAT_JSONP: + $this->getHeaders()->set('Content-Type', 'text/javascript'); + if (is_array($data) && isset($data['data'], $data['callback'])) { + return sprintf('%s(%s);', $data['callback'], Json::encode($data['data'])); + } 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: + // todo + default: + throw new InvalidConfigException("Unsupported response format: $format"); + } + } }