Completed
Push — master ( dac95d...cf9e7d )
by Thomas
02:37
created

src/FluentDOM/Loader/Json/JsonDOM.php (4 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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\QualifiedName;
16
  use FluentDOM\Loader\Supports\Json as SupportsJson;
17
18
  /**
19
   * Load a DOM document from a json string or file
20
   */
21
  class JsonDOM implements Loadable {
22
23
    use SupportsJson;
24
25
    const ON_MAP_KEY = 'onMapKey';
26
27
    const XMLNS = 'urn:carica-json-dom.2013';
28
    const DEFAULT_QNAME = '_';
29
30
    const OPTION_VERBOSE = 1;
31
32
    const TYPE_NULL = 'null';
33
    const TYPE_BOOLEAN = 'boolean';
34
    const TYPE_NUMBER = 'number';
35
    const TYPE_STRING = 'string';
36
    const TYPE_OBJECT = 'object';
37
    const TYPE_ARRAY = 'array';
38
39
    /**
40
     * Maximum recursions
41
     *
42
     * @var int
43
     */
44
    private $_recursions = 100;
45
46
    /**
47
     * Add json:type and json:name attributes to all elements, even if not necessary.
48
     *
49
     * @var bool
50
     */
51
    private $_verbose = FALSE;
52
53
    /**
54
     * Called to map key names tag names
55
     * @var null|callable
56
     */
57
    private $_onMapKey = NULL;
58
59
    /**
60
     * Create the loader for a json string.
61
     *
62
     * The string will be decoded into a php variable structure and convert into a DOM document
63
     * If options contains is self::OPTION_VERBOSE, the DOMNodes will all have
64
     * json:type and json:name attributes. Even if the information could be read from the structure.
65
     *
66
     * @param int $options
67
     * @param int $depth
68
     */
69 16
    public function __construct($options = 0, $depth = 100) {
70 16
      $this->_recursions = (int)$depth;
71 16
      $this->_verbose = ($options & self::OPTION_VERBOSE) == self::OPTION_VERBOSE;
72 16
    }
73
74
    /**
75
     * @return string[]
76
     */
77 16
    public function getSupported() {
78 16
      return ['json', 'application/json', 'text/json'];
79
    }
80
81
82
    /**
83
     * Load the json string into an DOMDocument
84
     *
85
     * @param mixed $source
86
     * @param string $contentType
87
     * @param array|\Traversable|Options $options
88
     * @return Document|Result|NULL
89
     */
90 12
    public function load($source, $contentType, $options = []) {
91 12
      if (FALSE !== ($json = $this->getJson($source, $contentType, $options))) {
0 ignored issues
show
It seems like $options defined by parameter $options on line 90 can also be of type object<Traversable>; however, FluentDOM\Loader\Supports\Json::getJson() does only seem to accept array, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
92 9
        $dom = new Document('1.0', 'UTF-8');
93 9
        $dom->appendChild(
94 9
          $root = $dom->createElementNS(self::XMLNS, 'json:json')
95 9
        );
96 9
        $onMapKey = $this->_onMapKey;
97 9 View Code Duplication
        if (isset($options[self::ON_MAP_KEY]) && is_callable($options[self::ON_MAP_KEY])) {
0 ignored issues
show
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
98 1
          $this->onMapKey($options[self::ON_MAP_KEY]);
99 1
        }
100 9
        $this->transferTo($root, $json, $this->_recursions);
101 9
        $this->_onMapKey = $onMapKey;
102 9
        return $dom;
103
      }
104 1
      return NULL;
105
    }
106
107
    /**
108
     * @param string $source
109
     * @param string $contentType
110
     * @param array|\Traversable|Options $options
111
     * @return \FluentDOM\DocumentFragment|null
112
     */
113 2
    public function loadFragment($source, $contentType, $options = []) {
114 2
      if ($this->supports($contentType)) {
115 1
        $dom = new Document('1.0', 'UTF-8');
116 1
        $fragment = $dom->createDocumentFragment();
117 1
        $onMapKey = $this->_onMapKey;
118 1 View Code Duplication
        if (isset($options[self::ON_MAP_KEY]) && is_callable($options[self::ON_MAP_KEY])) {
0 ignored issues
show
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
119 1
          $this->onMapKey($options[self::ON_MAP_KEY]);
120 1
        }
121 1
        $this->transferTo($fragment, json_decode($source), $this->_recursions);
122 1
        $this->_onMapKey = $onMapKey;
123 1
        return $fragment;
124
      }
125 1
      return NULL;
126
    }
127
128
    /**
129
     * Get/Set a mapping callback for the tag names. If it is a callable
130
     * it will be set. FALSE removes the callback.
131
     *
132
     * function callback(string $key, boolean $isArrayElement) {}
133
     *
134
     * @param NULL|FALSE|callable $callback
135
     * @return callable|null
136
     */
137 9
    public function onMapKey($callback = NULL) {
138 9
      if (isset($callback)) {
139 2
        $this->_onMapKey = is_callable($callback) ? $callback : NULL;
140 2
      }
141 9
      return $this->_onMapKey;
142
    }
143
144
    /**
145
     * Transfer a value into a target xml element node. This sets attributes on the
146
     * target node and creates child elements for object and array values.
147
     *
148
     * If the current element is an object or array the method is called recursive.
149
     * The $recursions parameter is used to limit the recursion depth of this function.
150
     *
151
     * @param \DOMElement|\DOMNode $target
152
     * @param mixed $value
153
     * @param int $recursions
154
     */
155 10
    protected function transferTo(\DOMNode $target, $value, $recursions = 100) {
156 10
      if ($recursions < 1) {
157 1
        return;
158 10
      } elseif ($target instanceof \DOMElement || $target instanceOf \DOMDocumentFragment) {
159 10
        $type = $this->getTypeFromValue($value);
160
        switch ($type) {
161 10
        case self::TYPE_ARRAY :
162 4
          $this->transferArrayTo($target, $value, $this->_recursions - 1);
163 4
          break;
164 9
        case self::TYPE_OBJECT :
165 9
          $this->transferObjectTo($target, $value, $this->_recursions - 1);
166 9
          break;
167 8
        default :
168 8
          if ($this->_verbose || $type != self::TYPE_STRING) {
169 4
            $target->setAttributeNS(self::XMLNS, 'json:type', $type);
0 ignored issues
show
The method setAttributeNS does only exist in DOMElement, but not in DOMDocumentFragment.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
170 4
          }
171 8
          $string = $this->getValueAsString($type, $value);
172 8
          if (is_string($string)) {
173 8
            $target->appendChild($target->ownerDocument->createTextNode($string));
174 8
          }
175 8
        }
176 10
      }
177 10
    }
178
179
    /**
180
     * Get the type from a variable value.
181
     *
182
     * @param mixed $value
183
     * @return string
184
     */
185 10
    public function getTypeFromValue($value) {
186 10
      if (is_array($value)) {
187 5
        if (empty($value) || array_keys($value) === range(0, count($value) - 1)) {
188 4
          return self::TYPE_ARRAY;
189
        }
190 2
        return self::TYPE_OBJECT;
191 9
      } elseif (is_object($value)) {
192 7
        return self::TYPE_OBJECT;
193 8
      } elseif (NULL === $value) {
194 1
        return self::TYPE_NULL;
195 8
      } elseif (is_bool($value)) {
196 1
        return self::TYPE_BOOLEAN;
197 8
      } elseif (is_int($value) || is_float($value)) {
198 3
        return self::TYPE_NUMBER;
199
      } else {
200 6
        return self::TYPE_STRING;
201
      }
202
    }
203
204
    /**
205
     * Get a valid qualified name (tag name) using the property name/key.
206
     *
207
     * @param string $key
208
     * @param string $default
209
     * @param bool $isArrayElement
210
     * @return string
211
     */
212 9
    private function getQualifiedName($key, $default, $isArrayElement = FALSE) {
213 9
      if ($callback = $this->onMapKey()) {
214 2
        $key = $callback($key, $isArrayElement);
215 9
      } elseif ($isArrayElement) {
216 1
        $key = $default;
217 1
      }
218 9
      return QualifiedName::normalizeString($key, self::DEFAULT_QNAME);
219
    }
220
221
    /**
222
     * @param string $type
223
     * @param mixed $value
224
     * @return null|string
225
     */
226 8
    public function getValueAsString($type, $value) {
227
      switch ($type) {
228 8
      case self::TYPE_NULL :
229 1
        return NULL;
230 8
      case self::TYPE_BOOLEAN :
231 1
        return $value ? 'true' : 'false';
232 8
      default :
233 8
        return (string)$value;
234 8
      }
235
    }
236
237
    /**
238
     * Transfer an array value into a target element node. Sets the json:type attribute to 'array' and
239
     * creates child element nodes for each array element using the default QName.
240
     *
241
     * @param \DOMNode|\DOMElement|\DOMDocumentFragment $target
242
     * @param array $value
243
     * @param int $recursions
244
     */
245 4
    private function transferArrayTo(\DOMNode $target, array $value, $recursions) {
246 4
      $parentName = '';
247 4
      if ($target instanceof Element) {
248 4
        $target->setAttributeNS(self::XMLNS, 'json:type', 'array');
249 4
        $parentName = $target->getAttributeNS(self::XMLNS, 'name') ?: $target->localName;
250 4
      }
251 4
      foreach ($value as $item) {
252 3
        $target->appendChild(
253 3
          $child = $target->ownerDocument->createElement(
254 3
            $this->getQualifiedName($parentName, self::DEFAULT_QNAME, TRUE
255 3
            )
256 3
          )
257 3
        );
258 3
        $this->transferTo($child, $item, $recursions);
259 4
      }
260 4
    }
261
262
    /**
263
     * Transfer an object value into a target element node. If the object has no properties,
264
     * the json:type attribute is always set to 'object'. If verbose is not set the json:type attribute will
265
     * be omitted if the object value has properties.
266
     *
267
     * The method creates child nodes for each property. The property name will be normalized to a valid NCName.
268
     * If the normalized NCName is different from the property name or verbose is TRUE, a json:name attribute
269
     * with the property name will be added.
270
     *
271
     * @param \DOMNode|\DOMElement|\DOMDocumentFragment $target
272
     * @param object $value
273
     * @param int $recursions
274
     */
275 9
    private function transferObjectTo(\DOMNode $target, $value, $recursions) {
276 9
      $properties = is_array($value) ? $value : get_object_vars($value);
277 9
      if ($this->_verbose || empty($properties)) {
278 2
        $target->setAttributeNS(self::XMLNS, 'json:type', 'object');
279 2
      }
280 9
      foreach ($properties as $property => $item) {
281 9
        $qname = $this->getQualifiedName($property, self::DEFAULT_QNAME);
282 9
        $target->appendChild(
283 9
          $child = $target->ownerDocument->createElement($qname)
284 9
        );
285 9
        if ($this->_verbose || $qname != $property) {
286 1
          $child->setAttributeNS(self::XMLNS, 'json:name', $property);
287 1
        }
288 9
        $this->transferTo($child, $item, $recursions);
289 9
      }
290 9
    }
291
  }
292
}