Passed
Branch master (edc8b7)
by Alexander
02:27
created

XmlConvertible   B

Complexity

Total Complexity 53

Size/Duplication

Total Lines 321
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 3

Test Coverage

Coverage 100%

Importance

Changes 0
Metric Value
wmc 53
lcom 1
cbo 3
dl 0
loc 321
ccs 139
cts 139
cp 1
rs 7.4757
c 0
b 0
f 0

11 Methods

Rating   Name   Duplication   Size   Complexity  
A getXmlProperties() 0 15 2
A __clone() 0 6 2
D xmlIntersect() 0 45 9
C xmlDiff() 0 57 11
A xmlEqual() 0 12 1
C fromXml() 0 63 14
D toXml() 0 29 9
A getXmlElementName() 0 4 1
A setXmlElementName() 0 5 1
A getXmlChildren() 0 4 1
A setXmlChildren() 0 5 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
7
/**
8
 * Class XmlConvertible
9
 * @package Horat1us
10
 */
11
trait XmlConvertible
12
{
13
    /**
14
     * @var XmlConvertibleInterface[]|\DOMNode[]|\DOMElement[]|null
15
     */
16
    public $xmlChildren;
17
18
    /**
19
     * Name of xml element (class name will be used by default)
20
     *
21
     * @var string
22
     */
23
    public $xmlElementName;
24
25
    /**
26
     * @param XmlConvertibleInterface $xml
27
     * @param bool $skipEmpty
28
     * @return XmlConvertible|XmlConvertibleInterface|null
29
     */
30 4
    public function xmlIntersect(
31
        XmlConvertibleInterface $xml,
32
        bool $skipEmpty = true
33
    )
34
    {
35 4
        $current = clone $this;
36 4
        $compared = clone $xml;
37
38 4
        if ($current->getXmlElementName() !== $compared->getXmlElementName()) {
39 1
            return null;
40
        }
41
42 3
        foreach ($current->getXmlProperties() as $property) {
43 3
            if (!property_exists($compared, $property) || $current->{$property} !== $compared->{$property}) {
44 3
                return null;
45
            }
46
        }
47
48 3
        $newChildren = array_uintersect(
49 3
            $compared->getXmlChildren() ?? [],
50 3
            $current->getXmlChildren() ?? [],
51
            function ($comparedChild, $currentChild) use ($skipEmpty) {
52 2
                if ($comparedChild === $currentChild) {
53
                    return 0;
54
                }
55
                if (
56 2
                    $currentChild instanceof XmlConvertibleInterface
57 2
                    xor $comparedChild instanceof XmlConvertibleInterface
58
                ) {
59
                    return -1;
60
                }
61 2
                if (!$currentChild instanceof XmlConvertibleInterface) {
62
                    $comparedChild = XmlConvertibleObject::fromXml($comparedChild);
63
                    $currentChild = XmlConvertibleObject::fromXml($currentChild);
64
65
                }
66 2
                $intersected = $currentChild->xmlIntersect($comparedChild, $skipEmpty) !== null;
67 2
                return $intersected ? 0 : -1;
68 3
            }
69
        );
70
71 3
        $current->setXmlChildren($newChildren);
72
73 3
        return $current;
74
    }
75
76
    /**
77
     * @param XmlConvertibleInterface $xml
78
     * @return XmlConvertible|null
79
     */
80 4
    public function xmlDiff(XmlConvertibleInterface $xml)
81
    {
82 4
        $current = $this;
83 4
        $compared = $xml;
84
85
        if (
86 4
            $current->getXmlElementName() !== $compared->getXmlElementName()
87 4
            || empty($current->getXmlChildren()) && !empty($compared->getXmlChildren())
88 3
            || array_reduce(
89 3
                $current->getXmlProperties(),
90
                function (bool $carry, string $property) use ($compared, $current) : bool {
91 3
                    return $carry
92 3
                        || (!property_exists($compared, $property))
93 3
                        || $current->{$property} !== $compared->{$property};
94 3
                },
95 4
                false
96
            )
97
        ) {
98 4
            return clone $current;
99
        }
100
101
102 2
        $newChildren = Collection::from($current->getXmlChildren() ?? [])
103
            ->map(function ($child) use ($compared) {
104 2
                return array_reduce(
105 2
                    $compared->getXmlChildren() ?? [],
106
                    function ($carry, $comparedChild) use ($child) {
107 2
                        if ($carry) {
108
                            return $carry;
109
                        }
110
111 2
                        $diff = ($child instanceof XmlConvertibleInterface
112 2
                            ? $child
113
                            : XmlConvertibleObject::fromXml($child)
114 2
                        )->xmlDiff(
115
                            $comparedChild instanceof XmlConvertibleInterface
116 2
                                ? $comparedChild
117 2
                                : XmlConvertibleObject::fromXml($comparedChild)
118
                        );
119
120 2
                        return $diff;
121 2
                    });
122 2
            })
123
            ->filter(function ($child) {
124 2
                return $child !== null;
125 2
            })
126 2
            ->array;
127
128 2
        if (empty($newChildren)) {
129 2
            return null;
130
        }
131
132 2
        $target = clone $current;
133 2
        $target->setXmlChildren($newChildren);
134
135 2
        return clone $target;
136
    }
137
138
    /**
139
     * Converts object to XML and compares it with given
140
     *
141
     * @param XmlConvertibleInterface $xml
142
     * @return bool
143
     */
144 3
    public function xmlEqual(XmlConvertibleInterface $xml): bool
145
    {
146 3
        $document = new \DOMDocument();
147 3
        $document->appendChild($this->toXml($document));
148 3
        $current = $document->saveXML();
149
150 3
        $document = new \DOMDocument();
151 3
        $document->appendChild($xml->toXml($document));
152 3
        $compared = $document->saveXML();
153
154 3
        return $current === $compared;
155
    }
156
157
    /**
158
     * @param \DOMDocument|\DOMElement $document
159
     * @param array $aliases
160
     * @return static
161
     */
162 12
    public static function fromXml($document, array $aliases = [])
163
    {
164 12
        if ($document instanceof \DOMDocument) {
165 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...
166
        }
167
168
        /** @var \DOMElement $document */
169 12
        if (!in_array(get_called_class(), $aliases)) {
170 9
            $aliases[(new \ReflectionClass(get_called_class()))->getShortName()] = get_called_class();
171
        }
172 12
        foreach ($aliases as $key => $alias) {
173 12
            if (is_object($alias)) {
174 3
                if (!$alias instanceof XmlConvertibleInterface) {
175 1
                    throw new \UnexpectedValueException(
176 1
                        "All aliases must be instance or class implements " . XmlConvertibleInterface::class,
177 1
                        1
178
                    );
179
                }
180 2
                $aliases[is_int($key) ? $alias->getXmlElementName() : $key] = $alias;
181 2
                continue;
182
            }
183 11
            if (!is_string($alias)) {
184 1
                throw new \UnexpectedValueException(
185 1
                    "All aliases must be instance or class implements " . XmlConvertibleInterface::class,
186 1
                    2
187
                );
188
            }
189 10
            $instance = new $alias;
190 10
            if (!$instance instanceof XmlConvertibleInterface) {
191 1
                throw new \UnexpectedValueException(
192 1
                    "All aliases must be instance of " . XmlConvertibleInterface::class,
193 1
                    3
194
                );
195
            }
196 9
            unset($aliases[$key]);
197 9
            $aliases[is_int($key) ? $instance->getXmlElementName() : $key] = $instance;
198
        }
199
200 9
        $nodeObject = $aliases[$document->nodeName] ?? new XmlConvertibleObject;
201 9
        $properties = $nodeObject->getXmlProperties();
202
203
        /** @var \DOMAttr $attribute */
204 9
        foreach ($document->attributes as $attribute) {
205 9
            if (!$nodeObject instanceof XmlConvertibleObject) {
206 6
                if (!in_array($attribute->name, $properties)) {
207 1
                    throw new \UnexpectedValueException(
208 1
                        get_class($nodeObject) . ' must have defined ' . $attribute->name . ' XML property',
209 1
                        4
210
                    );
211
                }
212
            }
213 9
            $nodeObject->{$attribute->name} = $attribute->value;
214
        }
215
216 8
        $nodeObject->xmlChildren = [];
217
        /** @var \DOMElement $childNode */
218 8
        foreach ($document->childNodes as $childNode) {
219 7
            $nodeObject->xmlChildren[] = static::fromXml($childNode, $aliases);
220
        }
221 8
        $nodeObject->xmlElementName = $document->nodeName;
222
223 8
        return $nodeObject;
224
    }
225
226
    /**
227
     * @param \DOMDocument|null $document
228
     * @return \DOMElement
229
     */
230 13
    public function toXml(\DOMDocument $document = null): \DOMElement
231
    {
232 13
        if (!$document) {
233 8
            $document = new \DOMDocument();
234
        }
235
236 13
        $xml = $document->createElement(
237 13
            $this->getXmlElementName()
238
        );
239 13
        if (!empty($this->xmlChildren)) {
240 7
            foreach ((array)$this->xmlChildren as $child) {
241 7
                $xml->appendChild(
242 5
                    $child instanceof XmlConvertibleInterface ? $child->toXml($document)
243 7
                        : $child
244
                );
245
            }
246
        }
247
248 12
        $properties = $this->getXmlProperties();
249 12
        foreach ($properties as $property) {
250 7
            $value = $this->{$property};
251 7
            if (is_array($value) || is_object($value) || is_null($value)) {
252 3
                continue;
253
            }
254 6
            $xml->setAttribute($property, $value);
255
        }
256
257 12
        return $xml;
258
    }
259
260
    /**
261
     * Name of xml element (class name will be used by default)
262
     *
263
     * @return string
264
     */
265 22
    public function getXmlElementName(): string
266
    {
267 22
        return $this->xmlElementName ?? (new \ReflectionClass(get_called_class()))->getShortName();
268
    }
269
270
    /**
271
     * Settings name of xml element
272
     *
273
     * @param string $name
274
     * @return static
275
     */
276 1
    public function setXmlElementName(string $name = null)
277
    {
278 1
        $this->xmlElementName = $name;
279 1
        return $this;
280
    }
281
282
    /**
283
     * @return XmlConvertibleInterface[]|\DOMNode[]|\DOMElement[]|null
284
     */
285 9
    public function getXmlChildren()
286
    {
287 9
        return $this->xmlChildren;
288
    }
289
290
    /**
291
     * @param XmlConvertibleInterface[]|\DOMNode[]|\DOMElement[]|null $xmlChildren
292
     * @return static
293
     */
294 6
    public function setXmlChildren(array $xmlChildren = null)
295
    {
296 6
        $this->xmlChildren = $xmlChildren ?: null;
297 6
        return $this;
298
    }
299
300
    /**
301
     * Getting array of property names which will be used as attributes in created XML
302
     *
303
     * @param array|null $properties
304
     * @return array|string[]
305
     */
306 22
    public function getXmlProperties(array $properties = null): array
307
    {
308 22
        $properties = $properties
309 12
            ? Collection::from($properties)
310 18
            : Collection::from((new \ReflectionClass(get_called_class()))->getProperties(\ReflectionProperty::IS_PUBLIC))
311
                ->map(function (\ReflectionProperty $property) {
312 18
                    return $property->name;
313 22
                });
314
315
        return $properties
316
            ->filter(function (string $property): bool {
317 22
                return !in_array($property, ['xmlChildren', 'xmlElementName']);
318 22
            })
319 22
            ->array;
320
    }
321
322
    /**
323
     * Cloning all children by default
324
     */
325
    public function __clone()
326
    {
327 11
        $this->xmlChildren = array_map(function ($xmlChild) {
328 5
            return clone $xmlChild;
329 11
        }, $this->xmlChildren ?? []) ?: null;
330
    }
331
}