Failed Conditions
Pull Request — master (#3)
by Chad
01:47
created

DOMDocument::arrayize()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 11
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 11
rs 9.4285
c 0
b 0
f 0
cc 3
eloc 6
nc 3
nop 2
1
<?php
2
3
namespace Chadicus\Util;
4
5
/**
6
 * Static helper class for working with \DOM objects.
7
 */
8
abstract class DOMDocument
9
{
10
    /**
11
     * Coverts the given array to a \DOMDocument.
12
     *
13
     * @param array $array The array to covert.
14
     *
15
     * @return \DOMDocument
16
     */
17
    final public static function fromArray(array $array)
18
    {
19
        $document = new \DOMDocument();
20
        foreach (self::flatten($array) as $path => $value) {
21
            self::addXPath($document, $path, $value);
22
        }
23
24
        return $document;
25
    }
26
27
    /**
28
     * Converts the given \DOMDocument to an array.
29
     *
30
     * @param \DOMDocument $document The document to convert.
31
     *
32
     * @return array
33
     */
34
    final public static function toArray(\DOMDocument $document)
35
    {
36
        $result = [];
37
        $domXPath = new \DOMXPath($document);
38
        foreach ($domXPath->query('//*[not(*)] | //@*') as $node) {
39
            self::pathToArray($result, $node->getNodePath(), $node->nodeValue);
40
        }
41
42
        return $result;
43
    }
44
45
    /**
46
     * Helper method to add a new \DOMNode to the given document with the given value.
47
     *
48
     * @param \DOMDocument $document The document to which the node will be added.
49
     * @param string       $xpath    A valid xpath destination of the new node.
50
     * @param mixed        $value    The value for the new node.
51
     *
52
     * @return void
53
     *
54
     * @throws \DOMException Thrown if the given $xpath is not valid.
55
     */
56
    final public static function addXPath(\DOMDocument $document, $xpath, $value = null)
57
    {
58
        $domXPath = new \DOMXPath($document);
59
        $list = @$domXPath->query($xpath);
60
        if ($list === false) {
61
            throw new \DOMException("XPath {$xpath} is not valid.");
62
        }
63
64
        if ($list->length) {
65
            $list->item(0)->nodeValue = $value;
66
            return;
67
        }
68
69
        $pointer = $document;
70
        foreach (array_filter(explode('/', $xpath)) as $tagName) {
71
            $pointer = self::parseFragment($domXPath, $pointer, $tagName);
72
        }
73
74
        $pointer->nodeValue = $value;
75
    }
76
77
    /**
78
     * Helper method to create element(s) from the given tagName.
79
     *
80
     * @param \DOMXPath $domXPath The DOMXPath object built using the owner document.
81
     * @param \DOMNode  $context  The node to which the new elements will be added.
82
     * @param string    $tagName  The tag name of the element.
83
     *
84
     * @return \DOMElement|\DOMAttr The DOMNode that was created.
85
     */
86
    final private static function parseFragment(\DOMXPath $domXPath, \DOMNode $context, $tagName)
87
    {
88
        $document = $domXPath->document;
89
90
        if ($tagName[0] === '@') {
91
            $attribute = $document->createAttribute(substr($tagName, 1));
92
            $context->appendChild($attribute);
93
            return $attribute;
94
        }
95
96
        $matches = [];
97
        if (preg_match('/^(?P<parent>[a-z][\w0-9-]*)\[(?P<child>[a-z][\w0-9-]*)\s*=\s*"(?P<value>.*)"\]$/i', $tagName, $matches)) {
98
            $parent = $document->createElement($matches['parent']);
99
            $parent->appendChild($document->createElement($matches['child'], $matches['value']));
100
            $context->appendChild($parent);
101
            return $parent;
102
        }
103
104
        $matches = [];
105
        preg_match('/^(?P<name>[a-z][\w0-9-]*)\[(?P<count>\d+)\]$/i', $tagName, $matches);
106
        $matches += ['count' => 1, 'name' => $tagName];
107
108
        $count = $matches['count'];
109
        $tagName = $matches['name'];
110
111
        $list = $domXPath->query($tagName, $context);
112
        self::addMultiple($document, $context, $tagName, $count - $list->length);
113
114
        return $domXPath->query($tagName, $context)->item($count - 1);
115
    }
116
117
    /**
118
     * Helper method to add multiple identical nodes to the given context node.
119
     *
120
     * @param \DOMDocument $document The parent document.
121
     * @param \DOMNode     $context  The node to which the new elements will be added.
122
     * @param string       $tagName  The tag name of the element.
123
     * @param integer      $limit    The number of elements to create.
124
     *
125
     * @return void
126
     */
127
    final private static function addMultiple(\DOMDocument $document, \DOMNode $context, $tagName, $limit)
128
    {
129
        for ($i = 0; $i < $limit; $i++) {
130
            $context->appendChild($document->createElement($tagName));
131
        }
132
    }
133
134
    /**
135
     * Helper method to create all sub elements in the given array based on the given xpath.
136
     *
137
     * @param array  $array The array to which the new elements will be added.
138
     * @param string $path  The xpath defining the new elements.
139
     * @param mixed  $value The value for the last child element.
140
     *
141
     * @return void
142
     */
143
    final private static function pathToArray(array &$array, $path, $value = null)
144
    {
145
        $path = str_replace(['[', ']'], ['/', ''], $path);
146
        $parts = array_filter(explode('/', $path));
147
        $key = array_shift($parts);
148
149
        if (is_numeric($key)) {
150
            $key = (int)$key -1;
151
        }
152
153
        if (empty($parts)) {
154
            $array[$key] = $value;
155
            return;
156
        }
157
158
        self::arrayize($array, $key);
159
160
        //RECURSION!!
161
        self::pathToArray($array[$key], implode('/', $parts), $value);
162
    }
163
164
    /**
165
     * Helper method to ensure the value at the given $key is an array.
166
     *
167
     * @param array  $input The array for which element $key should be checked.
168
     * @param string $key   The key for which the value will be made into an array.
169
     *
170
     * @return array
171
     */
172
    private static function arrayize(array &$input, $key)
0 ignored issues
show
Unused Code introduced by
The parameter $input is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
173
    {
174
        if (!array_key_exists($key, $array)) {
0 ignored issues
show
Bug introduced by
The variable $array seems only to be defined at a later point. Did you maybe move this code here without moving the variable definition?

This error can happen if you refactor code and forget to move the variable initialization.

Let’s take a look at a simple example:

function someFunction() {
    $x = 5;
    echo $x;
}

The above code is perfectly fine. Now imagine that we re-order the statements:

function someFunction() {
    echo $x;
    $x = 5;
}

In that case, $x would be read before it is initialized. This was a very basic example, however the principle is the same for the found issue.

Loading history...
175
            $array[$key] = [];
0 ignored issues
show
Coding Style Comprehensibility introduced by
$array was never initialized. Although not strictly required by PHP, it is generally a good practice to add $array = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
176
            return;
0 ignored issues
show
Coding Style introduced by
Function return type is not void, but function is returning void here
Loading history...
177
        }
178
179
        if (!is_array($array[$key])) {
0 ignored issues
show
Bug introduced by
The variable $array seems only to be defined at a later point. Did you maybe move this code here without moving the variable definition?

This error can happen if you refactor code and forget to move the variable initialization.

Let’s take a look at a simple example:

function someFunction() {
    $x = 5;
    echo $x;
}

The above code is perfectly fine. Now imagine that we re-order the statements:

function someFunction() {
    echo $x;
    $x = 5;
}

In that case, $x would be read before it is initialized. This was a very basic example, however the principle is the same for the found issue.

Loading history...
180
            $array[$key] = [$array[$key]];
0 ignored issues
show
Coding Style Comprehensibility introduced by
$array was never initialized. Although not strictly required by PHP, it is generally a good practice to add $array = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
Bug introduced by
The variable $array seems only to be defined at a later point. Did you maybe move this code here without moving the variable definition?

This error can happen if you refactor code and forget to move the variable initialization.

Let’s take a look at a simple example:

function someFunction() {
    $x = 5;
    echo $x;
}

The above code is perfectly fine. Now imagine that we re-order the statements:

function someFunction() {
    echo $x;
    $x = 5;
}

In that case, $x would be read before it is initialized. This was a very basic example, however the principle is the same for the found issue.

Loading history...
181
        }
182
    }
183
184
    /**
185
     * Helper method to flatten a multi-dimensional array into a single dimensional array whose keys are xpaths.
186
     *
187
     * @param array  $array  The array to flatten.
188
     * @param string $prefix The prefix to recursively add to the flattened keys.
189
     *
190
     * @return array
191
     */
192
    final private static function flatten(array $array, $prefix = '')
193
    {
194
        $result = [];
195
        foreach ($array as $key => $value) {
196
            if (is_int($key)) {
197
                $newKey = (substr($prefix, -1) == ']') ? $prefix : "{$prefix}[" . (++$key) . ']';
198
            } else {
0 ignored issues
show
introduced by
Use of ELSE and ELSEIF is discouraged. An if expression with an else branch is never necessary. You can rewrite the conditions in a way that the else is not necessary and the code becomes simpler to read.
Loading history...
199
                $newKey = $prefix . (empty($prefix) ? '' : '/') . $key;
200
            }
201
202
            if (is_array($value)) {
203
                $result = array_merge($result, self::flatten($value, $newKey));
204
                continue;
205
            }
206
207
            $result[$newKey] = $value;
208
        }
209
210
        return $result;
211
    }
212
}
213