Completed
Push — master ( ea6553...a25652 )
by Alexander
02:54
created

XmlConvertible::fromXml()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 8
ccs 4
cts 4
cp 1
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 4
nc 2
nop 2
crap 2
1
<?php
2
3
namespace Horat1us;
4
5
use Horat1us\Arrays\Collection;
6
use Horat1us\Services\XmlParserService;
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 bool $skipEmpty
29
     * @return XmlConvertible|XmlConvertibleInterface|null
30
     */
31 4
    public function xmlIntersect(
32
        XmlConvertibleInterface $xml,
33
        bool $skipEmpty = true
34
    )
35
    {
36 4
        $current = clone $this;
37 4
        $compared = clone $xml;
38
39
        if (
40 4
            $current->getXmlElementName() !== $compared->getXmlElementName()
41 3
            || array_reduce(
42 3
                $current->getXmlProperties(),
43
                function (bool $carry, string $property) use ($compared, $current) : bool {
44 3
                    return $carry
45 3
                        || (!property_exists($compared, $property))
46 3
                        || $current->{$property} !== $compared->{$property};
47 3
                },
48 4
                false
49
            )
50
        ) {
51 1
            return null;
52
        }
53
54 3
        $newChildren = array_uintersect(
55 3
            $compared->getXmlChildren() ?? [],
56 3
            $current->getXmlChildren() ?? [],
57
            function ($comparedChild, $currentChild) use ($skipEmpty) {
58 2
                if ($comparedChild === $currentChild) {
59
                    return 0;
60
                }
61
62 2
                $diff = ($currentChild instanceof XmlConvertibleInterface
63 2
                    ? $currentChild
64
                    : XmlConvertibleObject::fromXml($currentChild)
65 2
                )->xmlIntersect(
66
                    $comparedChild instanceof XmlConvertibleInterface
67 2
                        ? $comparedChild
68 2
                        : XmlConvertibleObject::fromXml($comparedChild)
69
                );
70
71 2
                return $diff === null ? -1 : 0;
72 3
            }
73
        );
74
75 3
        return $current->setXmlChildren($newChildren);
76
    }
77
78
    /**
79
     * @param XmlConvertibleInterface $xml
80
     * @return XmlConvertible|null
81
     */
82 4
    public function xmlDiff(XmlConvertibleInterface $xml)
83
    {
84 4
        $current = $this;
85 4
        $compared = $xml;
86
87
        if (
88 4
            $current->getXmlElementName() !== $compared->getXmlElementName()
89 4
            || empty($current->getXmlChildren()) && !empty($compared->getXmlChildren())
90 3
            || array_reduce(
91 3
                $current->getXmlProperties(),
92
                function (bool $carry, string $property) use ($compared, $current) : bool {
93 3
                    return $carry
94 3
                        || (!property_exists($compared, $property))
95 3
                        || $current->{$property} !== $compared->{$property};
96 3
                },
97 4
                false
98
            )
99
        ) {
100 4
            return clone $current;
101
        }
102
103
104 2
        $newChildren = Collection::from($current->getXmlChildren() ?? [])
105
            ->map(function ($child) use ($compared) {
106 2
                return array_reduce(
107 2
                    $compared->getXmlChildren() ?? [],
108
                    function ($carry, $comparedChild) use ($child) {
109 2
                        if ($carry) {
110
                            return $carry;
111
                        }
112
113 2
                        $diff = ($child instanceof XmlConvertibleInterface
114 2
                            ? $child
115
                            : XmlConvertibleObject::fromXml($child)
116 2
                        )->xmlDiff(
117
                            $comparedChild instanceof XmlConvertibleInterface
118 2
                                ? $comparedChild
119 2
                                : XmlConvertibleObject::fromXml($comparedChild)
120
                        );
121
122 2
                        return $diff;
123 2
                    });
124 2
            })
125
            ->filter(function ($child) {
126 2
                return $child !== null;
127 2
            })
128 2
            ->array;
129
130 2
        if (empty($newChildren)) {
131 2
            return null;
132
        }
133
134 2
        $target = clone $current;
135 2
        $target->setXmlChildren($newChildren);
136
137 2
        return clone $target;
138
    }
139
140
    /**
141
     * Converts object to XML and compares it with given
142
     *
143
     * @param XmlConvertibleInterface $xml
144
     * @return bool
145
     */
146 3
    public function xmlEqual(XmlConvertibleInterface $xml): bool
147
    {
148 3
        $document = new \DOMDocument();
149 3
        $document->appendChild($this->toXml($document));
150 3
        $current = $document->saveXML();
151
152 3
        $document = new \DOMDocument();
153 3
        $document->appendChild($xml->toXml($document));
154 3
        $compared = $document->saveXML();
155
156 3
        return $current === $compared;
157
    }
158
159
    /**
160
     * @param \DOMDocument|\DOMElement $document
161
     * @param array $aliases
162
     * @return XmlConvertibleInterface
163
     */
164 12
    public static function fromXml($document, array $aliases = [])
165
    {
166
        /** @var \DOMElement $document */
167 12
        if (!in_array(get_called_class(), $aliases)) {
168 7
            $aliases[(new \ReflectionClass(get_called_class()))->getShortName()] = get_called_class();
169
        }
170 12
        return (new XmlParserService($document, $aliases))->convert();
171
    }
172
173
    /**
174
     * @param \DOMDocument|null $document
175
     * @return \DOMElement
176
     */
177 13
    public function toXml(\DOMDocument $document = null): \DOMElement
178
    {
179 13
        $document = $document ?? new \DOMDocument();
180
181 13
        $xml = $document->createElement(
182 13
            $this->getXmlElementName()
183
        );
184
185 13
        Collection::from($this->xmlChildren ?? [])
186
            ->map(function($child) use($document) {
187 6
                return $child instanceof XmlConvertibleInterface
188 5
                    ? $child->toXml($document)
189 6
                    : $child;
190 12
            })
191
            ->forEach(function(\DOMNode $child) use($xml) {
192 6
                $xml->appendChild($child);
193 12
            });
194
195
196 12
        Collection::from($this->getXmlProperties())
197
            ->reduce(function(Collection $properties, string $property) {
198 7
                $properties[$property] = $this->{$property};
199 7
                return $properties;
200 12
            }, Collection::create())
201
            ->filter(function($value) :bool {
202 7
                return !is_array($value) && !is_object($value) && !is_null($value);
203 12
            })
204
            ->forEach(function($value, string $property) use($xml) {
205 6
                $xml->setAttribute($property, $value);
206 12
            });
207
208 12
        return $xml;
209
    }
210
211
    /**
212
     * Name of xml element (class name will be used by default)
213
     *
214
     * @return string
215
     */
216 22
    public function getXmlElementName(): string
217
    {
218 22
        return $this->xmlElementName ?? (new \ReflectionClass(get_called_class()))->getShortName();
219
    }
220
221
    /**
222
     * Settings name of xml element
223
     *
224
     * @param string $name
225
     * @return static
226
     */
227 1
    public function setXmlElementName(string $name = null)
228
    {
229 1
        $this->xmlElementName = $name;
230 1
        return $this;
231
    }
232
233
    /**
234
     * @return XmlConvertibleInterface[]|\DOMNode[]|\DOMElement[]|null
235
     */
236 9
    public function getXmlChildren()
237
    {
238 9
        return $this->xmlChildren;
239
    }
240
241
    /**
242
     * @param XmlConvertibleInterface[]|\DOMNode[]|\DOMElement[]|null $xmlChildren
243
     * @return static
244
     */
245 14
    public function setXmlChildren(array $xmlChildren = null)
246
    {
247 14
        $this->xmlChildren = $xmlChildren ?: null;
248 14
        return $this;
249
    }
250
251
    /**
252
     * Getting array of property names which will be used as attributes in created XML
253
     *
254
     * @param array|null $properties
255
     * @return array|string[]
256
     */
257 22
    public function getXmlProperties(array $properties = null): array
258
    {
259 22
        $properties = $properties
260
            ?: array_map(function(\ReflectionProperty $property) {
261 18
                return $property->getName();
262 22
            }, (new \ReflectionClass(get_called_class()))->getProperties(\ReflectionProperty::IS_PUBLIC));
263
264
265
        return array_filter($properties, function(string $property) {
266 22
            return !in_array($property, ['xmlChildren', 'xmlElementName']);
267 22
        });
268
    }
269
270
    /**
271
     * Cloning all children by default
272
     */
273
    public function __clone()
274
    {
275 18
        $this->xmlChildren = array_map(function ($xmlChild) {
276 5
            return clone $xmlChild;
277 18
        }, $this->xmlChildren ?? []) ?: null;
278
    }
279
}