Completed
Push — master ( f91abd...85fd40 )
by Thomas
07:15
created

JsonDOM::prepareOnMapKey()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 7
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 3

Importance

Changes 0
Metric Value
cc 3
eloc 5
nc 2
nop 1
dl 0
loc 7
ccs 6
cts 6
cp 1
crap 3
rs 9.4285
c 0
b 0
f 0
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
}