Passed
Push — master ( 881682...0f8d0a )
by Alexander
02:56
created

src/Data/Formatter/XmlDataResponseFormatter.php (4 issues)

Labels
Severity
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Yii\Web\Data\Formatter;
6
7
use DOMDocument;
8
use DOMElement;
9
use DOMException;
10
use DOMText;
11
use Psr\Http\Message\ResponseInterface;
12
use Yiisoft\Http\Header;
13
use Yiisoft\Strings\StringHelper;
14
use Yiisoft\Yii\Web\Data\DataResponse;
15
use Yiisoft\Yii\Web\Data\DataResponseFormatterInterface;
16
17
final class XmlDataResponseFormatter implements DataResponseFormatterInterface
18
{
19
    /**
20
     * @var string the Content-Type header for the response
21
     */
22
    private string $contentType = 'application/xml';
23
    /**
24
     * @var string the XML version
25
     */
26
    private string $version = '1.0';
27
    /**
28
     * @var string the XML encoding.
29
     */
30
    private string $encoding = 'UTF-8';
31
    /**
32
     * @var string the name of the root element. If set to false, null or is empty then no root tag should be added.
33
     */
34
    private string $rootTag = 'response';
35
    /**
36
     * @var string the name of the elements that represent the array elements with numeric keys.
37
     */
38
    private string $itemTag = 'item';
39
    /**
40
     * @var bool whether to interpret objects implementing the [[\Traversable]] interface as arrays.
41
     * Defaults to `true`.
42
     */
43
    private bool $useTraversableAsArray = true;
44
    /**
45
     * @var bool if object tags should be added
46
     */
47
    private bool $useObjectTags = true;
48
49 8
    public function format(DataResponse $dataResponse): ResponseInterface
50
    {
51 8
        $content = '';
52 8
        $data = $dataResponse->getData();
53 8
        if ($data !== null) {
54 8
            $dom = new DOMDocument($this->version, $this->encoding);
55 8
            if (!empty($this->rootTag)) {
56 8
                $root = new DOMElement($this->rootTag);
0 ignored issues
show
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

56
                $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...
57 8
                $dom->appendChild($root);
58 8
                $this->buildXml($root, $data);
59
            } else {
60
                $this->buildXml($dom, $data);
0 ignored issues
show
$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

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

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

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