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

src/DOMDocument.php (4 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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
0 ignored issues
show
Coding Style Documentation introduced by
Missing parameter comment
Loading history...
168
     * @param string $key
0 ignored issues
show
Coding Style Documentation introduced by
Missing parameter comment
Loading history...
169
     *
170
     * @return array
171
     */
172
    private static function arrayize(array &$input, $key)
173
    {
174
        if (!array_key_exists($key, $array)) {
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;
177
        }
178
179
        if (!is_array($array[$key])) {
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...
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 {
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