4
4
5
5
namespace Yiisoft \DataResponse \Formatter ;
6
6
7
- use function is_array ;
8
- use function is_float ;
9
- use function is_scalar ;
7
+ use DOMDocument ;
8
+ use DOMElement ;
9
+ use DOMException ;
10
+ use DOMText ;
10
11
use Psr \Http \Message \ResponseInterface ;
11
12
use Traversable ;
12
- use Yiisoft \DataResponse \DataResponse ;
13
- use Yiisoft \DataResponse \DataResponseFormatterInterface ;
14
13
use Yiisoft \DataResponse \HasContentTypeTrait ;
15
-
16
14
use Yiisoft \Http \Header ;
17
- use Yiisoft \Serializer \XmlSerializer ;
18
15
use Yiisoft \Strings \NumericHelper ;
16
+ use Yiisoft \Strings \StringHelper ;
17
+ use Yiisoft \DataResponse \DataResponse ;
18
+ use Yiisoft \DataResponse \DataResponseFormatterInterface ;
19
+
20
+ use function get_class ;
21
+ use function is_array ;
22
+ use function is_float ;
23
+ use function is_int ;
24
+ use function is_object ;
25
+ use function iterator_to_array ;
26
+ use function strpos ;
19
27
20
28
final class XmlDataResponseFormatter implements DataResponseFormatterInterface
21
29
{
22
30
use HasContentTypeTrait;
23
31
32
+ private const DEFAULT_ITEM_TAG_NAME = 'item ' ;
33
+ private const KEY_ATTRIBUTE_NAME = 'key ' ;
34
+
24
35
/**
25
36
* @var string The Content-Type header for the response.
26
37
*/
@@ -37,16 +48,31 @@ final class XmlDataResponseFormatter implements DataResponseFormatterInterface
37
48
private string $ encoding = 'UTF-8 ' ;
38
49
39
50
/**
40
- * @var string The name of the root element. If set to false, null or is empty then no root tag should be added.
51
+ * @var string The name of the root element. If an empty value is set, the root tag should not be added.
41
52
*/
42
53
private string $ rootTag = 'response ' ;
43
54
55
+ /**
56
+ * @var bool If true, the object tags will be formed from the class names,
57
+ * otherwise the {@see DEFAULT_ITEM_TAG_NAME} value will be used.
58
+ */
59
+ private bool $ useObjectTags = true ;
60
+
44
61
public function format (DataResponse $ dataResponse ): ResponseInterface
45
62
{
46
- $ serializer = new XmlSerializer ($ this ->rootTag , $ this ->version , $ this ->encoding );
47
-
48
63
if ($ dataResponse ->hasData ()) {
49
- $ content = $ serializer ->serialize ($ this ->formatData ($ dataResponse ->getData ()));
64
+ $ dom = new DOMDocument ($ this ->version , $ this ->encoding );
65
+ $ data = $ dataResponse ->getData ();
66
+
67
+ if (!empty ($ this ->rootTag )) {
68
+ $ root = new DOMElement ($ this ->rootTag );
69
+ $ dom ->appendChild ($ root );
70
+ $ this ->buildXml ($ dom , $ root , $ data );
71
+ } else {
72
+ $ this ->buildXml ($ dom , $ dom , $ data );
73
+ }
74
+
75
+ $ content = $ dom ->saveXML ();
50
76
}
51
77
52
78
$ response = $ dataResponse ->getResponse ();
@@ -55,20 +81,42 @@ public function format(DataResponse $dataResponse): ResponseInterface
55
81
return $ response ->withHeader (Header::CONTENT_TYPE , $ this ->contentType . '; ' . $ this ->encoding );
56
82
}
57
83
84
+ /**
85
+ * Returns a new instance with the specified version.
86
+ *
87
+ * @param string $version The XML version. Default is "1.0".
88
+ *
89
+ * @return self
90
+ */
58
91
public function withVersion (string $ version ): self
59
92
{
60
93
$ new = clone $ this ;
61
94
$ new ->version = $ version ;
62
95
return $ new ;
63
96
}
64
97
98
+ /**
99
+ * Returns a new instance with the specified encoding.
100
+ *
101
+ * @param string $encoding The XML encoding. Default is "UTF-8".
102
+ *
103
+ * @return self
104
+ */
65
105
public function withEncoding (string $ encoding ): self
66
106
{
67
107
$ new = clone $ this ;
68
108
$ new ->encoding = $ encoding ;
69
109
return $ new ;
70
110
}
71
111
112
+ /**
113
+ * Returns a new instance with the specified root tag.
114
+ *
115
+ * @param string $rootTag The name of the root element. Default is "response".
116
+ * If an empty value is set, the root tag should not be added.
117
+ *
118
+ * @return self
119
+ */
72
120
public function withRootTag (string $ rootTag ): self
73
121
{
74
122
$ new = clone $ this ;
@@ -77,41 +125,149 @@ public function withRootTag(string $rootTag): self
77
125
}
78
126
79
127
/**
80
- * Pre-formats the data before serialization .
128
+ * Returns a new instance with the specified value, whether to use class names as tags or not .
81
129
*
82
- * @param mixed $data to format.
130
+ * @param bool $useObjectTags If true, the object tags will be formed from the class names,
131
+ * otherwise the {@see DEFAULT_ITEM_TAG_NAME} value will be used. Default is true.
83
132
*
84
- * @return mixed formatted data.
133
+ * @return self
85
134
*/
86
- private function formatData ( $ data )
135
+ public function withUseObjectTags ( bool $ useObjectTags ): self
87
136
{
88
- if (is_scalar ($ data )) {
89
- return $ this ->formatScalarValue ($ data );
90
- }
137
+ $ new = clone $ this ;
138
+ $ new ->useObjectTags = $ useObjectTags ;
139
+ return $ new ;
140
+ }
91
141
92
- if (!is_array ($ data ) && !($ data instanceof Traversable)) {
93
- return $ data ;
142
+ /**
143
+ * Builds the data to use in XML.
144
+ *
145
+ * @param DOMDocument $dom The root DOM document.
146
+ * @param DOMDocument|DOMElement $element The current DOM element being processed.
147
+ * @param mixed $data Data for building XML.
148
+ */
149
+ private function buildXml (DOMDocument $ dom , $ element , $ data ): void
150
+ {
151
+ if (empty ($ data )) {
152
+ return ;
94
153
}
95
154
96
- $ formattedData = [];
155
+ if (is_array ($ data ) || $ data instanceof Traversable) {
156
+ $ data = $ data instanceof Traversable ? iterator_to_array ($ data ) : $ data ;
157
+ $ dataSize = count ($ data );
158
+
159
+ foreach ($ data as $ name => $ value ) {
160
+ if (is_int ($ name ) && is_object ($ value ) && !($ value instanceof Traversable)) {
161
+ $ this ->buildObject ($ dom , $ element , $ value , $ dataSize > 1 ? $ name : null );
162
+ continue ;
163
+ }
164
+
165
+ $ child = $ this ->safeCreateDomElement ($ dom , $ name );
97
166
98
- foreach ($ data as $ key => $ value ) {
99
- if (is_scalar ($ value )) {
100
- $ formattedData [$ key ] = $ this ->formatScalarValue ($ value );
167
+ if ($ dataSize > 1 && is_int ($ name )) {
168
+ $ child ->setAttribute (self ::KEY_ATTRIBUTE_NAME , (string ) $ name );
169
+ }
170
+
171
+ $ element ->appendChild ($ child );
172
+
173
+ if (is_array ($ value ) || is_object ($ value )) {
174
+ $ this ->buildXml ($ dom , $ child , $ value );
175
+ continue ;
176
+ }
177
+
178
+ $ this ->setScalarValueToDomElement ($ child , $ value );
101
179
}
102
180
103
- $ formattedData [$ key ] = $ this ->formatData ($ value );
181
+ return ;
182
+ }
183
+
184
+ if (is_object ($ data )) {
185
+ $ this ->buildObject ($ dom , $ element , $ data );
186
+ return ;
187
+ }
188
+
189
+ $ this ->setScalarValueToDomElement ($ element , $ data );
190
+ }
191
+
192
+ /**
193
+ * Builds the object to use in XML.
194
+ *
195
+ * @param DOMDocument $dom The root DOM document.
196
+ * @param DOMDocument|DOMElement $element The current DOM element being processed.
197
+ * @param object $object To build.
198
+ * @param int|null $key Key attribute value.
199
+ */
200
+ private function buildObject (DOMDocument $ dom , $ element , object $ object , int $ key = null ): void
201
+ {
202
+ if ($ this ->useObjectTags ) {
203
+ $ class = get_class ($ object );
204
+ $ class = strpos ($ class , 'class@anonymous ' ) === false ? StringHelper::baseName ($ class ) : 'AnonymousClass ' ;
205
+ }
206
+
207
+ $ child = $ this ->safeCreateDomElement ($ dom , $ class ?? self ::DEFAULT_ITEM_TAG_NAME );
208
+
209
+ if ($ key !== null ) {
210
+ $ child ->setAttribute (self ::KEY_ATTRIBUTE_NAME , (string ) $ key );
104
211
}
105
212
106
- return $ formattedData ;
213
+ $ element ->appendChild ($ child );
214
+ $ array = [];
215
+
216
+ foreach ($ object as $ property => $ value ) {
217
+ $ array [$ property ] = $ value ;
218
+ }
219
+
220
+ $ this ->buildXml ($ dom , $ child , $ array );
221
+ }
222
+
223
+ /**
224
+ * Safely creates a DOMElement instance by the specified tag name, if the tag name is not empty,
225
+ * not integer, and valid. Otherwise the {@see DEFAULT_ITEM_TAG_NAME} value will be used.
226
+ *
227
+ * @see http://stackoverflow.com/questions/2519845/how-to-check-if-string-is-a-valid-xml-element-name/2519943#2519943
228
+ *
229
+ * @param DOMDocument $dom The root DOM document.
230
+ * @param int|string|null $tagName The tag name.
231
+ *
232
+ * @return DOMElement
233
+ */
234
+ private function safeCreateDomElement (DOMDocument $ dom , $ tagName ): DOMElement
235
+ {
236
+ if (empty ($ tagName ) || is_int ($ tagName )) {
237
+ return $ dom ->createElement (self ::DEFAULT_ITEM_TAG_NAME );
238
+ }
239
+
240
+ try {
241
+ if (!$ element = $ dom ->createElement ($ tagName )) {
242
+ throw new DOMException ();
243
+ }
244
+ return $ element ;
245
+ } catch (DOMException $ e ) {
246
+ return $ dom ->createElement (self ::DEFAULT_ITEM_TAG_NAME );
247
+ }
248
+ }
249
+
250
+ /**
251
+ * Sets the scalar value to Dom Element instance if the value is not empty.
252
+ *
253
+ * @param DOMDocument|DOMElement $element The current DOM element being processed.
254
+ * @param bool|float|int|string|null $value
255
+ */
256
+ private function setScalarValueToDomElement ($ element , $ value ): void
257
+ {
258
+ $ value = $ this ->formatScalarValue ($ value );
259
+
260
+ if ($ value !== '' ) {
261
+ $ element ->appendChild (new DOMText ($ value ));
262
+ }
107
263
}
108
264
109
265
/**
110
266
* Formats scalar value to use in XML node.
111
267
*
112
- * @param bool|float|int|string $value to format.
268
+ * @param bool|float|int|string|null $value To format.
113
269
*
114
- * @return string string representation of the value.
270
+ * @return string The string representation of the value.
115
271
*/
116
272
private function formatScalarValue ($ value ): string
117
273
{
0 commit comments