Passed
Branch deserializer (f06445)
by Martin
01:28
created

MarshalXml::deserializeXml()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 5
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 1
nc 1
nop 2
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
    protected static $encoding = 'UTF-8';
33
34
    public static function setVersion(string $version) {
35
        static::$version = $version;
36
    }
37
38
    public static function setEncoding(string $encoding) {
39
        static::$encoding = $encoding;
40
    }
41
42
    /**
43
     * @param DataStructure $dataStructure
44
     * @return string
45
     * @throws \KingsonDe\Marshal\Exception\XmlSerializeException
46
     */
47
    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
        }
51
52
        $data = static::buildDataStructure($dataStructure);
53
54
        if (null === $data) {
55
            throw new XmlSerializeException('No data structure.');
56
        }
57
58
        try {
59
            $xml = new \DOMDocument(static::$version, static::$encoding);
60
61
            static::processNodes($data, $xml);
62
63
            return $xml->saveXML();
64
        } catch (\Exception $e) {
65
            throw new XmlSerializeException($e->getMessage(), $e->getCode(), $e);
66
        }
67
    }
68
69
    public static function serializeCollection(AbstractMapper $mapper, ...$data) {
70
        throw new XmlSerializeException('Collections in XML cannot be generated at root level.');
71
    }
72
73
    public static function serializeCollectionCallable(callable $mappingFunction, ...$data) {
74
        throw new XmlSerializeException('Collections in XML cannot be generated at root level.');
75
    }
76
77
    /**
78
     * @param array $nodes
79
     * @param \DOMElement|\DOMDocument $parentXmlNode
80
     */
81
    protected static function processNodes(array $nodes, $parentXmlNode) {
82
        $dom = $parentXmlNode->ownerDocument ?? $parentXmlNode;
83
84
        foreach ($nodes as $name => $data) {
85
            $node = XmlNodeParser::parseNode($name, $data);
86
87
            // new node with scalar value
88
            if ($node->hasNodeValue()) {
89
                if ($node->isCData()) {
90
                    $xmlNode      = $dom->createElement($node->getName());
0 ignored issues
show
Bug introduced by
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
                    $cdataSection = $dom->createCDATASection($node->getNodeValue());
0 ignored issues
show
Bug introduced by
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
                    $xmlNode->appendChild($cdataSection);
93
                } else {
94
                    $xmlNode = $dom->createElement($node->getName(), $node->getNodeValue());
95
                }
96
                static::addAttributes($node, $xmlNode);
97
                $parentXmlNode->appendChild($xmlNode);
98
                continue;
99
            }
100
101
            // node collection of the same type
102
            if ($node->isCollection()) {
103
                static::processNodes($node->getChildrenNodes(), $parentXmlNode);
104
                continue;
105
            }
106
107
            // new node that might contain other nodes
108
            $xmlNode = $dom->createElement($node->getName());
109
            static::addAttributes($node, $xmlNode);
110
            $parentXmlNode->appendChild($xmlNode);
111
            if ($node->hasChildrenNodes()) {
112
                static::processNodes($node->getChildrenNodes(), $xmlNode);
113
            }
114
        }
115
    }
116
117
    protected static function addAttributes(XmlNode $node, \DOMElement $xmlNode) {
118
        foreach ($node->getAttributes() as $name => $value) {
119
            $xmlNode->setAttribute($name, $value);
120
        }
121
    }
122
123
    /**
124
     * @param string $xml
125
     * @return FlexibleData
126
     * @throws \KingsonDe\Marshal\Exception\XmlDeserializeException
127
     */
128
    public static function deserializeXmlToData(string $xml): FlexibleData {
129
        try {
130
            $dom = new \DOMDocument();
131
            $dom->loadXML($xml);
132
            $data = [];
133
134
            static::deserializeNodes($dom, $data);
135
136
            // get namespaces
137
            $xpath = new \DOMXPath($dom);
138
            foreach ($xpath->query('namespace::*') as $namespace) {
139
                if ($namespace->nodeName !== 'xmlns:xml') {
140
                    $data[$dom->firstChild->nodeName][static::ATTRIBUTES_KEY][$namespace->nodeName]
141
                        = $namespace->nodeValue;
142
                }
143
            }
144
        } catch (\Exception $e) {
145
            throw new XmlDeserializeException($e->getMessage(), $e->getCode(), $e);
146
        }
147
148
        return new FlexibleData($data);
149
    }
150
151
    /**
152
     * @param \DOMElement|\DOMDocument $parentXmlNode
153
     * @param array $data
154
     */
155
    protected static function deserializeNodes($parentXmlNode, array &$data) {
156
        $isCollection = static::isCollection($parentXmlNode);
157
158
        foreach ($parentXmlNode->childNodes as $node) {
159
            if ($node instanceof \DOMText) {
160
                if (isset($data[static::ATTRIBUTES_KEY])) {
161
                    $data[static::DATA_KEY] = $node->textContent;
162
                } else {
163
                    $data = $node->textContent;
164
                }
165
            }
166
167
            if ($node instanceof \DOMCdataSection) {
168
                $data[static::CDATA_KEY] = $node->data;
169
            }
170
171
            if ($node instanceof \DOMElement) {
172
                $value = [];
173
174
                if ($node->hasAttributes()) {
175
                    foreach ($node->attributes as $attribute) {
176
                        $value[static::ATTRIBUTES_KEY][$attribute->name] = $attribute->value;
177
                    }
178
                }
179
180
                if ($isCollection) {
181
                    $i = \count($data);
0 ignored issues
show
Bug introduced by
It seems like $data can also be of type string; however, parameter $var of count() does only seem to accept Countable|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

181
                    $i = \count(/** @scrutinizer ignore-type */ $data);
Loading history...
182
183
                    $data[$i][$node->nodeName] = $value;
184
185
                    if ($node->hasChildNodes()) {
186
                        static::deserializeNodes($node, $data[$i][$node->nodeName]);
187
                    }
188
                } else {
189
                    $data[$node->nodeName] = $value;
190
191
                    if ($node->hasChildNodes()) {
192
                        static::deserializeNodes($node, $data[$node->nodeName]);
193
                    }
194
                }
195
            }
196
        }
197
    }
198
199
    protected static function isCollection($node) {
200
        if ($node->hasChildNodes()) {
201
            $name = '';
202
203
            foreach ($node->childNodes as $c) {
204
                if ($c->nodeType == XML_ELEMENT_NODE) {
205
                    if (empty($name)) {
206
                        $name = $c->nodeName;
207
                        continue;
208
                    }
209
210
                    return $name === $c->nodeName;
211
                }
212
            }
213
        }
214
        return false;
215
    }
216
217
    /**
218
     * @param string $xml
219
     * @param AbstractObjectMapper $mapper
220
     * @return mixed
221
     */
222
    public static function deserializeXml(
223
        string $xml,
224
        AbstractObjectMapper $mapper
225
    ) {
226
        return $mapper->map(static::deserializeXmlToData($xml));
227
    }
228
229
    /**
230
     * @param string $xml
231
     * @param callable $mappingFunction
232
     * @return mixed
233
     */
234
    public static function deserializeXmlCallable(
235
        string $xml,
236
        callable $mappingFunction
237
    ) {
238
        return $mappingFunction(static::deserializeXmlToData($xml));
239
    }
240
}
241