Issues (3)

src/MarshalXml.php (3 issues)

1
<?php
2
3
declare(strict_types = 1);
4
5
namespace KingsonDe\Marshal;
6
7
use KingsonDe\Marshal\Data\Collection;
8
use KingsonDe\Marshal\Data\CollectionCallable;
9
use KingsonDe\Marshal\Data\DataStructure;
10
use KingsonDe\Marshal\Data\FlexibleData;
11
use KingsonDe\Marshal\Exception\XmlDeserializeException;
12
use KingsonDe\Marshal\Exception\XmlSerializeException;
13
14
/**
15
 * @method static string serializeItem(AbstractMapper $mapper, ...$data)
16
 * @method static string serializeItemCallable(callable $mappingFunction, ...$data)
17
 */
18
class MarshalXml extends Marshal {
19
20
    const ATTRIBUTES_KEY = '@attributes';
21
    const DATA_KEY       = '@data';
22
    const CDATA_KEY      = '@cdata';
23
24
    /**
25
     * @var string
26
     */
27
    protected static $version = '1.0';
28
29
    /**
30
     * @var string
31
     */
32 1
    protected static $encoding = 'UTF-8';
33 1
34 1
    public static function setVersion(string $version) {
35
        static::$version = $version;
36 1
    }
37 1
38 1
    public static function setEncoding(string $encoding) {
39
        static::$encoding = $encoding;
40
    }
41
42
    /**
43
     * @param DataStructure $dataStructure
44
     * @return string
45 8
     * @throws \KingsonDe\Marshal\Exception\XmlSerializeException
46 8
     */
47 1
    public static function serialize(DataStructure $dataStructure) {
48
        if ($dataStructure instanceof Collection || $dataStructure instanceof CollectionCallable) {
49
            throw new XmlSerializeException('Collections in XML cannot be generated at root level.');
50 7
        }
51
52
        $data = static::buildDataStructure($dataStructure);
53 7
54
        if (null === $data) {
55 7
            throw new XmlSerializeException('No data structure.');
56 1
        }
57 1
58
        try {
59 1
            $xml = new \DOMDocument(static::$version, static::$encoding);
60
61
            static::processNodes($data, $xml);
62 6
63 6
            return $xml->saveXML();
64
        } catch (\Exception $e) {
65 6
            throw new XmlSerializeException($e->getMessage(), $e->getCode(), $e);
66 1
        }
67 1
    }
68 5
69 1
    public static function serializeCollection(AbstractMapper $mapper, ...$data) {
70 1
        throw new XmlSerializeException('Collections in XML cannot be generated at root level.');
71 1
    }
72 1
73
    public static function serializeCollectionCallable(callable $mappingFunction, ...$data) {
74 4
        throw new XmlSerializeException('Collections in XML cannot be generated at root level.');
75
    }
76
77 6
    /**
78 3
     * @param array $nodes
79 3
     * @param \DOMElement|\DOMDocument $parentXmlNode
80
     */
81 6
    protected static function processNodes(array $nodes, $parentXmlNode) {
82
        $dom = $parentXmlNode->ownerDocument ?? $parentXmlNode;
83 6
84
        foreach ($nodes as $name => $data) {
85 5
            $node = XmlNodeParser::parseNode($name, $data);
86 1
87 1
            // new node with scalar value
88
            if ($node->hasNodeValue()) {
89
                if ($node->isCData()) {
90
                    $xmlNode      = $dom->createElement($node->getName());
0 ignored issues
show
The method createElement() does not exist on DOMElement. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

90
                    /** @scrutinizer ignore-call */ 
91
                    $xmlNode      = $dom->createElement($node->getName());

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
91 1
                    $cdataSection = $dom->createCDATASection($node->getNodeValue());
0 ignored issues
show
The method createCDATASection() does not exist on DOMElement. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

91
                    /** @scrutinizer ignore-call */ 
92
                    $cdataSection = $dom->createCDATASection($node->getNodeValue());

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
92 1
                    $xmlNode->appendChild($cdataSection);
93
                } else {
94
                    $xmlNode = $dom->createElement($node->getName(), $node->getNodeValue());
95 1
                }
96 1
                static::addAttributes($node, $xmlNode);
97
                $parentXmlNode->appendChild($xmlNode);
98
                continue;
99 6
            }
100 6
101 4
            // node collection of the same type
102 4
            if ($node->isCollection()) {
103
                static::processNodes($node->getChildrenNodes(), $parentXmlNode);
0 ignored issues
show
It seems like $node->getChildrenNodes() can also be of type null; however, parameter $nodes of KingsonDe\Marshal\MarshalXml::processNodes() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

103
                static::processNodes(/** @scrutinizer ignore-type */ $node->getChildrenNodes(), $parentXmlNode);
Loading history...
104 4
                continue;
105 2
            }
106 2
107
            // new node that might contain other nodes
108
            $xmlNode = $dom->createElement($node->getName());
109 4
            static::addAttributes($node, $xmlNode);
110 2
            $parentXmlNode->appendChild($xmlNode);
111 4
            if ($node->hasChildrenNodes()) {
112 2
                static::processNodes($node->getChildrenNodes(), $xmlNode);
113 2
            }
114
        }
115
    }
116
117 4
    protected static function addAttributes(XmlNode $node, \DOMElement $xmlNode) {
118 4
        foreach ($node->getAttributes() as $name => $value) {
119 2
            $xmlNode->setAttribute($name, $value);
120 2
        }
121 2
    }
122
123 3
    /**
124
     * @param string $xml
125 3
     * @return array
126 3
     * @throws \KingsonDe\Marshal\Exception\XmlDeserializeException
127 3
     */
128
    public static function deserializeXmlToData(string $xml): array {
129
        try {
130
            $dom = new \DOMDocument();
131 2
            $dom->loadXML($xml);
132 2
            $data = [];
133 2
134
            static::deserializeNodes($dom, $data);
135
136
            // get namespaces
137 2
            $xpath = new \DOMXPath($dom);
138 2
            foreach ($xpath->query('namespace::*') as $namespace) {
139 2
                if ($namespace->nodeName !== 'xmlns:xml') {
140 2
                    $data[$dom->firstChild->nodeName][static::ATTRIBUTES_KEY][$namespace->nodeName]
141 2
                        = $namespace->nodeValue;
142
                }
143
            }
144 5
        } catch (\Exception $e) {
145
            throw new XmlDeserializeException($e->getMessage(), $e->getCode(), $e);
146 4
        }
147 4
148 3
        return $data;
149
    }
150 4
151
    /**
152 6
     * @param \DOMElement|\DOMDocument $parentXmlNode
153 6
     * @param array $data
154
     */
155
    protected static function deserializeNodes($parentXmlNode, array &$data) {
156
        $collectionNodeNames = [];
157
158
        foreach ($parentXmlNode->childNodes as $node) {
159
            if ($node instanceof \DOMText) {
160
                $data[static::DATA_KEY] = $node->textContent;
161
            }
162
163
            if ($node instanceof \DOMCdataSection) {
164
                $data[static::CDATA_KEY] = $node->data;
165
            }
166
167
            if ($node instanceof \DOMElement) {
168
                $value = [];
169
170
                if ($node->hasAttributes()) {
171
                    foreach ($node->attributes as $attribute) {
172
                        $value[static::ATTRIBUTES_KEY][$attribute->name] = $attribute->value;
173
                    }
174
                }
175
176
                // move node to collection if it exists twice
177
                if (isset($data[$node->nodeName])) {
178
                    $previousValue = $data[$node->nodeName];
179
                    unset($data[$node->nodeName]);
180
181
                    $data[][$node->nodeName]              = $previousValue;
182
                    $collectionNodeNames[$node->nodeName] = true;
183
184
                }
185
186
                if (isset($collectionNodeNames[$node->nodeName])) {
187
                    $data[][$node->nodeName] = $value;
188
189
                    if ($node->hasChildNodes()) {
190
                        \end($data);
191
                        static::deserializeNodes($node, $data[\key($data)][$node->nodeName]);
192
                    }
193
                } else {
194
                    $data[$node->nodeName] = $value;
195
196
                    if ($node->hasChildNodes()) {
197
                        static::deserializeNodes($node, $data[$node->nodeName]);
198
                    }
199
                }
200
            }
201
        }
202
203
        if (isset($data[static::DATA_KEY], $data[static::ATTRIBUTES_KEY]) && \count($data) === 2) {
204
            return;
205
        }
206
207
        if (isset($data[static::DATA_KEY])) {
208
            if (\count($data) === 1) {
209
                $data = $data[static::DATA_KEY];
210
            } else {
211
                unset($data[static::DATA_KEY]);
212
            }
213
        }
214
    }
215
216
    /**
217
     * @param string $xml
218
     * @param AbstractObjectMapper $mapper
219
     * @param mixed[] $additionalData
220
     * @return mixed
221
     */
222
    public static function deserializeXml(
223
        string $xml,
224
        AbstractObjectMapper $mapper,
225
        ...$additionalData
226
    ) {
227
        return $mapper->map(
228
            new FlexibleData(static::deserializeXmlToData($xml)),
229
            ...$additionalData
230
        );
231
    }
232
233
    /**
234
     * @param string $xml
235
     * @param callable $mappingFunction
236
     * @param mixed[] $additionalData
237
     * @return mixed
238
     */
239
    public static function deserializeXmlCallable(
240
        string $xml,
241
        callable $mappingFunction,
242
        ...$additionalData
243
    ) {
244
        return $mappingFunction(
245
            new FlexibleData(static::deserializeXmlToData($xml)),
246
            ...$additionalData
247
        );
248
    }
249
}
250