|
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 |
|
return $this->parseBody($classDef); |
|
73
|
4 |
|
// dump($this->parseBody($classDef)); exit; |
|
74
|
4 |
|
|
|
75
|
|
|
$class = []; |
|
|
|
|
|
|
76
|
|
|
$props = []; |
|
77
|
|
|
if (null === $classDef->attributes) { |
|
78
|
|
|
return $class; |
|
79
|
4 |
|
} |
|
80
|
4 |
|
|
|
81
|
|
|
/** @var \DOMNode $attribute */ |
|
82
|
|
|
foreach ($classDef->attributes as $attribute) { |
|
83
|
|
|
$class[$attribute->nodeName] = $attribute->nodeValue; |
|
84
|
4 |
|
} |
|
85
|
4 |
|
|
|
86
|
4 |
|
/** @var \DOMNode $node */ |
|
87
|
|
|
foreach ($classDef->childNodes as $node) { |
|
88
|
|
|
if (str_starts_with($node->nodeName, '#')) { |
|
89
|
4 |
|
continue; |
|
90
|
4 |
|
} |
|
91
|
4 |
|
|
|
92
|
1 |
|
$propCfg = []; |
|
93
|
|
|
$validation = $this->parseValidation($node); |
|
94
|
|
|
if ($validation) { |
|
95
|
4 |
|
$propCfg['validation'] = $validation; |
|
96
|
|
|
} |
|
97
|
|
|
|
|
98
|
|
|
if (null === $node->attributes) { |
|
99
|
4 |
|
continue; |
|
100
|
4 |
|
} |
|
101
|
|
|
|
|
102
|
|
|
foreach ($node->attributes as $attribute) { |
|
103
|
|
|
$propCfg[$attribute->nodeName] = $attribute->nodeValue; |
|
104
|
|
|
} |
|
105
|
|
|
/** |
|
106
|
4 |
|
* @psalm-suppress PossiblyInvalidArgument |
|
107
|
|
|
* @psalm-suppress InvalidArgument |
|
108
|
|
|
*/ |
|
109
|
|
|
if (\array_key_exists($propCfg[self::PROP_PROP_NAME], $props)) { |
|
110
|
|
|
throw new \RuntimeException(sprintf('Property "%s" already defined. Location: %s" ', $propCfg[self::PROP_PROP_NAME], $classDef->baseURI)); |
|
111
|
4 |
|
} |
|
112
|
|
|
|
|
113
|
|
|
/** @psalm-suppress PossiblyNullArrayOffset */ |
|
114
|
4 |
|
$props[$propCfg[self::PROP_PROP_NAME]] = $propCfg; |
|
115
|
|
|
} |
|
116
|
4 |
|
|
|
117
|
|
|
$class[self::PATH_PROP] = $props; |
|
118
|
|
|
|
|
119
|
|
|
return $class; |
|
120
|
|
|
} |
|
121
|
|
|
|
|
122
|
|
|
/** |
|
123
|
|
|
* @param \DOMNode $attribute |
|
124
|
4 |
|
* |
|
125
|
|
|
* @return mixed[] |
|
126
|
4 |
|
*/ |
|
127
|
4 |
|
protected function parseValidation(\DOMNode $attribute): array|null |
|
128
|
|
|
{ |
|
129
|
|
|
if (!$attribute->childNodes->count()) { |
|
130
|
1 |
|
return null; |
|
131
|
|
|
} |
|
132
|
1 |
|
|
|
133
|
1 |
|
$constraints = []; |
|
134
|
1 |
|
/** @var \DOMNode $validationNode */ |
|
135
|
|
|
foreach ($attribute->childNodes as $validationNode) { |
|
136
|
1 |
|
if (!$validationNode->childNodes->count() || 'validation' !== $validationNode->nodeName) { |
|
137
|
|
|
continue; |
|
138
|
1 |
|
} |
|
139
|
1 |
|
$groupConstraints = []; |
|
140
|
1 |
|
/** @var \DOMNode $constraintNode */ |
|
141
|
|
|
foreach ($validationNode->childNodes as $constraintNode) { |
|
142
|
|
|
if ('#text' === $constraintNode->nodeName) { |
|
143
|
1 |
|
continue; |
|
144
|
|
|
} |
|
145
|
1 |
|
|
|
146
|
1 |
|
$constraintAttributes = []; |
|
147
|
1 |
|
/** @var \DOMAttr $constraintItemAttribute */ |
|
148
|
|
|
if ($constraintNode->attributes) { |
|
149
|
|
|
foreach ($constraintNode->attributes as $constraintItemAttribute) { |
|
150
|
|
|
$constraintAttributes[$constraintItemAttribute->nodeName] = $constraintItemAttribute->nodeValue; |
|
151
|
1 |
|
} |
|
152
|
1 |
|
} |
|
153
|
|
|
|
|
154
|
|
|
if (empty($constraintAttributes)) { |
|
155
|
1 |
|
$constraintAttributes = []; |
|
156
|
|
|
} |
|
157
|
|
|
|
|
158
|
1 |
|
$groupConstraints[] = [$constraintNode->nodeName, $constraintAttributes]; |
|
159
|
|
|
} |
|
160
|
|
|
|
|
161
|
1 |
|
$constraints[] = $groupConstraints; |
|
162
|
|
|
} |
|
163
|
|
|
|
|
164
|
6 |
|
return $constraints; |
|
165
|
|
|
} |
|
166
|
6 |
|
|
|
167
|
1 |
|
protected function parseBody(\DOMNode $node): array |
|
168
|
|
|
{ |
|
169
|
|
|
$childNodes = []; |
|
|
|
|
|
|
170
|
5 |
|
$attributes = []; |
|
171
|
|
|
|
|
172
|
|
|
if ($node->hasAttributes()) { |
|
173
|
|
|
/** |
|
174
|
5 |
|
* @var \DOMAttr $tmpAttribute |
|
175
|
5 |
|
*/ |
|
176
|
|
|
foreach ($node->attributes as $tmpAttribute) { |
|
177
|
5 |
|
$attributes[$tmpAttribute->nodeName] = $tmpAttribute->nodeValue; |
|
178
|
5 |
|
} |
|
179
|
|
|
} |
|
180
|
5 |
|
|
|
181
|
|
|
if ($node->hasChildNodes()) { |
|
182
|
5 |
|
/** @var \DOMNode $child */ |
|
183
|
4 |
|
foreach ($node->childNodes as $child) { |
|
184
|
|
|
$childName = $child->nodeName; |
|
185
|
|
|
if ('#text' === $childName) { |
|
186
|
1 |
|
continue; |
|
187
|
|
|
} |
|
188
|
1 |
|
|
|
189
|
1 |
|
$attributes[$childName][] = $this->parseBody($child); |
|
190
|
|
|
} |
|
191
|
|
|
} |
|
192
|
1 |
|
|
|
193
|
|
|
return $attributes; |
|
194
|
1 |
|
} |
|
195
|
|
|
|
|
196
|
1 |
|
protected function createDom(string $filePath): \DOMDocument |
|
197
|
|
|
{ |
|
198
|
|
|
if (!file_exists($filePath)) { |
|
199
|
|
|
throw new \RuntimeException(sprintf('File %s is not found', $filePath)); |
|
200
|
|
|
} |
|
201
|
|
|
|
|
202
|
|
|
if (!is_readable($filePath)) { |
|
203
|
|
|
throw new \RuntimeException(sprintf('Has no access to read the file %s', $filePath)); |
|
204
|
|
|
} |
|
205
|
|
|
|
|
206
|
|
|
$xml = new \DOMDocument(); |
|
207
|
|
|
$xml->load($filePath); |
|
208
|
|
|
|
|
209
|
|
|
$xsdSchemaCfg = $this->lookupXsd($xml); |
|
210
|
|
|
$xsdSchemaLocation = $xsdSchemaCfg[1]; |
|
211
|
|
|
|
|
212
|
|
|
libxml_use_internal_errors(true); |
|
213
|
|
|
|
|
214
|
|
|
if ($xml->schemaValidate($xsdSchemaLocation)) { |
|
215
|
|
|
return $xml; |
|
216
|
|
|
} |
|
217
|
|
|
|
|
218
|
|
|
$errs = []; |
|
219
|
|
|
|
|
220
|
|
|
foreach (libxml_get_errors() as $error) { |
|
221
|
|
|
$errs[] = sprintf('%s in file `%s` on line %d', $error->message, $error->file, $error->line); |
|
222
|
|
|
} |
|
223
|
|
|
|
|
224
|
|
|
$errorMessage = implode("\n ", $errs); |
|
225
|
|
|
|
|
226
|
|
|
libxml_use_internal_errors(false); |
|
227
|
|
|
|
|
228
|
|
|
throw new \RuntimeException(sprintf("Schema validation exception: \r\n %s\r", $errorMessage)); |
|
229
|
|
|
} |
|
230
|
|
|
} |
|
231
|
|
|
|
This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.
Unreachable code is most often the result of
return,dieorexitstatements that have been added for debug purposes.In the above example, the last
return falsewill never be executed, because a return statement has already been met in every possible execution path.