Completed
Push — master ( 23a060...e747f7 )
by Luís
45s queued 37s
created

XmlDriver   F

Complexity

Total Complexity 169

Size/Duplication

Total Lines 889
Duplicated Lines 0 %

Test Coverage

Coverage 87.01%

Importance

Changes 0
Metric Value
eloc 437
dl 0
loc 889
ccs 375
cts 431
cp 0.8701
rs 2
c 0
b 0
f 0
wmc 169

10 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 3 1
A evaluateBoolean() 0 5 2
F convertFieldElementToFieldMetadata() 0 45 11
A convertJoinColumnElementToJoinColumnMetadata() 0 28 6
F loadMetadataForClass() 0 638 131
A getMethodCallbacks() 0 15 1
B loadMappingFile() 0 24 7
A getCascadeMappings() 0 15 2
A parseOptions() 0 25 5
A convertCacheElementToCacheMetadata() 0 14 3

How to fix   Complexity   

Complex Class

Complex classes like XmlDriver often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use XmlDriver, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Doctrine\ORM\Mapping\Driver;
6
7
use Doctrine\Common\Collections\Criteria;
8
use Doctrine\DBAL\Types\Type;
9
use Doctrine\ORM\Events;
10
use Doctrine\ORM\Mapping;
11
use InvalidArgumentException;
12
use ReflectionClass;
13
use ReflectionMethod;
14
use SimpleXMLElement;
15
use function array_filter;
16
use function class_exists;
17
use function constant;
18
use function explode;
19
use function file_get_contents;
20
use function get_class;
21
use function in_array;
22
use function simplexml_load_string;
23
use function sprintf;
24
use function str_replace;
25
use function strtolower;
26
use function strtoupper;
27
28
/**
29
 * XmlDriver is a metadata driver that enables mapping through XML files.
30
 */
31
class XmlDriver extends FileDriver
32
{
33
    public const DEFAULT_FILE_EXTENSION = '.dcm.xml';
34
35
    /**
36
     * {@inheritDoc}
37
     */
38 42
    public function __construct($locator, $fileExtension = self::DEFAULT_FILE_EXTENSION)
39
    {
40 42
        parent::__construct($locator, $fileExtension);
41 42
    }
42
43
    /**
44
     * {@inheritDoc}
45
     */
46 37
    public function loadMetadataForClass(
47
        string $className,
48
        Mapping\ClassMetadata $metadata,
49
        Mapping\ClassMetadataBuildingContext $metadataBuildingContext
50
    ) {
51
        /** @var SimpleXMLElement $xmlRoot */
52 37
        $xmlRoot = $this->getElement($className);
53
54 35
        if ($xmlRoot->getName() === 'entity') {
55 35
            if (isset($xmlRoot['repository-class'])) {
56
                $metadata->setCustomRepositoryClassName((string) $xmlRoot['repository-class']);
57
            }
58
59 35
            if (isset($xmlRoot['read-only']) && $this->evaluateBoolean($xmlRoot['read-only'])) {
60 35
                $metadata->asReadOnly();
61
            }
62 5
        } elseif ($xmlRoot->getName() === 'mapped-superclass') {
63 5
            if (isset($xmlRoot['repository-class'])) {
64 1
                $metadata->setCustomRepositoryClassName((string) $xmlRoot['repository-class']);
65
            }
66
67 5
            $metadata->isMappedSuperclass = true;
68
        } elseif ($xmlRoot->getName() === 'embeddable') {
69
            $metadata->isEmbeddedClass = true;
70
        } else {
71
            throw Mapping\MappingException::classIsNotAValidEntityOrMappedSuperClass($className);
72
        }
73
74
        // Process table information
75 35
        $parent = $metadata->getParent();
76
77 35
        if ($parent && $parent->inheritanceType === Mapping\InheritanceType::SINGLE_TABLE) {
78 2
            $metadata->setTable($parent->table);
79
        } else {
80 35
            $namingStrategy = $metadataBuildingContext->getNamingStrategy();
81 35
            $tableMetadata  = new Mapping\TableMetadata();
82
83 35
            $tableMetadata->setName($namingStrategy->classToTableName($metadata->getClassName()));
84
85
            // Evaluate <entity...> attributes
86 35
            if (isset($xmlRoot['table'])) {
87 13
                $tableMetadata->setName((string) $xmlRoot['table']);
88
            }
89
90 35
            if (isset($xmlRoot['schema'])) {
91 2
                $tableMetadata->setSchema((string) $xmlRoot['schema']);
92
            }
93
94 35
            if (isset($xmlRoot->options)) {
95 3
                $options = $this->parseOptions($xmlRoot->options->children());
96
97 3
                foreach ($options as $optionName => $optionValue) {
98 3
                    $tableMetadata->addOption($optionName, $optionValue);
99
                }
100
            }
101
102
            // Evaluate <indexes...>
103 35
            if (isset($xmlRoot->indexes)) {
104 4
                foreach ($xmlRoot->indexes->index as $indexXml) {
105 4
                    $indexName = isset($indexXml['name']) ? (string) $indexXml['name'] : null;
106 4
                    $columns   = explode(',', (string) $indexXml['columns']);
107 4
                    $isUnique  = isset($indexXml['unique']) && $indexXml['unique'];
108 4
                    $options   = isset($indexXml->options) ? $this->parseOptions($indexXml->options->children()) : [];
109 4
                    $flags     = isset($indexXml['flags']) ? explode(',', (string) $indexXml['flags']) : [];
110
111 4
                    $tableMetadata->addIndex([
112 4
                        'name'    => $indexName,
113 4
                        'columns' => $columns,
114 4
                        'unique'  => $isUnique,
115 4
                        'options' => $options,
116 4
                        'flags'   => $flags,
117
                    ]);
118
                }
119
            }
120
121
            // Evaluate <unique-constraints..>
122
123 35
            if (isset($xmlRoot->{'unique-constraints'})) {
124 3
                foreach ($xmlRoot->{'unique-constraints'}->{'unique-constraint'} as $uniqueXml) {
125 3
                    $indexName = isset($uniqueXml['name']) ? (string) $uniqueXml['name'] : null;
126 3
                    $columns   = explode(',', (string) $uniqueXml['columns']);
127 3
                    $options   = isset($uniqueXml->options) ? $this->parseOptions($uniqueXml->options->children()) : [];
128 3
                    $flags     = isset($uniqueXml['flags']) ? explode(',', (string) $uniqueXml['flags']) : [];
129
130 3
                    $tableMetadata->addUniqueConstraint([
131 3
                        'name'    => $indexName,
132 3
                        'columns' => $columns,
133 3
                        'options' => $options,
134 3
                        'flags'   => $flags,
135
                    ]);
136
                }
137
            }
138
139 35
            $metadata->setTable($tableMetadata);
140
        }
141
142
        // Evaluate second level cache
143 35
        if (isset($xmlRoot->cache)) {
144 2
            $cache = $this->convertCacheElementToCacheMetadata($xmlRoot->cache, $metadata);
145
146 2
            $metadata->setCache($cache);
147
        }
148
149 35
        if (isset($xmlRoot['inheritance-type'])) {
150 10
            $inheritanceType = strtoupper((string) $xmlRoot['inheritance-type']);
151
152 10
            $metadata->setInheritanceType(
153 10
                constant(sprintf('%s::%s', Mapping\InheritanceType::class, $inheritanceType))
154
            );
155
156 10
            if ($metadata->inheritanceType !== Mapping\InheritanceType::NONE) {
157 10
                $discriminatorColumn = new Mapping\DiscriminatorColumnMetadata();
158
159 10
                $discriminatorColumn->setTableName($metadata->getTableName());
160 10
                $discriminatorColumn->setColumnName('dtype');
161 10
                $discriminatorColumn->setType(Type::getType('string'));
162 10
                $discriminatorColumn->setLength(255);
163
164
                // Evaluate <discriminator-column...>
165 10
                if (isset($xmlRoot->{'discriminator-column'})) {
166 8
                    $discriminatorColumnMapping = $xmlRoot->{'discriminator-column'};
167 8
                    $typeName                   = (string) ($discriminatorColumnMapping['type'] ?? 'string');
168
169 8
                    $discriminatorColumn->setType(Type::getType($typeName));
170 8
                    $discriminatorColumn->setColumnName((string) $discriminatorColumnMapping['name']);
171
172 8
                    if (isset($discriminatorColumnMapping['column-definition'])) {
173 1
                        $discriminatorColumn->setColumnDefinition((string) $discriminatorColumnMapping['column-definition']);
174
                    }
175
176 8
                    if (isset($discriminatorColumnMapping['length'])) {
177 3
                        $discriminatorColumn->setLength((int) $discriminatorColumnMapping['length']);
178
                    }
179
                }
180
181 10
                $metadata->setDiscriminatorColumn($discriminatorColumn);
182
183
                // Evaluate <discriminator-map...>
184 10
                if (isset($xmlRoot->{'discriminator-map'})) {
185 10
                    $map = [];
186
187 10
                    foreach ($xmlRoot->{'discriminator-map'}->{'discriminator-mapping'} as $discrMapElement) {
188 10
                        $map[(string) $discrMapElement['value']] = (string) $discrMapElement['class'];
189
                    }
190
191 10
                    $metadata->setDiscriminatorMap($map);
192
                }
193
            }
194
        }
195
196
        // Evaluate <change-tracking-policy...>
197 35
        if (isset($xmlRoot['change-tracking-policy'])) {
198
            $changeTrackingPolicy = strtoupper((string) $xmlRoot['change-tracking-policy']);
199
200
            $metadata->setChangeTrackingPolicy(
201
                constant(sprintf('%s::%s', Mapping\ChangeTrackingPolicy::class, $changeTrackingPolicy))
202
            );
203
        }
204
205
        // Evaluate <field ...> mappings
206 35
        if (isset($xmlRoot->field)) {
207 21
            foreach ($xmlRoot->field as $fieldElement) {
208 21
                $fieldName        = (string) $fieldElement['name'];
209 21
                $isFieldVersioned = isset($fieldElement['version']) && $fieldElement['version'];
210 21
                $fieldMetadata    = $this->convertFieldElementToFieldMetadata($fieldElement, $fieldName, $isFieldVersioned);
211
212 21
                $metadata->addProperty($fieldMetadata);
213
            }
214
        }
215
216 35
        if (isset($xmlRoot->embedded)) {
217
            foreach ($xmlRoot->embedded as $embeddedMapping) {
218
                $columnPrefix = isset($embeddedMapping['column-prefix'])
219
                    ? (string) $embeddedMapping['column-prefix']
220
                    : null;
221
222
                $useColumnPrefix = isset($embeddedMapping['use-column-prefix'])
223
                    ? $this->evaluateBoolean($embeddedMapping['use-column-prefix'])
224
                    : true;
225
226
                $mapping = [
227
                    'fieldName' => (string) $embeddedMapping['name'],
228
                    'class' => (string) $embeddedMapping['class'],
229
                    'columnPrefix' => $useColumnPrefix ? $columnPrefix : false,
230
                ];
231
232
                $metadata->mapEmbedded($mapping);
233
            }
234
        }
235
236
        // Evaluate <id ...> mappings
237 35
        $associationIds = [];
238
239 35
        foreach ($xmlRoot->id as $idElement) {
240 30
            $fieldName = (string) $idElement['name'];
241
242 30
            if (isset($idElement['association-key']) && $this->evaluateBoolean($idElement['association-key'])) {
243 2
                $associationIds[$fieldName] = true;
244
245 2
                continue;
246
            }
247
248 29
            $fieldMetadata = $this->convertFieldElementToFieldMetadata($idElement, $fieldName, false);
249
250 29
            $fieldMetadata->setPrimaryKey(true);
251
252 29
            if (isset($idElement->generator)) {
253 28
                $strategy = (string) ($idElement->generator['strategy'] ?? 'AUTO');
254
255 28
                $idGeneratorType = constant(sprintf('%s::%s', Mapping\GeneratorType::class, strtoupper($strategy)));
256
257 28
                if ($idGeneratorType !== Mapping\GeneratorType::NONE) {
258 21
                    $idGeneratorDefinition = [];
259
260
                    // Check for SequenceGenerator/TableGenerator definition
261 21
                    if (isset($idElement->{'sequence-generator'})) {
262 3
                        $seqGenerator          = $idElement->{'sequence-generator'};
263
                        $idGeneratorDefinition = [
264 3
                            'sequenceName' => (string) $seqGenerator['sequence-name'],
265 3
                            'allocationSize' => (string) $seqGenerator['allocation-size'],
266
                        ];
267 18
                    } elseif (isset($idElement->{'custom-id-generator'})) {
268 2
                        $customGenerator = $idElement->{'custom-id-generator'};
269
270
                        $idGeneratorDefinition = [
271 2
                            'class' => (string) $customGenerator['class'],
272
                            'arguments' => [],
273
                        ];
274 16
                    } elseif (isset($idElement->{'table-generator'})) {
275
                        throw Mapping\MappingException::tableIdGeneratorNotImplemented($className);
276
                    }
277
278 21
                    $fieldMetadata->setValueGenerator(new Mapping\ValueGeneratorMetadata($idGeneratorType, $idGeneratorDefinition));
279
                }
280
            }
281
282 29
            $metadata->addProperty($fieldMetadata);
283
        }
284
285
        // Evaluate <one-to-one ...> mappings
286 35
        if (isset($xmlRoot->{'one-to-one'})) {
287 8
            foreach ($xmlRoot->{'one-to-one'} as $oneToOneElement) {
288 8
                $association  = new Mapping\OneToOneAssociationMetadata((string) $oneToOneElement['field']);
289 8
                $targetEntity = (string) $oneToOneElement['target-entity'];
290
291 8
                $association->setTargetEntity($targetEntity);
292
293 8
                if (isset($associationIds[$association->getName()])) {
294
                    $association->setPrimaryKey(true);
295
                }
296
297 8
                if (isset($oneToOneElement['fetch'])) {
298 3
                    $association->setFetchMode(
299 3
                        constant(sprintf('%s::%s', Mapping\FetchMode::class, (string) $oneToOneElement['fetch']))
300
                    );
301
                }
302
303 8
                if (isset($oneToOneElement['mapped-by'])) {
304 3
                    $association->setMappedBy((string) $oneToOneElement['mapped-by']);
305 3
                    $association->setOwningSide(false);
306
                } else {
307 6
                    if (isset($oneToOneElement['inversed-by'])) {
308 5
                        $association->setInversedBy((string) $oneToOneElement['inversed-by']);
309
                    }
310
311 6
                    $joinColumns = [];
312
313 6
                    if (isset($oneToOneElement->{'join-column'})) {
314 6
                        $joinColumns[] = $this->convertJoinColumnElementToJoinColumnMetadata($oneToOneElement->{'join-column'});
315
                    } elseif (isset($oneToOneElement->{'join-columns'})) {
316
                        foreach ($oneToOneElement->{'join-columns'}->{'join-column'} as $joinColumnElement) {
317
                            $joinColumns[] = $this->convertJoinColumnElementToJoinColumnMetadata($joinColumnElement);
318
                        }
319
                    }
320
321 6
                    $association->setJoinColumns($joinColumns);
322
                }
323
324 8
                if (isset($oneToOneElement->cascade)) {
325 7
                    $association->setCascade($this->getCascadeMappings($oneToOneElement->cascade));
326
                }
327
328 8
                if (isset($oneToOneElement['orphan-removal'])) {
329
                    $association->setOrphanRemoval($this->evaluateBoolean($oneToOneElement['orphan-removal']));
330
                }
331
332
                // Evaluate second level cache
333 8
                if (isset($oneToOneElement->cache)) {
334
                    $association->setCache(
335
                        $this->convertCacheElementToCacheMetadata(
336
                            $oneToOneElement->cache,
337
                            $metadata,
338
                            $association->getName()
339
                        )
340
                    );
341
                }
342
343 8
                $metadata->addProperty($association);
344
            }
345
        }
346
347
        // Evaluate <one-to-many ...> mappings
348 35
        if (isset($xmlRoot->{'one-to-many'})) {
349 9
            foreach ($xmlRoot->{'one-to-many'} as $oneToManyElement) {
350 9
                $association  = new Mapping\OneToManyAssociationMetadata((string) $oneToManyElement['field']);
351 9
                $targetEntity = (string) $oneToManyElement['target-entity'];
352
353 9
                $association->setTargetEntity($targetEntity);
354 9
                $association->setOwningSide(false);
355 9
                $association->setMappedBy((string) $oneToManyElement['mapped-by']);
356
357 9
                if (isset($associationIds[$association->getName()])) {
358
                    throw Mapping\MappingException::illegalToManyIdentifierAssociation($className, $association->getName());
359
                }
360
361 9
                if (isset($oneToManyElement['fetch'])) {
362
                    $association->setFetchMode(
363
                        constant(sprintf('%s::%s', Mapping\FetchMode::class, (string) $oneToManyElement['fetch']))
364
                    );
365
                }
366
367 9
                if (isset($oneToManyElement->cascade)) {
368 6
                    $association->setCascade($this->getCascadeMappings($oneToManyElement->cascade));
369
                }
370
371 9
                if (isset($oneToManyElement['orphan-removal'])) {
372 3
                    $association->setOrphanRemoval($this->evaluateBoolean($oneToManyElement['orphan-removal']));
373
                }
374
375 9
                if (isset($oneToManyElement->{'order-by'})) {
376 5
                    $orderBy = [];
377
378 5
                    foreach ($oneToManyElement->{'order-by'}->{'order-by-field'} as $orderByField) {
379 5
                        $orderBy[(string) $orderByField['name']] = isset($orderByField['direction'])
380 4
                            ? (string) $orderByField['direction']
381 1
                            : Criteria::ASC;
382
                    }
383
384 5
                    $association->setOrderBy($orderBy);
385
                }
386
387 9
                if (isset($oneToManyElement['index-by'])) {
388 3
                    $association->setIndexedBy((string) $oneToManyElement['index-by']);
389 6
                } elseif (isset($oneToManyElement->{'index-by'})) {
390
                    throw new InvalidArgumentException('<index-by /> is not a valid tag');
391
                }
392
393
                // Evaluate second level cache
394 9
                if (isset($oneToManyElement->cache)) {
395 1
                    $association->setCache(
396 1
                        $this->convertCacheElementToCacheMetadata(
397 1
                            $oneToManyElement->cache,
398 1
                            $metadata,
399 1
                            $association->getName()
400
                        )
401
                    );
402
                }
403
404 9
                $metadata->addProperty($association);
405
            }
406
        }
407
408
        // Evaluate <many-to-one ...> mappings
409 35
        if (isset($xmlRoot->{'many-to-one'})) {
410 8
            foreach ($xmlRoot->{'many-to-one'} as $manyToOneElement) {
411 8
                $association  = new Mapping\ManyToOneAssociationMetadata((string) $manyToOneElement['field']);
412 8
                $targetEntity = (string) $manyToOneElement['target-entity'];
413
414 8
                $association->setTargetEntity($targetEntity);
415
416 8
                if (isset($associationIds[$association->getName()])) {
417 2
                    $association->setPrimaryKey(true);
418
                }
419
420 8
                if (isset($manyToOneElement['fetch'])) {
421
                    $association->setFetchMode(
422
                        constant('Doctrine\ORM\Mapping\FetchMode::' . (string) $manyToOneElement['fetch'])
423
                    );
424
                }
425
426 8
                if (isset($manyToOneElement['inversed-by'])) {
427 2
                    $association->setInversedBy((string) $manyToOneElement['inversed-by']);
428
                }
429
430 8
                $joinColumns = [];
431
432 8
                if (isset($manyToOneElement->{'join-column'})) {
433 7
                    $joinColumns[] = $this->convertJoinColumnElementToJoinColumnMetadata($manyToOneElement->{'join-column'});
434 1
                } elseif (isset($manyToOneElement->{'join-columns'})) {
435 1
                    foreach ($manyToOneElement->{'join-columns'}->{'join-column'} as $joinColumnElement) {
436 1
                        $joinColumns[] = $this->convertJoinColumnElementToJoinColumnMetadata($joinColumnElement);
437
                    }
438
                }
439
440 8
                $association->setJoinColumns($joinColumns);
441
442 8
                if (isset($manyToOneElement->cascade)) {
443 4
                    $association->setCascade($this->getCascadeMappings($manyToOneElement->cascade));
444
                }
445
446
                // Evaluate second level cache
447 8
                if (isset($manyToOneElement->cache)) {
448 1
                    $association->setCache(
449 1
                        $this->convertCacheElementToCacheMetadata(
450 1
                            $manyToOneElement->cache,
451 1
                            $metadata,
452 1
                            $association->getName()
453
                        )
454
                    );
455
                }
456
457 8
                $metadata->addProperty($association);
458
            }
459
        }
460
461
        // Evaluate <many-to-many ...> mappings
462 34
        if (isset($xmlRoot->{'many-to-many'})) {
463 14
            foreach ($xmlRoot->{'many-to-many'} as $manyToManyElement) {
464 14
                $association  = new Mapping\ManyToManyAssociationMetadata((string) $manyToManyElement['field']);
465 14
                $targetEntity = (string) $manyToManyElement['target-entity'];
466
467 14
                $association->setTargetEntity($targetEntity);
468
469 14
                if (isset($associationIds[$association->getName()])) {
470
                    throw Mapping\MappingException::illegalToManyIdentifierAssociation($className, $association->getName());
471
                }
472
473 14
                if (isset($manyToManyElement['fetch'])) {
474 4
                    $association->setFetchMode(
475 4
                        constant(sprintf('%s::%s', Mapping\FetchMode::class, (string) $manyToManyElement['fetch']))
476
                    );
477
                }
478
479 14
                if (isset($manyToManyElement['orphan-removal'])) {
480
                    $association->setOrphanRemoval($this->evaluateBoolean($manyToManyElement['orphan-removal']));
481
                }
482
483 14
                if (isset($manyToManyElement['mapped-by'])) {
484 5
                    $association->setMappedBy((string) $manyToManyElement['mapped-by']);
485 5
                    $association->setOwningSide(false);
486 10
                } elseif (isset($manyToManyElement->{'join-table'})) {
487 8
                    if (isset($manyToManyElement['inversed-by'])) {
488 5
                        $association->setInversedBy((string) $manyToManyElement['inversed-by']);
489
                    }
490
491 8
                    $joinTableElement = $manyToManyElement->{'join-table'};
492 8
                    $joinTable        = new Mapping\JoinTableMetadata();
493
494 8
                    if (isset($joinTableElement['name'])) {
495 8
                        $joinTable->setName((string) $joinTableElement['name']);
496
                    }
497
498 8
                    if (isset($joinTableElement['schema'])) {
499
                        $joinTable->setSchema((string) $joinTableElement['schema']);
500
                    }
501
502 8
                    if (isset($joinTableElement->{'join-columns'})) {
503 8
                        foreach ($joinTableElement->{'join-columns'}->{'join-column'} as $joinColumnElement) {
504 8
                            $joinColumn = $this->convertJoinColumnElementToJoinColumnMetadata($joinColumnElement);
505
506 8
                            $joinTable->addJoinColumn($joinColumn);
507
                        }
508
                    }
509
510 8
                    if (isset($joinTableElement->{'inverse-join-columns'})) {
511 8
                        foreach ($joinTableElement->{'inverse-join-columns'}->{'join-column'} as $joinColumnElement) {
512 8
                            $joinColumn = $this->convertJoinColumnElementToJoinColumnMetadata($joinColumnElement);
513
514 8
                            $joinTable->addInverseJoinColumn($joinColumn);
515
                        }
516
                    }
517
518 8
                    $association->setJoinTable($joinTable);
519
                }
520
521 14
                if (isset($manyToManyElement->cascade)) {
522 8
                    $association->setCascade($this->getCascadeMappings($manyToManyElement->cascade));
523
                }
524
525 14
                if (isset($manyToManyElement->{'order-by'})) {
526 1
                    $orderBy = [];
527
528 1
                    foreach ($manyToManyElement->{'order-by'}->{'order-by-field'} as $orderByField) {
529 1
                        $orderBy[(string) $orderByField['name']] = isset($orderByField['direction'])
530
                            ? (string) $orderByField['direction']
531 1
                            : Criteria::ASC;
532
                    }
533
534 1
                    $association->setOrderBy($orderBy);
535
                }
536
537 14
                if (isset($manyToManyElement['index-by'])) {
538
                    $association->setIndexedBy((string) $manyToManyElement['index-by']);
539 14
                } elseif (isset($manyToManyElement->{'index-by'})) {
540
                    throw new InvalidArgumentException('<index-by /> is not a valid tag');
541
                }
542
543
                // Evaluate second level cache
544 14
                if (isset($manyToManyElement->cache)) {
545
                    $association->setCache(
546
                        $this->convertCacheElementToCacheMetadata(
547
                            $manyToManyElement->cache,
548
                            $metadata,
549
                            $association->getName()
550
                        )
551
                    );
552
                }
553
554 14
                $metadata->addProperty($association);
555
            }
556
        }
557
558
        // Evaluate association-overrides
559 34
        if (isset($xmlRoot->{'attribute-overrides'})) {
560 2
            foreach ($xmlRoot->{'attribute-overrides'}->{'attribute-override'} as $overrideElement) {
561 2
                $fieldName = (string) $overrideElement['name'];
562
563 2
                foreach ($overrideElement->field as $fieldElement) {
564 2
                    $fieldMetadata = $this->convertFieldElementToFieldMetadata($fieldElement, $fieldName, false);
565
566 2
                    $metadata->setPropertyOverride($fieldMetadata);
567
                }
568
            }
569
        }
570
571
        // Evaluate association-overrides
572 34
        if (isset($xmlRoot->{'association-overrides'})) {
573 4
            foreach ($xmlRoot->{'association-overrides'}->{'association-override'} as $overrideElement) {
574 4
                $fieldName = (string) $overrideElement['name'];
575 4
                $property  = $metadata->getProperty($fieldName);
576
577 4
                if (! $property) {
578
                    throw Mapping\MappingException::invalidOverrideFieldName($metadata->getClassName(), $fieldName);
579
                }
580
581 4
                $existingClass = get_class($property);
582 4
                $override      = new $existingClass($fieldName);
583
584
                // Check for join-columns
585 4
                if (isset($overrideElement->{'join-columns'})) {
586 2
                    $joinColumns = [];
587
588 2
                    foreach ($overrideElement->{'join-columns'}->{'join-column'} as $joinColumnElement) {
589 2
                        $joinColumns[] = $this->convertJoinColumnElementToJoinColumnMetadata($joinColumnElement);
590
                    }
591
592 2
                    $override->setJoinColumns($joinColumns);
593
                }
594
595
                // Check for join-table
596 4
                if ($overrideElement->{'join-table'}) {
597 2
                    $joinTableElement = $overrideElement->{'join-table'};
598 2
                    $joinTable        = new Mapping\JoinTableMetadata();
599
600 2
                    if (isset($joinTableElement['name'])) {
601 2
                        $joinTable->setName((string) $joinTableElement['name']);
602
                    }
603
604 2
                    if (isset($joinTableElement['schema'])) {
605
                        $joinTable->setSchema((string) $joinTableElement['schema']);
606
                    }
607
608 2
                    if (isset($joinTableElement->{'join-columns'})) {
609 2
                        foreach ($joinTableElement->{'join-columns'}->{'join-column'} as $joinColumnElement) {
610 2
                            $joinColumn = $this->convertJoinColumnElementToJoinColumnMetadata($joinColumnElement);
611
612 2
                            $joinTable->addJoinColumn($joinColumn);
613
                        }
614
                    }
615
616 2
                    if (isset($joinTableElement->{'inverse-join-columns'})) {
617 2
                        foreach ($joinTableElement->{'inverse-join-columns'}->{'join-column'} as $joinColumnElement) {
618 2
                            $joinColumn = $this->convertJoinColumnElementToJoinColumnMetadata($joinColumnElement);
619
620 2
                            $joinTable->addInverseJoinColumn($joinColumn);
621
                        }
622
                    }
623
624 2
                    $override->setJoinTable($joinTable);
625
                }
626
627
                // Check for inversed-by
628 4
                if (isset($overrideElement->{'inversed-by'})) {
629 1
                    $override->setInversedBy((string) $overrideElement->{'inversed-by'}['name']);
630
                }
631
632
                // Check for fetch
633 4
                if (isset($overrideElement['fetch'])) {
634 1
                    $override->setFetchMode(
635 1
                        constant('Doctrine\ORM\Mapping\FetchMode::' . (string) $overrideElement['fetch'])
636
                    );
637
                }
638
639 4
                $metadata->setPropertyOverride($override);
640
            }
641
        }
642
643
        // Evaluate <lifecycle-callbacks...>
644 34
        if (isset($xmlRoot->{'lifecycle-callbacks'})) {
645 3
            foreach ($xmlRoot->{'lifecycle-callbacks'}->{'lifecycle-callback'} as $lifecycleCallback) {
646 3
                $eventName  = constant(Events::class . '::' . (string) $lifecycleCallback['type']);
647 3
                $methodName = (string) $lifecycleCallback['method'];
648
649 3
                $metadata->addLifecycleCallback($methodName, $eventName);
650
            }
651
        }
652
653
        // Evaluate entity listener
654 34
        if (isset($xmlRoot->{'entity-listeners'})) {
655 3
            foreach ($xmlRoot->{'entity-listeners'}->{'entity-listener'} as $listenerElement) {
656 3
                $listenerClassName = (string) $listenerElement['class'];
657
658 3
                if (! class_exists($listenerClassName)) {
659
                    throw Mapping\MappingException::entityListenerClassNotFound(
660
                        $listenerClassName,
661
                        $metadata->getClassName()
662
                    );
663
                }
664
665 3
                $listenerClass = new ReflectionClass($listenerClassName);
666
667
                // Evaluate the listener using naming convention.
668 3
                if ($listenerElement->count() === 0) {
669
                    /** @var ReflectionMethod $method */
670 1
                    foreach ($listenerClass->getMethods(ReflectionMethod::IS_PUBLIC) as $method) {
671 1
                        foreach ($this->getMethodCallbacks($method) as $callback) {
672 1
                            $metadata->addEntityListener($callback, $listenerClassName, $method->getName());
673
                        }
674
                    }
675
676 1
                    continue;
677
                }
678
679 2
                foreach ($listenerElement as $callbackElement) {
680 2
                    $eventName  = (string) $callbackElement['type'];
681 2
                    $methodName = (string) $callbackElement['method'];
682
683 2
                    $metadata->addEntityListener($eventName, $listenerClassName, $methodName);
684
                }
685
            }
686
        }
687 34
    }
688
689
    /**
690
     * Parses (nested) option elements.
691
     *
692
     * @param SimpleXMLElement $options The XML element.
693
     *
694
     * @return mixed[] The options array.
695
     */
696 4
    private function parseOptions(SimpleXMLElement $options)
697
    {
698 4
        $array = [];
699
700
        /** @var SimpleXMLElement $option */
701 4
        foreach ($options as $option) {
702 4
            if ($option->count()) {
703 3
                $value = $this->parseOptions($option->children());
704
            } else {
705 4
                $value = (string) $option;
706
            }
707
708 4
            $attributes = $option->attributes();
709
710 4
            if (isset($attributes->name)) {
711 4
                $nameAttribute         = (string) $attributes->name;
712 4
                $array[$nameAttribute] = in_array($nameAttribute, ['unsigned', 'fixed'], true)
713 3
                    ? $this->evaluateBoolean($value)
714 4
                    : $value;
715
            } else {
716
                $array[] = $value;
717
            }
718
        }
719
720 4
        return $array;
721
    }
722
723
    /**
724
     * @return Mapping\FieldMetadata
725
     */
726 31
    private function convertFieldElementToFieldMetadata(SimpleXMLElement $fieldElement, string $fieldName, bool $isVersioned)
727
    {
728 31
        $fieldMetadata = $isVersioned
729 3
            ? new Mapping\VersionFieldMetadata($fieldName)
730 31
            : new Mapping\FieldMetadata($fieldName);
731
732 31
        $fieldMetadata->setType(Type::getType('string'));
733
734 31
        if (isset($fieldElement['type'])) {
735 22
            $fieldMetadata->setType(Type::getType((string) $fieldElement['type']));
736
        }
737
738 31
        if (isset($fieldElement['column'])) {
739 21
            $fieldMetadata->setColumnName((string) $fieldElement['column']);
740
        }
741
742 31
        if (isset($fieldElement['length'])) {
743 7
            $fieldMetadata->setLength((int) $fieldElement['length']);
744
        }
745
746 31
        if (isset($fieldElement['precision'])) {
747 1
            $fieldMetadata->setPrecision((int) $fieldElement['precision']);
748
        }
749
750 31
        if (isset($fieldElement['scale'])) {
751 1
            $fieldMetadata->setScale((int) $fieldElement['scale']);
752
        }
753
754 31
        if (isset($fieldElement['unique'])) {
755 7
            $fieldMetadata->setUnique($this->evaluateBoolean($fieldElement['unique']));
756
        }
757
758 31
        if (isset($fieldElement['nullable'])) {
759 7
            $fieldMetadata->setNullable($this->evaluateBoolean($fieldElement['nullable']));
760
        }
761
762 31
        if (isset($fieldElement['column-definition'])) {
763 4
            $fieldMetadata->setColumnDefinition((string) $fieldElement['column-definition']);
764
        }
765
766 31
        if (isset($fieldElement->options)) {
767 3
            $fieldMetadata->setOptions($this->parseOptions($fieldElement->options->children()));
768
        }
769
770 31
        return $fieldMetadata;
771
    }
772
773
    /**
774
     * Constructs a joinColumn mapping array based on the information
775
     * found in the given SimpleXMLElement.
776
     *
777
     * @param SimpleXMLElement $joinColumnElement The XML element.
778
     *
779
     * @return Mapping\JoinColumnMetadata
780
     */
781 14
    private function convertJoinColumnElementToJoinColumnMetadata(SimpleXMLElement $joinColumnElement)
782
    {
783 14
        $joinColumnMetadata = new Mapping\JoinColumnMetadata();
784
785 14
        $joinColumnMetadata->setColumnName((string) $joinColumnElement['name']);
786 14
        $joinColumnMetadata->setReferencedColumnName((string) $joinColumnElement['referenced-column-name']);
787
788 14
        if (isset($joinColumnElement['column-definition'])) {
789 3
            $joinColumnMetadata->setColumnDefinition((string) $joinColumnElement['column-definition']);
790
        }
791
792 14
        if (isset($joinColumnElement['field-name'])) {
793
            $joinColumnMetadata->setAliasedName((string) $joinColumnElement['field-name']);
794
        }
795
796 14
        if (isset($joinColumnElement['nullable'])) {
797 4
            $joinColumnMetadata->setNullable($this->evaluateBoolean($joinColumnElement['nullable']));
798
        }
799
800 14
        if (isset($joinColumnElement['unique'])) {
801 3
            $joinColumnMetadata->setUnique($this->evaluateBoolean($joinColumnElement['unique']));
802
        }
803
804 14
        if (isset($joinColumnElement['on-delete'])) {
805 3
            $joinColumnMetadata->setOnDelete(strtoupper((string) $joinColumnElement['on-delete']));
806
        }
807
808 14
        return $joinColumnMetadata;
809
    }
810
811
    /**
812
     * Parse the given Cache as CacheMetadata
813
     *
814
     * @param string|null $fieldName
815
     *
816
     * @return Mapping\CacheMetadata
817
     */
818 2
    private function convertCacheElementToCacheMetadata(
819
        SimpleXMLElement $cacheMapping,
820
        Mapping\ClassMetadata $metadata,
821
        $fieldName = null
822
    ) {
823 2
        $baseRegion    = strtolower(str_replace('\\', '_', $metadata->getRootClassName()));
824 2
        $defaultRegion = $baseRegion . ($fieldName ? '__' . $fieldName : '');
825
826 2
        $region = (string) ($cacheMapping['region'] ?? $defaultRegion);
827 2
        $usage  = isset($cacheMapping['usage'])
828 2
            ? constant(sprintf('%s::%s', Mapping\CacheUsage::class, strtoupper((string) $cacheMapping['usage'])))
829 2
            : Mapping\CacheUsage::READ_ONLY;
830
831 2
        return new Mapping\CacheMetadata($usage, $region);
832
    }
833
834
    /**
835
     * Parses the given method.
836
     *
837
     * @return string[]
838
     */
839 1
    private function getMethodCallbacks(ReflectionMethod $method)
840
    {
841
        $events = [
842 1
            Events::prePersist,
843
            Events::postPersist,
844
            Events::preUpdate,
845
            Events::postUpdate,
846
            Events::preRemove,
847
            Events::postRemove,
848
            Events::postLoad,
849
            Events::preFlush,
850
        ];
851
852
        return array_filter($events, static function ($eventName) use ($method) {
853 1
            return $eventName === $method->getName();
854 1
        });
855
    }
856
857
    /**
858
     * Gathers a list of cascade options found in the given cascade element.
859
     *
860
     * @param SimpleXMLElement $cascadeElement The cascade element.
861
     *
862
     * @return string[] The list of cascade options.
863
     */
864 10
    private function getCascadeMappings(SimpleXMLElement $cascadeElement)
865
    {
866 10
        $cascades = [];
867
868
        /** @var SimpleXMLElement $action */
869 10
        foreach ($cascadeElement->children() as $action) {
870
            // According to the JPA specifications, XML uses "cascade-persist"
871
            // instead of "persist". Here, both variations are supported
872
            // because Annotation use "persist" and we want to make sure that
873
            // this driver doesn't need to know anything about the supported
874
            // cascading actions
875 10
            $cascades[] = str_replace('cascade-', '', $action->getName());
876
        }
877
878 10
        return $cascades;
879
    }
880
881
    /**
882
     * {@inheritDoc}
883
     */
884 37
    protected function loadMappingFile($file)
885
    {
886 37
        $result = [];
887
        // Note: we do not use `simplexml_load_file()` because of https://bugs.php.net/bug.php?id=62577
888 37
        $xmlElement = simplexml_load_string(file_get_contents($file));
889
890 37
        if (isset($xmlElement->entity)) {
891 36
            foreach ($xmlElement->entity as $entityElement) {
892 36
                $entityName          = (string) $entityElement['name'];
893 36
                $result[$entityName] = $entityElement;
894
            }
895 6
        } elseif (isset($xmlElement->{'mapped-superclass'})) {
896 5
            foreach ($xmlElement->{'mapped-superclass'} as $mappedSuperClass) {
897 5
                $className          = (string) $mappedSuperClass['name'];
898 5
                $result[$className] = $mappedSuperClass;
899
            }
900 1
        } elseif (isset($xmlElement->embeddable)) {
901
            foreach ($xmlElement->embeddable as $embeddableElement) {
902
                $embeddableName          = (string) $embeddableElement['name'];
903
                $result[$embeddableName] = $embeddableElement;
904
            }
905
        }
906
907 37
        return $result;
908
    }
909
910
    /**
911
     * @param mixed $element
912
     *
913
     * @return bool
914
     */
915 9
    protected function evaluateBoolean($element)
916
    {
917 9
        $flag = (string) $element;
918
919 9
        return $flag === 'true' || $flag === '1';
920
    }
921
}
922