MappingXmlDriver::createXPath()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 15
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
eloc 8
nc 2
nop 1
dl 0
loc 15
ccs 8
cts 8
cp 1
crap 2
rs 10
c 0
b 0
f 0
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
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

175
        $xpath = new DOMXPath(/** @scrutinizer ignore-type */ $ownerDocument);
Loading history...
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
Unused Code introduced by
The assignment to $objectNodeAttributes is dead and can be removed.
Loading history...
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
Unused Code introduced by
The assignment to $fieldMappingProxy is dead and can be removed.
Loading history...
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