1
|
|
|
<?php |
2
|
|
|
/** |
3
|
|
|
* Load a DOM document from a json string or file |
4
|
|
|
* |
5
|
|
|
* @license http://www.opensource.org/licenses/mit-license.php The MIT License |
6
|
|
|
* @copyright Copyright (c) 2009-2014 Bastian Feder, Thomas Weinert |
7
|
|
|
*/ |
8
|
|
|
|
9
|
|
|
namespace FluentDOM\Loader\Json { |
10
|
|
|
|
11
|
|
|
use FluentDOM\Document; |
12
|
|
|
use FluentDOM\Element; |
13
|
|
|
use FluentDOM\Loadable; |
14
|
|
|
use FluentDOM\Loader\Result; |
15
|
|
|
use FluentDOM\Loader\Options; |
16
|
|
|
use FluentDOM\QualifiedName; |
17
|
|
|
use FluentDOM\Loader\Supports\Json as SupportsJson; |
18
|
|
|
|
19
|
|
|
/** |
20
|
|
|
* Load a DOM document from a json string or file |
21
|
|
|
*/ |
22
|
|
|
class JsonDOM implements Loadable { |
23
|
|
|
|
24
|
|
|
use SupportsJson; |
25
|
|
|
|
26
|
|
|
const ON_MAP_KEY = 'onMapKey'; |
27
|
|
|
|
28
|
|
|
const XMLNS = 'urn:carica-json-dom.2013'; |
29
|
|
|
const DEFAULT_QNAME = '_'; |
30
|
|
|
|
31
|
|
|
const OPTION_VERBOSE = 1; |
32
|
|
|
|
33
|
|
|
const TYPE_NULL = 'null'; |
34
|
|
|
const TYPE_BOOLEAN = 'boolean'; |
35
|
|
|
const TYPE_NUMBER = 'number'; |
36
|
|
|
const TYPE_STRING = 'string'; |
37
|
|
|
const TYPE_OBJECT = 'object'; |
38
|
|
|
const TYPE_ARRAY = 'array'; |
39
|
|
|
|
40
|
|
|
/** |
41
|
|
|
* Maximum recursions |
42
|
|
|
* |
43
|
|
|
* @var int |
44
|
|
|
*/ |
45
|
|
|
private $_recursions = 100; |
46
|
|
|
|
47
|
|
|
/** |
48
|
|
|
* Add json:type and json:name attributes to all elements, even if not necessary. |
49
|
|
|
* |
50
|
|
|
* @var bool |
51
|
|
|
*/ |
52
|
|
|
private $_verbose = FALSE; |
53
|
|
|
|
54
|
|
|
/** |
55
|
|
|
* Called to map key names tag names |
56
|
|
|
* @var null|callable |
57
|
|
|
*/ |
58
|
|
|
private $_onMapKey = NULL; |
59
|
|
|
|
60
|
|
|
/** |
61
|
|
|
* Create the loader for a json string. |
62
|
|
|
* |
63
|
|
|
* The string will be decoded into a php variable structure and convert into a DOM document |
64
|
|
|
* If options contains is self::OPTION_VERBOSE, the DOMNodes will all have |
65
|
|
|
* json:type and json:name attributes. Even if the information could be read from the structure. |
66
|
|
|
* |
67
|
|
|
* @param int $options |
68
|
|
|
* @param int $depth |
69
|
|
|
*/ |
70
|
16 |
|
public function __construct($options = 0, $depth = 100) { |
71
|
16 |
|
$this->_recursions = (int)$depth; |
72
|
16 |
|
$this->_verbose = ($options & self::OPTION_VERBOSE) == self::OPTION_VERBOSE; |
73
|
16 |
|
} |
74
|
|
|
|
75
|
|
|
/** |
76
|
|
|
* @return string[] |
77
|
|
|
*/ |
78
|
16 |
|
public function getSupported() { |
79
|
16 |
|
return ['json', 'application/json', 'text/json']; |
80
|
|
|
} |
81
|
|
|
|
82
|
|
|
|
83
|
|
|
/** |
84
|
|
|
* Load the json string into an DOMDocument |
85
|
|
|
* |
86
|
|
|
* @param mixed $source |
87
|
|
|
* @param string $contentType |
88
|
|
|
* @param array|\Traversable|Options $options |
89
|
|
|
* @return Document|Result|NULL |
90
|
|
|
*/ |
91
|
12 |
|
public function load($source, $contentType, $options = []) { |
92
|
12 |
|
if (FALSE !== ($json = $this->getJson($source, $contentType, $options))) { |
93
|
9 |
|
$dom = new Document('1.0', 'UTF-8'); |
94
|
9 |
|
$dom->appendChild( |
95
|
9 |
|
$root = $dom->createElementNS(self::XMLNS, 'json:json') |
96
|
9 |
|
); |
97
|
9 |
|
$onMapKey = $this->prepareOnMapKey($options); |
98
|
9 |
|
$this->transferTo($root, $json, $this->_recursions); |
99
|
9 |
|
$this->_onMapKey = $onMapKey; |
100
|
9 |
|
return $dom; |
101
|
|
|
} |
102
|
1 |
|
return NULL; |
103
|
|
|
} |
104
|
|
|
|
105
|
|
|
/** |
106
|
|
|
* @param string $source |
107
|
|
|
* @param string $contentType |
108
|
|
|
* @param array|\Traversable|Options $options |
109
|
|
|
* @return \FluentDOM\DocumentFragment|null |
110
|
|
|
*/ |
111
|
2 |
|
public function loadFragment($source, $contentType, $options = []) { |
112
|
2 |
|
if ($this->supports($contentType)) { |
113
|
1 |
|
$dom = new Document('1.0', 'UTF-8'); |
114
|
1 |
|
$fragment = $dom->createDocumentFragment(); |
115
|
1 |
|
$onMapKey = $this->prepareOnMapKey($options); |
116
|
1 |
|
$this->transferTo($fragment, json_decode($source), $this->_recursions); |
117
|
1 |
|
$this->_onMapKey = $onMapKey; |
118
|
1 |
|
return $fragment; |
119
|
|
|
} |
120
|
1 |
|
return NULL; |
121
|
|
|
} |
122
|
|
|
|
123
|
10 |
|
private function prepareOnMapKey($options) { |
124
|
10 |
|
$onMapKey = $this->_onMapKey; |
125
|
10 |
|
if (isset($options[self::ON_MAP_KEY]) && is_callable($options[self::ON_MAP_KEY])) { |
126
|
2 |
|
$this->onMapKey($options[self::ON_MAP_KEY]); |
127
|
2 |
|
} |
128
|
10 |
|
return $onMapKey; |
129
|
|
|
} |
130
|
|
|
|
131
|
|
|
/** |
132
|
|
|
* Get/Set a mapping callback for the tag names. If it is a callable |
133
|
|
|
* it will be set. FALSE removes the callback. |
134
|
|
|
* |
135
|
|
|
* function callback(string $key, boolean $isArrayElement) {} |
136
|
|
|
* |
137
|
|
|
* @param NULL|FALSE|callable $callback |
138
|
|
|
* @return callable|null |
139
|
|
|
*/ |
140
|
9 |
|
public function onMapKey($callback = NULL) { |
141
|
9 |
|
if (isset($callback)) { |
142
|
2 |
|
$this->_onMapKey = is_callable($callback) ? $callback : NULL; |
143
|
2 |
|
} |
144
|
9 |
|
return $this->_onMapKey; |
145
|
|
|
} |
146
|
|
|
|
147
|
|
|
/** |
148
|
|
|
* Transfer a value into a target xml element node. This sets attributes on the |
149
|
|
|
* target node and creates child elements for object and array values. |
150
|
|
|
* |
151
|
|
|
* If the current element is an object or array the method is called recursive. |
152
|
|
|
* The $recursions parameter is used to limit the recursion depth of this function. |
153
|
|
|
* |
154
|
|
|
* @param \DOMElement|\DOMNode $target |
155
|
|
|
* @param mixed $value |
156
|
|
|
* @param int $recursions |
157
|
|
|
*/ |
158
|
10 |
|
protected function transferTo(\DOMNode $target, $value, $recursions = 100) { |
159
|
10 |
|
if ($recursions < 1) { |
160
|
1 |
|
return; |
161
|
10 |
|
} elseif ($target instanceof \DOMElement || $target instanceOf \DOMDocumentFragment) { |
162
|
10 |
|
$type = $this->getTypeFromValue($value); |
163
|
|
|
switch ($type) { |
164
|
10 |
|
case self::TYPE_ARRAY : |
165
|
4 |
|
$this->transferArrayTo($target, $value, $this->_recursions - 1); |
166
|
4 |
|
break; |
167
|
9 |
|
case self::TYPE_OBJECT : |
168
|
9 |
|
$this->transferObjectTo($target, $value, $this->_recursions - 1); |
169
|
9 |
|
break; |
170
|
8 |
|
default : |
171
|
8 |
|
if ($target instanceof \DOMElement && ($this->_verbose || $type != self::TYPE_STRING)) { |
172
|
4 |
|
$target->setAttributeNS(self::XMLNS, 'json:type', $type); |
173
|
4 |
|
} |
174
|
8 |
|
$string = $this->getValueAsString($type, $value); |
175
|
8 |
|
if (is_string($string)) { |
176
|
8 |
|
$target->appendChild($target->ownerDocument->createTextNode($string)); |
177
|
8 |
|
} |
178
|
8 |
|
} |
179
|
10 |
|
} |
180
|
10 |
|
} |
181
|
|
|
|
182
|
|
|
/** |
183
|
|
|
* Get the type from a variable value. |
184
|
|
|
* |
185
|
|
|
* @param mixed $value |
186
|
|
|
* @return string |
187
|
|
|
*/ |
188
|
10 |
|
public function getTypeFromValue($value) { |
189
|
10 |
|
if (is_array($value)) { |
190
|
5 |
|
if (empty($value) || array_keys($value) === range(0, count($value) - 1)) { |
191
|
4 |
|
return self::TYPE_ARRAY; |
192
|
|
|
} |
193
|
2 |
|
return self::TYPE_OBJECT; |
194
|
9 |
|
} elseif (is_object($value)) { |
195
|
7 |
|
return self::TYPE_OBJECT; |
196
|
8 |
|
} elseif (NULL === $value) { |
197
|
1 |
|
return self::TYPE_NULL; |
198
|
8 |
|
} elseif (is_bool($value)) { |
199
|
1 |
|
return self::TYPE_BOOLEAN; |
200
|
8 |
|
} elseif (is_int($value) || is_float($value)) { |
201
|
3 |
|
return self::TYPE_NUMBER; |
202
|
|
|
} else { |
203
|
6 |
|
return self::TYPE_STRING; |
204
|
|
|
} |
205
|
|
|
} |
206
|
|
|
|
207
|
|
|
/** |
208
|
|
|
* Get a valid qualified name (tag name) using the property name/key. |
209
|
|
|
* |
210
|
|
|
* @param string $key |
211
|
|
|
* @param string $default |
212
|
|
|
* @param bool $isArrayElement |
213
|
|
|
* @return string |
214
|
|
|
*/ |
215
|
9 |
|
private function getQualifiedName($key, $default, $isArrayElement = FALSE) { |
216
|
9 |
|
if ($callback = $this->onMapKey()) { |
217
|
2 |
|
$key = $callback($key, $isArrayElement); |
218
|
9 |
|
} elseif ($isArrayElement) { |
219
|
1 |
|
$key = $default; |
220
|
1 |
|
} |
221
|
9 |
|
return QualifiedName::normalizeString($key, self::DEFAULT_QNAME); |
222
|
|
|
} |
223
|
|
|
|
224
|
|
|
/** |
225
|
|
|
* @param string $type |
226
|
|
|
* @param mixed $value |
227
|
|
|
* @return null|string |
228
|
|
|
*/ |
229
|
8 |
|
public function getValueAsString($type, $value) { |
230
|
|
|
switch ($type) { |
231
|
8 |
|
case self::TYPE_NULL : |
232
|
1 |
|
return NULL; |
233
|
8 |
|
case self::TYPE_BOOLEAN : |
234
|
1 |
|
return $value ? 'true' : 'false'; |
235
|
8 |
|
default : |
236
|
8 |
|
return (string)$value; |
237
|
8 |
|
} |
238
|
|
|
} |
239
|
|
|
|
240
|
|
|
/** |
241
|
|
|
* Transfer an array value into a target element node. Sets the json:type attribute to 'array' and |
242
|
|
|
* creates child element nodes for each array element using the default QName. |
243
|
|
|
* |
244
|
|
|
* @param \DOMNode|\DOMElement|\DOMDocumentFragment $target |
245
|
|
|
* @param array $value |
246
|
|
|
* @param int $recursions |
247
|
|
|
*/ |
248
|
4 |
|
private function transferArrayTo(\DOMNode $target, array $value, $recursions) { |
249
|
4 |
|
$parentName = ''; |
250
|
4 |
|
if ($target instanceof Element) { |
251
|
4 |
|
$target->setAttributeNS(self::XMLNS, 'json:type', 'array'); |
252
|
4 |
|
$parentName = $target->getAttributeNS(self::XMLNS, 'name') ?: $target->localName; |
253
|
4 |
|
} |
254
|
4 |
|
foreach ($value as $item) { |
255
|
3 |
|
$target->appendChild( |
256
|
3 |
|
$child = $target->ownerDocument->createElement( |
257
|
3 |
|
$this->getQualifiedName($parentName, self::DEFAULT_QNAME, TRUE |
258
|
3 |
|
) |
259
|
3 |
|
) |
260
|
3 |
|
); |
261
|
3 |
|
$this->transferTo($child, $item, $recursions); |
262
|
4 |
|
} |
263
|
4 |
|
} |
264
|
|
|
|
265
|
|
|
/** |
266
|
|
|
* Transfer an object value into a target element node. If the object has no properties, |
267
|
|
|
* the json:type attribute is always set to 'object'. If verbose is not set the json:type attribute will |
268
|
|
|
* be omitted if the object value has properties. |
269
|
|
|
* |
270
|
|
|
* The method creates child nodes for each property. The property name will be normalized to a valid NCName. |
271
|
|
|
* If the normalized NCName is different from the property name or verbose is TRUE, a json:name attribute |
272
|
|
|
* with the property name will be added. |
273
|
|
|
* |
274
|
|
|
* @param \DOMNode|\DOMElement|\DOMDocumentFragment $target |
275
|
|
|
* @param object $value |
276
|
|
|
* @param int $recursions |
277
|
|
|
*/ |
278
|
9 |
|
private function transferObjectTo(\DOMNode $target, $value, $recursions) { |
279
|
9 |
|
$properties = is_array($value) ? $value : get_object_vars($value); |
280
|
9 |
|
if ($this->_verbose || empty($properties)) { |
281
|
2 |
|
$target->setAttributeNS(self::XMLNS, 'json:type', 'object'); |
282
|
2 |
|
} |
283
|
9 |
|
foreach ($properties as $property => $item) { |
284
|
9 |
|
$qname = $this->getQualifiedName($property, self::DEFAULT_QNAME); |
285
|
9 |
|
$target->appendChild( |
286
|
9 |
|
$child = $target->ownerDocument->createElement($qname) |
287
|
9 |
|
); |
288
|
9 |
|
if ($this->_verbose || $qname != $property) { |
289
|
1 |
|
$child->setAttributeNS(self::XMLNS, 'json:name', $property); |
290
|
1 |
|
} |
291
|
9 |
|
$this->transferTo($child, $item, $recursions); |
292
|
9 |
|
} |
293
|
9 |
|
} |
294
|
|
|
} |
295
|
|
|
} |