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