1 | <?php |
||||
2 | |||||
3 | namespace kalanis\Restful\Mapping; |
||||
4 | |||||
5 | |||||
6 | use DOMDocument; |
||||
7 | use DOMNode; |
||||
8 | use kalanis\Restful\Exceptions\InvalidArgumentException; |
||||
9 | use kalanis\Restful\Mapping\Exceptions\MappingException; |
||||
10 | use Nette\Utils\Arrays; |
||||
11 | use Nette\Utils\Json; |
||||
12 | use Nette\Utils\JsonException; |
||||
13 | use Traversable; |
||||
14 | |||||
15 | |||||
16 | /** |
||||
17 | * XmlMapper |
||||
18 | * @package kalanis\Restful\Mapping |
||||
19 | */ |
||||
20 | 1 | class XmlMapper implements IMapper |
|||
21 | { |
||||
22 | |||||
23 | protected const ITEM_ELEMENT = 'item'; |
||||
24 | |||||
25 | private DOMDocument $xml; |
||||
26 | |||||
27 | 1 | public function __construct( |
|||
28 | private string $rootElement = 'root', |
||||
29 | ) |
||||
30 | { |
||||
31 | 1 | } |
|||
32 | |||||
33 | /** |
||||
34 | * Get XML root element |
||||
35 | * @return string |
||||
36 | */ |
||||
37 | public function getRootElement(): string |
||||
38 | { |
||||
39 | return $this->rootElement; |
||||
40 | } |
||||
41 | |||||
42 | /** |
||||
43 | * Set XML root element |
||||
44 | */ |
||||
45 | public function setRootElement(string $rootElement): self |
||||
46 | { |
||||
47 | 1 | $this->rootElement = $rootElement; |
|||
48 | 1 | return $this; |
|||
49 | } |
||||
50 | |||||
51 | /** |
||||
52 | * Parse traversable or array resource data to XML |
||||
53 | * @param string|object|iterable<string|int, mixed> $data |
||||
54 | * @param bool $prettyPrint |
||||
55 | * @throws InvalidArgumentException |
||||
56 | * @return string |
||||
57 | */ |
||||
58 | public function stringify(iterable|string|object $data, bool $prettyPrint = true): string |
||||
59 | { |
||||
60 | 1 | if (!is_string($data) && !is_array($data) && !($data instanceof Traversable)) { |
|||
61 | throw new InvalidArgumentException('Data must be of type string, array or Traversable'); |
||||
62 | } |
||||
63 | |||||
64 | 1 | if ($data instanceof Traversable) { |
|||
0 ignored issues
–
show
introduced
by
![]() |
|||||
65 | $data = iterator_to_array($data); |
||||
66 | } |
||||
67 | |||||
68 | 1 | $this->xml = new DOMDocument('1.0', 'UTF-8'); |
|||
69 | 1 | $this->xml->formatOutput = $prettyPrint; |
|||
70 | 1 | $this->xml->preserveWhiteSpace = $prettyPrint; |
|||
71 | 1 | $root = $this->xml->createElement($this->rootElement); |
|||
72 | 1 | $this->xml->appendChild($root); |
|||
73 | 1 | $this->toXml($data, $root, self::ITEM_ELEMENT); |
|||
74 | 1 | $stored = $this->xml->saveXML(); |
|||
75 | 1 | if (false === $stored) { |
|||
76 | throw new \RuntimeException('Storing XML failed'); |
||||
77 | } |
||||
78 | 1 | return $stored; |
|||
79 | } |
||||
80 | |||||
81 | /** |
||||
82 | * @param array<string|int, mixed>|string $data |
||||
83 | * @param DOMNode $xml |
||||
84 | * @param string $previousKey |
||||
85 | */ |
||||
86 | private function toXml(array|string $data, DOMNode $xml, string $previousKey): void |
||||
87 | { |
||||
88 | 1 | if (is_iterable($data)) { |
|||
89 | 1 | foreach ($data as $key => $value) { |
|||
90 | 1 | $node = $xml; |
|||
91 | 1 | if (is_int($key)) { |
|||
92 | 1 | $node = $this->xml->createElement($previousKey); |
|||
93 | 1 | $xml->appendChild($node); |
|||
94 | 1 | } elseif (!Arrays::isList($value)) { |
|||
95 | 1 | $node = $this->xml->createElement($key); |
|||
96 | 1 | $xml->appendChild($node); |
|||
97 | } |
||||
98 | 1 | $this->toXml($value, $node ?: $xml, is_string($key) ? $key : $previousKey); |
|||
99 | } |
||||
100 | } else { |
||||
101 | 1 | $xml->appendChild($this->xml->createTextNode($data)); |
|||
0 ignored issues
–
show
$data of type array<integer|string,mixed> is incompatible with the type string expected by parameter $data of DOMDocument::createTextNode() .
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||
102 | } |
||||
103 | 1 | } |
|||
104 | |||||
105 | /** |
||||
106 | * Parse XML to array |
||||
107 | * @param string $data |
||||
108 | * @throws MappingException If XML data is not valid |
||||
109 | * @return string|object|iterable<string|int, string|int|float|bool|null> |
||||
110 | * |
||||
111 | */ |
||||
112 | public function parse(mixed $data): iterable|string|object |
||||
113 | { |
||||
114 | 1 | return $this->fromXml(strval($data)); |
|||
0 ignored issues
–
show
|
|||||
115 | } |
||||
116 | |||||
117 | /** |
||||
118 | * @param string $data |
||||
119 | * @throws MappingException If XML data is not valid |
||||
120 | * @return iterable<string|int, string|int|float|bool|null> |
||||
121 | * |
||||
122 | */ |
||||
123 | private function fromXml(string $data): iterable |
||||
124 | { |
||||
125 | try { |
||||
126 | 1 | $useErrors = libxml_use_internal_errors(true); |
|||
127 | 1 | $xml = simplexml_load_string($data, null, LIBXML_NOCDATA); |
|||
128 | 1 | if (false === $xml) { |
|||
129 | 1 | $error = libxml_get_last_error(); |
|||
130 | 1 | if ($error) { |
|||
0 ignored issues
–
show
|
|||||
131 | 1 | throw new MappingException('Input is not valid XML document: ' . $error->message . ' on line ' . $error->line); |
|||
132 | } else { |
||||
133 | throw new MappingException('Total parser failure. Document not valid and cannot get last error.'); |
||||
134 | } |
||||
135 | } |
||||
136 | 1 | libxml_clear_errors(); |
|||
137 | 1 | libxml_use_internal_errors($useErrors); |
|||
138 | |||||
139 | 1 | $data = Json::decode(Json::encode((array) $xml), true); |
|||
140 | 1 | return $data ? $this->normalize((array) $data) : []; |
|||
141 | 1 | } catch (JsonException $e) { |
|||
142 | throw new MappingException('Error in parsing response: ' . $e->getMessage()); |
||||
143 | } |
||||
144 | } |
||||
145 | |||||
146 | /** |
||||
147 | * Normalize data structure to accepted form |
||||
148 | * @param array<string|int, mixed> $value |
||||
149 | * @return array<string|int, string|int|float|bool|null> |
||||
150 | */ |
||||
151 | private function normalize(array $value): array |
||||
152 | { |
||||
153 | 1 | if (isset($value['@attributes'])) { |
|||
154 | 1 | unset($value['@attributes']); |
|||
155 | } |
||||
156 | 1 | if (0 === count($value)) { |
|||
157 | 1 | return []; |
|||
158 | } |
||||
159 | |||||
160 | 1 | foreach ($value as $key => $node) { |
|||
161 | 1 | if (!is_array($node)) { |
|||
162 | 1 | continue; |
|||
163 | } |
||||
164 | 1 | $value[$key] = $this->normalize($node); |
|||
165 | } |
||||
166 | 1 | return $value; |
|||
167 | } |
||||
168 | } |
||||
169 |