Passed
Push — master ( e674b7...776652 )
by Stanislau
04:06
created

XmlReader   A

Complexity

Total Complexity 31

Size/Duplication

Total Lines 175
Duplicated Lines 0 %

Test Coverage

Coverage 92.31%

Importance

Changes 3
Bugs 0 Features 0
Metric Value
eloc 72
c 3
b 0
f 0
dl 0
loc 175
ccs 72
cts 78
cp 0.9231
rs 9.92
wmc 31

6 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 4 1
A read() 0 12 3
A lookupXsd() 0 13 3
B parseClass() 0 47 9
A createDom() 0 33 5
B parseValidation() 0 38 10
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 *  This file is part of the Micro framework package.
7
 *
8
 *  (c) Stanislau Komar <[email protected]>
9
 *
10
 *  For the full copyright and license information, please view the LICENSE
11
 *  file that was distributed with this source code.
12
 */
13
14
namespace Micro\Library\DTO\Reader;
15
16
use Micro\Library\DTO\Merger\MergerFactoryInterface;
17
18
/**
19
 * @TODO: Temporary solution. MVP
20
 * @TODO: Get XSD api version
21
 */
22
class XmlReader implements ReaderInterface
23
{
24
    /**
25
     * @param iterable<string> $classDefinitionFilesCollection
26
     */
27 6
    public function __construct(
28
        private iterable $classDefinitionFilesCollection,
29
        private MergerFactoryInterface $mergerFactory
30
    ) {
31 6
    }
32
33 6
    public function read(): iterable
34
    {
35 6
        $classCollection = [];
36 6
        foreach ($this->classDefinitionFilesCollection as $filePath) {
37 6
            $xml = $this->createDom($filePath);
38
39 4
            foreach ($xml->getElementsByTagName(self::TAG_CLASS_DEFINITION) as $classDef) {
40 4
                $classCollection[] = $this->parseClass($classDef);
41
            }
42
        }
43
44 4
        return $this->mergerFactory->create($classCollection)->merge();
45
    }
46
47
    /**
48
     * @param \DOMDocument $document
49
     *
50
     * @return string[]
51
     */
52 5
    protected function lookupXsd(\DOMDocument $document): array
53
    {
54 5
        $schemaLocation = $document->getElementsByTagName('dto')[0]->getAttribute('xsi:schemaLocation');
55 5
        if (!$schemaLocation) {
56
            throw new \RuntimeException('XSD Scheme should be declared on <dto xsi:schemaLocation="">');
57
        }
58
59 5
        $location = explode(' ', $schemaLocation);
60 5
        if (2 !== \count($location)) {
61
            throw new \RuntimeException(sprintf('XSD Scheme declaration failed <dto xsi:schemaLocation="%s">', $schemaLocation));
62
        }
63
64 5
        return $location;
65
    }
66
67
    /**
68
     * @return array<string, mixed>
69
     */
70 4
    protected function parseClass(\DOMNode $classDef): array
71
    {
72 4
        $class = [];
73 4
        $props = [];
74 4
        if (null === $classDef->attributes) {
75
            return $class;
76
        }
77
78
        /** @var \DOMNode $attribute */
79 4
        foreach ($classDef->attributes as $attribute) {
80 4
            $class[$attribute->nodeName] = $attribute->nodeValue;
81
        }
82
83
        /** @var \DOMNode $node */
84 4
        foreach ($classDef->childNodes as $node) {
85 4
            if (str_starts_with($node->nodeName, '#')) {
86 4
                continue;
87
            }
88
89 4
            $propCfg = [];
90 4
            $validation = $this->parseValidation($node);
91 4
            if ($validation) {
92 1
                $propCfg['validation'] = $validation;
93
            }
94
95 4
            if (null === $node->attributes) {
96
                continue;
97
            }
98
99 4
            foreach ($node->attributes as $attribute) {
100 4
                $propCfg[$attribute->nodeName] = $attribute->nodeValue;
101
            }
102
            /**
103
             * @psalm-suppress PossiblyInvalidArgument
104
             * @psalm-suppress InvalidArgument
105
             */
106 4
            if (\array_key_exists($propCfg[self::PROP_PROP_NAME], $props)) {
107
                throw new \RuntimeException(sprintf('Property "%s" already defined. Location: %s" ',  $propCfg[self::PROP_PROP_NAME], $classDef->baseURI));
108
            }
109
110
            /** @psalm-suppress PossiblyNullArrayOffset  */
111 4
            $props[$propCfg[self::PROP_PROP_NAME]] = $propCfg;
112
        }
113
114 4
        $class[self::PATH_PROP] = $props;
115
116 4
        return $class;
117
    }
118
119
    /**
120
     * @param \DOMNode $attribute
121
     *
122
     * @return mixed[]
123
     */
124 4
    protected function parseValidation(\DOMNode $attribute): array|null
125
    {
126 4
        if (!$attribute->childNodes->count()) {
127 4
            return null;
128
        }
129
130 1
        $constraints = [];
131
        /** @var \DOMNode $validationNode */
132 1
        foreach ($attribute->childNodes as $validationNode) {
133 1
            if (!$validationNode->childNodes->count() || 'validation' !== $validationNode->nodeName) {
134 1
                continue;
135
            }
136 1
            $groupConstraints = [];
137
            /** @var \DOMNode $constraintNode */
138 1
            foreach ($validationNode->childNodes as $constraintNode) {
139 1
                if ('#text' === $constraintNode->nodeName) {
140 1
                    continue;
141
                }
142
143 1
                $constraintAttributes = [];
144
                /** @var \DOMAttr $constraintItemAttribute */
145 1
                if ($constraintNode->attributes) {
146 1
                    foreach ($constraintNode->attributes as $constraintItemAttribute) {
147 1
                        $constraintAttributes[$constraintItemAttribute->nodeName] = $constraintItemAttribute->nodeValue;
148
                    }
149
                }
150
151 1
                if (empty($constraintAttributes)) {
152 1
                    $constraintAttributes = [];
153
                }
154
155 1
                $groupConstraints[] = [$constraintNode->nodeName, $constraintAttributes];
156
            }
157
158 1
            $constraints[] = $groupConstraints;
159
        }
160
161 1
        return $constraints;
162
    }
163
164 6
    protected function createDom(string $filePath): \DOMDocument
165
    {
166 6
        if (!file_exists($filePath)) {
167 1
            throw new \RuntimeException(sprintf('File %s is not found', $filePath));
168
        }
169
170 5
        if (!is_readable($filePath)) {
171
            throw new \RuntimeException(sprintf('Has no access to read the file %s', $filePath));
172
        }
173
174 5
        $xml = new \DOMDocument();
175 5
        $xml->load($filePath);
176
177 5
        $xsdSchemaCfg = $this->lookupXsd($xml);
178 5
        $xsdSchemaLocation = $xsdSchemaCfg[1];
179
180 5
        libxml_use_internal_errors(true);
181
182 5
        if ($xml->schemaValidate($xsdSchemaLocation)) {
183 4
            return $xml;
184
        }
185
186 1
        $errs = [];
187
188 1
        foreach (libxml_get_errors() as $error) {
189 1
            $errs[] = sprintf('%s in file `%s` on line %d', $error->message, $error->file, $error->line);
190
        }
191
192 1
        $errorMessage = implode("\n ", $errs);
193
194 1
        libxml_use_internal_errors(false);
195
196 1
        throw new \RuntimeException(sprintf("Schema validation exception: \r\n %s\r", $errorMessage));
197
    }
198
}
199