Passed
Branch master (8ee9ed)
by Alexander
02:30
created

XmlConvertible   C

Complexity

Total Complexity 64

Size/Duplication

Total Lines 352
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 3

Test Coverage

Coverage 97.97%

Importance

Changes 0
Metric Value
wmc 64
lcom 1
cbo 3
dl 0
loc 352
ccs 145
cts 148
cp 0.9797
rs 5.8364
c 0
b 0
f 0

11 Methods

Rating   Name   Duplication   Size   Complexity  
C xmlIntersect() 0 62 16
C xmlDiff() 0 65 14
A xmlEqual() 0 12 1
C fromXml() 0 63 14
C toXml() 0 34 11
A getXmlElementName() 0 4 1
A setXmlElementName() 0 5 1
A getXmlChildren() 0 4 1
A setXmlChildren() 0 5 1
A getXmlProperties() 0 15 2
A __clone() 0 6 2

How to fix   Complexity   

Complex Class

Complex classes like XmlConvertible 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 XmlConvertible, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace Horat1us;
4
5
use Horat1us\Arrays\Collection;
6
use Horat1us\Examples\Head;
7
8
/**
9
 * Class XmlConvertible
10
 * @package Horat1us
11
 */
12
trait XmlConvertible
13
{
14
    /**
15
     * @var XmlConvertibleInterface[]|\DOMNode[]|\DOMElement[]|null
16
     */
17
    public $xmlChildren;
18
19
    /**
20
     * Name of xml element (class name will be used by default)
21
     *
22
     * @var string
23
     */
24
    public $xmlElementName;
25
26
    /**
27
     * @param XmlConvertibleInterface $xml
28
     * @param XmlConvertibleInterface|null $target
29
     * @param bool $skipEmpty
30
     * @return XmlConvertible|XmlConvertibleInterface|null
31
     */
32 2
    public function xmlIntersect(
33
        XmlConvertibleInterface $xml,
34
        XmlConvertibleInterface $target = null,
35
        bool $skipEmpty = true
36
    )
37
    {
38 2
        $current = $this;
39 2
        $compared = $xml;
40
41 2
        if ($current->getXmlElementName() !== $compared->getXmlElementName()) {
42 1
            return null;
43
        }
44
45 1
        $newAttributes = [];
46 1
        foreach ($current->getXmlProperties() as $property) {
47 1
            if (!property_exists($compared, $property) || $current->{$property} !== $compared->{$property}) {
48 1
                continue;
49
            }
50 1
            $newAttributes[$property] = $compared->{$property};
51
        }
52
53 1
        $newChildren = array_uintersect(
54 1
            $compared->xmlChildren ?? [],
0 ignored issues
show
Bug introduced by
Accessing xmlChildren on the interface Horat1us\XmlConvertibleInterface suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
55 1
            $current->xmlChildren ?? [],
56
            function ($comparedChild, $currentChild) use ($skipEmpty) {
57 1
                if ($comparedChild === $currentChild) {
58
                    return 0;
59
                }
60 1
                if ($currentChild instanceof XmlConvertibleInterface) {
61 1
                    if (!$comparedChild instanceof XmlConvertibleInterface) {
62
                        return -1;
63
                    }
64 1
                    $intersected = $currentChild->xmlIntersect($comparedChild, null, $skipEmpty) !== null;
65 1
                    return $intersected ? 0 : -1;
66
                }
67
                if ($comparedChild instanceof XmlConvertibleInterface) {
68
                    return -1;
69
                }
70
                /** @var \DOMElement $comparedChild */
71
                $comparedChildObject = XmlConvertibleObject::fromXml($comparedChild);
72
                $currentChildObject = XmlConvertibleObject::fromXml($currentChild);
73
                return ($currentChildObject->xmlIntersect($comparedChildObject, null, $skipEmpty) !== null)
74
                    ? 0 : -1;
75 1
            }
76
        );
77
78 1
        if ($skipEmpty && empty($newAttributes) && empty($newChildren)) {
79 1
            return null;
80
        }
81
82 1
        if (!$target) {
83 1
            $targetClass = get_class($current);
84 1
            $target = new $targetClass;
85
        }
86 1
        $target->xmlElementName = $current->xmlElementName;
87 1
        $target->xmlChildren = $newChildren;
88 1
        foreach ($newAttributes as $name => $value) {
89 1
            $target->{$name} = $value;
90
        }
91
92 1
        return $target;
93
    }
94
95
    /**
96
     * @param XmlConvertibleInterface $xml
97
     * @return XmlConvertibleInterface|XmlConvertible
98
     */
99 4
    public function xmlDiff(XmlConvertibleInterface $xml)
100
    {
101 4
        $current = $this;
102 4
        $compared = $xml;
103
104 4
        if ($current->getXmlElementName() !== $compared->getXmlElementName()) {
105 1
            return clone $current;
106
        }
107
108 4
        foreach ($current->getXmlProperties() as $property) {
109 4
            if (!property_exists($compared, $property)) {
110 1
                return clone $current;
111
            }
112 3
            if ($current->{$property} !== $compared->{$property}) {
113 3
                return clone $current;
114
            }
115
        }
116 3
        if (empty($current->getXmlChildren()) && !empty($compared->getXmlChildren())) {
117 2
            return clone $current;
118
        }
119
120 2
        $newChildren = Collection::from($current->getXmlChildren() ?? [])
121
            ->map(function ($child) use ($compared) {
122 2
                return array_reduce(
123 2
                    $compared->getXmlChildren() ?? [],
124
                    function ($carry, $comparedChild) use ($child) {
125 2
                        if ($carry) {
126
                            return $carry;
127
                        }
128 2
                        if ($comparedChild === $child) {
129
                            return false;
130
                        }
131 2
                        if ($child instanceof XmlConvertibleInterface) {
132 2
                            if (!$comparedChild instanceof XmlConvertibleInterface) {
133
                                return false;
134
                            }
135 2
                            return $child->xmlDiff($comparedChild);
136
                        }
137
                        if ($comparedChild instanceof XmlConvertibleInterface) {
138
                            return false;
139
                        }
140
                        /** @var \DOMElement $comparedChild */
141
                        $comparedChildObject = XmlConvertibleObject::fromXml($comparedChild);
142
                        $currentChildObject = XmlConvertibleObject::fromXml($child);
143
                        $diff = $currentChildObject->xmlDiff($comparedChildObject);
144
                        if ($diff) {
145
                            return $diff->toXml($child->ownerDocument);
146
                        }
147
                        return null;
148 2
                    });
149 2
            })
150
            ->filter(function ($child) {
151 2
                return $child !== null;
152 2
            })
153 2
            ->array;
154
155 2
        if (empty($newChildren)) {
156 2
            return null;
157
        }
158
159 2
        $target = clone $current;
160 2
        $target->setXmlChildren($newChildren);
161
162 2
        return clone $target;
163
    }
164
165
    /**
166
     * Converts object to XML and compares it with given
167
     *
168
     * @param XmlConvertibleInterface $xml
169
     * @return bool
170
     */
171 3
    public function xmlEqual(XmlConvertibleInterface $xml): bool
172
    {
173 3
        $document = new \DOMDocument();
174 3
        $document->appendChild($this->toXml($document));
175 3
        $current = $document->saveXML();
176
177 3
        $document = new \DOMDocument();
178 3
        $document->appendChild($xml->toXml($document));
179 3
        $compared = $document->saveXML();
180
181 3
        return $current === $compared;
182
    }
183
184
    /**
185
     * @param \DOMDocument|\DOMElement $document
186
     * @param array $aliases
187
     * @return static
188
     */
189 12
    public static function fromXml($document, array $aliases = [])
190
    {
191 12
        if ($document instanceof \DOMDocument) {
192 10
            return static::fromXml($document->firstChild, $aliases);
0 ignored issues
show
Documentation introduced by
$document->firstChild is of type object<DOMNode>, but the function expects a object<DOMDocument>|object<DOMElement>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
193
        }
194
195
        /** @var \DOMElement $document */
196 12
        if (!in_array(get_called_class(), $aliases)) {
197 9
            $aliases[(new \ReflectionClass(get_called_class()))->getShortName()] = get_called_class();
198
        }
199 12
        foreach ($aliases as $key => $alias) {
200 12
            if (is_object($alias)) {
201 3
                if (!$alias instanceof XmlConvertibleInterface) {
202 1
                    throw new \UnexpectedValueException(
203 1
                        "All aliases must be instance or class implements " . XmlConvertibleInterface::class,
204 1
                        1
205
                    );
206
                }
207 2
                $aliases[is_int($key) ? $alias->getXmlElementName() : $key] = $alias;
208 2
                continue;
209
            }
210 11
            if (!is_string($alias)) {
211 1
                throw new \UnexpectedValueException(
212 1
                    "All aliases must be instance or class implements " . XmlConvertibleInterface::class,
213 1
                    2
214
                );
215
            }
216 10
            $instance = new $alias;
217 10
            if (!$instance instanceof XmlConvertibleInterface) {
218 1
                throw new \UnexpectedValueException(
219 1
                    "All aliases must be instance of " . XmlConvertibleInterface::class,
220 1
                    3
221
                );
222
            }
223 9
            unset($aliases[$key]);
224 9
            $aliases[is_int($key) ? $instance->getXmlElementName() : $key] = $instance;
225
        }
226
227 9
        $nodeObject = $aliases[$document->nodeName] ?? new XmlConvertibleObject;
228 9
        $properties = $nodeObject->getXmlProperties();
229
230
        /** @var \DOMAttr $attribute */
231 9
        foreach ($document->attributes as $attribute) {
232 9
            if (!$nodeObject instanceof XmlConvertibleObject) {
233 6
                if (!in_array($attribute->name, $properties)) {
234 1
                    throw new \UnexpectedValueException(
235 1
                        get_class($nodeObject) . ' must have defined ' . $attribute->name . ' XML property',
236 1
                        4
237
                    );
238
                }
239
            }
240 9
            $nodeObject->{$attribute->name} = $attribute->value;
241
        }
242
243 8
        $nodeObject->xmlChildren = [];
244
        /** @var \DOMElement $childNode */
245 8
        foreach ($document->childNodes as $childNode) {
246 7
            $nodeObject->xmlChildren[] = static::fromXml($childNode, $aliases);
247
        }
248 8
        $nodeObject->xmlElementName = $document->nodeName;
249
250 8
        return $nodeObject;
251
    }
252
253
    /**
254
     * @param \DOMDocument|null $document
255
     * @return \DOMElement
256
     */
257 13
    public function toXml(\DOMDocument $document = null): \DOMElement
258
    {
259 13
        if (!$document) {
260 8
            $document = new \DOMDocument();
261
        }
262
263 13
        $xml = $document->createElement(
264 13
            $this->getXmlElementName()
265
        );
266 13
        if (!is_null($this->xmlChildren)) {
267 7
            foreach ((array)$this->xmlChildren as $child) {
268 7
                if ($child instanceof XmlConvertibleInterface) {
269 5
                    $xml->appendChild($child->toXml($document));
270 3
                } elseif ($child instanceof \DOMNode || $child instanceof \DOMElement) {
271 2
                    $xml->appendChild($child);
272
                } else {
273 1
                    throw new \UnexpectedValueException(
274 7
                        "Each child element must be an instance of " . XmlConvertibleInterface::class
275
                    );
276
                }
277
            }
278
        }
279
280 12
        $properties = $this->getXmlProperties();
281 12
        foreach ($properties as $property) {
282 7
            $value = $this->{$property};
283 7
            if (is_array($value) || is_object($value) || is_null($value)) {
284 3
                continue;
285
            }
286 6
            $xml->setAttribute($property, $value);
287
        }
288
289 12
        return $xml;
290
    }
291
292
    /**
293
     * Name of xml element (class name will be used by default)
294
     *
295
     * @return string
296
     */
297 19
    public function getXmlElementName(): string
298
    {
299 19
        return $this->xmlElementName ?? (new \ReflectionClass(get_called_class()))->getShortName();
300
    }
301
302
    /**
303
     * Settings name of xml element
304
     *
305
     * @param string $name
306
     * @return static
307
     */
308
    public function setXmlElementName(string $name = null)
309
    {
310
        $this->xmlElementName = $name;
311
        return $this;
312
    }
313
314
    /**
315
     * @return XmlConvertibleInterface[]|\DOMNode[]|\DOMElement[]|null
316
     */
317 3
    public function getXmlChildren()
318
    {
319 3
        return $this->xmlChildren;
320
    }
321
322
    /**
323
     * @param XmlConvertibleInterface[]|\DOMNode[]|\DOMElement[]|null $xmlChildren
324
     * @return static
325
     */
326 2
    public function setXmlChildren(array $xmlChildren = null)
327
    {
328 2
        $this->xmlChildren = $xmlChildren;
329 2
        return $this;
330
    }
331
332
    /**
333
     * Getting array of property names which will be used as attributes in created XML
334
     *
335
     * @param array|null $properties
336
     * @return array|string[]
337
     */
338 21
    public function getXmlProperties(array $properties = null): array
339
    {
340 21
        $properties = $properties
341 11
            ? Collection::from($properties)
342 17
            : Collection::from((new \ReflectionClass(get_called_class()))->getProperties(\ReflectionProperty::IS_PUBLIC))
343
                ->map(function (\ReflectionProperty $property) {
344 17
                    return $property->name;
345 21
                });
346
347
        return $properties
348
            ->filter(function (string $property): bool {
349 21
                return !in_array($property, ['xmlChildren', 'xmlElementName']);
350 21
            })
351 21
            ->array;
352
    }
353
354
    /**
355
     * Cloning all children by default
356
     */
357
    public function __clone()
358
    {
359 7
        $this->xmlChildren = array_map(function ($xmlChild) {
360 3
            return clone $xmlChild;
361 7
        }, $this->xmlChildren ?? []) ?: null;
362
    }
363
}