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