Passed
Pull Request — master (#72)
by Dmitriy
11:13
created

XmlDataConverter   A

Complexity

Total Complexity 26

Size/Duplication

Total Lines 149
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 62
c 1
b 0
f 0
dl 0
loc 149
rs 10
wmc 26

6 Methods

Rating   Name   Duplication   Size   Complexity  
C buildXml() 0 32 12
A convertData() 0 17 3
A getValidXmlElementName() 0 7 4
A __construct() 0 3 1
A formatScalarValue() 0 12 4
A isValidXmlName() 0 7 2
1
<?php
2
3
namespace App;
4
5
use DOMDocument;
6
use DOMElement;
7
use DOMException;
8
use DOMText;
9
use Psr\Http\Message\ResponseInterface;
10
use Psr\Http\Message\StreamFactoryInterface;
11
use Psr\Http\Message\StreamInterface;
12
use Yiisoft\Strings\StringHelper;
13
14
class XmlDataConverter implements DataConverterInterface
15
{
16
    /**
17
     * @var string the Content-Type header for the response
18
     */
19
    private string $contentType = 'application/xml';
20
    /**
21
     * @var string the XML version
22
     */
23
    private string $version = '1.0';
24
    /**
25
     * @var string the XML encoding. If not set, it will use the value of [[Response::charset]].
26
     */
27
    private string $encoding = 'utf-8';
28
    /**
29
     * @var string the name of the root element. If set to false, null or is empty then no root tag should be added.
30
     */
31
    private string $rootTag = 'response';
32
    /**
33
     * @var string the name of the elements that represent the array elements with numeric keys.
34
     */
35
    private string $itemTag = 'item';
36
    /**
37
     * @var bool whether to interpret objects implementing the [[\Traversable]] interface as arrays.
38
     * Defaults to `true`.
39
     */
40
    private bool $useTraversableAsArray = true;
41
    /**
42
     * @var bool if object tags should be added
43
     */
44
    private bool $useObjectTags = true;
45
46
    private StreamFactoryInterface $streamFactory;
47
48
    public function __construct(StreamFactoryInterface $streamFactory)
49
    {
50
        $this->streamFactory = $streamFactory;
51
    }
52
53
    public function convertData($data, ResponseInterface &$response): StreamInterface
54
    {
55
        $content = '';
56
        if ($data !== null) {
57
            $dom = new DOMDocument($this->version, $this->encoding);
58
            if (!empty($this->rootTag)) {
59
                $root = new DOMElement($this->rootTag);
0 ignored issues
show
Bug introduced by
The call to DOMElement::__construct() has too few arguments starting with value. ( Ignorable by Annotation )

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

59
                $root = /** @scrutinizer ignore-call */ new DOMElement($this->rootTag);

This check compares calls to functions or methods with their respective definitions. If the call has less arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
60
                $dom->appendChild($root);
61
                $this->buildXml($root, $data);
62
            } else {
63
                $this->buildXml($dom, $data);
0 ignored issues
show
Bug introduced by
$dom of type DOMDocument is incompatible with the type DOMElement expected by parameter $element of App\XmlDataConverter::buildXml(). ( Ignorable by Annotation )

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

63
                $this->buildXml(/** @scrutinizer ignore-type */ $dom, $data);
Loading history...
64
            }
65
            $content = $dom->saveXML();
66
        }
67
        $response = $response->withHeader('Content-Type', $this->contentType);
68
69
        return $this->streamFactory->createStream($content);
70
    }
71
72
    /**
73
     * @param DOMElement $element
74
     * @param mixed $data
75
     */
76
    protected function buildXml($element, $data): void
77
    {
78
        if (is_array($data) ||
79
            ($data instanceof \Traversable && $this->useTraversableAsArray)
80
        ) {
81
            foreach ($data as $name => $value) {
82
                if (is_int($name) && is_object($value)) {
83
                    $this->buildXml($element, $value);
84
                } elseif (is_array($value) || is_object($value)) {
85
                    $child = new DOMElement($this->getValidXmlElementName($name));
0 ignored issues
show
Bug introduced by
The call to DOMElement::__construct() has too few arguments starting with value. ( Ignorable by Annotation )

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

85
                    $child = /** @scrutinizer ignore-call */ new DOMElement($this->getValidXmlElementName($name));

This check compares calls to functions or methods with their respective definitions. If the call has less arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
86
                    $element->appendChild($child);
87
                    $this->buildXml($child, $value);
88
                } else {
89
                    $child = new DOMElement($this->getValidXmlElementName($name));
90
                    $element->appendChild($child);
91
                    $child->appendChild(new DOMText($this->formatScalarValue($value)));
92
                }
93
            }
94
        } elseif (is_object($data)) {
95
            if ($this->useObjectTags) {
96
                $child = new DOMElement(StringHelper::basename(get_class($data)));
97
                $element->appendChild($child);
98
            } else {
99
                $child = $element;
100
            }
101
            $array = [];
102
            foreach ($data as $name => $value) {
103
                $array[$name] = $value;
104
            }
105
            $this->buildXml($child, $array);
106
        } else {
107
            $element->appendChild(new DOMText($this->formatScalarValue($data)));
108
        }
109
    }
110
111
    /**
112
     * Formats scalar value to use in XML text node.
113
     *
114
     * @param int|string|bool|float $value a scalar value.
115
     * @return string string representation of the value.
116
     */
117
    protected function formatScalarValue($value): string
118
    {
119
        if ($value === true) {
120
            return 'true';
121
        }
122
        if ($value === false) {
123
            return 'false';
124
        }
125
        if (is_float($value)) {
126
            return StringHelper::floatToString($value);
127
        }
128
        return (string) $value;
129
    }
130
131
    /**
132
     * Returns element name ready to be used in DOMElement if
133
     * name is not empty, is not int and is valid.
134
     *
135
     * Falls back to [[itemTag]] otherwise.
136
     *
137
     * @param mixed $name
138
     * @return string
139
     */
140
    protected function getValidXmlElementName($name): string
141
    {
142
        if (empty($name) || is_int($name) || !$this->isValidXmlName($name)) {
143
            return $this->itemTag;
144
        }
145
146
        return $name;
147
    }
148
149
    /**
150
     * Checks if name is valid to be used in XML.
151
     *
152
     * @param mixed $name
153
     * @return bool
154
     * @see http://stackoverflow.com/questions/2519845/how-to-check-if-string-is-a-valid-xml-element-name/2519943#2519943
155
     */
156
    protected function isValidXmlName($name): bool
157
    {
158
        try {
159
            new DOMElement($name);
0 ignored issues
show
Bug introduced by
The call to DOMElement::__construct() has too few arguments starting with value. ( Ignorable by Annotation )

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

159
            /** @scrutinizer ignore-call */ 
160
            new DOMElement($name);

This check compares calls to functions or methods with their respective definitions. If the call has less arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
160
            return true;
161
        } catch (DOMException $e) {
162
            return false;
163
        }
164
    }
165
}
166