Issues (40)

php-src/Mapping/XmlMapper.php (4 issues)

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
$data is never a sub-type of Traversable.
Loading history...
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 ignore-type  annotation

101
            $xml->appendChild($this->xml->createTextNode(/** @scrutinizer ignore-type */ $data));
Loading history...
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
Bug Best Practice introduced by
The expression return $this->fromXml(strval($data)) returns the type array|array<integer|string,mixed> which is incompatible with the type-hinted return iterable|object|string.
Loading history...
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
$error is of type LibXMLError, thus it always evaluated to true.
Loading history...
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