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

XmlResponseFormatter::formatScalarValue()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 12
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 4
eloc 7
c 1
b 0
f 0
nc 4
nop 1
dl 0
loc 12
rs 10
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 Psr\Http\Message\StreamFactoryInterface;
11
use Yiisoft\Strings\StringHelper;
12
use Yiisoft\Yii\Web\Response;
13
14
final class XmlResponseFormatter implements ResponseFormatterInterface
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
    private StreamFactoryInterface $streamFactory;
47
48
    public function __construct(StreamFactoryInterface $streamFactory)
49
    {
50
        $this->streamFactory = $streamFactory;
51
    }
52
53
    public function format(Response $deferredResponse): ResponseInterface
54
    {
55
        $content = '';
56
        $data = $deferredResponse->getData();
57
        if ($data !== null) {
58
            $dom = new DOMDocument($this->version, $this->encoding);
59
            if (!empty($this->rootTag)) {
60
                $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

60
                $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...
61
                $dom->appendChild($root);
62
                $this->buildXml($root, $data);
63
            } else {
64
                $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

64
                $this->buildXml(/** @scrutinizer ignore-type */ $dom, $data);
Loading history...
65
            }
66
            $content = $dom->saveXML();
67
        }
68
        $response = $deferredResponse->getResponse();
69
        $response->getBody()->write($content);
70
71
        return $response->withHeader('Content-Type', $this->contentType . '; ' . $this->encoding);
72
    }
73
74
    public function setVersion(string $version): void
75
    {
76
        $this->version = $version;
77
    }
78
79
    public function setEncoding(string $encoding): void
80
    {
81
        $this->encoding = $encoding;
82
    }
83
84
    public function setRootTag(string $rootTag): void
85
    {
86
        $this->rootTag = $rootTag;
87
    }
88
89
    public function setItemTag(string $itemTag): void
90
    {
91
        $this->itemTag = $itemTag;
92
    }
93
94
    public function setUseTraversableAsArray(bool $useTraversableAsArray): void
95
    {
96
        $this->useTraversableAsArray = $useTraversableAsArray;
97
    }
98
99
    public function setUseObjectTags(bool $useObjectTags): void
100
    {
101
        $this->useObjectTags = $useObjectTags;
102
    }
103
104
    /**
105
     * @param DOMElement $element
106
     * @param mixed $data
107
     */
108
    protected function buildXml($element, $data): void
109
    {
110
        if (is_array($data) ||
111
            ($data instanceof \Traversable && $this->useTraversableAsArray)
112
        ) {
113
            foreach ($data as $name => $value) {
114
                if (is_int($name) && is_object($value)) {
115
                    $this->buildXml($element, $value);
116
                } elseif (is_array($value) || is_object($value)) {
117
                    $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

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

191
            /** @scrutinizer ignore-call */ 
192
            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...
192
            return true;
193
        } catch (DOMException $e) {
194
            return false;
195
        }
196
    }
197
}
198