Completed
Pull Request — master (#3)
by Chad
03:26 queued 01:39
created

DOMDocument   A

Complexity

Total Complexity 26

Size/Duplication

Total Lines 216
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 0

Importance

Changes 0
Metric Value
wmc 26
lcom 1
cbo 0
dl 0
loc 216
rs 10
c 0
b 0
f 0

8 Methods

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