Passed
Push — master ( 7b5f02...eec92d )
by Gerrit
02:49
created

MappingXmlDriver::readObject()   F

Complexity

Conditions 14
Paths 545

Size

Total Lines 138

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 53
CRAP Score 14.2061

Importance

Changes 0
Metric Value
dl 0
loc 138
ccs 53
cts 59
cp 0.8983
rs 2.1855
c 0
b 0
f 0
cc 14
nc 545
nop 2
crap 14.2061

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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