DOMDocument   A
last analyzed

Complexity

Total Complexity 30

Size/Duplication

Total Lines 235
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
dl 0
loc 235
rs 10
c 0
b 0
f 0
wmc 30

9 Methods

Rating   Name   Duplication   Size   Complexity  
A pathToArray() 0 19 3
A addMultiple() 0 4 2
B addXPath() 0 20 5
A fromArray() 0 8 2
A toArray() 0 9 2
A arrayize() 0 11 3
B parseFragment() 0 53 6
A getNewKey() 0 7 4
A flatten() 0 14 3
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, string $xpath, $value = null)
57
    {
58
        $domXPath = new \DOMXPath($document);
59
        $list = @$domXPath->query($xpath);
60
        if ($list === false) {
0 ignored issues
show
introduced by
The condition $list === false is always false.
Loading history...
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
        if ($value !== null) {
75
            $pointer->nodeValue = htmlentities($value, ENT_XML1, $document->encoding, false);
76
        }
77
    }
78
79
    /**
80
     * Helper method to create element(s) from the given tagName.
81
     *
82
     * @param \DOMXPath $domXPath The DOMXPath object built using the owner document.
83
     * @param \DOMNode  $context  The node to which the new elements will be added.
84
     * @param string    $fragment The tag name of the element.
85
     *
86
     * @return \DOMElement|\DOMAttr The DOMNode that was created.
87
     */
88
    final private static function parseFragment(\DOMXPath $domXPath, \DOMNode $context, string $fragment)
89
    {
90
        $document = $domXPath->document;
91
92
        if ($fragment[0] === '@') {
93
            $attributeName = substr($fragment, 1);
94
            $attribute = $context->attributes->getNamedItem($attributeName);
95
            if ($attribute === null) {
96
                $attribute = $document->createAttribute($attributeName);
97
                $context->appendChild($attribute);
98
            }
99
100
            return $attribute;
101
        }
102
103
        $matches = [];
104
105
        //match fragment with comparision operator (ex parent[child="foo"])
106
        $pattern = '^(?P<parent>[a-z][\w0-9-]*)\[(?P<child>[a-z@][\w0-9-]*)\s*=\s*["\'](?P<value>.*)[\'"]\]$';
107
        if (preg_match("/{$pattern}/i", $fragment, $matches)) {
108
            //Find or create the parent node
109
            $list = $domXPath->query($fragment, $context);
110
            $parent = $list->length ? $list->item(0) : $document->createElement($matches['parent']);
111
            //If child is an attribute, create and append. Attributes are overwritten if they exist
112
            if ($matches['child'][0] == '@') {
113
                $attribute = $document->createAttribute(substr($matches['child'], 1));
114
                $attribute->value = $matches['value'];
115
                $parent->appendChild($attribute);
116
                $context->appendChild($parent);
117
                return $parent;
118
            }
119
120
            //Assume child does not exist
121
            $parent->appendChild($document->createElement($matches['child'], $matches['value']));
122
            $context->appendChild($parent);
123
            return $parent;
124
        }
125
126
        //If the fragment did not match the above pattern, then assume it is
127
        //either '/parent/child  or /parent/child[n] Where n is the nth occurence of the child node
128
        $matches = [];
129
        preg_match('/^(?P<name>[a-z][\w0-9-]*)\[(?P<count>\d+)\]$/i', $fragment, $matches);
130
        //default count and name if pattern doesn't match.
131
        //There may be another pattern that I'm missing and should account for
132
        $matches += ['count' => 1, 'name' => $fragment];
133
134
        $count = $matches['count'];
135
        $tagName = $matches['name'];
136
137
        $list = $domXPath->query($tagName, $context);
138
        self::addMultiple($document, $context, $tagName, $count - $list->length);
139
140
        return $domXPath->query($tagName, $context)->item($count - 1);
141
    }
142
143
    /**
144
     * Helper method to add multiple identical nodes to the given context node.
145
     *
146
     * @param \DOMDocument $document The parent document.
147
     * @param \DOMNode     $context  The node to which the new elements will be added.
148
     * @param string       $tagName  The tag name of the element.
149
     * @param integer      $limit    The number of elements to create.
150
     *
151
     * @return void
152
     */
153
    final private static function addMultiple(\DOMDocument $document, \DOMNode $context, string $tagName, int $limit)
154
    {
155
        for ($i = 0; $i < $limit; $i++) {
156
            $context->appendChild($document->createElement($tagName));
157
        }
158
    }
159
160
    /**
161
     * Helper method to create all sub elements in the given array based on the given xpath.
162
     *
163
     * @param array  $array The array to which the new elements will be added.
164
     * @param string $path  The xpath defining the new elements.
165
     * @param mixed  $value The value for the last child element.
166
     *
167
     * @return void
168
     */
169
    final private static function pathToArray(array &$array, string $path, $value = null)
170
    {
171
        $path = str_replace(['[', ']'], ['/', ''], $path);
172
        $parts = array_filter(explode('/', $path));
173
        $key = array_shift($parts);
174
175
        if (is_numeric($key)) {
176
            $key = (int)$key -1;
177
        }
178
179
        if (empty($parts)) {
180
            $array[$key] = $value;
181
            return;
182
        }
183
184
        self::arrayize($array, $key);
185
186
        //RECURSION!!
187
        self::pathToArray($array[$key], implode('/', $parts), $value);
188
    }
189
190
    /**
191
     * Helper method to ensure the value at the given $key is an array.
192
     *
193
     * @param array  $array The array for which element $key should be checked.
194
     * @param string $key   The key for which the value will be made into an array.
195
     *
196
     * @return void
197
     */
198
    final private static function arrayize(array &$array, string $key)
199
    {
200
        if (!array_key_exists($key, $array)) {
201
            //key does not exist, set to empty array and return
202
            $array[$key] = [];
203
            return;
204
        }
205
206
        if (!is_array($array[$key])) {
207
            //key exists but is not an array
208
            $array[$key] = [$array[$key]];
209
        }//else key exists and is an array
210
    }
211
212
    /**
213
     * Helper method to flatten a multi-dimensional array into a single dimensional array whose keys are xpaths.
214
     *
215
     * @param array  $array  The array to flatten.
216
     * @param string $prefix The prefix to recursively add to the flattened keys.
217
     *
218
     * @return array
219
     */
220
    final private static function flatten(array $array, string $prefix = '')
221
    {
222
        $result = [];
223
        foreach ($array as $key => $value) {
224
            $newKey = self::getNewKey($key, $prefix);
225
            if (is_array($value)) {
226
                $result = array_merge($result, self::flatten($value, $newKey));
227
                continue;
228
            }
229
230
            $result[$newKey] = $value;
231
        }
232
233
        return $result;
234
    }
235
236
    final private static function getNewKey(&$key, string $prefix) : string
237
    {
238
        if (is_int($key)) {
239
            return (substr($prefix, -1) == ']') ? $prefix : "{$prefix}[" . (++$key) . ']';
240
        }
241
242
        return $prefix . (empty($prefix) ? '' : '/') . $key;
243
    }
244
}
245