Passed
Branch master (94149a)
by Alexander
02:22
created

XmlConvertible::xmlEqual()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 12
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 12
rs 9.4285
c 0
b 0
f 0
ccs 8
cts 8
cp 1
cc 1
eloc 8
nc 1
nop 1
crap 1
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
                        if (
111 2
                            $comparedChild === $child
112
                            || (
113
                                $child instanceof XmlConvertibleInterface
114 2
                                xor $comparedChild instanceof XmlConvertibleInterface
115
                            )
116
                        ) {
117
                            return false;
118
                        }
119 2
                        $isXml = false;
120 2
                        if (!$child instanceof XmlConvertibleInterface) {
121
                            $comparedChild = XmlConvertibleObject::fromXml($comparedChild);
122
                            $child = XmlConvertibleObject::fromXml($child);
0 ignored issues
show
Bug introduced by
Consider using a different name than the imported variable $child, or did you forget to import by reference?

It seems like you are assigning to a variable which was imported through a use statement which was not imported by reference.

For clarity, we suggest to use a different name or import by reference depending on whether you would like to have the change visibile in outer-scope.

Change not visible in outer-scope

$x = 1;
$callable = function() use ($x) {
    $x = 2; // Not visible in outer scope. If you would like this, how
            // about using a different variable name than $x?
};

$callable();
var_dump($x); // integer(1)

Change visible in outer-scope

$x = 1;
$callable = function() use (&$x) {
    $x = 2;
};

$callable();
var_dump($x); // integer(2)
Loading history...
123
                            $isXml = true;
124
                        }
125 2
                        $diff = $child->xmlDiff($comparedChild);
126 2
                        if ($diff) {
127 2
                            return $isXml ? $diff->toXml($child) : $diff;
0 ignored issues
show
Documentation introduced by
$child is of type object<Horat1us\XmlConvertibleInterface>, but the function expects a null|object<DOMDocument>.

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...
128
                        }
129 2
                        return null;
130 2
                    });
131 2
            })
132
            ->filter(function ($child) {
133 2
                return $child !== null;
134 2
            })
135 2
            ->array;
136
137 2
        if (empty($newChildren)) {
138 2
            return null;
139
        }
140
141 2
        $target = clone $current;
142 2
        $target->setXmlChildren($newChildren);
143
144 2
        return clone $target;
145
    }
146
147
    /**
148
     * Converts object to XML and compares it with given
149
     *
150
     * @param XmlConvertibleInterface $xml
151
     * @return bool
152
     */
153 3
    public function xmlEqual(XmlConvertibleInterface $xml): bool
154
    {
155 3
        $document = new \DOMDocument();
156 3
        $document->appendChild($this->toXml($document));
157 3
        $current = $document->saveXML();
158
159 3
        $document = new \DOMDocument();
160 3
        $document->appendChild($xml->toXml($document));
161 3
        $compared = $document->saveXML();
162
163 3
        return $current === $compared;
164
    }
165
166
    /**
167
     * @param \DOMDocument|\DOMElement $document
168
     * @param array $aliases
169
     * @return static
170
     */
171 12
    public static function fromXml($document, array $aliases = [])
172
    {
173 12
        if ($document instanceof \DOMDocument) {
174 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...
175
        }
176
177
        /** @var \DOMElement $document */
178 12
        if (!in_array(get_called_class(), $aliases)) {
179 9
            $aliases[(new \ReflectionClass(get_called_class()))->getShortName()] = get_called_class();
180
        }
181 12
        foreach ($aliases as $key => $alias) {
182 12
            if (is_object($alias)) {
183 3
                if (!$alias instanceof XmlConvertibleInterface) {
184 1
                    throw new \UnexpectedValueException(
185 1
                        "All aliases must be instance or class implements " . XmlConvertibleInterface::class,
186 1
                        1
187
                    );
188
                }
189 2
                $aliases[is_int($key) ? $alias->getXmlElementName() : $key] = $alias;
190 2
                continue;
191
            }
192 11
            if (!is_string($alias)) {
193 1
                throw new \UnexpectedValueException(
194 1
                    "All aliases must be instance or class implements " . XmlConvertibleInterface::class,
195 1
                    2
196
                );
197
            }
198 10
            $instance = new $alias;
199 10
            if (!$instance instanceof XmlConvertibleInterface) {
200 1
                throw new \UnexpectedValueException(
201 1
                    "All aliases must be instance of " . XmlConvertibleInterface::class,
202 1
                    3
203
                );
204
            }
205 9
            unset($aliases[$key]);
206 9
            $aliases[is_int($key) ? $instance->getXmlElementName() : $key] = $instance;
207
        }
208
209 9
        $nodeObject = $aliases[$document->nodeName] ?? new XmlConvertibleObject;
210 9
        $properties = $nodeObject->getXmlProperties();
211
212
        /** @var \DOMAttr $attribute */
213 9
        foreach ($document->attributes as $attribute) {
214 9
            if (!$nodeObject instanceof XmlConvertibleObject) {
215 6
                if (!in_array($attribute->name, $properties)) {
216 1
                    throw new \UnexpectedValueException(
217 1
                        get_class($nodeObject) . ' must have defined ' . $attribute->name . ' XML property',
218 1
                        4
219
                    );
220
                }
221
            }
222 9
            $nodeObject->{$attribute->name} = $attribute->value;
223
        }
224
225 8
        $nodeObject->xmlChildren = [];
226
        /** @var \DOMElement $childNode */
227 8
        foreach ($document->childNodes as $childNode) {
228 7
            $nodeObject->xmlChildren[] = static::fromXml($childNode, $aliases);
229
        }
230 8
        $nodeObject->xmlElementName = $document->nodeName;
231
232 8
        return $nodeObject;
233
    }
234
235
    /**
236
     * @param \DOMDocument|null $document
237
     * @return \DOMElement
238
     */
239 13
    public function toXml(\DOMDocument $document = null): \DOMElement
240
    {
241 13
        if (!$document) {
242 8
            $document = new \DOMDocument();
243
        }
244
245 13
        $xml = $document->createElement(
246 13
            $this->getXmlElementName()
247
        );
248 13
        if (!empty($this->xmlChildren)) {
249 7
            foreach ((array)$this->xmlChildren as $child) {
250 7
                $xml->appendChild(
251 5
                    $child instanceof XmlConvertibleInterface ? $child->toXml($document)
252 7
                        : $child
253
                );
254
            }
255
        }
256
257 12
        $properties = $this->getXmlProperties();
258 12
        foreach ($properties as $property) {
259 7
            $value = $this->{$property};
260 7
            if (is_array($value) || is_object($value) || is_null($value)) {
261 3
                continue;
262
            }
263 6
            $xml->setAttribute($property, $value);
264
        }
265
266 12
        return $xml;
267
    }
268
269
    /**
270
     * Name of xml element (class name will be used by default)
271
     *
272
     * @return string
273
     */
274 22
    public function getXmlElementName(): string
275
    {
276 22
        return $this->xmlElementName ?? (new \ReflectionClass(get_called_class()))->getShortName();
277
    }
278
279
    /**
280
     * Settings name of xml element
281
     *
282
     * @param string $name
283
     * @return static
284
     */
285 1
    public function setXmlElementName(string $name = null)
286
    {
287 1
        $this->xmlElementName = $name;
288 1
        return $this;
289
    }
290
291
    /**
292
     * @return XmlConvertibleInterface[]|\DOMNode[]|\DOMElement[]|null
293
     */
294 9
    public function getXmlChildren()
295
    {
296 9
        return $this->xmlChildren;
297
    }
298
299
    /**
300
     * @param XmlConvertibleInterface[]|\DOMNode[]|\DOMElement[]|null $xmlChildren
301
     * @return static
302
     */
303 6
    public function setXmlChildren(array $xmlChildren = null)
304
    {
305 6
        $this->xmlChildren = $xmlChildren ?: null;
306 6
        return $this;
307
    }
308
309
    /**
310
     * Getting array of property names which will be used as attributes in created XML
311
     *
312
     * @param array|null $properties
313
     * @return array|string[]
314
     */
315 22
    public function getXmlProperties(array $properties = null): array
316
    {
317 22
        $properties = $properties
318 12
            ? Collection::from($properties)
319 18
            : Collection::from((new \ReflectionClass(get_called_class()))->getProperties(\ReflectionProperty::IS_PUBLIC))
320
                ->map(function (\ReflectionProperty $property) {
321 18
                    return $property->name;
322 22
                });
323
324
        return $properties
325
            ->filter(function (string $property): bool {
326 22
                return !in_array($property, ['xmlChildren', 'xmlElementName']);
327 22
            })
328 22
            ->array;
329
    }
330
331
    /**
332
     * Cloning all children by default
333
     */
334
    public function __clone()
335
    {
336 11
        $this->xmlChildren = array_map(function ($xmlChild) {
337 5
            return clone $xmlChild;
338 11
        }, $this->xmlChildren ?? []) ?: null;
339
    }
340
}