Passed
Push — master ( 50749f...7b5f02 )
by Gerrit
12:36
created

MappingXmlDriver::readArray()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 33

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 4.0092

Importance

Changes 0
Metric Value
dl 0
loc 33
ccs 11
cts 12
cp 0.9167
rs 9.392
c 0
b 0
f 0
cc 4
nc 4
nop 2
crap 4.0092
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\Common\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 3
    }
86
87 2
    public function loadRDMMetadataForClass(string $className): ?EntityMappingInterface
88
    {
89
        /** @var ?EntityMappingInterface $mapping */
0 ignored issues
show
Documentation introduced by
The doc-type ?EntityMappingInterface could not be parsed: Unknown type name "?EntityMappingInterface" at position 0. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
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 1
                "Missing referenced orm file '%s'%s!",
126
                $mappingFile,
127 1
                is_string($parentMappingFile) ?sprintf(", referenced in file '%s'", $parentMappingFile) :''
128
            ));
129
        }
130
131 2
        $dom = new DOMDocument();
132 2
        $dom->loadXML(file_get_contents($mappingFile));
133
134
        /** @var DOMXPath $xpath */
135 2
        $xpath = $this->createXPath($dom->documentElement);
136
137
        /** @var array<MappingInterface> $fieldMappings */
138 2
        $fieldMappings = $this->readFieldMappings(
139 2
            $dom,
140
            $mappingFile,
141 2
            null,
142 2
            false
143
        );
144
145 2
        foreach ($xpath->query("//orm:entity", $dom) as $entityNode) {
146
            /** @var DOMNode $entityNode */
147
148 2
            $fieldMappings = array_merge($fieldMappings, $this->readFieldMappings(
149 2
                $entityNode,
150
                $mappingFile,
151 2
                null,
152 2
                false
153
            ));
154
        }
155
156 1
        return $fieldMappings;
157
    }
158
159 2
    private function createXPath(DOMNode $node): DOMXPath
160
    {
161
        /** @var DOMNode $ownerDocument */
162 2
        $ownerDocument = $node;
163
164 2
        if (!$ownerDocument instanceof DOMDocument) {
165 2
            $ownerDocument = $node->ownerDocument;
166 2
            Assert::object($ownerDocument);
167
        }
168
169 2
        $xpath = new DOMXPath($ownerDocument);
170 2
        $xpath->registerNamespace('rdm', self::RDM_SCHEMA_URI);
171 2
        $xpath->registerNamespace('orm', self::DOCTRINE_SCHEMA_URI);
172
173 2
        return $xpath;
174
    }
175
176 1
    private function readObject(DOMNode $objectNode, string $mappingFile): ObjectMapping
177
    {
178
        /** @var DOMNamedNodeMap|null $attributes */
179 1
        $objectNodeAttributes = $objectNode->attributes;
0 ignored issues
show
Unused Code introduced by
$objectNodeAttributes is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
180
181 1
        if (!$this->hasAttributeValue($objectNode, "class")) {
182
            throw new InvalidMappingException(sprintf(
183
                "Missing 'class' attribute on 'object' mapping in %s",
184
                $mappingFile
185
            ));
186
        }
187
188
        /** @var class-string $className */
0 ignored issues
show
Documentation introduced by
The doc-type class-string could not be parsed: Unknown type name "class-string" at position 0. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
189 1
        $className = (string)$this->readAttributeValue($objectNode, "class");
190
191 1
        Assert::true(class_exists($className) || interface_exists($className));
192
193
        /** @var CallDefinitionInterface|null $factory */
194 1
        $factory = null;
195
196
        /** @var CallDefinitionInterface|null $factory */
197 1
        $serializer = null;
198
199
        /** @var DOMXPath $xpath */
200 1
        $xpath = $this->createXPath($objectNode);
201
202 1
        foreach ($xpath->query('./rdm:factory', $objectNode) as $factoryNode) {
203
            /** @var DOMNode $factoryNode */
204
205
            /** @var array<MappingInterface> $argumentMappings */
206 1
            $argumentMappings = $this->readFieldMappings($factoryNode, $mappingFile);
207
208
            /** @var string $routineName */
209 1
            $routineName = (string)$this->readAttributeValue($factoryNode, "method");
210
211
            /** @var string $objectReference */
212 1
            $objectReference = (string)$this->readAttributeValue($factoryNode, "object");
213
214 1
            $factory = new CallDefinition(
215 1
                $this->serviceContainer,
216
                $routineName,
217
                $objectReference,
218
                $argumentMappings
219
            );
220
        }
221
222 1
        if ($this->hasAttributeValue($objectNode, "factory") && is_null($factory)) {
223 1
            $factory = $this->readCallDefinition(
224 1
                (string)$this->readAttributeValue($objectNode, "factory")
225
            );
226
        }
227
228 1
        if ($this->hasAttributeValue($objectNode, "serialize")) {
229 1
            $serializer = $this->readCallDefinition(
230 1
                (string)$this->readAttributeValue($objectNode, "serialize")
231
            );
232
        }
233
234
        /** @var array<MappingInterface> $fieldMappings */
235 1
        $fieldMappings = $this->readFieldMappings($objectNode, $mappingFile);
236
237
        /** @var Column|null $dbalColumn */
238 1
        $dbalColumn = null;
239
240 1
        if ($this->hasAttributeValue($objectNode, "column")) {
241
            /** @var bool $notnull */
242 1
            $notnull = true;
243
244
            /** @var string $type */
245 1
            $type = "string";
246
247
            /** @var int $length */
248 1
            $length = 255;
249
250
            /** @var string|null $default */
251 1
            $default = null;
252
253 1
            if ($this->hasAttributeValue($objectNode, "nullable")) {
254 1
                $notnull = (strtolower((string)$this->readAttributeValue($objectNode, "nullable")) !== 'true');
255
            }
256
257 1
            if ($this->hasAttributeValue($objectNode, "column-type")) {
258 1
                $type = (string)$this->readAttributeValue($objectNode, "column-type");
259
            }
260
261 1
            if ($this->hasAttributeValue($objectNode, "column-length")) {
262
                $length = (string)$this->readAttributeValue($objectNode, "column-length");
263
            }
264
265 1
            if ($this->hasAttributeValue($objectNode, "column-default")) {
266 1
                $default = (string)$this->readAttributeValue($objectNode, "column-default");
267
            }
268
269 1
            $dbalColumn = new Column(
270 1
                (string)$this->readAttributeValue($objectNode, "column"),
271 1
                Type::getType($type),
272
                [
273 1
                    'notnull' => $notnull,
274 1
                    'length' => $length,
275 1
                    'default' => $default
276
                ]
277
            );
278
        }
279
280
        /** @var string|null $id */
281 1
        $id = null;
282
283
        /** @var string|null $referencedId */
284 1
        $referencedId = null;
285
286 1
        if ($this->hasAttributeValue($objectNode, "id")) {
287
            $id = (string)$this->readAttributeValue($objectNode, "id");
288
        }
289
290 1
        if ($this->hasAttributeValue($objectNode, "references-id")) {
291
            $referencedId = (string)$this->readAttributeValue($objectNode, "references-id");
292
        }
293
294 1
        return new ObjectMapping(
295 1
            $className,
296
            $fieldMappings,
297
            $dbalColumn,
298 1
            sprintf(
299 1
                "in file '%s'",
300
                $mappingFile
301
            ),
302
            $factory,
303
            $serializer,
304
            $id,
305
            $referencedId
306
        );
307
    }
308
309 1
    private function readCallDefinition(string $callDefinition): CallDefinitionInterface
310
    {
311
        /** @var string $routineName */
312 1
        $routineName = $callDefinition;
313
314
        /** @var string|null $objectReference */
315 1
        $objectReference = null;
316
317
        /** @var bool $isStaticCall */
318 1
        $isStaticCall = false;
319
320 1
        if (strpos($callDefinition, '::') !== false) {
321 1
            [$objectReference, $routineName] = explode('::', $callDefinition);
322 1
            $isStaticCall = true;
323
        }
324
325 1
        if (strpos($callDefinition, '->') !== false) {
326
            [$objectReference, $routineName] = explode('->', $callDefinition);
327
        }
328
329 1
        return new CallDefinition(
330 1
            $this->serviceContainer,
331
            $routineName,
332
            $objectReference,
333 1
            [],
334
            $isStaticCall
335
        );
336
    }
337
338 1
    private function readChoice(
339
        DOMNode $choiceNode,
340
        string $mappingFile,
341
        string $defaultColumnName
342
    ): ChoiceMapping {
343
        /** @var string|Column $columnName */
344 1
        $column = $defaultColumnName;
345
346 1
        if ($this->hasAttributeValue($choiceNode, "column")) {
347 1
            $column = (string)$this->readAttributeValue($choiceNode, "column");
348
        }
349
350
        /** @var array<MappingInterface> $choiceMappings */
351 1
        $choiceMappings = array();
352
353
        /** @var DOMXPath $xpath */
354 1
        $xpath = $this->createXPath($choiceNode);
355
356 1
        foreach ($xpath->query('./rdm:option', $choiceNode) as $optionNode) {
357
            /** @var DOMNode $optionNode */
358
359
            /** @var string $determinator */
360 1
            $determinator = (string)$this->readAttributeValue($optionNode, "name");
361
362
            /** @var string $optionDefaultColumnName */
363 1
            $optionDefaultColumnName = sprintf("%s_%s", $defaultColumnName, $determinator);
364
365 1
            foreach ($this->readFieldMappings($optionNode, $mappingFile, $optionDefaultColumnName) as $mapping) {
366
                /** @var MappingInterface $mapping */
367
368 1
                $choiceMappings[$determinator] = $mapping;
369
            }
370
        }
371
372 1
        foreach ($xpath->query('./orm:field', $choiceNode) as $fieldNode) {
373
            /** @var DOMNode $fieldNode */
374
375 1
            $column = $this->readDoctrineField($fieldNode);
376
        }
377
378 1
        return new ChoiceMapping($column, $choiceMappings, sprintf(
379 1
            "in file '%s'",
380
            $mappingFile
381
        ));
382
    }
383
384
    /**
385
     * @return array<MappingInterface>
386
     */
387 2
    private function readFieldMappings(
388
        DOMNode $parentNode,
389
        string $mappingFile,
390
        string $choiceDefaultColumnName = null,
391
        bool $readFields = true
392
    ): array {
393
        /** @var DOMXPath $xpath */
394 2
        $xpath = $this->createXPath($parentNode);
395
396
        /** @var array<MappingInterface> $fieldMappings */
397 2
        $fieldMappings = array();
398
399 2
        foreach ($xpath->query('./rdm:service', $parentNode) as $serviceNode) {
400
            /** @var DOMNode $serviceNode */
401
402 1
            $serviceMapping = $this->readService($serviceNode, $mappingFile);
403
404 1
            if ($this->hasAttributeValue($serviceNode, "field")) {
405
                /** @var string $fieldName */
406 1
                $fieldName = (string)$this->readAttributeValue($serviceNode, "field");
407
408 1
                $fieldMappings[$fieldName] = $serviceMapping;
409
410
            } else {
411 1
                $fieldMappings[] = $serviceMapping;
412
            }
413
        }
414
415 2
        foreach ($xpath->query('./rdm:choice', $parentNode) as $choiceNode) {
416
            /** @var DOMNode $choiceNode */
417
418
            /** @var string $defaultColumnName */
419 1
            $defaultColumnName = "";
420
421 1
            if (!is_null($choiceDefaultColumnName)) {
422
                $defaultColumnName = $choiceDefaultColumnName;
423
424 1
            } elseif ($this->hasAttributeValue($choiceNode, "field")) {
425 1
                $defaultColumnName = (string)$this->readAttributeValue($choiceNode, "field");
426
            }
427
428 1
            $choiceMapping = $this->readChoice($choiceNode, $mappingFile, $defaultColumnName);
429
430 1
            if ($this->hasAttributeValue($choiceNode, "field")) {
431
                /** @var string $fieldName */
432 1
                $fieldName = (string)$this->readAttributeValue($choiceNode, "field");
433
434 1
                $fieldMappings[$fieldName] = $choiceMapping;
435
436
            } else {
437 1
                $fieldMappings[] = $choiceMapping;
438
            }
439
        }
440
441 2
        foreach ($xpath->query('./rdm:object', $parentNode) as $objectNode) {
442
            /** @var DOMNode $objectNode */
443
444
            /** @var ObjectMapping $objectMapping */
445 1
            $objectMapping = $this->readObject($objectNode, $mappingFile);
446
447 1
            if ($this->hasAttributeValue($objectNode, "field")) {
448
                /** @var string $fieldName */
449 1
                $fieldName = (string)$this->readAttributeValue($objectNode, "field");
450
451 1
                $fieldMappings[$fieldName] = $objectMapping;
452
453
            } else {
454 1
                $fieldMappings[] = $objectMapping;
455
            }
456
        }
457
458 2
        if ($readFields) {
459 1
            foreach ($xpath->query('./orm:field', $parentNode) as $fieldNode) {
460
                /** @var DOMNode $fieldNode */
461
462
                /** @var Column $column */
463 1
                $column = $this->readDoctrineField($fieldNode);
464
465 1
                $fieldName = (string)$this->readAttributeValue($fieldNode, "name");
466
467 1
                $fieldMappings[$fieldName] = new FieldMapping(
468 1
                    $column,
469 1
                    sprintf("in file '%s'", $mappingFile)
470
                );
471
            }
472
        }
473
474 2
        foreach ($xpath->query('./rdm:array', $parentNode) as $arrayNode) {
475
            /** @var DOMNode $arrayNode */
476
477
            /** @var ArrayMapping $arrayMapping */
478 1
            $arrayMapping = $this->readArray($arrayNode, $mappingFile);
479
480 1
            if ($this->hasAttributeValue($arrayNode, "field")) {
481
                /** @var string $fieldName */
482 1
                $fieldName = (string)$this->readAttributeValue($arrayNode, "field");
483
484 1
                $fieldMappings[$fieldName] = $arrayMapping;
485
486
            } else {
487
                $fieldMappings[] = $arrayMapping;
488
            }
489
        }
490
491 2
        foreach ($xpath->query('./rdm:list', $parentNode) as $listNode) {
492
            /** @var DOMNode $listNode */
493
494
            /** @var string $defaultColumnName */
495 1
            $defaultColumnName = "";
496
497 1
            if (!is_null($choiceDefaultColumnName)) {
498
                $defaultColumnName = $choiceDefaultColumnName;
499
500 1
            } elseif ($this->hasAttributeValue($listNode, "field")) {
501 1
                $defaultColumnName = (string)$this->readAttributeValue($listNode, "field");
502
            }
503
504
            /** @var ListMapping $listMapping */
505 1
            $listMapping = $this->readList($listNode, $mappingFile, $defaultColumnName);
506
507 1
            if ($this->hasAttributeValue($listNode, "field")) {
508
                /** @var string $fieldName */
509 1
                $fieldName = (string)$this->readAttributeValue($listNode, "field");
510
511 1
                $fieldMappings[$fieldName] = $listMapping;
512
513
            } else {
514 1
                $fieldMappings[] = $listMapping;
515
            }
516
        }
517
518 2
        foreach ($xpath->query('./rdm:null', $parentNode) as $nullNode) {
519
            /** @var DOMNode $nullNode */
520
521 1
            if ($this->hasAttributeValue($nullNode, "field")) {
522
                /** @var string $fieldName */
523
                $fieldName = (string)$this->readAttributeValue($nullNode, "field");
524
525
                $fieldMappings[$fieldName] = new NullMapping("in file '{$mappingFile}'");
526
527
            } else {
528 1
                $fieldMappings[] = new NullMapping("in file '{$mappingFile}'");
529
            }
530
        }
531
532 2
        foreach ($xpath->query('./rdm:nullable', $parentNode) as $nullableNode) {
533
            /** @var DOMNode $nullableNode */
534
535
            /** @var NullableMapping $nullableMapping */
536 1
            $nullableMapping = $this->readNullable($nullableNode, $mappingFile);
537
538 1
            if ($this->hasAttributeValue($nullableNode, "field")) {
539
                /** @var string $fieldName */
540 1
                $fieldName = (string)$this->readAttributeValue($nullableNode, "field");
541
542 1
                $fieldMappings[$fieldName] = $nullableMapping;
543
544
            } else {
545
                $fieldMappings[] = $nullableMapping;
546
            }
547
        }
548
549 2
        foreach ($xpath->query('./rdm:import', $parentNode) as $importNode) {
550
            /** @var DOMNode $importNode */
551
552
            /** @var string $path */
553 2
            $path = (string)$this->readAttributeValue($importNode, "path");
554
555
            /** @var string|null $forcedFieldName */
556 2
            $forcedFieldName = $this->readAttributeValue($importNode, "field");
557
558
            /** @var string $columnPrefix */
559 2
            $columnPrefix = (string)$this->readAttributeValue($importNode, "column-prefix");
560
561 2
            foreach ($this->readFieldMappingsFromFile($path, $mappingFile) as $fieldName => $fieldMapping) {
562
                /** @var MappingInterface $fieldMapping */
563
564 1
                $fieldMappingProxy = new MappingProxy(
0 ignored issues
show
Unused Code introduced by
$fieldMappingProxy is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
565 1
                    $fieldMapping,
566
                    $columnPrefix
567
                );
568
569 1
                if (!empty($forcedFieldName)) {
570 1
                    $fieldMappings[$forcedFieldName] = $fieldMapping;
571
                } else {
572
573
                    $fieldMappings[$fieldName] = $fieldMapping;
574
                }
575
            }
576
        }
577
578 2
        return $fieldMappings;
579
    }
580
581 1
    private function readService(DOMNode $serviceNode, string $mappingFile): ServiceMapping
582
    {
583
        /** @var bool $lax */
584 1
        $lax = strtolower((string)$this->readAttributeValue($serviceNode, "lax")) === 'true';
585
586
        /** @var string $serviceId */
587 1
        $serviceId = (string)$this->readAttributeValue($serviceNode, "id");
588
589 1
        return new ServiceMapping(
590 1
            $this->serviceContainer,
591
            $serviceId,
592
            $lax,
593 1
            sprintf(
594 1
                "in file '%s'",
595
                $mappingFile
596
            )
597
        );
598
    }
599
600 1
    private function readArray(DOMNode $arrayNode, string $mappingFile): ArrayMapping
601
    {
602
        /** @var array<MappingInterface> $entryMappings */
603 1
        $entryMappings = $this->readFieldMappings($arrayNode, $mappingFile);
604
605
        /** @var DOMXPath $xpath */
606 1
        $xpath = $this->createXPath($arrayNode);
607
608 1
        foreach ($xpath->query('./rdm:entry', $arrayNode) as $entryNode) {
609
            /** @var DOMNode $entryNode */
610
611
            /** @var string|null $key */
612 1
            $key = $this->readAttributeValue($entryNode, "key");
613
614 1
            foreach ($this->readFieldMappings($entryNode, $mappingFile) as $entryMapping) {
615
                /** @var MappingInterface $entryMapping */
616
617 1
                if (is_null($key)) {
618
                    $entryMappings[] = $entryMapping;
619
620
                } else {
621 1
                    $entryMappings[$key] = $entryMapping;
622
                }
623
624 1
                break;
625
            }
626
        }
627
628 1
        return new ArrayMapping($entryMappings, sprintf(
629 1
            "in file '%s'",
630
            $mappingFile
631
        ));
632
    }
633
634 1
    private function readList(
635
        DOMNode $listNode,
636
        string $mappingFile,
637
        string $columnName
638
    ): ListMapping {
639 1
        if ($this->hasAttributeValue($listNode, "column")) {
640 1
            $columnName = (string)$this->readAttributeValue($listNode, "column");
641
        }
642
643
        /** @var array<MappingInterface> $entryMappings */
644 1
        $entryMappings = $this->readFieldMappings($listNode, $mappingFile);
645
646
        /** @var array<string, mixed> $columnOptions */
647 1
        $columnOptions = array();
648
649 1
        if ($this->hasAttributeValue($listNode, "column-length")) {
650
            $columnOptions['length'] = (int)$this->readAttributeValue($listNode, "column-length", "0");
651
        }
652
653 1
        $column = new Column(
654 1
            $columnName,
655 1
            Type::getType("string"),
656
            $columnOptions
657
        );
658
659 1
        return new ListMapping($column, array_values($entryMappings)[0], sprintf(
660 1
            "in file '%s'",
661
            $mappingFile
662
        ));
663
    }
664
665 1
    private function readNullable(
666
        DOMNode $nullableNode,
667
        string $mappingFile
668
    ): NullableMapping {
669
        /** @var array<MappingInterface> $innerMappings */
670 1
        $innerMappings = $this->readFieldMappings($nullableNode, $mappingFile);
671
672 1
        if (count($innerMappings) !== 1) {
673
            throw new InvalidMappingException(sprintf(
674
                "A nullable mapping must contain exactly one inner mapping in '%s' at line %d!",
675
                $mappingFile,
676
                $nullableNode->getLineNo()
677
            ));
678
        }
679
680
        /** @var MappingInterface $innerMapping */
681 1
        $innerMapping = array_values($innerMappings)[0];
682
683
        /** @var Column|null $column */
684 1
        $column = null;
685
686 1
        if ($this->hasAttributeValue($nullableNode, "column")) {
687
            /** @var string $columnName */
688 1
            $columnName = $this->readAttributeValue($nullableNode, "column", "");
689
690 1
            $column = new Column(
691 1
                $columnName,
692 1
                Type::getType("boolean"),
693
                [
694 1
                    'notnull' => false
695
                ]
696
            );
697
        }
698
699 1
        $strict = $this->readAttributeValue($nullableNode, "strict", "false") === "true" ? true : false;
700
701 1
        return new NullableMapping($innerMapping, $column, sprintf(
702 1
            "in file '%s' at line %d",
703
            $mappingFile,
704 1
            $nullableNode->getLineNo()
705
        ),
706
            $strict);
707
    }
708
709 1
    private function readDoctrineField(DOMNode $fieldNode): Column
710
    {
711
        /** @var array<string> $attributes */
712 1
        $attributes = array();
713
714
        /** @var array<string> $keyMap */
715
        $keyMap = array(
716 1
            'column'            => 'name',
717
            'type'              => 'type',
718
            'nullable'          => 'notnull',
719
            'length'            => 'length',
720
            'precision'         => 'precision',
721
            'scale'             => 'scale',
722
            'column-definition' => 'columnDefinition',
723
        );
724
725
        /** @var string $columnName */
726 1
        $columnName = null;
727
728
        /** @var Type $type */
729 1
        $type = Type::getType('string');
730
731
        /** @var DOMNamedNodeMap|null $fieldNodeAttributes */
732 1
        $fieldNodeAttributes = $fieldNode->attributes;
733
734 1
        if (is_object($fieldNodeAttributes)) {
735 1
            foreach ($fieldNodeAttributes as $key => $attribute) {
736
                /** @var DOMAttr $attribute */
737
738 1
                $attributeValue = $attribute->nodeValue;
739
740 1
                if ($key === 'column') {
741
                    $columnName = $attributeValue;
742
743 1
                } elseif ($key === 'name') {
744 1
                    if (empty($columnName)) {
745 1
                        $columnName = $attributeValue;
746
                    }
747
748 1
                } elseif ($key === 'type') {
749 1
                    $type = Type::getType($attributeValue);
750
751 1
                } elseif (isset($keyMap[$key])) {
752 1
                    if ($key === 'nullable') {
753
                        # target is 'notnull', so falue is reversed
754 1
                        $attributeValue = ($attributeValue === 'false');
755
                    }
756
757 1
                    $attributes[$keyMap[$key]] = $attributeValue;
758
                }
759
            }
760
        }
761
762 1
        $column = new Column(
763 1
            $columnName,
764
            $type,
765
            $attributes
766
        );
767
768 1
        return $column;
769
    }
770
771 1
    private function hasAttributeValue(DOMNode $node, string $attributeName): bool
772
    {
773
        /** @var DOMNamedNodeMap $nodeAttributes */
774 1
        $nodeAttributes = $node->attributes;
775
776
        /** @var DOMNode|null $attributeNode */
777 1
        $attributeNode = $nodeAttributes->getNamedItem($attributeName);
778
779 1
        return is_object($attributeNode);
780
    }
781
782 2
    private function readAttributeValue(DOMNode $node, string $attributeName, ?string $default = null): ?string
783
    {
784
        /** @var DOMNamedNodeMap $nodeAttributes */
785 2
        $nodeAttributes = $node->attributes;
786
787
        /** @var DOMNode|null $attributeNode */
788 2
        $attributeNode = $nodeAttributes->getNamedItem($attributeName);
789
790
        /** @var string|null $value */
791 2
        $value = $default;
792
793 2
        if (is_object($attributeNode)) {
794 2
            $value = $attributeNode->nodeValue;
795
        }
796
797 2
        return $value;
798
    }
799
800
}
801