Passed
Pull Request — master (#233)
by Dmitriy
02:03
created

XmlResponseFormatter::format()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 19
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 3
eloc 14
c 1
b 0
f 0
nc 3
nop 1
dl 0
loc 19
rs 9.7998
1
<?php
2
3
namespace Yiisoft\Yii\Web\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\WebResponse;
12
13
final class XmlResponseFormatter implements ResponseFormatterInterface
14
{
15
    /**
16
     * @var string the Content-Type header for the response
17
     */
18
    private string $contentType = 'application/xml';
19
    /**
20
     * @var string the XML version
21
     */
22
    private string $version = '1.0';
23
    /**
24
     * @var string the XML encoding.
25
     */
26
    private string $encoding = 'UTF-8';
27
    /**
28
     * @var string the name of the root element. If set to false, null or is empty then no root tag should be added.
29
     */
30
    private string $rootTag = 'response';
31
    /**
32
     * @var string the name of the elements that represent the array elements with numeric keys.
33
     */
34
    private string $itemTag = 'item';
35
    /**
36
     * @var bool whether to interpret objects implementing the [[\Traversable]] interface as arrays.
37
     * Defaults to `true`.
38
     */
39
    private bool $useTraversableAsArray = true;
40
    /**
41
     * @var bool if object tags should be added
42
     */
43
    private bool $useObjectTags = true;
44
45
    public function format(WebResponse $webResponse): ResponseInterface
46
    {
47
        $content = '';
48
        $data = $webResponse->getData();
49
        if ($data !== null) {
50
            $dom = new DOMDocument($this->version, $this->encoding);
51
            if (!empty($this->rootTag)) {
52
                $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

52
                $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...
53
                $dom->appendChild($root);
54
                $this->buildXml($root, $data);
55
            } else {
56
                $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\Formatte...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

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

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

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