1 | <?php |
||
2 | /** |
||
3 | * Copyright (C) 2018 Gerrit Addiks. |
||
4 | * This package (including this file) was released under the terms of the GPL-3.0. |
||
5 | * You should have received a copy of the GNU General Public License along with this program. |
||
6 | * If not, see <http://www.gnu.org/licenses/> or send me a mail so i can send you a copy. |
||
7 | * @license GPL-3.0 |
||
8 | * @author Gerrit Addiks <[email protected]> |
||
9 | */ |
||
10 | |||
11 | namespace Addiks\RDMBundle\Mapping\Drivers; |
||
12 | |||
13 | use DOMDocument; |
||
14 | use DOMXPath; |
||
15 | use DOMNode; |
||
16 | use DOMAttr; |
||
17 | use DOMNamedNodeMap; |
||
18 | use Doctrine\DBAL\Schema\Column; |
||
19 | use Doctrine\DBAL\Types\Type; |
||
20 | use Doctrine\Persistence\Mapping\Driver\FileLocator; |
||
21 | use Addiks\RDMBundle\Mapping\Drivers\MappingDriverInterface; |
||
22 | use Addiks\RDMBundle\Mapping\EntityMappingInterface; |
||
23 | use Addiks\RDMBundle\Mapping\EntityMapping; |
||
24 | use Addiks\RDMBundle\Mapping\ServiceMapping; |
||
25 | use Addiks\RDMBundle\Mapping\MappingInterface; |
||
26 | use Addiks\RDMBundle\Mapping\ChoiceMapping; |
||
27 | use Addiks\RDMBundle\Mapping\ObjectMapping; |
||
28 | use Addiks\RDMBundle\Mapping\CallDefinitionInterface; |
||
29 | use Addiks\RDMBundle\Mapping\CallDefinition; |
||
30 | use Addiks\RDMBundle\Mapping\FieldMapping; |
||
31 | use Addiks\RDMBundle\Mapping\ArrayMapping; |
||
32 | use Addiks\RDMBundle\Mapping\ListMapping; |
||
33 | use Addiks\RDMBundle\Mapping\NullMapping; |
||
34 | use Addiks\RDMBundle\Mapping\NullableMapping; |
||
35 | use Addiks\RDMBundle\Mapping\MappingProxy; |
||
36 | use Addiks\RDMBundle\Exception\InvalidMappingException; |
||
37 | use Symfony\Component\HttpKernel\KernelInterface; |
||
38 | use Symfony\Component\DependencyInjection\ContainerInterface; |
||
39 | use ErrorException; |
||
40 | use DOMElement; |
||
41 | use Webmozart\Assert\Assert; |
||
42 | use Addiks\RDMBundle\Mapping\FixNativeMapping; |
||
43 | |||
44 | final class MappingXmlDriver implements MappingDriverInterface |
||
45 | { |
||
46 | |||
47 | const RDM_SCHEMA_URI = "http://github.com/addiks/symfony_rdm/tree/master/Resources/mapping-schema.v1.xsd"; |
||
48 | const DOCTRINE_SCHEMA_URI = "http://doctrine-project.org/schemas/orm/doctrine-mapping"; |
||
49 | |||
50 | /** |
||
51 | * @var FileLocator |
||
52 | */ |
||
53 | private $doctrineFileLocator; |
||
54 | |||
55 | /** |
||
56 | * @var KernelInterface |
||
57 | */ |
||
58 | private $kernel; |
||
59 | |||
60 | /** |
||
61 | * @var ContainerInterface |
||
62 | */ |
||
63 | private $serviceContainer; |
||
64 | |||
65 | /** |
||
66 | * @var string |
||
67 | */ |
||
68 | private $schemaFilePath; |
||
69 | 3 | ||
70 | public function __construct( |
||
71 | FileLocator $doctrineFileLocator, |
||
72 | KernelInterface $kernel, |
||
73 | string $schemaFilePath |
||
74 | ) { |
||
75 | 3 | /** @var ContainerInterface|null $serviceContainer */ |
|
76 | $serviceContainer = $kernel->getContainer(); |
||
77 | 3 | ||
78 | if (is_null($serviceContainer)) { |
||
79 | throw new ErrorException("Kernel does not have a container!"); |
||
80 | } |
||
81 | 3 | ||
82 | 3 | $this->doctrineFileLocator = $doctrineFileLocator; |
|
83 | 3 | $this->kernel = $kernel; |
|
84 | 3 | $this->schemaFilePath = $schemaFilePath; |
|
85 | $this->serviceContainer = $serviceContainer; |
||
86 | } |
||
87 | 2 | ||
88 | public function loadRDMMetadataForClass(string $className): ?EntityMappingInterface |
||
89 | { |
||
90 | 2 | /** @var ?EntityMappingInterface $mapping */ |
|
91 | $mapping = null; |
||
92 | |||
93 | 2 | /** @var array<MappingInterface> $fieldMappings */ |
|
94 | $fieldMappings = array(); |
||
95 | 2 | ||
96 | if ($this->doctrineFileLocator->fileExists($className)) { |
||
97 | 2 | /** @var string $mappingFile */ |
|
98 | $mappingFile = $this->doctrineFileLocator->findMappingFile($className); |
||
99 | 2 | ||
100 | $fieldMappings = $this->readFieldMappingsFromFile($mappingFile); |
||
101 | } |
||
102 | 1 | ||
103 | 1 | if (!empty($fieldMappings)) { |
|
104 | $mapping = new EntityMapping($className, $fieldMappings); |
||
105 | } |
||
106 | 1 | ||
107 | return $mapping; |
||
108 | } |
||
109 | |||
110 | /** |
||
111 | * @return array<MappingInterface> |
||
112 | 2 | */ |
|
113 | private function readFieldMappingsFromFile(string $mappingFile, string $parentMappingFile = null): array |
||
114 | 2 | { |
|
115 | if ($mappingFile[0] === '@') { |
||
116 | 1 | /** @var string $mappingFile */ |
|
117 | $mappingFile = $this->kernel->locateResource($mappingFile); |
||
118 | } |
||
119 | 2 | ||
120 | 1 | if ($mappingFile[0] !== DIRECTORY_SEPARATOR && !empty($parentMappingFile)) { |
|
121 | $mappingFile = dirname($parentMappingFile) . DIRECTORY_SEPARATOR . $mappingFile; |
||
122 | } |
||
123 | 2 | ||
124 | 1 | if (!file_exists($mappingFile)) { |
|
125 | throw new InvalidMappingException(sprintf( |
||
126 | "Missing referenced orm file '%s'%s!", |
||
127 | 1 | $mappingFile, |
|
128 | is_string($parentMappingFile) ?sprintf(", referenced in file '%s'", $parentMappingFile) :'' |
||
129 | )); |
||
130 | } |
||
131 | |||
132 | 2 | /** @var string|null $mappingXml */ |
|
133 | $mappingXml = file_get_contents($mappingFile); |
||
134 | 2 | ||
135 | Assert::notEmpty($mappingXml, sprintf('ORM-Mapping file "%s" is empty!', $mappingFile)); |
||
136 | 2 | ||
137 | 2 | $dom = new DOMDocument(); |
|
138 | $dom->loadXML($mappingXml); |
||
139 | |||
140 | 2 | /** @var DOMXPath $xpath */ |
|
141 | $xpath = $this->createXPath($dom->documentElement); |
||
142 | |||
143 | 2 | /** @var array<MappingInterface> $fieldMappings */ |
|
144 | $fieldMappings = $this->readFieldMappings( |
||
145 | $dom, |
||
146 | $mappingFile, |
||
147 | null, |
||
148 | false |
||
149 | ); |
||
150 | 2 | ||
151 | foreach ($xpath->query("//orm:entity", $dom) as $entityNode) { |
||
152 | /** @var DOMNode $entityNode */ |
||
153 | 2 | ||
154 | $fieldMappings = array_merge($fieldMappings, $this->readFieldMappings( |
||
155 | $entityNode, |
||
156 | $mappingFile, |
||
157 | null, |
||
158 | false |
||
159 | )); |
||
160 | } |
||
161 | 1 | ||
162 | return $fieldMappings; |
||
163 | } |
||
164 | 2 | ||
165 | private function createXPath(DOMNode $node): DOMXPath |
||
166 | { |
||
167 | 2 | /** @var DOMNode $ownerDocument */ |
|
168 | $ownerDocument = $node; |
||
169 | 2 | ||
170 | 2 | if (!$ownerDocument instanceof DOMDocument) { |
|
171 | 2 | $ownerDocument = $node->ownerDocument; |
|
172 | Assert::object($ownerDocument); |
||
173 | } |
||
174 | 2 | ||
175 | 2 | $xpath = new DOMXPath($ownerDocument); |
|
0 ignored issues
–
show
Bug
introduced
by
![]() |
|||
176 | 2 | $xpath->registerNamespace('rdm', self::RDM_SCHEMA_URI); |
|
177 | $xpath->registerNamespace('orm', self::DOCTRINE_SCHEMA_URI); |
||
178 | 2 | ||
179 | return $xpath; |
||
180 | } |
||
181 | 1 | ||
182 | private function readObject(DOMNode $objectNode, string $mappingFile): ObjectMapping |
||
183 | { |
||
184 | 1 | /** @var DOMNamedNodeMap|null $attributes */ |
|
185 | $objectNodeAttributes = $objectNode->attributes; |
||
0 ignored issues
–
show
|
|||
186 | 1 | ||
187 | if (!$this->hasAttributeValue($objectNode, "class")) { |
||
188 | throw new InvalidMappingException(sprintf( |
||
189 | "Missing 'class' attribute on 'object' mapping in %s in line %d", |
||
190 | $mappingFile, |
||
191 | $objectNode->getLineNo() |
||
192 | )); |
||
193 | } |
||
194 | |||
195 | 1 | /** @var class-string $className */ |
|
196 | $className = (string)$this->readAttributeValue($objectNode, "class"); |
||
197 | 1 | ||
198 | Assert::true(class_exists($className) || interface_exists($className), sprintf( |
||
199 | 'Class or interface "%s" does not exist!', |
||
200 | 1 | $className |
|
201 | )); |
||
202 | |||
203 | 1 | /** @var CallDefinitionInterface|null $factory */ |
|
204 | $factory = null; |
||
205 | |||
206 | 1 | /** @var CallDefinitionInterface|null $factory */ |
|
207 | $serializer = null; |
||
208 | 1 | ||
209 | /** @var DOMXPath $xpath */ |
||
210 | $xpath = $this->createXPath($objectNode); |
||
211 | |||
212 | 1 | foreach ($xpath->query('./rdm:factory', $objectNode) as $factoryNode) { |
|
213 | /** @var DOMNode $factoryNode */ |
||
214 | |||
215 | 1 | /** @var array<MappingInterface> $argumentMappings */ |
|
216 | $argumentMappings = $this->readFieldMappings($factoryNode, $mappingFile); |
||
217 | |||
218 | 1 | /** @var string $routineName */ |
|
219 | $routineName = (string)$this->readAttributeValue($factoryNode, "method"); |
||
220 | 1 | ||
221 | 1 | /** @var string $objectReference */ |
|
222 | $objectReference = (string)$this->readAttributeValue($factoryNode, "object"); |
||
223 | |||
224 | $factory = new CallDefinition( |
||
225 | $this->serviceContainer, |
||
226 | 1 | $routineName, |
|
227 | $objectReference, |
||
228 | $argumentMappings, |
||
229 | false, |
||
230 | 1 | $mappingFile . " in line " . $objectNode->getLineNo() |
|
231 | 1 | ); |
|
232 | 1 | } |
|
233 | 1 | ||
234 | if ($this->hasAttributeValue($objectNode, "factory") && is_null($factory)) { |
||
235 | $factory = $this->readCallDefinition( |
||
236 | (string)$this->readAttributeValue($objectNode, "factory"), |
||
237 | 1 | $mappingFile . " in line " . $objectNode->getLineNo() |
|
238 | 1 | ); |
|
239 | 1 | } |
|
240 | 1 | ||
241 | if ($this->hasAttributeValue($objectNode, "serialize")) { |
||
242 | $serializer = $this->readCallDefinition( |
||
243 | (string)$this->readAttributeValue($objectNode, "serialize"), |
||
244 | $mappingFile . " in line " . $objectNode->getLineNo() |
||
245 | 1 | ); |
|
246 | } |
||
247 | |||
248 | 1 | /** @var array<MappingInterface> $fieldMappings */ |
|
249 | $fieldMappings = $this->readFieldMappings($objectNode, $mappingFile); |
||
250 | 1 | ||
251 | /** @var Column|null $dbalColumn */ |
||
252 | 1 | $dbalColumn = null; |
|
253 | |||
254 | if ($this->hasAttributeValue($objectNode, "column")) { |
||
255 | 1 | /** @var bool $notnull */ |
|
256 | $notnull = true; |
||
257 | |||
258 | 1 | /** @var string $type */ |
|
259 | $type = "string"; |
||
260 | |||
261 | 1 | /** @var int $length */ |
|
262 | $length = 255; |
||
263 | 1 | ||
264 | 1 | /** @var string|null $default */ |
|
265 | $default = null; |
||
266 | |||
267 | 1 | if ($this->hasAttributeValue($objectNode, "nullable")) { |
|
268 | 1 | $notnull = (strtolower((string)$this->readAttributeValue($objectNode, "nullable")) !== 'true'); |
|
269 | } |
||
270 | |||
271 | 1 | if ($this->hasAttributeValue($objectNode, "column-type")) { |
|
272 | $type = (string)$this->readAttributeValue($objectNode, "column-type"); |
||
273 | } |
||
274 | |||
275 | 1 | if ($this->hasAttributeValue($objectNode, "column-length")) { |
|
276 | 1 | $length = (string)$this->readAttributeValue($objectNode, "column-length"); |
|
277 | } |
||
278 | |||
279 | 1 | if ($this->hasAttributeValue($objectNode, "column-default")) { |
|
280 | 1 | $default = (string)$this->readAttributeValue($objectNode, "column-default"); |
|
281 | 1 | } |
|
282 | |||
283 | 1 | $dbalColumn = new Column( |
|
284 | (string)$this->readAttributeValue($objectNode, "column"), |
||
285 | Type::getType($type), |
||
286 | [ |
||
287 | 'notnull' => $notnull, |
||
288 | 'length' => $length, |
||
289 | 'default' => $default, |
||
290 | ] |
||
291 | 1 | ); |
|
292 | } |
||
293 | |||
294 | 1 | /** @var string|null $id */ |
|
295 | $id = null; |
||
296 | 1 | ||
297 | /** @var string|null $referencedId */ |
||
298 | $referencedId = null; |
||
299 | |||
300 | 1 | if ($this->hasAttributeValue($objectNode, "id")) { |
|
301 | $id = (string)$this->readAttributeValue($objectNode, "id"); |
||
302 | } |
||
303 | |||
304 | 1 | if ($this->hasAttributeValue($objectNode, "references-id")) { |
|
305 | $referencedId = (string)$this->readAttributeValue($objectNode, "references-id"); |
||
306 | } |
||
307 | |||
308 | 1 | return new ObjectMapping( |
|
309 | $className, |
||
310 | $fieldMappings, |
||
311 | 1 | $dbalColumn, |
|
312 | sprintf( |
||
313 | "in file '%s' in line %d", |
||
314 | $mappingFile, |
||
315 | $objectNode->getLineNo() |
||
316 | ), |
||
317 | $factory, |
||
318 | $serializer, |
||
319 | $id, |
||
320 | 1 | $referencedId |
|
321 | ); |
||
322 | } |
||
323 | |||
324 | private function readCallDefinition( |
||
325 | 1 | string $callDefinition, |
|
326 | string $origin = "unknown" |
||
327 | ): CallDefinitionInterface { |
||
328 | 1 | /** @var string $routineName */ |
|
329 | $routineName = $callDefinition; |
||
330 | |||
331 | 1 | /** @var string|null $objectReference */ |
|
332 | $objectReference = null; |
||
333 | 1 | ||
334 | 1 | /** @var bool $isStaticCall */ |
|
335 | 1 | $isStaticCall = false; |
|
336 | |||
337 | if (strpos($callDefinition, '::') !== false) { |
||
338 | 1 | [$objectReference, $routineName] = explode('::', $callDefinition); |
|
339 | $isStaticCall = true; |
||
340 | } |
||
341 | |||
342 | 1 | if (strpos($callDefinition, '->') !== false) { |
|
343 | 1 | [$objectReference, $routineName] = explode('->', $callDefinition); |
|
344 | } |
||
345 | |||
346 | 1 | return new CallDefinition( |
|
347 | $this->serviceContainer, |
||
348 | $routineName, |
||
349 | $objectReference, |
||
350 | [], |
||
351 | $isStaticCall, |
||
352 | 1 | $origin |
|
353 | ); |
||
354 | } |
||
355 | |||
356 | private function readChoice( |
||
357 | DOMNode $choiceNode, |
||
358 | 1 | string $mappingFile, |
|
359 | string $defaultColumnName |
||
360 | 1 | ): ChoiceMapping { |
|
361 | 1 | /** @var string|Column $columnName */ |
|
362 | $column = $defaultColumnName; |
||
363 | |||
364 | if ($this->hasAttributeValue($choiceNode, "column")) { |
||
365 | 1 | $column = (string)$this->readAttributeValue($choiceNode, "column"); |
|
366 | } |
||
367 | |||
368 | 1 | /** @var array<MappingInterface> $choiceMappings */ |
|
369 | $choiceMappings = array(); |
||
370 | 1 | ||
371 | /** @var DOMXPath $xpath */ |
||
372 | $xpath = $this->createXPath($choiceNode); |
||
373 | |||
374 | 1 | foreach ($xpath->query('./rdm:option', $choiceNode) as $optionNode) { |
|
375 | /** @var DOMNode $optionNode */ |
||
376 | |||
377 | 1 | /** @var string $determinator */ |
|
378 | $determinator = (string)$this->readAttributeValue($optionNode, "name"); |
||
379 | 1 | ||
380 | /** @var string $optionDefaultColumnName */ |
||
381 | $optionDefaultColumnName = sprintf("%s_%s", $defaultColumnName, $determinator); |
||
382 | 1 | ||
383 | foreach ($this->readFieldMappings($optionNode, $mappingFile, $optionDefaultColumnName) as $mapping) { |
||
384 | /** @var MappingInterface $mapping */ |
||
385 | |||
386 | 1 | $choiceMappings[$determinator] = $mapping; |
|
387 | } |
||
388 | } |
||
389 | 1 | ||
390 | foreach ($xpath->query('./orm:field', $choiceNode) as $fieldNode) { |
||
391 | /** @var DOMNode $fieldNode */ |
||
392 | 1 | ||
393 | $column = $this->readDoctrineField($fieldNode); |
||
394 | } |
||
395 | 1 | ||
396 | return new ChoiceMapping($column, $choiceMappings, sprintf( |
||
397 | "in file '%s' in line %d", |
||
398 | $mappingFile, |
||
399 | $choiceNode->getLineNo() |
||
400 | )); |
||
401 | } |
||
402 | 2 | ||
403 | /** |
||
404 | * @return array<MappingInterface> |
||
405 | */ |
||
406 | private function readFieldMappings( |
||
407 | DOMNode $parentNode, |
||
408 | string $mappingFile, |
||
409 | 2 | string $choiceDefaultColumnName = null, |
|
410 | bool $readFields = true |
||
411 | ): array { |
||
412 | 2 | /** @var DOMXPath $xpath */ |
|
413 | $xpath = $this->createXPath($parentNode); |
||
414 | 2 | ||
415 | /** @var array<MappingInterface> $fieldMappings */ |
||
416 | $fieldMappings = array(); |
||
417 | 1 | ||
418 | /** @var DOMNode $serviceNode */ |
||
419 | 1 | foreach ($xpath->query('./rdm:service', $parentNode) as $serviceNode) { |
|
420 | |||
421 | 1 | $serviceMapping = $this->readService($serviceNode, $mappingFile); |
|
422 | |||
423 | 1 | if ($this->hasAttributeValue($serviceNode, "field")) { |
|
424 | /** @var string $fieldName */ |
||
425 | $fieldName = (string)$this->readAttributeValue($serviceNode, "field"); |
||
426 | 1 | ||
427 | $fieldMappings[$fieldName] = $serviceMapping; |
||
428 | |||
429 | } else { |
||
430 | 2 | $fieldMappings[] = $serviceMapping; |
|
431 | } |
||
432 | } |
||
433 | |||
434 | 1 | /** @var DOMNode $choiceNode */ |
|
435 | foreach ($xpath->query('./rdm:choice', $parentNode) as $choiceNode) { |
||
436 | 1 | ||
437 | /** @var string $defaultColumnName */ |
||
438 | $defaultColumnName = ""; |
||
439 | 1 | ||
440 | 1 | if (!is_null($choiceDefaultColumnName)) { |
|
441 | $defaultColumnName = $choiceDefaultColumnName; |
||
442 | |||
443 | 1 | } elseif ($this->hasAttributeValue($choiceNode, "field")) { |
|
444 | $defaultColumnName = (string)$this->readAttributeValue($choiceNode, "field"); |
||
445 | 1 | } |
|
446 | |||
447 | 1 | $choiceMapping = $this->readChoice($choiceNode, $mappingFile, $defaultColumnName); |
|
448 | |||
449 | 1 | if ($this->hasAttributeValue($choiceNode, "field")) { |
|
450 | /** @var string $fieldName */ |
||
451 | $fieldName = (string)$this->readAttributeValue($choiceNode, "field"); |
||
452 | 1 | ||
453 | $fieldMappings[$fieldName] = $choiceMapping; |
||
454 | |||
455 | } else { |
||
456 | 2 | $fieldMappings[] = $choiceMapping; |
|
457 | } |
||
458 | } |
||
459 | |||
460 | 1 | /** @var DOMNode $objectNode */ |
|
461 | foreach ($xpath->query('./rdm:object', $parentNode) as $objectNode) { |
||
462 | 1 | ||
463 | /** @var ObjectMapping $objectMapping */ |
||
464 | 1 | $objectMapping = $this->readObject($objectNode, $mappingFile); |
|
465 | |||
466 | 1 | if ($this->hasAttributeValue($objectNode, "field")) { |
|
467 | /** @var string $fieldName */ |
||
468 | $fieldName = (string)$this->readAttributeValue($objectNode, "field"); |
||
469 | 1 | ||
470 | $fieldMappings[$fieldName] = $objectMapping; |
||
471 | |||
472 | } else { |
||
473 | 2 | $fieldMappings[] = $objectMapping; |
|
474 | 1 | } |
|
475 | } |
||
476 | |||
477 | if ($readFields) { |
||
478 | 1 | /** @var DOMNode $fieldNode */ |
|
479 | foreach ($xpath->query('./orm:field', $parentNode) as $fieldNode) { |
||
480 | 1 | ||
481 | /** @var Column $column */ |
||
482 | 1 | $column = $this->readDoctrineField($fieldNode); |
|
483 | |||
484 | 1 | $fieldName = (string)$this->readAttributeValue($fieldNode, "name"); |
|
485 | |||
486 | $fieldMappings[$fieldName] = new FieldMapping( |
||
487 | $column, |
||
488 | sprintf("in file '%s' in line %d", $mappingFile, $fieldNode->getLineNo()) |
||
489 | 2 | ); |
|
490 | } |
||
491 | } |
||
492 | |||
493 | 1 | /** @var DOMNode $arrayNode */ |
|
494 | foreach ($xpath->query('./rdm:array', $parentNode) as $arrayNode) { |
||
495 | 1 | ||
496 | /** @var ArrayMapping $arrayMapping */ |
||
497 | 1 | $arrayMapping = $this->readArray($arrayNode, $mappingFile); |
|
498 | |||
499 | 1 | if ($this->hasAttributeValue($arrayNode, "field")) { |
|
500 | /** @var string $fieldName */ |
||
501 | $fieldName = (string)$this->readAttributeValue($arrayNode, "field"); |
||
502 | |||
503 | $fieldMappings[$fieldName] = $arrayMapping; |
||
504 | |||
505 | } else { |
||
506 | 2 | $fieldMappings[] = $arrayMapping; |
|
507 | } |
||
508 | } |
||
509 | |||
510 | 1 | /** @var DOMNode $listNode */ |
|
511 | foreach ($xpath->query('./rdm:list', $parentNode) as $listNode) { |
||
512 | 1 | ||
513 | /** @var string $defaultColumnName */ |
||
514 | $defaultColumnName = ""; |
||
515 | 1 | ||
516 | 1 | if (!is_null($choiceDefaultColumnName)) { |
|
517 | $defaultColumnName = $choiceDefaultColumnName; |
||
518 | |||
519 | } elseif ($this->hasAttributeValue($listNode, "field")) { |
||
520 | 1 | $defaultColumnName = (string)$this->readAttributeValue($listNode, "field"); |
|
521 | } |
||
522 | 1 | ||
523 | /** @var ListMapping $listMapping */ |
||
524 | 1 | $listMapping = $this->readList($listNode, $mappingFile, $defaultColumnName); |
|
525 | |||
526 | 1 | if ($this->hasAttributeValue($listNode, "field")) { |
|
527 | /** @var string $fieldName */ |
||
528 | $fieldName = (string)$this->readAttributeValue($listNode, "field"); |
||
529 | 1 | ||
530 | $fieldMappings[$fieldName] = $listMapping; |
||
531 | |||
532 | } else { |
||
533 | 2 | $fieldMappings[] = $listMapping; |
|
534 | } |
||
535 | } |
||
536 | 1 | ||
537 | /** @var DOMNode $nullNode */ |
||
538 | foreach ($xpath->query('./rdm:null', $parentNode) as $nullNode) { |
||
539 | |||
540 | if ($this->hasAttributeValue($nullNode, "field")) { |
||
541 | /** @var string $fieldName */ |
||
542 | $fieldName = (string)$this->readAttributeValue($nullNode, "field"); |
||
543 | |||
544 | $fieldMappings[$fieldName] = new NullMapping(sprintf( |
||
545 | "in file '%s' in line %d", |
||
546 | $mappingFile, |
||
547 | 1 | $nullNode->getLineNo() |
|
548 | )); |
||
549 | |||
550 | 1 | } else { |
|
551 | $fieldMappings[] = new NullMapping(sprintf( |
||
552 | "in file '%s' in line %d", |
||
553 | $mappingFile, |
||
554 | $nullNode->getLineNo() |
||
555 | 2 | )); |
|
556 | } |
||
557 | } |
||
558 | |||
559 | 1 | /** @var DOMNode $nullableNode */ |
|
560 | foreach ($xpath->query('./rdm:nullable', $parentNode) as $nullableNode) { |
||
561 | 1 | ||
562 | /** @var NullableMapping $nullableMapping */ |
||
563 | 1 | $nullableMapping = $this->readNullable($nullableNode, $mappingFile); |
|
564 | |||
565 | 1 | if ($this->hasAttributeValue($nullableNode, "field")) { |
|
566 | /** @var string $fieldName */ |
||
567 | $fieldName = (string)$this->readAttributeValue($nullableNode, "field"); |
||
568 | |||
569 | $fieldMappings[$fieldName] = $nullableMapping; |
||
570 | |||
571 | } else { |
||
572 | 2 | $fieldMappings[] = $nullableMapping; |
|
573 | } |
||
574 | } |
||
575 | |||
576 | 2 | /** @var DOMNode $importNode */ |
|
577 | foreach ($xpath->query('./rdm:import', $parentNode) as $importNode) { |
||
578 | |||
579 | 2 | /** @var string $path */ |
|
580 | $path = (string)$this->readAttributeValue($importNode, "path"); |
||
581 | |||
582 | 2 | /** @var string|null $forcedFieldName */ |
|
583 | $forcedFieldName = $this->readAttributeValue($importNode, "field"); |
||
584 | 2 | ||
585 | /** @var string $columnPrefix */ |
||
586 | $columnPrefix = (string)$this->readAttributeValue($importNode, "column-prefix"); |
||
587 | 1 | ||
588 | foreach ($this->readFieldMappingsFromFile($path, $mappingFile) as $fieldName => $fieldMapping) { |
||
589 | /** @var MappingInterface $fieldMapping */ |
||
590 | |||
591 | $fieldMappingProxy = new MappingProxy( |
||
0 ignored issues
–
show
|
|||
592 | 1 | $fieldMapping, |
|
593 | 1 | $columnPrefix |
|
594 | ); |
||
595 | |||
596 | if (!empty($forcedFieldName)) { |
||
597 | $fieldMappings[$forcedFieldName] = $fieldMapping; |
||
598 | } else { |
||
599 | |||
600 | $fieldMappings[$fieldName] = $fieldMapping; |
||
601 | 2 | } |
|
602 | } |
||
603 | } |
||
604 | 1 | ||
605 | foreach ($xpath->query('./rdm:fix', $parentNode) as $fixNode) { |
||
606 | |||
607 | 1 | /** @var FixNativeMapping $fixMapping */ |
|
608 | $fixMapping = $this->readFixMapping($fixNode, $mappingFile); |
||
609 | |||
610 | 1 | if ($this->hasAttributeValue($fixNode, "field")) { |
|
611 | /** @var string $fieldName */ |
||
612 | 1 | $fieldName = (string)$this->readAttributeValue($fixNode, "field"); |
|
613 | 1 | ||
614 | $fieldMappings[$fieldName] = $fixMapping; |
||
615 | |||
616 | 1 | } else { |
|
617 | $fieldMappings[] = $fixMapping; |
||
618 | } |
||
619 | 1 | } |
|
620 | |||
621 | return $fieldMappings; |
||
622 | } |
||
623 | |||
624 | 1 | private function readFixMapping(DOMNode $fixNode, string $mappingFile): FixNativeMapping |
|
625 | { |
||
626 | /** @var string $jsonSerializedValue */ |
||
627 | 1 | $jsonSerializedValue = ""; |
|
628 | |||
629 | if ($this->hasAttributeValue($fixNode, "json")) { |
||
630 | 1 | $jsonSerializedValue = (string) $this->readAttributeValue($fixNode, "json"); |
|
631 | } |
||
632 | 1 | ||
633 | if ($this->hasAttributeValue($fixNode, "string")) { |
||
634 | $jsonSerializedValue = sprintf( |
||
635 | '"%s"', |
||
636 | 1 | addslashes((string) $this->readAttributeValue($fixNode, "string")) |
|
637 | ); |
||
638 | 1 | } |
|
639 | |||
640 | return new FixNativeMapping( |
||
641 | 1 | $jsonSerializedValue, |
|
642 | sprintf( |
||
643 | "in file '%s' in line %d", |
||
644 | $mappingFile, |
||
645 | 1 | $fixNode->getLineNo() |
|
646 | ) |
||
647 | ); |
||
648 | 1 | } |
|
649 | |||
650 | private function readService(DOMNode $serviceNode, string $mappingFile): ServiceMapping |
||
651 | { |
||
652 | 1 | /** @var bool $lax */ |
|
653 | $lax = strtolower((string)$this->readAttributeValue($serviceNode, "lax")) === 'true'; |
||
654 | |||
655 | 1 | /** @var string $serviceId */ |
|
656 | $serviceId = (string)$this->readAttributeValue($serviceNode, "id"); |
||
657 | |||
658 | return new ServiceMapping( |
||
659 | 1 | $this->serviceContainer, |
|
660 | $serviceId, |
||
661 | $lax, |
||
662 | sprintf( |
||
663 | "in file '%s' in line %d", |
||
664 | 1 | $mappingFile, |
|
665 | 1 | $serviceNode->getLineNo() |
|
666 | ) |
||
667 | ); |
||
668 | } |
||
669 | 1 | ||
670 | private function readArray(DOMNode $arrayNode, string $mappingFile): ArrayMapping |
||
671 | { |
||
672 | 1 | /** @var array<MappingInterface> $entryMappings */ |
|
673 | $entryMappings = $this->readFieldMappings($arrayNode, $mappingFile); |
||
674 | 1 | ||
675 | /** @var DOMXPath $xpath */ |
||
676 | $xpath = $this->createXPath($arrayNode); |
||
677 | |||
678 | 1 | foreach ($xpath->query('./rdm:entry', $arrayNode) as $entryNode) { |
|
679 | /** @var DOMNode $entryNode */ |
||
680 | 1 | ||
681 | /** @var string|null $key */ |
||
682 | $key = $this->readAttributeValue($entryNode, "key"); |
||
683 | |||
684 | 1 | foreach ($this->readFieldMappings($entryNode, $mappingFile) as $entryMapping) { |
|
685 | /** @var MappingInterface $entryMapping */ |
||
686 | |||
687 | 1 | if (is_null($key)) { |
|
688 | $entryMappings[] = $entryMapping; |
||
689 | |||
690 | } else { |
||
691 | 1 | $entryMappings[$key] = $entryMapping; |
|
692 | } |
||
693 | |||
694 | break; |
||
695 | } |
||
696 | 1 | } |
|
697 | |||
698 | 1 | return new ArrayMapping($entryMappings, sprintf( |
|
699 | "in file '%s' in line %d", |
||
700 | $mappingFile, |
||
701 | $arrayNode->getLineNo() |
||
702 | )); |
||
703 | } |
||
704 | |||
705 | private function readList( |
||
706 | DOMNode $listNode, |
||
707 | 1 | string $mappingFile, |
|
708 | string $columnName |
||
709 | ): ListMapping { |
||
710 | 1 | if ($this->hasAttributeValue($listNode, "column")) { |
|
711 | $columnName = (string)$this->readAttributeValue($listNode, "column"); |
||
712 | 1 | } |
|
713 | |||
714 | 1 | /** @var array<MappingInterface> $entryMappings */ |
|
715 | $entryMappings = $this->readFieldMappings($listNode, $mappingFile); |
||
716 | 1 | ||
717 | /** @var array<string, mixed> $columnOptions */ |
||
718 | 1 | $columnOptions = array(); |
|
719 | |||
720 | 1 | if ($this->hasAttributeValue($listNode, "column-length")) { |
|
721 | $columnOptions['length'] = (int)$this->readAttributeValue($listNode, "column-length", "0"); |
||
722 | } |
||
723 | |||
724 | /** @var Type $type */ |
||
725 | 1 | $type = Type::getType("string"); |
|
726 | |||
727 | 1 | if ($columnOptions['length'] ?? 0 >= 255) { |
|
728 | $type = Type::getType("text"); |
||
729 | } |
||
730 | 1 | ||
731 | $column = new Column($columnName, $type, $columnOptions); |
||
732 | |||
733 | return new ListMapping($column, array_values($entryMappings)[0], sprintf( |
||
734 | 1 | "in file '%s' in line %d", |
|
735 | $mappingFile, |
||
736 | $listNode->getLineNo() |
||
737 | 1 | )); |
|
738 | } |
||
739 | |||
740 | 1 | private function readNullable( |
|
741 | DOMNode $nullableNode, |
||
742 | string $mappingFile |
||
743 | ): NullableMapping { |
||
744 | /** @var array<MappingInterface> $innerMappings */ |
||
745 | $innerMappings = $this->readFieldMappings($nullableNode, $mappingFile); |
||
746 | |||
747 | if (count($innerMappings) !== 1) { |
||
748 | throw new InvalidMappingException(sprintf( |
||
749 | "A nullable mapping must contain exactly one inner mapping in '%s' at line %d!", |
||
750 | $mappingFile, |
||
751 | 1 | $nullableNode->getLineNo() |
|
752 | )); |
||
753 | } |
||
754 | 1 | ||
755 | /** @var MappingInterface $innerMapping */ |
||
756 | $innerMapping = array_values($innerMappings)[0]; |
||
757 | 1 | ||
758 | /** @var Column|null $column */ |
||
759 | 1 | $column = null; |
|
760 | 1 | ||
761 | if ($this->hasAttributeValue($nullableNode, "column")) { |
||
762 | /** @var string $columnName */ |
||
763 | 1 | $columnName = $this->readAttributeValue($nullableNode, "column", ""); |
|
764 | |||
765 | 1 | $column = new Column( |
|
766 | $columnName, |
||
767 | Type::getType("boolean"), |
||
768 | 1 | [ |
|
769 | 1 | 'notnull' => false |
|
770 | 1 | ] |
|
771 | ); |
||
772 | } |
||
773 | 1 | ||
774 | 1 | $strict = $this->readAttributeValue($nullableNode, "strict", "false") === "true" ? true : false; |
|
775 | |||
776 | 1 | return new NullableMapping($innerMapping, $column, sprintf( |
|
777 | 1 | "in file '%s' at line %d", |
|
778 | $mappingFile, |
||
779 | 1 | $nullableNode->getLineNo() |
|
780 | ), $strict); |
||
781 | } |
||
782 | 1 | ||
783 | private function readDoctrineField(DOMNode $fieldNode): Column |
||
784 | { |
||
785 | /** @var array<string> $attributes */ |
||
786 | $attributes = array(); |
||
787 | 1 | ||
788 | /** @var array<string> $keyMap */ |
||
789 | 1 | $keyMap = array( |
|
790 | 'column' => 'name', |
||
791 | 'type' => 'type', |
||
792 | 'nullable' => 'notnull', |
||
793 | 'length' => 'length', |
||
794 | 'precision' => 'precision', |
||
795 | 1 | 'scale' => 'scale', |
|
796 | 'column-definition' => 'columnDefinition', |
||
797 | ); |
||
798 | 1 | ||
799 | /** @var string|null $columnName */ |
||
800 | $columnName = null; |
||
801 | 1 | ||
802 | /** @var Type $type */ |
||
803 | $type = Type::getType('string'); |
||
804 | 1 | ||
805 | /** @var DOMNamedNodeMap|null $fieldNodeAttributes */ |
||
806 | 1 | $fieldNodeAttributes = $fieldNode->attributes; |
|
807 | |||
808 | if (is_object($fieldNodeAttributes)) { |
||
809 | 2 | foreach ($fieldNodeAttributes as $key => $attribute) { |
|
810 | /** @var DOMAttr $attribute */ |
||
811 | |||
812 | 2 | $attributeValue = $attribute->nodeValue; |
|
813 | |||
814 | if ($key === 'column') { |
||
815 | 2 | $columnName = $attributeValue; |
|
816 | |||
817 | } elseif ($key === 'name') { |
||
818 | 2 | if (empty($columnName)) { |
|
819 | $columnName = $attributeValue; |
||
820 | 2 | } |
|
821 | 2 | ||
822 | } elseif ($key === 'type') { |
||
823 | $type = Type::getType((string) $attributeValue); |
||
824 | 2 | ||
825 | } elseif (isset($keyMap[$key])) { |
||
826 | if ($key === 'nullable') { |
||
827 | # target is 'notnull', so falue is reversed |
||
828 | $attributeValue = ($attributeValue === 'false'); |
||
829 | } |
||
830 | |||
831 | $attributes[$keyMap[$key]] = $attributeValue; |
||
832 | } |
||
833 | } |
||
834 | } |
||
835 | |||
836 | Assert::notEmpty($columnName, 'Column name cannot be empty!'); |
||
837 | |||
838 | $column = new Column( |
||
839 | $columnName, |
||
840 | $type, |
||
841 | $attributes |
||
842 | ); |
||
843 | |||
844 | return $column; |
||
845 | } |
||
846 | |||
847 | private function hasAttributeValue(DOMNode $node, string $attributeName): bool |
||
848 | { |
||
849 | /** @var DOMNamedNodeMap $nodeAttributes */ |
||
850 | $nodeAttributes = $node->attributes; |
||
851 | |||
852 | /** @var DOMNode|null $attributeNode */ |
||
853 | $attributeNode = $nodeAttributes->getNamedItem($attributeName); |
||
854 | |||
855 | return is_object($attributeNode); |
||
856 | } |
||
857 | |||
858 | private function readAttributeValue(DOMNode $node, string $attributeName, ?string $default = null): ?string |
||
859 | { |
||
860 | /** @var DOMNamedNodeMap $nodeAttributes */ |
||
861 | $nodeAttributes = $node->attributes; |
||
862 | |||
863 | /** @var DOMNode|null $attributeNode */ |
||
864 | $attributeNode = $nodeAttributes->getNamedItem($attributeName); |
||
865 | |||
866 | /** @var string|null $value */ |
||
867 | $value = $default; |
||
868 | |||
869 | if (is_object($attributeNode)) { |
||
870 | $value = $attributeNode->nodeValue; |
||
871 | } |
||
872 | |||
873 | return $value; |
||
874 | } |
||
875 | |||
876 | } |
||
877 |