XmlLocation   B
last analyzed

Complexity

Total Complexity 45

Size/Duplication

Total Lines 300
Duplicated Lines 4 %

Coupling/Cohesion

Components 1
Dependencies 2

Test Coverage

Coverage 93.29%

Importance

Changes 0
Metric Value
wmc 45
lcom 1
cbo 2
dl 12
loc 300
ccs 139
cts 149
cp 0.9329
rs 8.8
c 0
b 0
f 0

8 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 4 1
A before() 0 9 1
A after() 0 20 3
A visit() 0 21 3
A recursiveProcess() 0 28 5
A processArray() 7 32 5
C processObject() 5 63 14
C xmlToArray() 0 59 13

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like XmlLocation often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use XmlLocation, and based on these observations, apply Extract Interface, too.

1
<?php
2
namespace GuzzleHttp\Command\Guzzle\ResponseLocation;
3
4
use GuzzleHttp\Command\Guzzle\Parameter;
5
use GuzzleHttp\Command\Result;
6
use GuzzleHttp\Command\ResultInterface;
7
use Psr\Http\Message\ResponseInterface;
8
9
/**
10
 * Extracts elements from an XML document
11
 */
12
class XmlLocation extends AbstractLocation
13
{
14
    /** @var \SimpleXMLElement XML document being visited */
15
    private $xml;
16
17
    /**
18
     * Set the name of the location
19
     *
20
     * @param string $locationName
21
     */
22 17
    public function __construct($locationName = 'xml')
23
    {
24 17
        parent::__construct($locationName);
25 17
    }
26
27
    /**
28
     * @param ResultInterface $result
29
     * @param ResponseInterface $response
30
     * @param Parameter $model
31
     * @return ResultInterface
32
     */
33 17
    public function before(
34
        ResultInterface $result,
35
        ResponseInterface $response,
36
        Parameter $model
37
    ) {
38 17
        $this->xml = simplexml_load_string((string) $response->getBody());
39
40 17
        return $result;
41
    }
42
43
    /**
44
     * @param ResultInterface $result
45
     * @param ResponseInterface $response
46
     * @param Parameter $model
47
     * @return Result|ResultInterface
48
     */
49 17
    public function after(
50
        ResultInterface $result,
51
        ResponseInterface $response,
52
        Parameter $model
53
    ) {
54
        // Handle additional, undefined properties
55 17
        $additional = $model->getAdditionalProperties();
56 17
        if ($additional instanceof Parameter &&
57 1
            $additional->getLocation() == $this->locationName
58 17
        ) {
59 1
            $result = new Result(array_merge(
0 ignored issues
show
Bug Compatibility introduced by
The expression new \GuzzleHttp\Command\...lToArray($this->xml))); of type GuzzleHttp\Command\Result adds the type GuzzleHttp\Command\Result to the return on line 67 which is incompatible with the return type declared by the interface GuzzleHttp\Command\Guzzl...ocationInterface::after of type GuzzleHttp\Command\ResultInterface.
Loading history...
60 1
                $result->toArray(),
61 1
                self::xmlToArray($this->xml)
62 1
            ));
63 1
        }
64
65 17
        $this->xml = null;
66
67 17
        return $result;
68
    }
69
70
    /**
71
     * @param ResultInterface $result
72
     * @param ResponseInterface $response
73
     * @param Parameter $param
74
     * @return ResultInterface
75
     */
76 17
    public function visit(
77
        ResultInterface $result,
78
        ResponseInterface $response,
79
        Parameter $param
80
    ) {
81 17
        $sentAs = $param->getWireName();
82 17
        $ns = null;
83 17
        if (strstr($sentAs, ':')) {
84
            list($ns, $sentAs) = explode(':', $sentAs);
85
        }
86
87
        // Process the primary property
88 17
        if (count($this->xml->children($ns, true)->{$sentAs})) {
89 16
            $result[$param->getName()] = $this->recursiveProcess(
0 ignored issues
show
Bug introduced by
Consider using $param->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
90 16
                $param,
91 16
                $this->xml->children($ns, true)->{$sentAs}
92 16
            );
93 16
        }
94
95 17
        return $result;
96
    }
97
98
    /**
99
     * Recursively process a parameter while applying filters
100
     *
101
     * @param Parameter         $param API parameter being processed
102
     * @param \SimpleXMLElement $node  Node being processed
103
     * @return array
104
     */
105 16
    private function recursiveProcess(
106
        Parameter $param,
107
        \SimpleXMLElement $node
108
    ) {
109 16
        $result = [];
110 16
        $type = $param->getType();
111
112 16
        if ($type == 'object') {
113 11
            $result = $this->processObject($param, $node);
114 16
        } elseif ($type == 'array') {
115 12
            $result = $this->processArray($param, $node);
116 12
        } else {
117
            // We are probably handling a flat data node (i.e. string or
118
            // integer), so let's check if it's childless, which indicates a
119
            // node containing plain text.
120 12
            if ($node->children()->count() == 0) {
121
                // Retrieve text from node
122 12
                $result = (string) $node;
123 12
            }
124
        }
125
126
        // Filter out the value
127 16
        if (isset($result)) {
128 16
            $result = $param->filter($result);
129 16
        }
130
131 16
        return $result;
132
    }
133
134
    /**
135
     * @param Parameter $param
136
     * @param \SimpleXMLElement $node
137
     * @return array
138
     */
139 12
    private function processArray(Parameter $param, \SimpleXMLElement $node)
140
    {
141
        // Cast to an array if the value was a string, but should be an array
142 12
        $items = $param->getItems();
143 12
        $sentAs = $items->getWireName();
144 12
        $result = [];
145 12
        $ns = null;
146
147 12 View Code Duplication
        if (strstr($sentAs, ':')) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
148
            // Get namespace from the wire name
149 1
            list($ns, $sentAs) = explode(':', $sentAs);
150 1
        } else {
151
            // Get namespace from data
152 11
            $ns = $items->getData('xmlNs');
153
        }
154
155 12
        if ($sentAs === null) {
156
            // A general collection of nodes
157 3
            foreach ($node as $child) {
158 3
                $result[] = $this->recursiveProcess($items, $child);
159 3
            }
160 3
        } else {
161
            // A collection of named, repeating nodes
162
            // (i.e. <collection><foo></foo><foo></foo></collection>)
163 9
            $children = $node->children($ns, true)->{$sentAs};
164 9
            foreach ($children as $child) {
165 8
                $result[] = $this->recursiveProcess($items, $child);
166 9
            }
167
        }
168
169 12
        return $result;
170
    }
171
172
    /**
173
     * Process an object
174
     *
175
     * @param Parameter         $param API parameter being parsed
176
     * @param \SimpleXMLElement $node  Value to process
177
     * @return array
178
     */
179 11
    private function processObject(Parameter $param, \SimpleXMLElement $node)
180
    {
181 11
        $result = $knownProps = $knownAttributes = [];
182
183
        // Handle known properties
184 11
        if ($properties = $param->getProperties()) {
185 10
            foreach ($properties as $property) {
186 10
                $name = $property->getName();
0 ignored issues
show
Bug introduced by
Consider using $property->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
187 10
                $sentAs = $property->getWireName();
188 10
                $knownProps[$sentAs] = 1;
189 10 View Code Duplication
                if (strpos($sentAs, ':')) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
190 2
                    list($ns, $sentAs) = explode(':', $sentAs);
191 2
                } else {
192 10
                    $ns = $property->getData('xmlNs');
193
                }
194
195 10
                if ($property->getData('xmlAttribute')) {
196
                    // Handle XML attributes
197 2
                    $result[$name] = (string) $node->attributes($ns, true)->{$sentAs};
198 2
                    $knownAttributes[$sentAs] = 1;
199 10
                } elseif (count($node->children($ns, true)->{$sentAs})) {
200
                    // Found a child node matching wire name
201 10
                    $childNode = $node->children($ns, true)->{$sentAs};
202 10
                    $result[$name] = $this->recursiveProcess(
203 10
                        $property,
204
                        $childNode
205 10
                    );
206 10
                }
207 10
            }
208 10
        }
209
210
        // Handle additional, undefined properties
211 11
        $additional = $param->getAdditionalProperties();
212 11
        if ($additional instanceof Parameter) {
213
            // Process all child elements according to the given schema
214 2
            foreach ($node->children($additional->getData('xmlNs'), true) as $childNode) {
215 2
                $sentAs = $childNode->getName();
216 2
                if (!isset($knownProps[$sentAs])) {
217 2
                    $result[$sentAs] = $this->recursiveProcess(
218 2
                        $additional,
219
                        $childNode
220 2
                    );
221 2
                }
222 2
            }
223 11
        } elseif ($additional === null || $additional === true) {
224
            // Blindly transform the XML into an array preserving as much data
225
            // as possible. Remove processed, aliased properties.
226 9
            $array = array_diff_key(self::xmlToArray($node), $knownProps);
227
            // Remove @attributes that were explicitly plucked from the
228
            // attributes list.
229 9
            if (isset($array['@attributes']) && $knownAttributes) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $knownAttributes of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
230 1
                $array['@attributes'] = array_diff_key($array['@attributes'], $knownProps);
231 1
                if (!$array['@attributes']) {
232 1
                    unset($array['@attributes']);
233 1
                }
234 1
            }
235
236
            // Merge it together with the original result
237 9
            $result = array_merge($array, $result);
238 9
        }
239
240 11
        return $result;
241
    }
242
243
    /**
244
     * Convert an XML document to an array.
245
     *
246
     * @param \SimpleXMLElement $xml
247
     * @param int               $nesting
248
     * @param null              $ns
249
     *
250
     * @return array
251
     */
252 10
    private static function xmlToArray(
253
        \SimpleXMLElement $xml,
254
        $ns = null,
255
        $nesting = 0
256
    ) {
257 10
        $result = [];
258 10
        $children = $xml->children($ns, true);
259
260 10
        foreach ($children as $name => $child) {
261 10
            $attributes = (array) $child->attributes($ns, true);
262 10
            if (!isset($result[$name])) {
263 10
                $childArray = self::xmlToArray($child, $ns, $nesting + 1);
264 10
                $result[$name] = $attributes
265 10
                    ? array_merge($attributes, $childArray)
266 1
                    : $childArray;
267 10
                continue;
268
            }
269
            // A child element with this name exists so we're assuming
270
            // that the node contains a list of elements
271 2
            if (!is_array($result[$name])) {
272 2
                $result[$name] = [$result[$name]];
273 2
            } elseif (!isset($result[$name][0])) {
274
                // Convert the first child into the first element of a numerically indexed array
275
                $firstResult = $result[$name];
276
                $result[$name] = [];
277
                $result[$name][] = $firstResult;
278
            }
279 2
            $childArray = self::xmlToArray($child, $ns, $nesting + 1);
280 2
            if ($attributes) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $attributes of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
281 1
                $result[$name][] = array_merge($attributes, $childArray);
282 1
            } else {
283 2
                $result[$name][] = $childArray;
284
            }
285 10
        }
286
287
        // Extract text from node
288 10
        $text = trim((string) $xml);
289 10
        if ($text === '') {
290 10
            $text = null;
291 10
        }
292
293
        // Process attributes
294 10
        $attributes = (array) $xml->attributes($ns, true);
295 10
        if ($attributes) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $attributes of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
296 2
            if ($text !== null) {
297
                $result['value'] = $text;
298
            }
299 2
            $result = array_merge($attributes, $result);
300 10
        } elseif ($text !== null) {
301 8
            $result = $text;
302 8
        }
303
304
        // Make sure we're always returning an array
305 10
        if ($nesting == 0 && !is_array($result)) {
306
            $result = [$result];
307
        }
308
309 10
        return $result;
310
    }
311
}
312