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

XmlParserService::convert()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 17
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 4

Importance

Changes 0
Metric Value
dl 0
loc 17
ccs 9
cts 9
cp 1
rs 9.2
c 0
b 0
f 0
cc 4
eloc 10
nc 4
nop 0
crap 4
1
<?php
2
3
namespace Horat1us\Services;
4
5
6
use Horat1us\Arrays\Collection;
7
use Horat1us\XmlAliasKey;
8
use Horat1us\XmlConvertibleInterface;
9
use Horat1us\XmlConvertibleObject;
10
11
/**
12
 * Class XmlParserService
13
 * @package Horat1us\Services
14
 */
15
class XmlParserService
16
{
17
    /**
18
     * @var \DOMElement
19
     */
20
    protected $element;
21
22
    /**
23
     * @var XmlConvertibleInterface[]
24
     */
25
    protected $aliases;
26
27
    /**
28
     * XmlParserService constructor.
29
     * @param $document
30
     * @param array $aliases
31
     */
32 12
    public function __construct($document, array $aliases = [])
33
    {
34
        $this
35 12
            ->setAliases($aliases)
36 9
            ->setDocument($document);
37 9
    }
38
39 8
    public function __clone()
40
    {
41 8
        $this->element = clone $this->element;
42 8
    }
43
44
    /**
45
     * @return XmlConvertibleInterface
46
     */
47 9
    public function convert()
48
    {
49
        /** @var XmlConvertibleInterface $nodeObject */
50 9
        $nodeObject = Collection::from($this->aliases)
51
            ->reduce(function (XmlConvertibleInterface $carry, XmlConvertibleInterface $alias, string $key) {
52 9
                return $this->getIsAliasMatch($key, $alias) ? clone $alias : $carry;
53 9
            }, new XmlConvertibleObject($this->element->nodeName));
54
55 9
        if ($this->element->hasChildNodes()) {
56 8
            $this->convertChildren($nodeObject);
57
        }
58 9
        if ($this->element->hasAttributes()) {
59 9
            $this->convertAttributes($nodeObject);
60
        }
61
62 9
        return $nodeObject;
63
    }
64
65
    /**
66
     * @param XmlConvertibleInterface $object
67
     * @return $this
68
     */
69 8
    public function convertChildren(XmlConvertibleInterface &$object)
70
    {
71 8
        $children = [];
72 8
        $service = clone $this;
73
74 8
        foreach ($this->element->childNodes as $childNode) {
75 8
            $service->setDocument($childNode);
76 8
            $children[] = $service->convert();
77
        }
78 8
        $object->setXmlChildren($children);
79
80 8
        return $this;
81
    }
82
83 9
    public function convertAttributes(XmlConvertibleInterface &$object)
84
    {
85 9
        $properties = $object->getXmlProperties();
86
        /** @var \DOMAttr $attribute */
87 9
        foreach ($this->element->attributes as $attribute) {
88
            if (
89 9
                !$object instanceof XmlConvertibleObject
90 9
                && !in_array($attribute->name, $properties)
91
            ) {
92 1
                throw new \UnexpectedValueException(
93 1
                    get_class($object) . ' must have defined ' . $attribute->name . ' XML property',
94 1
                    4
95
                );
96
            }
97 9
            $object->{$attribute->name} = $attribute->value;
98
        }
99 9
    }
100
101
    /**
102
     * @param \DOMNode|\DOMDocument $document
103
     * @return $this
104
     */
105 9
    public function setDocument($document)
106
    {
107 9
        if ($document instanceof \DOMDocument) {
108 7
            return $this->setDocument($document->firstChild);
109
        }
110
111 9
        if (!$document instanceof \DOMNode) {
112
            throw new \InvalidArgumentException("Document must be instance of DOMElement or DOMNode");
113
        }
114 9
        $this->element = $document;
0 ignored issues
show
Documentation Bug introduced by
$document is of type object<DOMNode>, but the property $element was declared to be of type object<DOMElement>. Are you sure that you always receive this specific sub-class here, or does it make sense to add an instanceof check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a given class or a super-class is assigned to a property that is type hinted more strictly.

Either this assignment is in error or an instanceof check should be added for that assignment.

class Alien {}

class Dalek extends Alien {}

class Plot
{
    /** @var  Dalek */
    public $villain;
}

$alien = new Alien();
$plot = new Plot();
if ($alien instanceof Dalek) {
    $plot->villain = $alien;
}
Loading history...
115
116 9
        return $this;
117
    }
118
119
    /**
120
     * @return \DOMElement
121
     */
122
    public function getDocument()
123
    {
124
        return $this->element;
125
    }
126
127
    /**
128
     * @param XmlConvertibleInterface[]|string $aliases
129
     * @return $this
130
     */
131 12
    public function setAliases(array $aliases = [])
132
    {
133 12
        $this->aliases = [];
134
135 12
        Collection::from($aliases)
136
            ->map(function ($alias) {
137 12
                return $this->mapAlias($alias);
138 12
            })
139
            ->forEach(function (XmlConvertibleInterface $alias, string $key) {
140 9
                $this->aliases[$this->mapKey($key, $alias)] = $alias;
141 9
            });
142
143 9
        return $this;
144
    }
145
146
    /**
147
     * @return XmlConvertibleInterface[]
148
     */
149
    public function getAliases()
150
    {
151
        return $this->aliases;
152
    }
153
154
    /**
155
     * @param string|XmlConvertibleInterface $element
156
     * @return XmlConvertibleInterface
157
     */
158 12
    protected function mapAlias($element)
159
    {
160 12
        if ($element instanceof XmlConvertibleInterface) {
161 9
            return $element;
162
        }
163 12
        if (!is_string($element)) {
164 3
            throw new \UnexpectedValueException(
165 3
                "All aliases must be instance or class implements " . XmlConvertibleInterface::class
166
            );
167
        }
168
169
        // Additional type-checking
170 10
        return $this->mapAlias(new $element);
171
    }
172
173
    /**
174
     * @param string|integer $key
175
     * @param XmlConvertibleInterface $element
176
     * @return string
177
     */
178 9
    protected function mapKey($key, XmlConvertibleInterface $element): string
179
    {
180 9
        return is_numeric($key)
181
            ? $element->getXmlElementName()
182 9
            : $key;
183
    }
184
185
    /**
186
     * @param string $key
187
     * @param XmlConvertibleInterface $element
188
     * @return bool
189
     */
190 9
    protected function getIsAliasMatch(string $key, XmlConvertibleInterface $element): bool
191
    {
192 9
        if ($this->element->nodeName !== $key) {
193 7
            return false;
194
        }
195
196 8
        return Collection::from($element->getXmlProperties())
197
            ->filter(function ($property) use ($element) {
198 6
                return empty($element->{$property});
199 8
            })
200 8
            ->reduce(function (bool $carry, $property) use ($element) {
201 6
                return $carry && $this->element->attributes->getNamedItem($property) != $element->{$property};
202 8
            }, true);
203
    }
204
}