• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

DoclerLabs / api-client-generator / 7259428899

19 Dec 2023 08:47AM UTC coverage: 89.733% (-0.3%) from 90.043%
7259428899

push

github

web-flow
Merge pull request #102 from DoclerLabs/generator-php-8

generator php 8

355 of 381 new or added lines in 40 files covered. (93.18%)

6 existing lines in 4 files now uncovered.

2657 of 2961 relevant lines covered (89.73%)

3.35 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

86.42
/src/Output/Copy/Serializer/ContentType/XmlContentTypeSerializer.php
1
<?php
2

3
declare(strict_types=1);
4

5
namespace DoclerLabs\ApiClientGenerator\Output\Copy\Serializer\ContentType;
6

7
use DoclerLabs\ApiClientGenerator\Output\Copy\Schema\SerializableInterface;
8
use DOMDocument;
9
use DOMElement;
10
use DOMNode;
11
use Psr\Http\Message\StreamInterface;
12

13
class XmlContentTypeSerializer implements ContentTypeSerializerInterface
14
{
15
    const MIME_TYPE = 'application/xml';
16

17
    const ATTRIBUTE_NAMESPACE = 'xmlns';
18

19
    const ATTRIBUTE_NAMESPACE_SEPARATOR = ':';
20

21
    /** @var array */
22
    private $config = [
23
        'version'          => '1.0',
24
        'encoding'         => 'UTF-8',
25
        'attributesKey'    => '@attributes',
26
        'cdataKey'         => '@cdata',
27
        'valueKey'         => '@value',
28
        'namespacesOnRoot' => true,
29
    ];
30

31
    private $xml;
32

33
    /** @var array */
34
    private $namespaces = [];
35

36
    /** @var array */
37
    private $items = [];
38

39
    public function decode(StreamInterface $body): array
40
    {
41
        $body->rewind();
3✔
42
        $this->loadXml($body->getContents());
3✔
43

44
        if ($this->xml->documentElement === null) {
3✔
45
            return [];
×
46
        }
47
        // Convert the XML to an array, omitting the root node, as it is the name of the entity
48
        $child = $this->xml->documentElement->firstChild;
3✔
49

50
        if ($child === null) {
3✔
51
            return [];
×
52
        }
53
        $childValue    = $this->parseNode($child);
3✔
54
        $childNodeName = $child->nodeName;
3✔
55

56
        $this->items[$childNodeName] = $childValue;
3✔
57

58
        // Add namespacing information to the root node
59
        if (!empty($this->namespaces) && $this->config['namespacesOnRoot']) {
3✔
60
            if (!isset($this->items[$childNodeName][$this->config['attributesKey']])) {
1✔
61
                $this->items[$childNodeName][$this->config['attributesKey']] = [];
1✔
62
            }
63

64
            foreach ($this->namespaces as $uri => $prefix) {
1✔
65
                if (!is_string($prefix)) {
1✔
66
                    continue;
×
67
                }
68
                $prefix = sprintf(
1✔
69
                    '%s%s%s',
1✔
70
                    self::ATTRIBUTE_NAMESPACE,
1✔
71
                    self::ATTRIBUTE_NAMESPACE_SEPARATOR,
1✔
72
                    $prefix
73
                );
74

75
                $this->items[$childNodeName][$this->config['attributesKey']][$prefix] = $uri;
1✔
76
            }
77
        }
78

79
        return $this->items;
3✔
80
    }
81

82
    public function encode(SerializableInterface $body): string
83
    {
84
        $this->xml = new DOMDocument($this->config['version'], $this->config['encoding']);
3✔
85
        if (strrpos(get_class($body), '\\') === false) {
3✔
86
            $rootKey = get_class($body);
3✔
87
        } else {
88
            $rootKey = substr(get_class($body), strrpos(get_class($body), '\\') + 1);
×
89
        }
90
        $this->xml->appendChild($this->buildNode($rootKey, $body->toArray()));
3✔
91

92
        $result = $this->xml->saveXML();
3✔
93
        if ($result === false) {
3✔
94
            throw new SerializeException('Failed to save xml during serialization');
×
95
        }
96

97
        return $result;
3✔
98
    }
99

100
    public function getMimeType(): string
101
    {
102
        return self::MIME_TYPE;
1✔
103
    }
104

105
    /**
106
     * @throws SerializeException
107
     *
108
     * @return void
109
     */
110
    private function loadXml(string $inputXml)
111
    {
112
        $this->xml = new DOMDocument($this->config['version'], $this->config['encoding']);
3✔
113

114
        $parse = @$this->xml->loadXML($inputXml);
3✔
115

116
        if ($parse === false) {
3✔
117
            throw new SerializeException('Error parsing XML string, input is not a well-formed XML string.');
×
118
        }
119
    }
3✔
120

121
    /**
122
     * @throws SerializeException
123
     */
124
    private function parseNode(DOMNode $node)
125
    {
126
        $output = $this->collectNodeNamespaces($node, []);
3✔
127

128
        switch ($node->nodeType) {
3✔
129
            case XML_CDATA_SECTION_NODE:
3✔
130
                $output[$this->config['cdataKey']] = $this->normalizeTextContent($node->textContent);
×
131

UNCOV
132
                break;
×
133

134
            case XML_TEXT_NODE:
3✔
135
                $output = $this->normalizeTextContent($node->textContent);
3✔
136

137
                break;
3✔
138

139
            case XML_ELEMENT_NODE:
3✔
140
                $output = $this->parseChildNodes($node, $output);
3✔
141
                $output = $this->normalizeNodeValues($output);
3✔
142
                $output = $this->collectAttributes($node, $output);
3✔
143

144
                break;
3✔
145
        }
146

147
        return $output;
3✔
148
    }
149

150
    /**
151
     * @throws SerializeException
152
     */
153
    private function parseChildNodes(DOMNode $node, $output)
154
    {
155
        foreach ($node->childNodes as $child) {
3✔
156
            if ($child->nodeType === XML_CDATA_SECTION_NODE) {
3✔
157
                if (!is_array($output)) {
1✔
158
                    if (!empty($output)) {
1✔
159
                        $output = [$this->config['valueKey'] => $output];
1✔
160
                    } else {
161
                        $output = [];
×
162
                    }
163
                }
164

165
                $output[$this->config['cdataKey']] = $this->normalizeTextContent($child->textContent);
1✔
166
            } else {
167
                $value = $this->parseNode($child);
3✔
168

169
                if ($child->nodeType === XML_TEXT_NODE) {
3✔
170
                    if ($value !== '') {
3✔
171
                        if (!empty($output)) {
3✔
172
                            $output[$this->config['valueKey']] = $value;
×
173
                        } else {
174
                            $output = $value;
3✔
175
                        }
176
                    }
177
                } elseif ($child->nodeType !== XML_COMMENT_NODE) {
3✔
178
                    $nodeName = $child->nodeName;
3✔
179

180
                    if (!isset($output[$nodeName])) {
3✔
181
                        $output[$nodeName] = [];
3✔
182
                    }
183

184
                    $output[$nodeName][] = $value;
3✔
185
                }
186
            }
187
        }
188

189
        return $output;
3✔
190
    }
191

192
    /**
193
     * @param string|string[] $textContent
194
     *
195
     * @throws SerializeException
196
     */
197
    private function normalizeTextContent($textContent): string
198
    {
199
        $normalized = preg_replace(
3✔
200
            [
201
                '/\n+\s+/',
3✔
202
                '/\r+\s+/',
203
                '/\n+\t+/',
204
                '/\r+\t+/',
205
            ],
206
            ' ',
3✔
207
            $textContent
208
        );
209
        if (!is_string($normalized)) {
3✔
210
            throw new SerializeException(sprintf('Normalization of %s failed', json_encode($textContent)));
×
211
        }
212

213
        return trim($normalized);
3✔
214
    }
215

216
    private function normalizeNodeValues($values)
217
    {
218
        if (!is_array($values)) {
3✔
219
            return $values;
3✔
220
        }
221
        if (empty($values)) {
3✔
222
            return '';
×
223
        }
224

225
        // if there is only one node of its kind, assign it directly instead of array($value);
226
        foreach ($values as $key => $value) {
3✔
227
            if (is_array($value) && count($value) === 1) {
3✔
228
                $keyName = array_keys($value)[0];
2✔
229

230
                if (is_numeric($keyName)) {
2✔
231
                    $values[$key] = $value[$keyName];
2✔
232
                }
233
            }
234
        }
235

236
        return $values;
3✔
237
    }
238

239
    private function collectAttributes(DOMNode $node, $output)
240
    {
241
        if ($node->attributes === null || !$node->attributes->length) {
3✔
242
            return $output;
3✔
243
        }
244

245
        $attributes = [];
1✔
246
        $namespaces = [];
1✔
247

248
        foreach ($node->attributes as $attributeName => $attributeNode) {
1✔
249
            $attributeName              = $attributeNode->nodeName;
1✔
250
            $attributes[$attributeName] = (string)$attributeNode->value;
1✔
251

252
            if ($attributeNode->namespaceURI) {
1✔
253
                $namespaces = $this->collectNamespaces($attributeNode);
×
254
            }
255
        }
256

257
        // if it is a leaf node, store the value in @value
258
        if (!is_array($output)) {
1✔
259
            if (!empty($output)) {
1✔
260
                $output = [$this->config['valueKey'] => $output];
1✔
261
            } else {
262
                $output = [];
×
263
            }
264
        }
265

266
        foreach (array_merge($attributes, $namespaces) as $key => $value) {
1✔
267
            $output[$this->config['attributesKey']][$key] = $value;
1✔
268
        }
269

270
        return $output;
1✔
271
    }
272

273
    private function collectNodeNamespaces(DOMNode $node, array $output): array
274
    {
275
        $namespaces = $this->collectNamespaces($node);
3✔
276

277
        if (!empty($namespaces)) {
3✔
278
            $output[$this->config['attributesKey']] = $namespaces;
×
279
        }
280

281
        return $output;
3✔
282
    }
283

284
    private function collectNamespaces(DOMNode $node): array
285
    {
286
        $namespaces = [];
3✔
287

288
        if ($node->namespaceURI) {
3✔
289
            $nsUri    = $node->namespaceURI;
1✔
290
            $nsPrefix = $node->lookupPrefix($nsUri);
1✔
291

292
            if (!array_key_exists($nsUri, $this->namespaces)) {
1✔
293
                $this->namespaces[$nsUri] = $nsPrefix;
1✔
294

295
                if (!$this->config['namespacesOnRoot']) {
1✔
296
                    if ($nsPrefix) {
×
297
                        $nsPrefix = self::ATTRIBUTE_NAMESPACE_SEPARATOR . $nsPrefix;
×
298
                    }
299

300
                    $namespaces[self::ATTRIBUTE_NAMESPACE . $nsPrefix] = $nsUri;
×
301
                }
302
            }
303
        }
304

305
        return $namespaces;
3✔
306
    }
307

308
    /**
309
     * @throws SerializeException
310
     */
311
    private function buildNode(string $nodeName, $data): DOMElement
312
    {
313
        if (!$this->isValidTagName($nodeName)) {
3✔
314
            throw new SerializeException('Invalid character in the tag name being generated: ' . $nodeName);
×
315
        }
316

317
        $node = $this->xml->createElement($nodeName);
3✔
318

319
        if ($data === false) {
3✔
320
            throw new SerializeException('Failed to create a node for: ' . $nodeName);
×
321
        }
322

323
        if (is_array($data)) {
3✔
324
            $this->parseArray($node, $data);
3✔
325
        } else {
326
            $node->appendChild($this->xml->createTextNode($this->normalizeValues($data)));
3✔
327
        }
328

329
        return $node;
3✔
330
    }
331

332
    /**
333
     * @throws SerializeException
334
     *
335
     * @return void
336
     */
337
    private function parseArray(DOMElement $node, array $array)
338
    {
339
        // get the attributes first
340
        $array = $this->parseAttributes($node, $array);
3✔
341

342
        // get value stored in @value
343
        $array = $this->parseValue($node, $array);
3✔
344

345
        // get value stored in @cdata
346
        $array = $this->parseCdata($node, $array);
3✔
347

348
        // recurse to build child nodes for this node
349
        foreach ($array as $key => $value) {
3✔
350
            if (!$this->isValidTagName($key)) {
3✔
351
                throw new SerializeException('Invalid character in the tag name being generated: ' . $key);
×
352
            }
353

354
            if (is_array($value) && is_numeric(key($value))) {
3✔
355
                // MORE THAN ONE NODE OF ITS KIND
356
                // if the new array is numeric index, means it is array of nodes of the same kind
357
                // it should follow the parent key name
358
                foreach ($value as $v) {
3✔
359
                    $node->appendChild($this->buildNode($key, $v));
3✔
360
                }
361
            } else {
362
                // ONLY ONE NODE OF ITS KIND
363
                $node->appendChild($this->buildNode($key, $value));
3✔
364
            }
365

366
            unset($array[$key]);
3✔
367
        }
368
    }
3✔
369

370
    /**
371
     * @throws SerializeException
372
     */
373
    private function parseAttributes(DOMElement $node, array $array): array
374
    {
375
        $attributesKey = $this->config['attributesKey'];
3✔
376

377
        if (array_key_exists($attributesKey, $array) && is_array($array[$attributesKey])) {
3✔
378
            foreach ($array[$attributesKey] as $key => $value) {
1✔
379
                if (!$this->isValidTagName($key)) {
1✔
380
                    throw new SerializeException('Invalid character in the attribute name being generated: ' . $key);
×
381
                }
382

383
                $node->setAttribute($key, $this->normalizeValues($value));
1✔
384
            }
385

386
            unset($array[$attributesKey]);
1✔
387
        }
388

389
        return $array;
3✔
390
    }
391

392
    private function parseValue(DOMElement $node, array $array): array
393
    {
394
        $valueKey = $this->config['valueKey'];
3✔
395

396
        if (array_key_exists($valueKey, $array)) {
3✔
397
            $node->appendChild($this->xml->createTextNode($this->normalizeValues($array[$valueKey])));
1✔
398

399
            unset($array[$valueKey]);
1✔
400
        }
401

402
        return $array;
3✔
403
    }
404

405
    private function parseCdata(DOMElement $node, array $array): array
406
    {
407
        $cdataKey = $this->config['cdataKey'];
3✔
408

409
        if (array_key_exists($cdataKey, $array)) {
3✔
410
            $node->appendChild($this->xml->createCDATASection($this->normalizeValues($array[$cdataKey])));
1✔
411

412
            unset($array[$cdataKey]);
1✔
413
        }
414

415
        return $array;
3✔
416
    }
417

418
    private function normalizeValues($value): string
419
    {
420
        $value = $value === true ? 'true' : $value;
3✔
421
        $value = $value === false ? 'false' : $value;
3✔
422
        $value = $value === null ? '' : $value;
3✔
423

424
        return (string)$value;
3✔
425
    }
426

427
    private function isValidTagName(string $tag): bool
428
    {
429
        $pattern = '/^[a-zA-Z_][\w\:\-\.]*$/';
3✔
430

431
        return preg_match($pattern, $tag, $matches) && $matches[0] === $tag && $tag[strlen($tag) - 1] !== ':';
3✔
432
    }
433
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc