1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace Spatie\ArrayToXml; |
4
|
|
|
|
5
|
|
|
use Exception; |
6
|
|
|
use DOMElement; |
7
|
|
|
use DOMDocument; |
8
|
|
|
use DOMException; |
9
|
|
|
use DOMDocumentType; |
10
|
|
|
use DOMImplementation; |
11
|
|
|
|
12
|
|
|
class ArrayToXml |
13
|
|
|
{ |
14
|
|
|
protected $document; |
15
|
|
|
|
16
|
|
|
protected $replaceSpacesByUnderScoresInKeyNames = true; |
17
|
|
|
|
18
|
|
|
protected $numericTagNamePrefix = 'numeric_'; |
19
|
|
|
|
20
|
|
|
public function __construct( |
21
|
|
|
array $array, |
22
|
|
|
$rootElement = '', |
23
|
|
|
$replaceSpacesByUnderScoresInKeyNames = true, |
24
|
|
|
$xmlEncoding = null, |
25
|
|
|
$xmlVersion = '1.0', |
26
|
|
|
$domProperties = [], |
27
|
|
|
$docDefinition = [] |
28
|
|
|
) { |
29
|
|
|
$this->document = new DOMDocument($xmlVersion, $xmlEncoding); |
30
|
|
|
|
31
|
|
|
if (! empty($docDefinition)) { |
32
|
|
|
$this->document->appendChild($this->createDocType($docDefinition)); |
33
|
|
|
} |
34
|
|
|
|
35
|
|
|
if (! empty($domProperties)) { |
36
|
|
|
$this->setDomProperties($domProperties); |
37
|
|
|
} |
38
|
|
|
|
39
|
|
|
$this->replaceSpacesByUnderScoresInKeyNames = $replaceSpacesByUnderScoresInKeyNames; |
40
|
|
|
|
41
|
|
|
if ($this->isArrayAllKeySequential($array) && ! empty($array)) { |
42
|
|
|
throw new DOMException('Invalid Character Error'); |
43
|
|
|
} |
44
|
|
|
|
45
|
|
|
$root = $this->createRootElement($rootElement); |
46
|
|
|
|
47
|
|
|
$this->document->appendChild($root); |
48
|
|
|
|
49
|
|
|
$this->convertElement($root, $array); |
50
|
|
|
} |
51
|
|
|
|
52
|
|
|
public function setNumericTagNamePrefix(string $prefix) |
53
|
|
|
{ |
54
|
|
|
$this->numericTagNamePrefix = $prefix; |
55
|
|
|
} |
56
|
|
|
|
57
|
|
|
public static function convert( |
58
|
|
|
array $array, |
59
|
|
|
$rootElement = '', |
60
|
|
|
bool $replaceSpacesByUnderScoresInKeyNames = true, |
61
|
|
|
string $xmlEncoding = null, |
62
|
|
|
string $xmlVersion = '1.0', |
63
|
|
|
array $domProperties = [], |
64
|
|
|
array $docDefinition = [] |
65
|
|
|
) { |
66
|
|
|
$converter = new static( |
67
|
|
|
$array, |
68
|
|
|
$rootElement, |
69
|
|
|
$replaceSpacesByUnderScoresInKeyNames, |
70
|
|
|
$xmlEncoding, |
71
|
|
|
$xmlVersion, |
72
|
|
|
$domProperties, |
73
|
|
|
$docDefinition |
74
|
|
|
); |
75
|
|
|
|
76
|
|
|
return $converter->toXml(); |
77
|
|
|
} |
78
|
|
|
|
79
|
|
|
public function toXml(): string |
80
|
|
|
{ |
81
|
|
|
return $this->document->saveXML(); |
82
|
|
|
} |
83
|
|
|
|
84
|
|
|
public function toDom(): DOMDocument |
85
|
|
|
{ |
86
|
|
|
return $this->document; |
87
|
|
|
} |
88
|
|
|
|
89
|
|
|
protected function ensureValidDomProperties(array $domProperties) |
90
|
|
|
{ |
91
|
|
|
foreach ($domProperties as $key => $value) { |
92
|
|
|
if (! property_exists($this->document, $key)) { |
93
|
|
|
throw new Exception($key.' is not a valid property of DOMDocument'); |
94
|
|
|
} |
95
|
|
|
} |
96
|
|
|
} |
97
|
|
|
|
98
|
|
|
public function setDomProperties(array $domProperties) |
99
|
|
|
{ |
100
|
|
|
$this->ensureValidDomProperties($domProperties); |
101
|
|
|
|
102
|
|
|
foreach ($domProperties as $key => $value) { |
103
|
|
|
$this->document->{$key} = $value; |
104
|
|
|
} |
105
|
|
|
|
106
|
|
|
return $this; |
107
|
|
|
} |
108
|
|
|
|
109
|
|
|
public function setDocType(array $docDefinition): self |
110
|
|
|
{ |
111
|
|
|
$firstChild = $this->document->firstChild; |
112
|
|
|
$docType = $this->createDocType($docDefinition); |
113
|
|
|
|
114
|
|
|
if ($firstChild instanceof DOMDocumentType) { |
115
|
|
|
$this->document->replaceChild($docType, $firstChild); |
116
|
|
|
} else { |
117
|
|
|
$this->document->insertBefore($docType, $firstChild); |
118
|
|
|
} |
119
|
|
|
|
120
|
|
|
return $this; |
121
|
|
|
} |
122
|
|
|
|
123
|
|
|
private function convertElement(DOMElement $element, $value) |
124
|
|
|
{ |
125
|
|
|
$sequential = $this->isArrayAllKeySequential($value); |
126
|
|
|
|
127
|
|
|
if (! is_array($value)) { |
128
|
|
|
$value = htmlspecialchars($value); |
129
|
|
|
|
130
|
|
|
$value = $this->removeControlCharacters($value); |
131
|
|
|
|
132
|
|
|
$element->nodeValue = $value; |
133
|
|
|
|
134
|
|
|
return; |
135
|
|
|
} |
136
|
|
|
|
137
|
|
|
foreach ($value as $key => $data) { |
138
|
|
|
if (! $sequential) { |
139
|
|
|
if (($key === '_attributes') || ($key === '@attributes')) { |
140
|
|
|
$this->addAttributes($element, $data); |
141
|
|
|
} elseif ((($key === '_value') || ($key === '@value')) && is_string($data)) { |
142
|
|
|
$element->nodeValue = htmlspecialchars($data); |
143
|
|
|
} elseif ((($key === '_cdata') || ($key === '@cdata')) && is_string($data)) { |
144
|
|
|
$element->appendChild($this->document->createCDATASection($data)); |
145
|
|
|
} elseif ((($key === '_mixed') || ($key === '@mixed')) && is_string($data)) { |
146
|
|
|
$fragment = $this->document->createDocumentFragment(); |
147
|
|
|
$fragment->appendXML($data); |
148
|
|
|
$element->appendChild($fragment); |
149
|
|
|
} elseif ($key === '__numeric') { |
150
|
|
|
$this->addNumericNode($element, $data); |
151
|
|
|
} else { |
152
|
|
|
$this->addNode($element, $key, $data); |
153
|
|
|
} |
154
|
|
|
} elseif (is_array($data)) { |
155
|
|
|
$this->addCollectionNode($element, $data); |
156
|
|
|
} else { |
157
|
|
|
$this->addSequentialNode($element, $data); |
158
|
|
|
} |
159
|
|
|
} |
160
|
|
|
} |
161
|
|
|
|
162
|
|
|
protected function addNumericNode(DOMElement $element, $value) |
163
|
|
|
{ |
164
|
|
|
foreach ($value as $key => $item) { |
165
|
|
|
$this->convertElement($element, [$this->numericTagNamePrefix.$key => $item]); |
166
|
|
|
} |
167
|
|
|
} |
168
|
|
|
|
169
|
|
|
protected function addNode(DOMElement $element, $key, $value) |
170
|
|
|
{ |
171
|
|
|
if ($this->replaceSpacesByUnderScoresInKeyNames) { |
172
|
|
|
$key = str_replace(' ', '_', $key); |
173
|
|
|
} |
174
|
|
|
|
175
|
|
|
$child = $this->document->createElement($key); |
176
|
|
|
$element->appendChild($child); |
177
|
|
|
$this->convertElement($child, $value); |
178
|
|
|
} |
179
|
|
|
|
180
|
|
|
protected function addCollectionNode(DOMElement $element, $value) |
181
|
|
|
{ |
182
|
|
|
if ($element->childNodes->length === 0 && $element->attributes->length === 0) { |
|
|
|
|
183
|
|
|
$this->convertElement($element, $value); |
184
|
|
|
|
185
|
|
|
return; |
186
|
|
|
} |
187
|
|
|
|
188
|
|
|
$child = $this->document->createElement($element->tagName); |
189
|
|
|
$element->parentNode->appendChild($child); |
190
|
|
|
$this->convertElement($child, $value); |
191
|
|
|
} |
192
|
|
|
|
193
|
|
|
protected function addSequentialNode(DOMElement $element, $value) |
194
|
|
|
{ |
195
|
|
|
if (empty($element->nodeValue) && ! is_numeric($element->nodeValue)) { |
196
|
|
|
$element->nodeValue = htmlspecialchars($value); |
197
|
|
|
|
198
|
|
|
return; |
199
|
|
|
} |
200
|
|
|
|
201
|
|
|
$child = new DOMElement($element->tagName); |
202
|
|
|
$child->nodeValue = htmlspecialchars($value); |
203
|
|
|
$element->parentNode->appendChild($child); |
204
|
|
|
} |
205
|
|
|
|
206
|
|
|
protected function isArrayAllKeySequential($value) |
207
|
|
|
{ |
208
|
|
|
if (! is_array($value)) { |
209
|
|
|
return false; |
210
|
|
|
} |
211
|
|
|
|
212
|
|
|
if (count($value) <= 0) { |
213
|
|
|
return true; |
214
|
|
|
} |
215
|
|
|
|
216
|
|
|
if (\key($value) === '__numeric') { |
217
|
|
|
return false; |
218
|
|
|
} |
219
|
|
|
|
220
|
|
|
return array_unique(array_map('is_int', array_keys($value))) === [true]; |
221
|
|
|
} |
222
|
|
|
|
223
|
|
|
protected function addAttributes(DOMElement $element, array $data) |
224
|
|
|
{ |
225
|
|
|
foreach ($data as $attrKey => $attrVal) { |
226
|
|
|
$element->setAttribute($attrKey, $attrVal); |
227
|
|
|
} |
228
|
|
|
} |
229
|
|
|
|
230
|
|
|
protected function createRootElement($rootElement): DOMElement |
231
|
|
|
{ |
232
|
|
|
if (is_string($rootElement)) { |
233
|
|
|
$rootElementName = $rootElement ?: 'root'; |
234
|
|
|
|
235
|
|
|
return $this->document->createElement($rootElementName); |
236
|
|
|
} |
237
|
|
|
|
238
|
|
|
$rootElementName = $rootElement['rootElementName'] ?? 'root'; |
239
|
|
|
|
240
|
|
|
$element = $this->document->createElement($rootElementName); |
241
|
|
|
|
242
|
|
|
foreach ($rootElement as $key => $value) { |
243
|
|
|
if ($key !== '_attributes' && $key !== '@attributes') { |
244
|
|
|
continue; |
245
|
|
|
} |
246
|
|
|
|
247
|
|
|
$this->addAttributes($element, $rootElement[$key]); |
248
|
|
|
} |
249
|
|
|
|
250
|
|
|
return $element; |
251
|
|
|
} |
252
|
|
|
|
253
|
|
|
protected function ensureValidDocTypeProperties($docDefinition) |
254
|
|
|
{ |
255
|
|
|
if (! isset($docDefinition[0])) { |
256
|
|
|
throw new Exception('Your doctype must include a DOMDocumentType name'); |
257
|
|
|
} |
258
|
|
|
|
259
|
|
|
if (! isset($docDefinition[1])) { |
260
|
|
|
throw new Exception('Your doctype must include a DOMDocumentType systemId'); |
261
|
|
|
} |
262
|
|
|
} |
263
|
|
|
|
264
|
|
|
protected function createDocType(array $docDefinition): DOMDocumentType |
265
|
|
|
{ |
266
|
|
|
$this->ensureValidDocTypeProperties($docDefinition); |
267
|
|
|
|
268
|
|
|
$domImplementation = new DOMImplementation(); |
269
|
|
|
|
270
|
|
|
return $domImplementation->createDocumentType($docDefinition[0], '', $docDefinition[1]); |
271
|
|
|
} |
272
|
|
|
|
273
|
|
|
protected function removeControlCharacters(string $value): string |
274
|
|
|
{ |
275
|
|
|
return preg_replace('/[\x00-\x09\x0B\x0C\x0E-\x1F\x7F]/', '', $value); |
276
|
|
|
} |
277
|
|
|
} |
278
|
|
|
|
An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.
If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.