Passed
Push — master ( 064bed...d815ea )
by Gerrit
04:18
created

MappingXmlDriver::__construct()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 16
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 2.0078

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 7
c 1
b 0
f 0
nc 2
nop 3
dl 0
loc 16
ccs 7
cts 8
cp 0.875
crap 2.0078
rs 10
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
43
final class MappingXmlDriver implements MappingDriverInterface
44
{
45
46
    const RDM_SCHEMA_URI = "http://github.com/addiks/symfony_rdm/tree/master/Resources/mapping-schema.v1.xsd";
47
    const DOCTRINE_SCHEMA_URI = "http://doctrine-project.org/schemas/orm/doctrine-mapping";
48
49
    /**
50
     * @var FileLocator
51
     */
52
    private $doctrineFileLocator;
53
54
    /**
55
     * @var KernelInterface
56
     */
57
    private $kernel;
58
59
    /**
60
     * @var ContainerInterface
61
     */
62
    private $serviceContainer;
63
64
    /**
65
     * @var string
66
     */
67
    private $schemaFilePath;
68
69 3
    public function __construct(
70
        FileLocator $doctrineFileLocator,
71
        KernelInterface $kernel,
72
        string $schemaFilePath
73
    ) {
74
        /** @var ContainerInterface|null $serviceContainer */
75 3
        $serviceContainer = $kernel->getContainer();
76
77 3
        if (is_null($serviceContainer)) {
78
            throw new ErrorException("Kernel does not have a container!");
79
        }
80
81 3
        $this->doctrineFileLocator = $doctrineFileLocator;
82 3
        $this->kernel = $kernel;
83 3
        $this->schemaFilePath = $schemaFilePath;
84 3
        $this->serviceContainer = $serviceContainer;
85
    }
86
87 2
    public function loadRDMMetadataForClass(string $className): ?EntityMappingInterface
88
    {
89
        /** @var ?EntityMappingInterface $mapping */
90 2
        $mapping = null;
91
92
        /** @var array<MappingInterface> $fieldMappings */
93 2
        $fieldMappings = array();
94
95 2
        if ($this->doctrineFileLocator->fileExists($className)) {
96
            /** @var string $mappingFile */
97 2
            $mappingFile = $this->doctrineFileLocator->findMappingFile($className);
98
99 2
            $fieldMappings = $this->readFieldMappingsFromFile($mappingFile);
100
        }
101
102 1
        if (!empty($fieldMappings)) {
103 1
            $mapping = new EntityMapping($className, $fieldMappings);
104
        }
105
106 1
        return $mapping;
107
    }
108
109
    /**
110
     * @return array<MappingInterface>
111
     */
112 2
    private function readFieldMappingsFromFile(string $mappingFile, string $parentMappingFile = null): array
113
    {
114 2
        if ($mappingFile[0] === '@') {
115
            /** @var string $mappingFile */
116 1
            $mappingFile = $this->kernel->locateResource($mappingFile);
117
        }
118
119 2
        if ($mappingFile[0] !== DIRECTORY_SEPARATOR && !empty($parentMappingFile)) {
120 1
            $mappingFile = dirname($parentMappingFile) . DIRECTORY_SEPARATOR . $mappingFile;
121
        }
122
123 2
        if (!file_exists($mappingFile)) {
124 1
            throw new InvalidMappingException(sprintf(
125
                "Missing referenced orm file '%s'%s!",
126
                $mappingFile,
127 1
                is_string($parentMappingFile) ?sprintf(", referenced in file '%s'", $parentMappingFile) :''
128
            ));
129
        }
130
        
131
        /** @var string|null $mappingXml */
132 2
        $mappingXml = file_get_contents($mappingFile);
133
        
134 2
        Assert::notEmpty($mappingXml, sprintf('ORM-Mapping file "%s" is empty!', $mappingFile));
135
136 2
        $dom = new DOMDocument();
137 2
        $dom->loadXML($mappingXml);
138
139
        /** @var DOMXPath $xpath */
140 2
        $xpath = $this->createXPath($dom->documentElement);
141
142
        /** @var array<MappingInterface> $fieldMappings */
143 2
        $fieldMappings = $this->readFieldMappings(
144
            $dom,
145
            $mappingFile,
146
            null,
147
            false
148
        );
149
150 2
        foreach ($xpath->query("//orm:entity", $dom) as $entityNode) {
151
            /** @var DOMNode $entityNode */
152
153 2
            $fieldMappings = array_merge($fieldMappings, $this->readFieldMappings(
154
                $entityNode,
155
                $mappingFile,
156
                null,
157
                false
158
            ));
159
        }
160
161 1
        return $fieldMappings;
162
    }
163
164 2
    private function createXPath(DOMNode $node): DOMXPath
165
    {
166
        /** @var DOMNode $ownerDocument */
167 2
        $ownerDocument = $node;
168
169 2
        if (!$ownerDocument instanceof DOMDocument) {
170 2
            $ownerDocument = $node->ownerDocument;
171 2
            Assert::object($ownerDocument);
172
        }
173
174 2
        $xpath = new DOMXPath($ownerDocument);
0 ignored issues
show
Bug introduced by
It seems like $ownerDocument can also be of type null; however, parameter $document of DOMXPath::__construct() does only seem to accept DOMDocument, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

174
        $xpath = new DOMXPath(/** @scrutinizer ignore-type */ $ownerDocument);
Loading history...
175 2
        $xpath->registerNamespace('rdm', self::RDM_SCHEMA_URI);
176 2
        $xpath->registerNamespace('orm', self::DOCTRINE_SCHEMA_URI);
177
178 2
        return $xpath;
179
    }
180
181 1
    private function readObject(DOMNode $objectNode, string $mappingFile): ObjectMapping
182
    {
183
        /** @var DOMNamedNodeMap|null $attributes */
184 1
        $objectNodeAttributes = $objectNode->attributes;
0 ignored issues
show
Unused Code introduced by
The assignment to $objectNodeAttributes is dead and can be removed.
Loading history...
185
186 1
        if (!$this->hasAttributeValue($objectNode, "class")) {
187
            throw new InvalidMappingException(sprintf(
188
                "Missing 'class' attribute on 'object' mapping in %s in line %d",
189
                $mappingFile,
190
                $objectNode->getLineNo()
191
            ));
192
        }
193
194
        /** @var class-string $className */
195 1
        $className = (string)$this->readAttributeValue($objectNode, "class");
196
197 1
        Assert::true(class_exists($className) || interface_exists($className));
198
199
        /** @var CallDefinitionInterface|null $factory */
200 1
        $factory = null;
201
202
        /** @var CallDefinitionInterface|null $factory */
203 1
        $serializer = null;
204
205
        /** @var DOMXPath $xpath */
206 1
        $xpath = $this->createXPath($objectNode);
207
208 1
        foreach ($xpath->query('./rdm:factory', $objectNode) as $factoryNode) {
209
            /** @var DOMNode $factoryNode */
210
211
            /** @var array<MappingInterface> $argumentMappings */
212 1
            $argumentMappings = $this->readFieldMappings($factoryNode, $mappingFile);
213
214
            /** @var string $routineName */
215 1
            $routineName = (string)$this->readAttributeValue($factoryNode, "method");
216
217
            /** @var string $objectReference */
218 1
            $objectReference = (string)$this->readAttributeValue($factoryNode, "object");
219
220 1
            $factory = new CallDefinition(
221 1
                $this->serviceContainer,
222
                $routineName,
223
                $objectReference,
224
                $argumentMappings,
225
                false,
226 1
                $mappingFile . " in line " . $objectNode->getLineNo()
227
            );
228
        }
229
230 1
        if ($this->hasAttributeValue($objectNode, "factory") && is_null($factory)) {
231 1
            $factory = $this->readCallDefinition(
232 1
                (string)$this->readAttributeValue($objectNode, "factory"),
233 1
                $mappingFile . " in line " . $objectNode->getLineNo()
234
            );
235
        }
236
237 1
        if ($this->hasAttributeValue($objectNode, "serialize")) {
238 1
            $serializer = $this->readCallDefinition(
239 1
                (string)$this->readAttributeValue($objectNode, "serialize"),
240 1
                $mappingFile . " in line " . $objectNode->getLineNo()
241
            );
242
        }
243
244
        /** @var array<MappingInterface> $fieldMappings */
245 1
        $fieldMappings = $this->readFieldMappings($objectNode, $mappingFile);
246
247
        /** @var Column|null $dbalColumn */
248 1
        $dbalColumn = null;
249
250 1
        if ($this->hasAttributeValue($objectNode, "column")) {
251
            /** @var bool $notnull */
252 1
            $notnull = true;
253
254
            /** @var string $type */
255 1
            $type = "string";
256
257
            /** @var int $length */
258 1
            $length = 255;
259
260
            /** @var string|null $default */
261 1
            $default = null;
262
263 1
            if ($this->hasAttributeValue($objectNode, "nullable")) {
264 1
                $notnull = (strtolower((string)$this->readAttributeValue($objectNode, "nullable")) !== 'true');
265
            }
266
267 1
            if ($this->hasAttributeValue($objectNode, "column-type")) {
268 1
                $type = (string)$this->readAttributeValue($objectNode, "column-type");
269
            }
270
271 1
            if ($this->hasAttributeValue($objectNode, "column-length")) {
272
                $length = (string)$this->readAttributeValue($objectNode, "column-length");
273
            }
274
275 1
            if ($this->hasAttributeValue($objectNode, "column-default")) {
276 1
                $default = (string)$this->readAttributeValue($objectNode, "column-default");
277
            }
278
279 1
            $dbalColumn = new Column(
280 1
                (string)$this->readAttributeValue($objectNode, "column"),
281 1
                Type::getType($type),
282
                [
283 1
                    'notnull' => $notnull,
284
                    'length' => $length,
285
                    'default' => $default
286
                ]
287
            );
288
        }
289
290
        /** @var string|null $id */
291 1
        $id = null;
292
293
        /** @var string|null $referencedId */
294 1
        $referencedId = null;
295
296 1
        if ($this->hasAttributeValue($objectNode, "id")) {
297
            $id = (string)$this->readAttributeValue($objectNode, "id");
298
        }
299
300 1
        if ($this->hasAttributeValue($objectNode, "references-id")) {
301
            $referencedId = (string)$this->readAttributeValue($objectNode, "references-id");
302
        }
303
304 1
        return new ObjectMapping(
305
            $className,
306
            $fieldMappings,
307
            $dbalColumn,
308 1
            sprintf(
309
                "in file '%s' in line %d",
310
                $mappingFile,
311 1
                $objectNode->getLineNo()
312
            ),
313
            $factory,
314
            $serializer,
315
            $id,
316
            $referencedId
317
        );
318
    }
319
320 1
    private function readCallDefinition(
321
        string $callDefinition,
322
        string $origin = "unknown"
323
    ): CallDefinitionInterface {
324
        /** @var string $routineName */
325 1
        $routineName = $callDefinition;
326
327
        /** @var string|null $objectReference */
328 1
        $objectReference = null;
329
330
        /** @var bool $isStaticCall */
331 1
        $isStaticCall = false;
332
333 1
        if (strpos($callDefinition, '::') !== false) {
334 1
            [$objectReference, $routineName] = explode('::', $callDefinition);
335 1
            $isStaticCall = true;
336
        }
337
338 1
        if (strpos($callDefinition, '->') !== false) {
339
            [$objectReference, $routineName] = explode('->', $callDefinition);
340
        }
341
342 1
        return new CallDefinition(
343 1
            $this->serviceContainer,
344
            $routineName,
345
            $objectReference,
346 1
            [],
347
            $isStaticCall,
348
            $origin
349
        );
350
    }
351
352 1
    private function readChoice(
353
        DOMNode $choiceNode,
354
        string $mappingFile,
355
        string $defaultColumnName
356
    ): ChoiceMapping {
357
        /** @var string|Column $columnName */
358 1
        $column = $defaultColumnName;
359
360 1
        if ($this->hasAttributeValue($choiceNode, "column")) {
361 1
            $column = (string)$this->readAttributeValue($choiceNode, "column");
362
        }
363
364
        /** @var array<MappingInterface> $choiceMappings */
365 1
        $choiceMappings = array();
366
367
        /** @var DOMXPath $xpath */
368 1
        $xpath = $this->createXPath($choiceNode);
369
370 1
        foreach ($xpath->query('./rdm:option', $choiceNode) as $optionNode) {
371
            /** @var DOMNode $optionNode */
372
373
            /** @var string $determinator */
374 1
            $determinator = (string)$this->readAttributeValue($optionNode, "name");
375
376
            /** @var string $optionDefaultColumnName */
377 1
            $optionDefaultColumnName = sprintf("%s_%s", $defaultColumnName, $determinator);
378
379 1
            foreach ($this->readFieldMappings($optionNode, $mappingFile, $optionDefaultColumnName) as $mapping) {
380
                /** @var MappingInterface $mapping */
381
382 1
                $choiceMappings[$determinator] = $mapping;
383
            }
384
        }
385
386 1
        foreach ($xpath->query('./orm:field', $choiceNode) as $fieldNode) {
387
            /** @var DOMNode $fieldNode */
388
389 1
            $column = $this->readDoctrineField($fieldNode);
390
        }
391
392 1
        return new ChoiceMapping($column, $choiceMappings, sprintf(
393
            "in file '%s' in line %d",
394
            $mappingFile,
395 1
            $choiceNode->getLineNo()
396
        ));
397
    }
398
399
    /**
400
     * @return array<MappingInterface>
401
     */
402 2
    private function readFieldMappings(
403
        DOMNode $parentNode,
404
        string $mappingFile,
405
        string $choiceDefaultColumnName = null,
406
        bool $readFields = true
407
    ): array {
408
        /** @var DOMXPath $xpath */
409 2
        $xpath = $this->createXPath($parentNode);
410
411
        /** @var array<MappingInterface> $fieldMappings */
412 2
        $fieldMappings = array();
413
414 2
        foreach ($xpath->query('./rdm:service', $parentNode) as $serviceNode) {
415
            /** @var DOMNode $serviceNode */
416
417 1
            $serviceMapping = $this->readService($serviceNode, $mappingFile);
418
419 1
            if ($this->hasAttributeValue($serviceNode, "field")) {
420
                /** @var string $fieldName */
421 1
                $fieldName = (string)$this->readAttributeValue($serviceNode, "field");
422
423 1
                $fieldMappings[$fieldName] = $serviceMapping;
424
425
            } else {
426 1
                $fieldMappings[] = $serviceMapping;
427
            }
428
        }
429
430 2
        foreach ($xpath->query('./rdm:choice', $parentNode) as $choiceNode) {
431
            /** @var DOMNode $choiceNode */
432
433
            /** @var string $defaultColumnName */
434 1
            $defaultColumnName = "";
435
436 1
            if (!is_null($choiceDefaultColumnName)) {
437
                $defaultColumnName = $choiceDefaultColumnName;
438
439 1
            } elseif ($this->hasAttributeValue($choiceNode, "field")) {
440 1
                $defaultColumnName = (string)$this->readAttributeValue($choiceNode, "field");
441
            }
442
443 1
            $choiceMapping = $this->readChoice($choiceNode, $mappingFile, $defaultColumnName);
444
445 1
            if ($this->hasAttributeValue($choiceNode, "field")) {
446
                /** @var string $fieldName */
447 1
                $fieldName = (string)$this->readAttributeValue($choiceNode, "field");
448
449 1
                $fieldMappings[$fieldName] = $choiceMapping;
450
451
            } else {
452 1
                $fieldMappings[] = $choiceMapping;
453
            }
454
        }
455
456 2
        foreach ($xpath->query('./rdm:object', $parentNode) as $objectNode) {
457
            /** @var DOMNode $objectNode */
458
459
            /** @var ObjectMapping $objectMapping */
460 1
            $objectMapping = $this->readObject($objectNode, $mappingFile);
461
462 1
            if ($this->hasAttributeValue($objectNode, "field")) {
463
                /** @var string $fieldName */
464 1
                $fieldName = (string)$this->readAttributeValue($objectNode, "field");
465
466 1
                $fieldMappings[$fieldName] = $objectMapping;
467
468
            } else {
469 1
                $fieldMappings[] = $objectMapping;
470
            }
471
        }
472
473 2
        if ($readFields) {
474 1
            foreach ($xpath->query('./orm:field', $parentNode) as $fieldNode) {
475
                /** @var DOMNode $fieldNode */
476
477
                /** @var Column $column */
478 1
                $column = $this->readDoctrineField($fieldNode);
479
480 1
                $fieldName = (string)$this->readAttributeValue($fieldNode, "name");
481
482 1
                $fieldMappings[$fieldName] = new FieldMapping(
483
                    $column,
484 1
                    sprintf("in file '%s' in line %d", $mappingFile, $fieldNode->getLineNo())
485
                );
486
            }
487
        }
488
489 2
        foreach ($xpath->query('./rdm:array', $parentNode) as $arrayNode) {
490
            /** @var DOMNode $arrayNode */
491
492
            /** @var ArrayMapping $arrayMapping */
493 1
            $arrayMapping = $this->readArray($arrayNode, $mappingFile);
494
495 1
            if ($this->hasAttributeValue($arrayNode, "field")) {
496
                /** @var string $fieldName */
497 1
                $fieldName = (string)$this->readAttributeValue($arrayNode, "field");
498
499 1
                $fieldMappings[$fieldName] = $arrayMapping;
500
501
            } else {
502
                $fieldMappings[] = $arrayMapping;
503
            }
504
        }
505
506 2
        foreach ($xpath->query('./rdm:list', $parentNode) as $listNode) {
507
            /** @var DOMNode $listNode */
508
509
            /** @var string $defaultColumnName */
510 1
            $defaultColumnName = "";
511
512 1
            if (!is_null($choiceDefaultColumnName)) {
513
                $defaultColumnName = $choiceDefaultColumnName;
514
515 1
            } elseif ($this->hasAttributeValue($listNode, "field")) {
516 1
                $defaultColumnName = (string)$this->readAttributeValue($listNode, "field");
517
            }
518
519
            /** @var ListMapping $listMapping */
520 1
            $listMapping = $this->readList($listNode, $mappingFile, $defaultColumnName);
521
522 1
            if ($this->hasAttributeValue($listNode, "field")) {
523
                /** @var string $fieldName */
524 1
                $fieldName = (string)$this->readAttributeValue($listNode, "field");
525
526 1
                $fieldMappings[$fieldName] = $listMapping;
527
528
            } else {
529 1
                $fieldMappings[] = $listMapping;
530
            }
531
        }
532
533 2
        foreach ($xpath->query('./rdm:null', $parentNode) as $nullNode) {
534
            /** @var DOMNode $nullNode */
535
536 1
            if ($this->hasAttributeValue($nullNode, "field")) {
537
                /** @var string $fieldName */
538
                $fieldName = (string)$this->readAttributeValue($nullNode, "field");
539
540
                $fieldMappings[$fieldName] = new NullMapping(sprintf(
541
                    "in file '%s' in line %d",
542
                    $mappingFile,
543
                    $nullNode->getLineNo()
544
                ));
545
546
            } else {
547 1
                $fieldMappings[] = new NullMapping(sprintf(
548
                    "in file '%s' in line %d",
549
                    $mappingFile,
550 1
                    $nullNode->getLineNo()
551
                ));
552
            }
553
        }
554
555 2
        foreach ($xpath->query('./rdm:nullable', $parentNode) as $nullableNode) {
556
            /** @var DOMNode $nullableNode */
557
558
            /** @var NullableMapping $nullableMapping */
559 1
            $nullableMapping = $this->readNullable($nullableNode, $mappingFile);
560
561 1
            if ($this->hasAttributeValue($nullableNode, "field")) {
562
                /** @var string $fieldName */
563 1
                $fieldName = (string)$this->readAttributeValue($nullableNode, "field");
564
565 1
                $fieldMappings[$fieldName] = $nullableMapping;
566
567
            } else {
568
                $fieldMappings[] = $nullableMapping;
569
            }
570
        }
571
572 2
        foreach ($xpath->query('./rdm:import', $parentNode) as $importNode) {
573
            /** @var DOMNode $importNode */
574
575
            /** @var string $path */
576 2
            $path = (string)$this->readAttributeValue($importNode, "path");
577
578
            /** @var string|null $forcedFieldName */
579 2
            $forcedFieldName = $this->readAttributeValue($importNode, "field");
580
581
            /** @var string $columnPrefix */
582 2
            $columnPrefix = (string)$this->readAttributeValue($importNode, "column-prefix");
583
584 2
            foreach ($this->readFieldMappingsFromFile($path, $mappingFile) as $fieldName => $fieldMapping) {
585
                /** @var MappingInterface $fieldMapping */
586
587 1
                $fieldMappingProxy = new MappingProxy(
0 ignored issues
show
Unused Code introduced by
The assignment to $fieldMappingProxy is dead and can be removed.
Loading history...
588
                    $fieldMapping,
589
                    $columnPrefix
590
                );
591
592 1
                if (!empty($forcedFieldName)) {
593 1
                    $fieldMappings[$forcedFieldName] = $fieldMapping;
594
                } else {
595
596
                    $fieldMappings[$fieldName] = $fieldMapping;
597
                }
598
            }
599
        }
600
601 2
        return $fieldMappings;
602
    }
603
604 1
    private function readService(DOMNode $serviceNode, string $mappingFile): ServiceMapping
605
    {
606
        /** @var bool $lax */
607 1
        $lax = strtolower((string)$this->readAttributeValue($serviceNode, "lax")) === 'true';
608
609
        /** @var string $serviceId */
610 1
        $serviceId = (string)$this->readAttributeValue($serviceNode, "id");
611
612 1
        return new ServiceMapping(
613 1
            $this->serviceContainer,
614
            $serviceId,
615
            $lax,
616 1
            sprintf(
617
                "in file '%s' in line %d",
618
                $mappingFile,
619 1
                $serviceNode->getLineNo()
620
            )
621
        );
622
    }
623
624 1
    private function readArray(DOMNode $arrayNode, string $mappingFile): ArrayMapping
625
    {
626
        /** @var array<MappingInterface> $entryMappings */
627 1
        $entryMappings = $this->readFieldMappings($arrayNode, $mappingFile);
628
629
        /** @var DOMXPath $xpath */
630 1
        $xpath = $this->createXPath($arrayNode);
631
632 1
        foreach ($xpath->query('./rdm:entry', $arrayNode) as $entryNode) {
633
            /** @var DOMNode $entryNode */
634
635
            /** @var string|null $key */
636 1
            $key = $this->readAttributeValue($entryNode, "key");
637
638 1
            foreach ($this->readFieldMappings($entryNode, $mappingFile) as $entryMapping) {
639
                /** @var MappingInterface $entryMapping */
640
641 1
                if (is_null($key)) {
642
                    $entryMappings[] = $entryMapping;
643
644
                } else {
645 1
                    $entryMappings[$key] = $entryMapping;
646
                }
647
648 1
                break;
649
            }
650
        }
651
652 1
        return new ArrayMapping($entryMappings, sprintf(
653
            "in file '%s' in line %d",
654
            $mappingFile,
655 1
            $arrayNode->getLineNo()
656
        ));
657
    }
658
659 1
    private function readList(
660
        DOMNode $listNode,
661
        string $mappingFile,
662
        string $columnName
663
    ): ListMapping {
664 1
        if ($this->hasAttributeValue($listNode, "column")) {
665 1
            $columnName = (string)$this->readAttributeValue($listNode, "column");
666
        }
667
668
        /** @var array<MappingInterface> $entryMappings */
669 1
        $entryMappings = $this->readFieldMappings($listNode, $mappingFile);
670
671
        /** @var array<string, mixed> $columnOptions */
672 1
        $columnOptions = array();
673
674 1
        if ($this->hasAttributeValue($listNode, "column-length")) {
675
            $columnOptions['length'] = (int)$this->readAttributeValue($listNode, "column-length", "0");
676
        }
677
678 1
        $column = new Column(
679
            $columnName,
680 1
            Type::getType("string"),
681
            $columnOptions
682
        );
683
684 1
        return new ListMapping($column, array_values($entryMappings)[0], sprintf(
685
            "in file '%s' in line %d",
686
            $mappingFile,
687 1
            $listNode->getLineNo()
688
        ));
689
    }
690
691 1
    private function readNullable(
692
        DOMNode $nullableNode,
693
        string $mappingFile
694
    ): NullableMapping {
695
        /** @var array<MappingInterface> $innerMappings */
696 1
        $innerMappings = $this->readFieldMappings($nullableNode, $mappingFile);
697
698 1
        if (count($innerMappings) !== 1) {
699
            throw new InvalidMappingException(sprintf(
700
                "A nullable mapping must contain exactly one inner mapping in '%s' at line %d!",
701
                $mappingFile,
702
                $nullableNode->getLineNo()
703
            ));
704
        }
705
706
        /** @var MappingInterface $innerMapping */
707 1
        $innerMapping = array_values($innerMappings)[0];
708
709
        /** @var Column|null $column */
710 1
        $column = null;
711
712 1
        if ($this->hasAttributeValue($nullableNode, "column")) {
713
            /** @var string $columnName */
714 1
            $columnName = $this->readAttributeValue($nullableNode, "column", "");
715
716 1
            $column = new Column(
717
                $columnName,
718 1
                Type::getType("boolean"),
719
                [
720 1
                    'notnull' => false
721
                ]
722
            );
723
        }
724
725 1
        $strict = $this->readAttributeValue($nullableNode, "strict", "false") === "true" ? true : false;
726
727 1
        return new NullableMapping($innerMapping, $column, sprintf(
728
            "in file '%s' at line %d",
729
            $mappingFile,
730 1
            $nullableNode->getLineNo()
731
        ), $strict);
732
    }
733
734 1
    private function readDoctrineField(DOMNode $fieldNode): Column
735
    {
736
        /** @var array<string> $attributes */
737 1
        $attributes = array();
738
739
        /** @var array<string> $keyMap */
740 1
        $keyMap = array(
741
            'column'            => 'name',
742
            'type'              => 'type',
743
            'nullable'          => 'notnull',
744
            'length'            => 'length',
745
            'precision'         => 'precision',
746
            'scale'             => 'scale',
747
            'column-definition' => 'columnDefinition',
748
        );
749
750
        /** @var string|null $columnName */
751 1
        $columnName = null;
752
753
        /** @var Type $type */
754 1
        $type = Type::getType('string');
755
756
        /** @var DOMNamedNodeMap|null $fieldNodeAttributes */
757 1
        $fieldNodeAttributes = $fieldNode->attributes;
758
759 1
        if (is_object($fieldNodeAttributes)) {
760 1
            foreach ($fieldNodeAttributes as $key => $attribute) {
761
                /** @var DOMAttr $attribute */
762
763 1
                $attributeValue = $attribute->nodeValue;
764
765 1
                if ($key === 'column') {
766
                    $columnName = $attributeValue;
767
768 1
                } elseif ($key === 'name') {
769 1
                    if (empty($columnName)) {
770 1
                        $columnName = $attributeValue;
771
                    }
772
773 1
                } elseif ($key === 'type') {
774 1
                    $type = Type::getType((string) $attributeValue);
775
776 1
                } elseif (isset($keyMap[$key])) {
777 1
                    if ($key === 'nullable') {
778
                        # target is 'notnull', so falue is reversed
779 1
                        $attributeValue = ($attributeValue === 'false');
780
                    }
781
782 1
                    $attributes[$keyMap[$key]] = $attributeValue;
783
                }
784
            }
785
        }
786
        
787 1
        Assert::notEmpty($columnName, 'Column name cannot be empty!');
788
789 1
        $column = new Column(
790
            $columnName,
791
            $type,
792
            $attributes
793
        );
794
795 1
        return $column;
796
    }
797
798 1
    private function hasAttributeValue(DOMNode $node, string $attributeName): bool
799
    {
800
        /** @var DOMNamedNodeMap $nodeAttributes */
801 1
        $nodeAttributes = $node->attributes;
802
803
        /** @var DOMNode|null $attributeNode */
804 1
        $attributeNode = $nodeAttributes->getNamedItem($attributeName);
805
806 1
        return is_object($attributeNode);
807
    }
808
809 2
    private function readAttributeValue(DOMNode $node, string $attributeName, ?string $default = null): ?string
810
    {
811
        /** @var DOMNamedNodeMap $nodeAttributes */
812 2
        $nodeAttributes = $node->attributes;
813
814
        /** @var DOMNode|null $attributeNode */
815 2
        $attributeNode = $nodeAttributes->getNamedItem($attributeName);
816
817
        /** @var string|null $value */
818 2
        $value = $default;
819
820 2
        if (is_object($attributeNode)) {
821 2
            $value = $attributeNode->nodeValue;
822
        }
823
824 2
        return $value;
825
    }
826
827
}
828