Passed
Push — master ( 4b20bd...439edc )
by Alexander
06:09
created

XmlDataResponseFormatter::getValidXmlElementName()   A

Complexity

Conditions 4
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 4

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 4
eloc 3
c 1
b 0
f 0
nc 2
nop 1
dl 0
loc 7
rs 10
ccs 4
cts 4
cp 1
crap 4
1
<?php
2
3
namespace Yiisoft\Yii\Web\Data\Formatter;
4
5
use DOMDocument;
6
use DOMElement;
7
use DOMException;
8
use DOMText;
9
use Psr\Http\Message\ResponseInterface;
10
use Yiisoft\Strings\StringHelper;
11
use Yiisoft\Yii\Web\Data\DataResponse;
12
use Yiisoft\Yii\Web\Data\DataResponseFormatterInterface;
13
14
final class XmlDataResponseFormatter implements DataResponseFormatterInterface
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.
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 8
    public function format(DataResponse $dataResponse): ResponseInterface
47
    {
48 8
        $content = '';
49 8
        $data = $dataResponse->getData();
50 8
        if ($data !== null) {
51 8
            $dom = new DOMDocument($this->version, $this->encoding);
52 8
            if (!empty($this->rootTag)) {
53 8
                $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

53
                $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...
54 8
                $dom->appendChild($root);
55 8
                $this->buildXml($root, $data);
56
            } else {
57
                $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 Yiisoft\Yii\Web\Data\For...seFormatter::buildXml(). ( Ignorable by Annotation )

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

57
                $this->buildXml(/** @scrutinizer ignore-type */ $dom, $data);
Loading history...
58
            }
59 8
            $content = $dom->saveXML();
60
        }
61 8
        $response = $dataResponse->getResponse();
62 8
        $response->getBody()->write($content);
63
64 8
        return $response->withHeader('Content-Type', $this->contentType . '; ' . $this->encoding);
65
    }
66
67 1
    public function withVersion(string $version): self
68
    {
69 1
        $formatter = clone $this;
70 1
        $formatter->version = $version;
71 1
        return $formatter;
72
    }
73
74 1
    public function withEncoding(string $encoding): self
75
    {
76 1
        $formatter = clone $this;
77 1
        $formatter->encoding = $encoding;
78 1
        return $formatter;
79
    }
80
81 1
    public function withRootTag(string $rootTag): self
82
    {
83 1
        $formatter = clone $this;
84 1
        $formatter->rootTag = $rootTag;
85 1
        return $formatter;
86
    }
87
88 1
    public function withItemTag(string $itemTag): self
89
    {
90 1
        $formatter = clone $this;
91 1
        $formatter->itemTag = $itemTag;
92 1
        return $formatter;
93
    }
94
95
    public function withUseTraversableAsArray(bool $useTraversableAsArray): self
96
    {
97
        $formatter = clone $this;
98
        $formatter->useTraversableAsArray = $useTraversableAsArray;
99
        return $formatter;
100
    }
101
102 2
    public function withUseObjectTags(bool $useObjectTags): self
103
    {
104 2
        $formatter = clone $this;
105 2
        $formatter->useObjectTags = $useObjectTags;
106 2
        return $formatter;
107
    }
108
109 1
    public function withContentType(string $contentType): self
110
    {
111 1
        $formatter = clone $this;
112 1
        $formatter->contentType = $contentType;
113 1
        return $formatter;
114
    }
115
116
    /**
117
     * @param DOMElement $element
118
     * @param mixed $data
119
     */
120 8
    protected function buildXml($element, $data): void
121
    {
122 8
        if (is_array($data) ||
123 8
            ($this->useTraversableAsArray && $data instanceof \Traversable)
124
        ) {
125 3
            foreach ($data as $name => $value) {
126 3
                if (is_int($name) && is_object($value)) {
127
                    $this->buildXml($element, $value);
128 3
                } elseif (is_array($value) || is_object($value)) {
129
                    $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

129
                    $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...
130
                    $element->appendChild($child);
131
                    $this->buildXml($child, $value);
132
                } else {
133 3
                    $child = new DOMElement($this->getValidXmlElementName($name));
134 3
                    $element->appendChild($child);
135 3
                    $child->appendChild(new DOMText($this->formatScalarValue($value)));
136
                }
137
            }
138 7
        } elseif (is_object($data)) {
139 2
            if ($this->useObjectTags) {
140 1
                $child = new DOMElement(StringHelper::basename(get_class($data)));
141 1
                $element->appendChild($child);
142
            } else {
143 1
                $child = $element;
144
            }
145 2
            $array = [];
146 2
            foreach ($data as $name => $value) {
147 2
                $array[$name] = $value;
148
            }
149 2
            $this->buildXml($child, $array);
150
        } else {
151 5
            $element->appendChild(new DOMText($this->formatScalarValue($data)));
152
        }
153 8
    }
154
155
    /**
156
     * Formats scalar value to use in XML text node.
157
     *
158
     * @param int|string|bool|float $value a scalar value.
159
     * @return string string representation of the value.
160
     */
161 8
    protected function formatScalarValue($value): string
162
    {
163 8
        if ($value === true) {
164
            return 'true';
165
        }
166 8
        if ($value === false) {
167
            return 'false';
168
        }
169 8
        if (is_float($value)) {
170
            return StringHelper::floatToString($value);
171
        }
172 8
        return (string)$value;
173
    }
174
175
    /**
176
     * Returns element name ready to be used in DOMElement if
177
     * name is not empty, is not int and is valid.
178
     *
179
     * Falls back to [[itemTag]] otherwise.
180
     *
181
     * @param mixed $name
182
     * @return string
183
     */
184 3
    protected function getValidXmlElementName($name): string
185
    {
186 3
        if (empty($name) || is_int($name) || !$this->isValidXmlName($name)) {
187 1
            return $this->itemTag;
188
        }
189
190 3
        return $name;
191
    }
192
193
    /**
194
     * Checks if name is valid to be used in XML.
195
     *
196
     * @param mixed $name
197
     * @return bool
198
     * @see http://stackoverflow.com/questions/2519845/how-to-check-if-string-is-a-valid-xml-element-name/2519943#2519943
199
     */
200 3
    protected function isValidXmlName($name): bool
201
    {
202
        try {
203 3
            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

203
            /** @scrutinizer ignore-call */ 
204
            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...
204 1
        } catch (DOMException $e) {
205 1
            return false;
206
        }
207 3
        return true;
208
    }
209
}
210