Passed
Branch master (532e7d)
by Alexander
03:23
created

XmlConvertible::xmlIntersect()   C

Complexity

Conditions 16
Paths 16

Size

Total Lines 62
Code Lines 40

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 29
CRAP Score 16

Importance

Changes 0
Metric Value
dl 0
loc 62
rs 6.1338
c 0
b 0
f 0
ccs 29
cts 29
cp 1
cc 16
eloc 40
nc 16
nop 3
crap 16

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