|
1 <?php |
|
2 |
|
3 namespace Symfony\Component\Serializer\Encoder; |
|
4 |
|
5 use Symfony\Component\Serializer\SerializerInterface; |
|
6 use Symfony\Component\Serializer\Exception\UnexpectedValueException; |
|
7 |
|
8 /* |
|
9 * This file is part of the Symfony framework. |
|
10 * |
|
11 * (c) Fabien Potencier <fabien@symfony.com> |
|
12 * |
|
13 * This source file is subject to the MIT license that is bundled |
|
14 * with this source code in the file LICENSE. |
|
15 */ |
|
16 |
|
17 /** |
|
18 * Encodes XML data |
|
19 * |
|
20 * @author Jordi Boggiano <j.boggiano@seld.be> |
|
21 * @author John Wards <jwards@whiteoctober.co.uk> |
|
22 * @author Fabian Vogler <fabian@equivalence.ch> |
|
23 */ |
|
24 class XmlEncoder extends SerializerAwareEncoder implements EncoderInterface, DecoderInterface, NormalizationAwareInterface |
|
25 { |
|
26 private $dom; |
|
27 private $format; |
|
28 private $rootNodeName = 'response'; |
|
29 |
|
30 /** |
|
31 * {@inheritdoc} |
|
32 */ |
|
33 public function encode($data, $format) |
|
34 { |
|
35 if ($data instanceof \DOMDocument) { |
|
36 return $data->saveXML(); |
|
37 } |
|
38 |
|
39 $this->dom = new \DOMDocument(); |
|
40 $this->format = $format; |
|
41 |
|
42 if (null !== $data && !is_scalar($data)) { |
|
43 $root = $this->dom->createElement($this->rootNodeName); |
|
44 $this->dom->appendChild($root); |
|
45 $this->buildXml($root, $data); |
|
46 } else { |
|
47 $this->appendNode($this->dom, $data, $this->rootNodeName); |
|
48 } |
|
49 |
|
50 return $this->dom->saveXML(); |
|
51 } |
|
52 |
|
53 /** |
|
54 * {@inheritdoc} |
|
55 */ |
|
56 public function decode($data, $format) |
|
57 { |
|
58 $xml = simplexml_load_string($data); |
|
59 if (!$xml->count()) { |
|
60 if (!$xml->attributes()) { |
|
61 return (string) $xml; |
|
62 } |
|
63 $data = array(); |
|
64 foreach ($xml->attributes() as $attrkey => $attr) { |
|
65 $data['@'.$attrkey] = (string) $attr; |
|
66 } |
|
67 $data['#'] = (string) $xml; |
|
68 |
|
69 return $data; |
|
70 } |
|
71 |
|
72 return $this->parseXml($xml); |
|
73 } |
|
74 |
|
75 /** |
|
76 * Sets the root node name |
|
77 * @param string $name root node name |
|
78 */ |
|
79 public function setRootNodeName($name) |
|
80 { |
|
81 $this->rootNodeName = $name; |
|
82 } |
|
83 |
|
84 /** |
|
85 * Returns the root node name |
|
86 * @return string |
|
87 */ |
|
88 public function getRootNodeName() |
|
89 { |
|
90 return $this->rootNodeName; |
|
91 } |
|
92 |
|
93 /** |
|
94 * @param DOMNode $node |
|
95 * @param string $val |
|
96 * @return Boolean |
|
97 */ |
|
98 final protected function appendXMLString($node, $val) |
|
99 { |
|
100 if (strlen($val) > 0) { |
|
101 $frag = $this->dom->createDocumentFragment(); |
|
102 $frag->appendXML($val); |
|
103 $node->appendChild($frag); |
|
104 |
|
105 return true; |
|
106 } |
|
107 |
|
108 return false; |
|
109 } |
|
110 |
|
111 /** |
|
112 * @param DOMNode $node |
|
113 * @param string $val |
|
114 * @return Boolean |
|
115 */ |
|
116 final protected function appendText($node, $val) |
|
117 { |
|
118 $nodeText = $this->dom->createTextNode($val); |
|
119 $node->appendChild($nodeText); |
|
120 |
|
121 return true; |
|
122 } |
|
123 |
|
124 /** |
|
125 * @param DOMNode $node |
|
126 * @param string $val |
|
127 * @return Boolean |
|
128 */ |
|
129 final protected function appendCData($node, $val) |
|
130 { |
|
131 $nodeText = $this->dom->createCDATASection($val); |
|
132 $node->appendChild($nodeText); |
|
133 |
|
134 return true; |
|
135 } |
|
136 |
|
137 /** |
|
138 * @param DOMNode $node |
|
139 * @param DOMDocumentFragment $fragment |
|
140 * @return Boolean |
|
141 */ |
|
142 final protected function appendDocumentFragment($node, $fragment) |
|
143 { |
|
144 if ($fragment instanceof \DOMDocumentFragment) { |
|
145 $node->appendChild($fragment); |
|
146 |
|
147 return true; |
|
148 } |
|
149 |
|
150 return false; |
|
151 } |
|
152 |
|
153 /** |
|
154 * Checks the name is a valid xml element name |
|
155 * @param string $name |
|
156 * @return Boolean |
|
157 */ |
|
158 final protected function isElementNameValid($name) |
|
159 { |
|
160 return $name && |
|
161 false === strpos($name, ' ') && |
|
162 preg_match('#^[\pL_][\pL0-9._-]*$#ui', $name); |
|
163 } |
|
164 |
|
165 /** |
|
166 * Parse the input SimpleXmlElement into an array |
|
167 * |
|
168 * @param SimpleXmlElement $node xml to parse |
|
169 * @return array |
|
170 */ |
|
171 private function parseXml($node) |
|
172 { |
|
173 $data = array(); |
|
174 if ($node->attributes()) { |
|
175 foreach ($node->attributes() as $attrkey => $attr) { |
|
176 $data['@'.$attrkey] = (string) $attr; |
|
177 } |
|
178 } |
|
179 foreach ($node->children() as $key => $subnode) { |
|
180 if ($subnode->count()) { |
|
181 $value = $this->parseXml($subnode); |
|
182 } elseif ($subnode->attributes()) { |
|
183 $value = array(); |
|
184 foreach ($subnode->attributes() as $attrkey => $attr) { |
|
185 $value['@'.$attrkey] = (string) $attr; |
|
186 } |
|
187 $value['#'] = (string) $subnode; |
|
188 } else { |
|
189 $value = (string) $subnode; |
|
190 } |
|
191 |
|
192 if ($key === 'item') { |
|
193 if (isset($value['@key'])) { |
|
194 $data[(string)$value['@key']] = $value['#']; |
|
195 } elseif (isset($data['item'])) { |
|
196 $tmp = $data['item']; |
|
197 unset($data['item']); |
|
198 $data[] = $tmp; |
|
199 $data[] = $value; |
|
200 } |
|
201 } elseif (key_exists($key, $data)) { |
|
202 if ((false === is_array($data[$key])) || (false === isset($data[$key][0]))) { |
|
203 $data[$key] = array($data[$key]); |
|
204 } |
|
205 $data[$key][] = $value; |
|
206 } else { |
|
207 $data[$key] = $value; |
|
208 } |
|
209 } |
|
210 |
|
211 return $data; |
|
212 } |
|
213 |
|
214 /** |
|
215 * Parse the data and convert it to DOMElements |
|
216 * |
|
217 * @param DOMNode $parentNode |
|
218 * @param array|object $data data |
|
219 * @return Boolean |
|
220 */ |
|
221 private function buildXml($parentNode, $data) |
|
222 { |
|
223 $append = true; |
|
224 |
|
225 if (is_array($data) || $data instanceof \Traversable) { |
|
226 foreach ($data as $key => $data) { |
|
227 //Ah this is the magic @ attribute types. |
|
228 if (0 === strpos($key, "@") && is_scalar($data) && $this->isElementNameValid($attributeName = substr($key,1))) { |
|
229 $parentNode->setAttribute($attributeName, $data); |
|
230 } elseif ($key === '#') { |
|
231 $append = $this->selectNodeType($parentNode, $data); |
|
232 } elseif (is_array($data) && false === is_numeric($key)) { |
|
233 /** |
|
234 * Is this array fully numeric keys? |
|
235 */ |
|
236 if (ctype_digit(implode('', array_keys($data)))) { |
|
237 /** |
|
238 * Create nodes to append to $parentNode based on the $key of this array |
|
239 * Produces <xml><item>0</item><item>1</item></xml> |
|
240 * From array("item" => array(0,1)); |
|
241 */ |
|
242 foreach ($data as $subData) { |
|
243 $append = $this->appendNode($parentNode, $subData, $key); |
|
244 } |
|
245 } else { |
|
246 $append = $this->appendNode($parentNode, $data, $key); |
|
247 } |
|
248 } elseif (is_numeric($key) || !$this->isElementNameValid($key)) { |
|
249 $append = $this->appendNode($parentNode, $data, "item", $key); |
|
250 } else { |
|
251 $append = $this->appendNode($parentNode, $data, $key); |
|
252 } |
|
253 } |
|
254 |
|
255 return $append; |
|
256 } |
|
257 if (is_object($data)) { |
|
258 $data = $this->serializer->normalize($data, $this->format); |
|
259 if (null !== $data && !is_scalar($data)) { |
|
260 return $this->buildXml($parentNode, $data); |
|
261 } |
|
262 // top level data object was normalized into a scalar |
|
263 if (!$parentNode->parentNode->parentNode) { |
|
264 $root = $parentNode->parentNode; |
|
265 $root->removeChild($parentNode); |
|
266 |
|
267 return $this->appendNode($root, $data, $this->rootNodeName); |
|
268 } |
|
269 |
|
270 return $this->appendNode($parentNode, $data, 'data'); |
|
271 } |
|
272 throw new UnexpectedValueException('An unexpected value could not be serialized: '.var_export($data, true)); |
|
273 } |
|
274 |
|
275 /** |
|
276 * Selects the type of node to create and appends it to the parent. |
|
277 * |
|
278 * @param DOMNode $parentNode |
|
279 * @param array|object $data |
|
280 * @param string $nodename |
|
281 * @param string $key |
|
282 * @return Boolean |
|
283 */ |
|
284 private function appendNode($parentNode, $data, $nodeName, $key = null) |
|
285 { |
|
286 $node = $this->dom->createElement($nodeName); |
|
287 if (null !== $key) { |
|
288 $node->setAttribute('key', $key); |
|
289 } |
|
290 $appendNode = $this->selectNodeType($node, $data); |
|
291 // we may have decided not to append this node, either in error or if its $nodeName is not valid |
|
292 if ($appendNode) { |
|
293 $parentNode->appendChild($node); |
|
294 } |
|
295 |
|
296 return $appendNode; |
|
297 } |
|
298 |
|
299 /** |
|
300 * Tests the value being passed and decide what sort of element to create |
|
301 * |
|
302 * @param DOMNode $node |
|
303 * @param mixed $val |
|
304 * @return Boolean |
|
305 */ |
|
306 private function selectNodeType($node, $val) |
|
307 { |
|
308 if (is_array($val)) { |
|
309 return $this->buildXml($node, $val); |
|
310 } elseif ($val instanceof \SimpleXMLElement) { |
|
311 $child = $this->dom->importNode(dom_import_simplexml($val), true); |
|
312 $node->appendChild($child); |
|
313 } elseif ($val instanceof \Traversable) { |
|
314 $this->buildXml($node, $val); |
|
315 } elseif (is_object($val)) { |
|
316 return $this->buildXml($node, $this->serializer->normalize($val, $this->format)); |
|
317 } elseif (is_numeric($val)) { |
|
318 return $this->appendText($node, (string) $val); |
|
319 } elseif (is_string($val)) { |
|
320 return $this->appendCData($node, $val); |
|
321 } elseif (is_bool($val)) { |
|
322 return $this->appendText($node, (int) $val); |
|
323 } elseif ($val instanceof \DOMNode) { |
|
324 $child = $this->dom->importNode($val, true); |
|
325 $node->appendChild($child); |
|
326 } |
|
327 |
|
328 return true; |
|
329 } |
|
330 } |