From 5a8e4b648bf47ad8f3885564a100912a3ff2148f Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Sat, 15 Jun 2013 15:19:00 -0400 Subject: [PATCH] Added XmlResponseFormatter. --- framework/yii/base/Controller.php | 2 +- framework/yii/web/Response.php | 52 +++++++-- framework/yii/web/ResponseFormatter.php | 25 +++++ framework/yii/web/XmlResponseFormatter.php | 99 +++++++++++++++++ .../framework/web/XmlResponseFormatterTest.php | 117 +++++++++++++++++++++ 5 files changed, 288 insertions(+), 7 deletions(-) create mode 100644 framework/yii/web/ResponseFormatter.php create mode 100644 framework/yii/web/XmlResponseFormatter.php create mode 100644 tests/unit/framework/web/XmlResponseFormatterTest.php diff --git a/framework/yii/base/Controller.php b/framework/yii/base/Controller.php index c22b584..5b8debb 100644 --- a/framework/yii/base/Controller.php +++ b/framework/yii/base/Controller.php @@ -132,7 +132,7 @@ class Controller extends Component * the route will start from the application; otherwise, it will start from the parent module of this controller. * @param string $route the route to be handled, e.g., 'view', 'comment/view', '/admin/comment/view'. * @param array $params the parameters to be passed to the action. - * @return integer the status code returned by the action execution. 0 means normal, and other values mean abnormal. + * @return mixed the result of the action * @see runAction * @see forward */ diff --git a/framework/yii/web/Response.php b/framework/yii/web/Response.php index ad9d29f..4783ffd 100644 --- a/framework/yii/web/Response.php +++ b/framework/yii/web/Response.php @@ -34,6 +34,12 @@ class Response extends \yii\base\Response */ 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. + */ + public $formatters; + /** * @var string the charset of the text response. If not set, it will use * the value of [[Application::charset]]. */ @@ -134,7 +140,9 @@ class Response extends \yii\base\Response */ private $_headers; - + /** + * Initializes this component. + */ public function init() { if ($this->version === null) { @@ -157,6 +165,13 @@ class Response extends \yii\base\Response return $this->_statusCode; } + /** + * Sets the response status code. + * This method will set the corresponding status text if `$text` is null. + * @param integer $value the status code + * @param string $text the status text. If not set, it will be set automatically based on the status code. + * @throws InvalidParamException if the status code is invalid. + */ public function setStatusCode($value, $text = null) { $this->_statusCode = (int)$value; @@ -194,9 +209,13 @@ class Response extends \yii\base\Response $this->clear(); } + /** + * Clears the headers, cookies, content, status code of the response. + */ public function clear() { $this->_headers = null; + $this->_cookies = null; $this->_statusCode = null; $this->_content = null; $this->statusText = null; @@ -643,8 +662,9 @@ class Response extends \yii\base\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 following formats are - * supported by the default implementation: + * @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. @@ -656,8 +676,8 @@ class Response extends \yii\base\Response * 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, and the "Content-Type" - * header will be set as "application/xml" if no previous "Content-Type" header is set. + * - [[FORMAT_XML]]: the data will be converted into XML format. Please refer to [[XmlResponseFormatter]] + * for more details. */ public function setContent($data, $format = null) { @@ -667,8 +687,28 @@ class Response extends \yii\base\Response $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. + * @throws InvalidParamException if `$format` is not supported + * @throws InvalidConfigException if the formatter for the specified format is invalid + */ protected function formatContent($data, $format) { + if (isset($this->formatters[$format])) { + $formatter = $this->formatters[$format]; + if (!is_object($formatter)) { + $formatter = Yii::createObject($formatter); + } + if ($formatter instanceof ResponseFormatter) { + return $formatter->format($this, $data); + } else { + throw new InvalidConfigException("The '$format' response formatter is invalid. It must implement the ResponseFormatter interface."); + } + } + switch ($this->format) { case self::FORMAT_RAW: return $data; @@ -686,7 +726,7 @@ class Response extends \yii\base\Response 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 + return Yii::createObject('yii\web\XmlResponseFormatter')->format($this, $data); default: throw new InvalidConfigException("Unsupported response format: $format"); } diff --git a/framework/yii/web/ResponseFormatter.php b/framework/yii/web/ResponseFormatter.php new file mode 100644 index 0000000..0e9af47 --- /dev/null +++ b/framework/yii/web/ResponseFormatter.php @@ -0,0 +1,25 @@ + + * @since 2.0 + */ +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 + */ + function format($response, $data); +} diff --git a/framework/yii/web/XmlResponseFormatter.php b/framework/yii/web/XmlResponseFormatter.php new file mode 100644 index 0000000..32b7213 --- /dev/null +++ b/framework/yii/web/XmlResponseFormatter.php @@ -0,0 +1,99 @@ + + * @since 2.0 + */ +class XmlResponseFormatter extends Component implements ResponseFormatter +{ + /** + * @var string the Content-Type header for the response + */ + public $contentType = 'application/xml'; + /** + * @var string the XML version + */ + public $version = '1.0'; + /** + * @var string the XML encoding. If not set, it will use the value of [[Response::charset]]. + */ + public $encoding; + /** + * @var string the name of the root element. + */ + public $rootTag = 'response'; + /** + * @var string the name of the elements that represent the array elements with numeric keys. + */ + 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 + */ + public function format($response, $data) + { + $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(); + } + + /** + * @param DOMElement $element + * @param mixed $data + */ + protected function buildXml($element, $data) + { + if (is_object($data)) { + $child = new DOMElement(StringHelper::basename(get_class($data))); + $element->appendChild($child); + if ($data instanceof Arrayable) { + $this->buildXml($child, $data->toArray()); + } else { + $array = array(); + foreach ($data as $name => $value) { + $array[$name] = $value; + } + $this->buildXml($child, $array); + } + } elseif (is_array($data)) { + foreach ($data as $name => $value) { + if (is_int($name) && is_object($value)) { + $this->buildXml($element, $value); + } elseif (is_array($value) || is_object($value)) { + $child = new DOMElement(is_int($name) ? $this->itemTag : $name); + $element->appendChild($child); + $this->buildXml($child, $value); + } else { + $child = new DOMElement(is_int($name) ? $this->itemTag : $name); + $element->appendChild($child); + $child->appendChild(new DOMText((string)$value)); + } + } + } else { + $element->appendChild(new DOMText((string)$data)); + } + } +} diff --git a/tests/unit/framework/web/XmlResponseFormatterTest.php b/tests/unit/framework/web/XmlResponseFormatterTest.php new file mode 100644 index 0000000..f39d474 --- /dev/null +++ b/tests/unit/framework/web/XmlResponseFormatterTest.php @@ -0,0 +1,117 @@ +id = $id; + $this->title = $title; + } +} + +/** + * @author Qiang Xue + * @since 2.0 + */ +class XmlResponseFormatterTest extends \yiiunit\TestCase +{ + /** + * @var Response + */ + public $response; + /** + * @var XmlResponseFormatter + */ + public $formatter; + + protected function setUp() + { + $this->mockApplication(); + $this->response = new Response; + $this->formatter = new XmlResponseFormatter; + } + + public function testFormatScalars() + { + $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)); + } + + public function testFormatArrays() + { + $head = "\n"; + + $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 testFormatObjects() + { + $head = "\n"; + + $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'), + ))); + } +}