XmlResponseFormatter.php 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209
  1. <?php
  2. /**
  3. * @link https://www.yiiframework.com/
  4. * @copyright Copyright (c) 2008 Yii Software LLC
  5. * @license https://www.yiiframework.com/license/
  6. */
  7. namespace yii\web;
  8. use DOMDocument;
  9. use DOMElement;
  10. use DOMException;
  11. use yii\base\Arrayable;
  12. use yii\base\Component;
  13. use yii\helpers\StringHelper;
  14. /**
  15. * XmlResponseFormatter formats the given data into an XML response content.
  16. *
  17. * It is used by [[Response]] to format response data.
  18. *
  19. * @author Qiang Xue <qiang.xue@gmail.com>
  20. * @since 2.0
  21. */
  22. class XmlResponseFormatter extends Component implements ResponseFormatterInterface
  23. {
  24. /**
  25. * @var string the Content-Type header for the response
  26. */
  27. public $contentType = 'application/xml';
  28. /**
  29. * @var string the XML version
  30. */
  31. public $version = '1.0';
  32. /**
  33. * @var string|null the XML encoding. If not set, it will use the value of [[Response::charset]].
  34. */
  35. public $encoding;
  36. /**
  37. * @var string|string[]|null|false the name of the root element. If set to false, null or is empty then no root tag
  38. * should be added.
  39. *
  40. * Since 2.0.44 URI namespace could be specified by passing `[namespace, tag name]` array.
  41. */
  42. public $rootTag = 'response';
  43. /**
  44. * @var string the name of the elements that represent the array elements with numeric keys.
  45. */
  46. public $itemTag = 'item';
  47. /**
  48. * @var bool whether to interpret objects implementing the [[\Traversable]] interface as arrays.
  49. * Defaults to `true`.
  50. * @since 2.0.7
  51. */
  52. public $useTraversableAsArray = true;
  53. /**
  54. * @var bool if object class names should be used as tag names
  55. * @since 2.0.11
  56. */
  57. public $useObjectTags = true;
  58. /**
  59. * @var bool if true, converts object tags to lowercase, `$useObjectTags` must be enabled
  60. * @since 2.0.43
  61. */
  62. public $objectTagToLowercase = false;
  63. /**
  64. * @var DOMDocument the XML document, serves as the root of the document tree
  65. * @since 2.0.43
  66. */
  67. protected $dom;
  68. /**
  69. * Formats the specified response.
  70. *
  71. * @param Response $response the response to be formatted.
  72. */
  73. public function format($response)
  74. {
  75. $charset = $this->encoding === null ? $response->charset : $this->encoding;
  76. if (stripos($this->contentType, 'charset') === false) {
  77. $this->contentType .= '; charset=' . $charset;
  78. }
  79. $response->getHeaders()->set('Content-Type', $this->contentType);
  80. if ($response->data !== null) {
  81. $this->dom = new DOMDocument($this->version, $charset);
  82. if (!empty($this->rootTag)) {
  83. if (is_array($this->rootTag)) {
  84. $root = $this->dom->createElementNS($this->rootTag[0], $this->rootTag[1]);
  85. } else {
  86. $root = $this->dom->createElement($this->rootTag);
  87. }
  88. $this->dom->appendChild($root);
  89. $this->buildXml($root, $response->data);
  90. } else {
  91. $this->buildXml($this->dom, $response->data);
  92. }
  93. $response->content = $this->dom->saveXML();
  94. }
  95. }
  96. /**
  97. * Recursively adds data to XML document.
  98. *
  99. * @param DOMElement|DOMDocument $element current element
  100. * @param mixed $data content of the current element
  101. */
  102. protected function buildXml($element, $data)
  103. {
  104. if (
  105. is_array($data) ||
  106. ($data instanceof \Traversable && $this->useTraversableAsArray && !$data instanceof Arrayable)
  107. ) {
  108. foreach ($data as $name => $value) {
  109. if (is_int($name) && is_object($value)) {
  110. $this->buildXml($element, $value);
  111. } elseif (is_array($value) || is_object($value)) {
  112. $child = $this->dom->createElement($this->getValidXmlElementName($name));
  113. $element->appendChild($child);
  114. $this->buildXml($child, $value);
  115. } else {
  116. $child = $this->dom->createElement($this->getValidXmlElementName($name));
  117. $child->appendChild($this->dom->createTextNode($this->formatScalarValue($value)));
  118. $element->appendChild($child);
  119. }
  120. }
  121. } elseif (is_object($data)) {
  122. if ($this->useObjectTags) {
  123. $name = StringHelper::basename(get_class($data));
  124. if ($this->objectTagToLowercase) {
  125. $name = strtolower($name);
  126. }
  127. $child = $this->dom->createElement($name);
  128. $element->appendChild($child);
  129. } else {
  130. $child = $element;
  131. }
  132. if ($data instanceof Arrayable) {
  133. $this->buildXml($child, $data->toArray());
  134. } else {
  135. $array = [];
  136. foreach ($data as $name => $value) {
  137. $array[$name] = $value;
  138. }
  139. $this->buildXml($child, $array);
  140. }
  141. } else {
  142. $element->appendChild($this->dom->createTextNode($this->formatScalarValue($data)));
  143. }
  144. }
  145. /**
  146. * Formats scalar value to use in XML text node.
  147. *
  148. * @param int|string|bool|float $value a scalar value.
  149. * @return string string representation of the value.
  150. * @since 2.0.11
  151. */
  152. protected function formatScalarValue($value)
  153. {
  154. if ($value === true) {
  155. return 'true';
  156. }
  157. if ($value === false) {
  158. return 'false';
  159. }
  160. if (is_float($value)) {
  161. return StringHelper::floatToString($value);
  162. }
  163. return (string) $value;
  164. }
  165. /**
  166. * Returns element name ready to be used in DOMElement if
  167. * name is not empty, is not int and is valid.
  168. *
  169. * Falls back to [[itemTag]] otherwise.
  170. *
  171. * @param mixed $name the original name
  172. * @return string
  173. * @since 2.0.12
  174. */
  175. protected function getValidXmlElementName($name)
  176. {
  177. if (empty($name) || is_int($name) || !$this->isValidXmlName($name)) {
  178. return $this->itemTag;
  179. }
  180. return $name;
  181. }
  182. /**
  183. * Checks if name is valid to be used in XML.
  184. *
  185. * @param mixed $name the name to test
  186. * @return bool
  187. * @see https://stackoverflow.com/questions/2519845/how-to-check-if-string-is-a-valid-xml-element-name/2519943#2519943
  188. * @since 2.0.12
  189. */
  190. protected function isValidXmlName($name)
  191. {
  192. try {
  193. return $this->dom->createElement($name) !== false;
  194. } catch (DOMException $e) {
  195. return false;
  196. }
  197. }
  198. }