Passed
Branch master (bd58e5)
by Alexander
12:57
created

XmlConvertible::getXmlElementName()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

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