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

DOMDocument::arrayize()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 13
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 13
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    $fragment 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, $fragment)
87
    {
88
        $document = $domXPath->document;
89
90
        if ($fragment[0] === '@') {
91
            $attributeName = substr($fragment, 1);
92
            $attribute = $context->attributes->getNamedItem($attributeName);
93
            if ($attribute === null) {
94
                $attribute = $document->createAttribute($attributeName);
95
                $context->appendChild($attribute);
96
            }
97
98
            return $attribute;
99
        }
100
101
        $matches = [];
102
103
        //match fragment with comparision operator (ex parent[child="foo"])
104
        $pattern = '^(?P<parent>[a-z][\w0-9-]*)\[(?P<child>[a-z@][\w0-9-]*)\s*=\s*["\'](?P<value>.*)[\'"]\]$';
105
        if (preg_match("/{$pattern}/i", $fragment, $matches)) {
106
            $parent = null;
0 ignored issues
show
Unused Code introduced by
$parent is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
107
            $list = $domXPath->query($fragment, $context);
108
            if ($list->length === 0) {
109
                $parent = $document->createElement($matches['parent']);
110
            } 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...
111
                $parent = $list->item(0);
112
            }
113
114
            if ($matches['child'][0] == '@') {
115
                $attribute = $document->createAttribute(substr($matches['child'], 1));
116
                $attribute->value = $matches['value'];
117
                $parent->appendChild($attribute);
118
                $context->appendChild($parent);
119
                return $parent;
120
            }
121
122
            $parent->appendChild($document->createElement($matches['child'], $matches['value']));
123
            $context->appendChild($parent);
124
            return $parent;
125
        }
126
127
        $matches = [];
128
        preg_match('/^(?P<name>[a-z][\w0-9-]*)\[(?P<count>\d+)\]$/i', $fragment, $matches);
129
        $matches += ['count' => 1, 'name' => $fragment];
130
131
        $count = $matches['count'];
132
        $tagName = $matches['name'];
133
134
        $list = $domXPath->query($tagName, $context);
135
        self::addMultiple($document, $context, $tagName, $count - $list->length);
136
137
        return $domXPath->query($tagName, $context)->item($count - 1);
138
    }
139
140
    /**
141
     * Helper method to add multiple identical nodes to the given context node.
142
     *
143
     * @param \DOMDocument $document The parent document.
144
     * @param \DOMNode     $context  The node to which the new elements will be added.
145
     * @param string       $tagName  The tag name of the element.
146
     * @param integer      $limit    The number of elements to create.
147
     *
148
     * @return void
149
     */
150
    final private static function addMultiple(\DOMDocument $document, \DOMNode $context, $tagName, $limit)
151
    {
152
        for ($i = 0; $i < $limit; $i++) {
153
            $context->appendChild($document->createElement($tagName));
154
        }
155
    }
156
157
    /**
158
     * Helper method to create all sub elements in the given array based on the given xpath.
159
     *
160
     * @param array  $array The array to which the new elements will be added.
161
     * @param string $path  The xpath defining the new elements.
162
     * @param mixed  $value The value for the last child element.
163
     *
164
     * @return void
165
     */
166
    final private static function pathToArray(array &$array, $path, $value = null)
167
    {
168
        $path = str_replace(['[', ']'], ['/', ''], $path);
169
        $parts = array_filter(explode('/', $path));
170
        $key = array_shift($parts);
171
172
        if (is_numeric($key)) {
173
            $key = (int)$key -1;
174
        }
175
176
        if (empty($parts)) {
177
            $array[$key] = $value;
178
            return;
179
        }
180
181
        self::arrayize($array, $key);
182
183
        //RECURSION!!
184
        self::pathToArray($array[$key], implode('/', $parts), $value);
185
    }
186
187
    /**
188
     * Helper method to ensure the value at the given $key is an array.
189
     *
190
     * @param array  $array The array for which element $key should be checked.
191
     * @param string $key   The key for which the value will be made into an array.
192
     *
193
     * @return void
194
     */
195
    final private static function arrayize(array &$array, $key)
196
    {
197
        if (!array_key_exists($key, $array)) {
198
            //key does not exist, set to empty array and return
199
            $array[$key] = [];
200
            return;
201
        }
202
203
        if (!is_array($array[$key])) {
204
            //key exists but is not an array
205
            $array[$key] = [$array[$key]];
206
        }//else key exists and is an array
207
    }
208
209
    /**
210
     * Helper method to flatten a multi-dimensional array into a single dimensional array whose keys are xpaths.
211
     *
212
     * @param array  $array  The array to flatten.
213
     * @param string $prefix The prefix to recursively add to the flattened keys.
214
     *
215
     * @return array
216
     */
217
    final private static function flatten(array $array, $prefix = '')
218
    {
219
        $result = [];
220
        foreach ($array as $key => $value) {
221
            if (is_int($key)) {
222
                $newKey = (substr($prefix, -1) == ']') ? $prefix : "{$prefix}[" . (++$key) . ']';
223
            } 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...
224
                $newKey = $prefix . (empty($prefix) ? '' : '/') . $key;
225
            }
226
227
            if (is_array($value)) {
228
                $result = array_merge($result, self::flatten($value, $newKey));
229
                continue;
230
            }
231
232
            $result[$newKey] = $value;
233
        }
234
235
        return $result;
236
    }
237
}
238