Passed
Pull Request — master (#73)
by Dmitriy
19:02 queued 04:11
created

XmlResponseFormatter::buildXml()   C

Complexity

Conditions 12
Paths 9

Size

Total Lines 32
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 25
c 0
b 0
f 0
dl 0
loc 32
rs 6.9666
cc 12
nc 9
nop 2

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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 Yiisoft\Strings\StringHelper;
12
13
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
    private StreamFactoryInterface $streamFactory;
46
47
    public function __construct(StreamFactoryInterface $streamFactory)
48
    {
49
        $this->streamFactory = $streamFactory;
50
    }
51
52
    public function format(Response $deferredResponse): ResponseInterface
53
    {
54
        $content = '';
55
        $data = $deferredResponse->getData();
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\XmlResponseFormatter::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 = $deferredResponse->getResponse();
68
        $response->getBody()->write($content);
69
70
        return $response->withHeader('Content-Type', $this->contentType . ';' . $this->encoding);
71
    }
72
73
    public function setVersion(string $version): void
74
    {
75
        $this->version = $version;
76
    }
77
78
    public function setEncoding(string $encoding): void
79
    {
80
        $this->encoding = $encoding;
81
    }
82
83
    public function setRootTag(string $rootTag): void
84
    {
85
        $this->rootTag = $rootTag;
86
    }
87
88
    public function setItemTag(string $itemTag): void
89
    {
90
        $this->itemTag = $itemTag;
91
    }
92
93
    public function setUseTraversableAsArray(bool $useTraversableAsArray): void
94
    {
95
        $this->useTraversableAsArray = $useTraversableAsArray;
96
    }
97
98
    public function setUseObjectTags(bool $useObjectTags): void
99
    {
100
        $this->useObjectTags = $useObjectTags;
101
    }
102
103
    /**
104
     * @param DOMElement $element
105
     * @param mixed $data
106
     */
107
    protected function buildXml($element, $data): void
108
    {
109
        if (is_array($data) ||
110
            ($data instanceof \Traversable && $this->useTraversableAsArray)
111
        ) {
112
            foreach ($data as $name => $value) {
113
                if (is_int($name) && is_object($value)) {
114
                    $this->buildXml($element, $value);
115
                } elseif (is_array($value) || is_object($value)) {
116
                    $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

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

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