summaryrefslogtreecommitdiff
path: root/vendor/doctrine/orm/src/Tools/SchemaTool.php
diff options
context:
space:
mode:
authorpolo <ordipolo@gmx.fr>2024-08-13 23:45:21 +0200
committerpolo <ordipolo@gmx.fr>2024-08-13 23:45:21 +0200
commitbf6655a534a6775d30cafa67bd801276bda1d98d (patch)
treec6381e3f6c81c33eab72508f410b165ba05f7e9c /vendor/doctrine/orm/src/Tools/SchemaTool.php
parent94d67a4b51f8e62e7d518cce26a526ae1ec48278 (diff)
downloadAppliGestionPHP-bf6655a534a6775d30cafa67bd801276bda1d98d.zip
VERSION 0.2 doctrine ORM et entités
Diffstat (limited to 'vendor/doctrine/orm/src/Tools/SchemaTool.php')
-rw-r--r--vendor/doctrine/orm/src/Tools/SchemaTool.php932
1 files changed, 932 insertions, 0 deletions
diff --git a/vendor/doctrine/orm/src/Tools/SchemaTool.php b/vendor/doctrine/orm/src/Tools/SchemaTool.php
new file mode 100644
index 0000000..42b52df
--- /dev/null
+++ b/vendor/doctrine/orm/src/Tools/SchemaTool.php
@@ -0,0 +1,932 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Tools;
6
7use BackedEnum;
8use Doctrine\DBAL\Platforms\AbstractPlatform;
9use Doctrine\DBAL\Schema\AbstractAsset;
10use Doctrine\DBAL\Schema\AbstractSchemaManager;
11use Doctrine\DBAL\Schema\Index;
12use Doctrine\DBAL\Schema\Schema;
13use Doctrine\DBAL\Schema\Table;
14use Doctrine\ORM\EntityManagerInterface;
15use Doctrine\ORM\Mapping\AssociationMapping;
16use Doctrine\ORM\Mapping\ClassMetadata;
17use Doctrine\ORM\Mapping\DiscriminatorColumnMapping;
18use Doctrine\ORM\Mapping\FieldMapping;
19use Doctrine\ORM\Mapping\JoinColumnMapping;
20use Doctrine\ORM\Mapping\ManyToManyOwningSideMapping;
21use Doctrine\ORM\Mapping\MappingException;
22use Doctrine\ORM\Mapping\QuoteStrategy;
23use Doctrine\ORM\Tools\Event\GenerateSchemaEventArgs;
24use Doctrine\ORM\Tools\Event\GenerateSchemaTableEventArgs;
25use Doctrine\ORM\Tools\Exception\MissingColumnException;
26use Doctrine\ORM\Tools\Exception\NotSupported;
27use Throwable;
28
29use function array_diff;
30use function array_diff_key;
31use function array_filter;
32use function array_flip;
33use function array_intersect_key;
34use function assert;
35use function count;
36use function current;
37use function implode;
38use function in_array;
39use function is_numeric;
40use function strtolower;
41
42/**
43 * The SchemaTool is a tool to create/drop/update database schemas based on
44 * <tt>ClassMetadata</tt> class descriptors.
45 *
46 * @link www.doctrine-project.org
47 */
48class SchemaTool
49{
50 private const KNOWN_COLUMN_OPTIONS = ['comment', 'unsigned', 'fixed', 'default'];
51
52 private readonly AbstractPlatform $platform;
53 private readonly QuoteStrategy $quoteStrategy;
54 private readonly AbstractSchemaManager $schemaManager;
55
56 /**
57 * Initializes a new SchemaTool instance that uses the connection of the
58 * provided EntityManager.
59 */
60 public function __construct(private readonly EntityManagerInterface $em)
61 {
62 $this->platform = $em->getConnection()->getDatabasePlatform();
63 $this->quoteStrategy = $em->getConfiguration()->getQuoteStrategy();
64 $this->schemaManager = $em->getConnection()->createSchemaManager();
65 }
66
67 /**
68 * Creates the database schema for the given array of ClassMetadata instances.
69 *
70 * @psalm-param list<ClassMetadata> $classes
71 *
72 * @throws ToolsException
73 */
74 public function createSchema(array $classes): void
75 {
76 $createSchemaSql = $this->getCreateSchemaSql($classes);
77 $conn = $this->em->getConnection();
78
79 foreach ($createSchemaSql as $sql) {
80 try {
81 $conn->executeStatement($sql);
82 } catch (Throwable $e) {
83 throw ToolsException::schemaToolFailure($sql, $e);
84 }
85 }
86 }
87
88 /**
89 * Gets the list of DDL statements that are required to create the database schema for
90 * the given list of ClassMetadata instances.
91 *
92 * @psalm-param list<ClassMetadata> $classes
93 *
94 * @return list<string> The SQL statements needed to create the schema for the classes.
95 */
96 public function getCreateSchemaSql(array $classes): array
97 {
98 $schema = $this->getSchemaFromMetadata($classes);
99
100 return $schema->toSql($this->platform);
101 }
102
103 /**
104 * Detects instances of ClassMetadata that don't need to be processed in the SchemaTool context.
105 *
106 * @psalm-param array<string, bool> $processedClasses
107 */
108 private function processingNotRequired(
109 ClassMetadata $class,
110 array $processedClasses,
111 ): bool {
112 return isset($processedClasses[$class->name]) ||
113 $class->isMappedSuperclass ||
114 $class->isEmbeddedClass ||
115 ($class->isInheritanceTypeSingleTable() && $class->name !== $class->rootEntityName) ||
116 in_array($class->name, $this->em->getConfiguration()->getSchemaIgnoreClasses());
117 }
118
119 /**
120 * Resolves fields in index mapping to column names
121 *
122 * @param mixed[] $indexData index or unique constraint data
123 *
124 * @return list<string> Column names from combined fields and columns mappings
125 */
126 private function getIndexColumns(ClassMetadata $class, array $indexData): array
127 {
128 $columns = [];
129
130 if (
131 isset($indexData['columns'], $indexData['fields'])
132 || (
133 ! isset($indexData['columns'])
134 && ! isset($indexData['fields'])
135 )
136 ) {
137 throw MappingException::invalidIndexConfiguration(
138 (string) $class,
139 $indexData['name'] ?? 'unnamed',
140 );
141 }
142
143 if (isset($indexData['columns'])) {
144 $columns = $indexData['columns'];
145 }
146
147 if (isset($indexData['fields'])) {
148 foreach ($indexData['fields'] as $fieldName) {
149 if ($class->hasField($fieldName)) {
150 $columns[] = $this->quoteStrategy->getColumnName($fieldName, $class, $this->platform);
151 } elseif ($class->hasAssociation($fieldName)) {
152 $assoc = $class->getAssociationMapping($fieldName);
153 assert($assoc->isToOneOwningSide());
154 foreach ($assoc->joinColumns as $joinColumn) {
155 $columns[] = $this->quoteStrategy->getJoinColumnName($joinColumn, $class, $this->platform);
156 }
157 }
158 }
159 }
160
161 return $columns;
162 }
163
164 /**
165 * Creates a Schema instance from a given set of metadata classes.
166 *
167 * @psalm-param list<ClassMetadata> $classes
168 *
169 * @throws NotSupported
170 */
171 public function getSchemaFromMetadata(array $classes): Schema
172 {
173 // Reminder for processed classes, used for hierarchies
174 $processedClasses = [];
175 $eventManager = $this->em->getEventManager();
176 $metadataSchemaConfig = $this->schemaManager->createSchemaConfig();
177
178 $schema = new Schema([], [], $metadataSchemaConfig);
179
180 $addedFks = [];
181 $blacklistedFks = [];
182
183 foreach ($classes as $class) {
184 if ($this->processingNotRequired($class, $processedClasses)) {
185 continue;
186 }
187
188 $table = $schema->createTable($this->quoteStrategy->getTableName($class, $this->platform));
189
190 if ($class->isInheritanceTypeSingleTable()) {
191 $this->gatherColumns($class, $table);
192 $this->gatherRelationsSql($class, $table, $schema, $addedFks, $blacklistedFks);
193
194 // Add the discriminator column
195 $this->addDiscriminatorColumnDefinition($class, $table);
196
197 // Aggregate all the information from all classes in the hierarchy
198 foreach ($class->parentClasses as $parentClassName) {
199 // Parent class information is already contained in this class
200 $processedClasses[$parentClassName] = true;
201 }
202
203 foreach ($class->subClasses as $subClassName) {
204 $subClass = $this->em->getClassMetadata($subClassName);
205 $this->gatherColumns($subClass, $table);
206 $this->gatherRelationsSql($subClass, $table, $schema, $addedFks, $blacklistedFks);
207 $processedClasses[$subClassName] = true;
208 }
209 } elseif ($class->isInheritanceTypeJoined()) {
210 // Add all non-inherited fields as columns
211 foreach ($class->fieldMappings as $fieldName => $mapping) {
212 if (! isset($mapping->inherited)) {
213 $this->gatherColumn($class, $mapping, $table);
214 }
215 }
216
217 $this->gatherRelationsSql($class, $table, $schema, $addedFks, $blacklistedFks);
218
219 // Add the discriminator column only to the root table
220 if ($class->name === $class->rootEntityName) {
221 $this->addDiscriminatorColumnDefinition($class, $table);
222 } else {
223 // Add an ID FK column to child tables
224 $pkColumns = [];
225 $inheritedKeyColumns = [];
226
227 foreach ($class->identifier as $identifierField) {
228 if (isset($class->fieldMappings[$identifierField]->inherited)) {
229 $idMapping = $class->fieldMappings[$identifierField];
230 $this->gatherColumn($class, $idMapping, $table);
231 $columnName = $this->quoteStrategy->getColumnName(
232 $identifierField,
233 $class,
234 $this->platform,
235 );
236 // TODO: This seems rather hackish, can we optimize it?
237 $table->getColumn($columnName)->setAutoincrement(false);
238
239 $pkColumns[] = $columnName;
240 $inheritedKeyColumns[] = $columnName;
241
242 continue;
243 }
244
245 if (isset($class->associationMappings[$identifierField]->inherited)) {
246 $idMapping = $class->associationMappings[$identifierField];
247 assert($idMapping->isToOneOwningSide());
248
249 $targetEntity = current(
250 array_filter(
251 $classes,
252 static fn (ClassMetadata $class): bool => $class->name === $idMapping->targetEntity,
253 ),
254 );
255
256 foreach ($idMapping->joinColumns as $joinColumn) {
257 if (isset($targetEntity->fieldMappings[$joinColumn->referencedColumnName])) {
258 $columnName = $this->quoteStrategy->getJoinColumnName(
259 $joinColumn,
260 $class,
261 $this->platform,
262 );
263
264 $pkColumns[] = $columnName;
265 $inheritedKeyColumns[] = $columnName;
266 }
267 }
268 }
269 }
270
271 if ($inheritedKeyColumns !== []) {
272 // Add a FK constraint on the ID column
273 $table->addForeignKeyConstraint(
274 $this->quoteStrategy->getTableName(
275 $this->em->getClassMetadata($class->rootEntityName),
276 $this->platform,
277 ),
278 $inheritedKeyColumns,
279 $inheritedKeyColumns,
280 ['onDelete' => 'CASCADE'],
281 );
282 }
283
284 if ($pkColumns !== []) {
285 $table->setPrimaryKey($pkColumns);
286 }
287 }
288 } else {
289 $this->gatherColumns($class, $table);
290 $this->gatherRelationsSql($class, $table, $schema, $addedFks, $blacklistedFks);
291 }
292
293 $pkColumns = [];
294
295 foreach ($class->identifier as $identifierField) {
296 if (isset($class->fieldMappings[$identifierField])) {
297 $pkColumns[] = $this->quoteStrategy->getColumnName($identifierField, $class, $this->platform);
298 } elseif (isset($class->associationMappings[$identifierField])) {
299 $assoc = $class->associationMappings[$identifierField];
300 assert($assoc->isToOneOwningSide());
301
302 foreach ($assoc->joinColumns as $joinColumn) {
303 $pkColumns[] = $this->quoteStrategy->getJoinColumnName($joinColumn, $class, $this->platform);
304 }
305 }
306 }
307
308 if (! $table->hasIndex('primary')) {
309 $table->setPrimaryKey($pkColumns);
310 }
311
312 // there can be unique indexes automatically created for join column
313 // if join column is also primary key we should keep only primary key on this column
314 // so, remove indexes overruled by primary key
315 $primaryKey = $table->getIndex('primary');
316
317 foreach ($table->getIndexes() as $idxKey => $existingIndex) {
318 if ($primaryKey->overrules($existingIndex)) {
319 $table->dropIndex($idxKey);
320 }
321 }
322
323 if (isset($class->table['indexes'])) {
324 foreach ($class->table['indexes'] as $indexName => $indexData) {
325 if (! isset($indexData['flags'])) {
326 $indexData['flags'] = [];
327 }
328
329 $table->addIndex(
330 $this->getIndexColumns($class, $indexData),
331 is_numeric($indexName) ? null : $indexName,
332 (array) $indexData['flags'],
333 $indexData['options'] ?? [],
334 );
335 }
336 }
337
338 if (isset($class->table['uniqueConstraints'])) {
339 foreach ($class->table['uniqueConstraints'] as $indexName => $indexData) {
340 $uniqIndex = new Index('tmp__' . $indexName, $this->getIndexColumns($class, $indexData), true, false, [], $indexData['options'] ?? []);
341
342 foreach ($table->getIndexes() as $tableIndexName => $tableIndex) {
343 if ($tableIndex->isFulfilledBy($uniqIndex)) {
344 $table->dropIndex($tableIndexName);
345 break;
346 }
347 }
348
349 $table->addUniqueIndex($uniqIndex->getColumns(), is_numeric($indexName) ? null : $indexName, $indexData['options'] ?? []);
350 }
351 }
352
353 if (isset($class->table['options'])) {
354 foreach ($class->table['options'] as $key => $val) {
355 $table->addOption($key, $val);
356 }
357 }
358
359 $processedClasses[$class->name] = true;
360
361 if ($class->isIdGeneratorSequence() && $class->name === $class->rootEntityName) {
362 $seqDef = $class->sequenceGeneratorDefinition;
363 $quotedName = $this->quoteStrategy->getSequenceName($seqDef, $class, $this->platform);
364 if (! $schema->hasSequence($quotedName)) {
365 $schema->createSequence(
366 $quotedName,
367 (int) $seqDef['allocationSize'],
368 (int) $seqDef['initialValue'],
369 );
370 }
371 }
372
373 if ($eventManager->hasListeners(ToolEvents::postGenerateSchemaTable)) {
374 $eventManager->dispatchEvent(
375 ToolEvents::postGenerateSchemaTable,
376 new GenerateSchemaTableEventArgs($class, $schema, $table),
377 );
378 }
379 }
380
381 if ($eventManager->hasListeners(ToolEvents::postGenerateSchema)) {
382 $eventManager->dispatchEvent(
383 ToolEvents::postGenerateSchema,
384 new GenerateSchemaEventArgs($this->em, $schema),
385 );
386 }
387
388 return $schema;
389 }
390
391 /**
392 * Gets a portable column definition as required by the DBAL for the discriminator
393 * column of a class.
394 */
395 private function addDiscriminatorColumnDefinition(ClassMetadata $class, Table $table): void
396 {
397 $discrColumn = $class->discriminatorColumn;
398 assert($discrColumn !== null);
399
400 if (strtolower($discrColumn->type) === 'string' && ! isset($discrColumn->length)) {
401 $discrColumn->type = 'string';
402 $discrColumn->length = 255;
403 }
404
405 $options = [
406 'length' => $discrColumn->length ?? null,
407 'notnull' => true,
408 ];
409
410 if (isset($discrColumn->columnDefinition)) {
411 $options['columnDefinition'] = $discrColumn->columnDefinition;
412 }
413
414 $options = $this->gatherColumnOptions($discrColumn) + $options;
415 $table->addColumn($discrColumn->name, $discrColumn->type, $options);
416 }
417
418 /**
419 * Gathers the column definitions as required by the DBAL of all field mappings
420 * found in the given class.
421 */
422 private function gatherColumns(ClassMetadata $class, Table $table): void
423 {
424 $pkColumns = [];
425
426 foreach ($class->fieldMappings as $mapping) {
427 if ($class->isInheritanceTypeSingleTable() && isset($mapping->inherited)) {
428 continue;
429 }
430
431 $this->gatherColumn($class, $mapping, $table);
432
433 if ($class->isIdentifier($mapping->fieldName)) {
434 $pkColumns[] = $this->quoteStrategy->getColumnName($mapping->fieldName, $class, $this->platform);
435 }
436 }
437 }
438
439 /**
440 * Creates a column definition as required by the DBAL from an ORM field mapping definition.
441 *
442 * @param ClassMetadata $class The class that owns the field mapping.
443 * @psalm-param FieldMapping $mapping The field mapping.
444 */
445 private function gatherColumn(
446 ClassMetadata $class,
447 FieldMapping $mapping,
448 Table $table,
449 ): void {
450 $columnName = $this->quoteStrategy->getColumnName($mapping->fieldName, $class, $this->platform);
451 $columnType = $mapping->type;
452
453 $options = [];
454 $options['length'] = $mapping->length ?? null;
455 $options['notnull'] = isset($mapping->nullable) ? ! $mapping->nullable : true;
456 if ($class->isInheritanceTypeSingleTable() && $class->parentClasses) {
457 $options['notnull'] = false;
458 }
459
460 $options['platformOptions'] = [];
461 $options['platformOptions']['version'] = $class->isVersioned && $class->versionField === $mapping->fieldName;
462
463 if (strtolower($columnType) === 'string' && $options['length'] === null) {
464 $options['length'] = 255;
465 }
466
467 if (isset($mapping->precision)) {
468 $options['precision'] = $mapping->precision;
469 }
470
471 if (isset($mapping->scale)) {
472 $options['scale'] = $mapping->scale;
473 }
474
475 if (isset($mapping->default)) {
476 $options['default'] = $mapping->default;
477 }
478
479 if (isset($mapping->columnDefinition)) {
480 $options['columnDefinition'] = $mapping->columnDefinition;
481 }
482
483 // the 'default' option can be overwritten here
484 $options = $this->gatherColumnOptions($mapping) + $options;
485
486 if ($class->isIdGeneratorIdentity() && $class->getIdentifierFieldNames() === [$mapping->fieldName]) {
487 $options['autoincrement'] = true;
488 }
489
490 if ($class->isInheritanceTypeJoined() && $class->name !== $class->rootEntityName) {
491 $options['autoincrement'] = false;
492 }
493
494 if ($table->hasColumn($columnName)) {
495 // required in some inheritance scenarios
496 $table->modifyColumn($columnName, $options);
497 } else {
498 $table->addColumn($columnName, $columnType, $options);
499 }
500
501 $isUnique = $mapping->unique ?? false;
502 if ($isUnique) {
503 $table->addUniqueIndex([$columnName]);
504 }
505 }
506
507 /**
508 * Gathers the SQL for properly setting up the relations of the given class.
509 * This includes the SQL for foreign key constraints and join tables.
510 *
511 * @psalm-param array<string, array{
512 * foreignTableName: string,
513 * foreignColumns: list<string>
514 * }> $addedFks
515 * @psalm-param array<string, bool> $blacklistedFks
516 *
517 * @throws NotSupported
518 */
519 private function gatherRelationsSql(
520 ClassMetadata $class,
521 Table $table,
522 Schema $schema,
523 array &$addedFks,
524 array &$blacklistedFks,
525 ): void {
526 foreach ($class->associationMappings as $id => $mapping) {
527 if (isset($mapping->inherited) && ! in_array($id, $class->identifier, true)) {
528 continue;
529 }
530
531 $foreignClass = $this->em->getClassMetadata($mapping->targetEntity);
532
533 if ($mapping->isToOneOwningSide()) {
534 $primaryKeyColumns = []; // PK is unnecessary for this relation-type
535
536 $this->gatherRelationJoinColumns(
537 $mapping->joinColumns,
538 $table,
539 $foreignClass,
540 $mapping,
541 $primaryKeyColumns,
542 $addedFks,
543 $blacklistedFks,
544 );
545 } elseif ($mapping instanceof ManyToManyOwningSideMapping) {
546 // create join table
547 $joinTable = $mapping->joinTable;
548
549 $theJoinTable = $schema->createTable(
550 $this->quoteStrategy->getJoinTableName($mapping, $foreignClass, $this->platform),
551 );
552
553 foreach ($joinTable->options as $key => $val) {
554 $theJoinTable->addOption($key, $val);
555 }
556
557 $primaryKeyColumns = [];
558
559 // Build first FK constraint (relation table => source table)
560 $this->gatherRelationJoinColumns(
561 $joinTable->joinColumns,
562 $theJoinTable,
563 $class,
564 $mapping,
565 $primaryKeyColumns,
566 $addedFks,
567 $blacklistedFks,
568 );
569
570 // Build second FK constraint (relation table => target table)
571 $this->gatherRelationJoinColumns(
572 $joinTable->inverseJoinColumns,
573 $theJoinTable,
574 $foreignClass,
575 $mapping,
576 $primaryKeyColumns,
577 $addedFks,
578 $blacklistedFks,
579 );
580
581 $theJoinTable->setPrimaryKey($primaryKeyColumns);
582 }
583 }
584 }
585
586 /**
587 * Gets the class metadata that is responsible for the definition of the referenced column name.
588 *
589 * Previously this was a simple task, but with DDC-117 this problem is actually recursive. If its
590 * not a simple field, go through all identifier field names that are associations recursively and
591 * find that referenced column name.
592 *
593 * TODO: Is there any way to make this code more pleasing?
594 *
595 * @psalm-return array{ClassMetadata, string}|null
596 */
597 private function getDefiningClass(ClassMetadata $class, string $referencedColumnName): array|null
598 {
599 $referencedFieldName = $class->getFieldName($referencedColumnName);
600
601 if ($class->hasField($referencedFieldName)) {
602 return [$class, $referencedFieldName];
603 }
604
605 if (in_array($referencedColumnName, $class->getIdentifierColumnNames(), true)) {
606 // it seems to be an entity as foreign key
607 foreach ($class->getIdentifierFieldNames() as $fieldName) {
608 if (
609 $class->hasAssociation($fieldName)
610 && $class->getSingleAssociationJoinColumnName($fieldName) === $referencedColumnName
611 ) {
612 return $this->getDefiningClass(
613 $this->em->getClassMetadata($class->associationMappings[$fieldName]->targetEntity),
614 $class->getSingleAssociationReferencedJoinColumnName($fieldName),
615 );
616 }
617 }
618 }
619
620 return null;
621 }
622
623 /**
624 * Gathers columns and fk constraints that are required for one part of relationship.
625 *
626 * @psalm-param list<JoinColumnMapping> $joinColumns
627 * @psalm-param list<string> $primaryKeyColumns
628 * @psalm-param array<string, array{
629 * foreignTableName: string,
630 * foreignColumns: list<string>
631 * }> $addedFks
632 * @psalm-param array<string,bool> $blacklistedFks
633 *
634 * @throws MissingColumnException
635 */
636 private function gatherRelationJoinColumns(
637 array $joinColumns,
638 Table $theJoinTable,
639 ClassMetadata $class,
640 AssociationMapping $mapping,
641 array &$primaryKeyColumns,
642 array &$addedFks,
643 array &$blacklistedFks,
644 ): void {
645 $localColumns = [];
646 $foreignColumns = [];
647 $fkOptions = [];
648 $foreignTableName = $this->quoteStrategy->getTableName($class, $this->platform);
649 $uniqueConstraints = [];
650
651 foreach ($joinColumns as $joinColumn) {
652 [$definingClass, $referencedFieldName] = $this->getDefiningClass(
653 $class,
654 $joinColumn->referencedColumnName,
655 );
656
657 if (! $definingClass) {
658 throw MissingColumnException::fromColumnSourceAndTarget(
659 $joinColumn->referencedColumnName,
660 $mapping->sourceEntity,
661 $mapping->targetEntity,
662 );
663 }
664
665 $quotedColumnName = $this->quoteStrategy->getJoinColumnName($joinColumn, $class, $this->platform);
666 $quotedRefColumnName = $this->quoteStrategy->getReferencedJoinColumnName(
667 $joinColumn,
668 $class,
669 $this->platform,
670 );
671
672 $primaryKeyColumns[] = $quotedColumnName;
673 $localColumns[] = $quotedColumnName;
674 $foreignColumns[] = $quotedRefColumnName;
675
676 if (! $theJoinTable->hasColumn($quotedColumnName)) {
677 // Only add the column to the table if it does not exist already.
678 // It might exist already if the foreign key is mapped into a regular
679 // property as well.
680
681 $fieldMapping = $definingClass->getFieldMapping($referencedFieldName);
682
683 $columnOptions = ['notnull' => false];
684
685 if (isset($joinColumn->columnDefinition)) {
686 $columnOptions['columnDefinition'] = $joinColumn->columnDefinition;
687 } elseif (isset($fieldMapping->columnDefinition)) {
688 $columnOptions['columnDefinition'] = $fieldMapping->columnDefinition;
689 }
690
691 if (isset($joinColumn->nullable)) {
692 $columnOptions['notnull'] = ! $joinColumn->nullable;
693 }
694
695 $columnOptions += $this->gatherColumnOptions($fieldMapping);
696
697 if (isset($fieldMapping->length)) {
698 $columnOptions['length'] = $fieldMapping->length;
699 }
700
701 if ($fieldMapping->type === 'decimal') {
702 $columnOptions['scale'] = $fieldMapping->scale;
703 $columnOptions['precision'] = $fieldMapping->precision;
704 }
705
706 $columnOptions = $this->gatherColumnOptions($joinColumn) + $columnOptions;
707
708 $theJoinTable->addColumn($quotedColumnName, $fieldMapping->type, $columnOptions);
709 }
710
711 if (isset($joinColumn->unique) && $joinColumn->unique === true) {
712 $uniqueConstraints[] = ['columns' => [$quotedColumnName]];
713 }
714
715 if (isset($joinColumn->onDelete)) {
716 $fkOptions['onDelete'] = $joinColumn->onDelete;
717 }
718 }
719
720 // Prefer unique constraints over implicit simple indexes created for foreign keys.
721 // Also avoids index duplication.
722 foreach ($uniqueConstraints as $indexName => $unique) {
723 $theJoinTable->addUniqueIndex($unique['columns'], is_numeric($indexName) ? null : $indexName);
724 }
725
726 $compositeName = $theJoinTable->getName() . '.' . implode('', $localColumns);
727 if (
728 isset($addedFks[$compositeName])
729 && ($foreignTableName !== $addedFks[$compositeName]['foreignTableName']
730 || 0 < count(array_diff($foreignColumns, $addedFks[$compositeName]['foreignColumns'])))
731 ) {
732 foreach ($theJoinTable->getForeignKeys() as $fkName => $key) {
733 if (
734 count(array_diff($key->getLocalColumns(), $localColumns)) === 0
735 && (($key->getForeignTableName() !== $foreignTableName)
736 || 0 < count(array_diff($key->getForeignColumns(), $foreignColumns)))
737 ) {
738 $theJoinTable->removeForeignKey($fkName);
739 break;
740 }
741 }
742
743 $blacklistedFks[$compositeName] = true;
744 } elseif (! isset($blacklistedFks[$compositeName])) {
745 $addedFks[$compositeName] = ['foreignTableName' => $foreignTableName, 'foreignColumns' => $foreignColumns];
746 $theJoinTable->addForeignKeyConstraint(
747 $foreignTableName,
748 $localColumns,
749 $foreignColumns,
750 $fkOptions,
751 );
752 }
753 }
754
755 /** @return mixed[] */
756 private function gatherColumnOptions(JoinColumnMapping|FieldMapping|DiscriminatorColumnMapping $mapping): array
757 {
758 $mappingOptions = $mapping->options ?? [];
759
760 if (isset($mapping->enumType)) {
761 $mappingOptions['enumType'] = $mapping->enumType;
762 }
763
764 if (($mappingOptions['default'] ?? null) instanceof BackedEnum) {
765 $mappingOptions['default'] = $mappingOptions['default']->value;
766 }
767
768 if (empty($mappingOptions)) {
769 return [];
770 }
771
772 $options = array_intersect_key($mappingOptions, array_flip(self::KNOWN_COLUMN_OPTIONS));
773 $options['platformOptions'] = array_diff_key($mappingOptions, $options);
774
775 return $options;
776 }
777
778 /**
779 * Drops the database schema for the given classes.
780 *
781 * In any way when an exception is thrown it is suppressed since drop was
782 * issued for all classes of the schema and some probably just don't exist.
783 *
784 * @psalm-param list<ClassMetadata> $classes
785 */
786 public function dropSchema(array $classes): void
787 {
788 $dropSchemaSql = $this->getDropSchemaSQL($classes);
789 $conn = $this->em->getConnection();
790
791 foreach ($dropSchemaSql as $sql) {
792 try {
793 $conn->executeStatement($sql);
794 } catch (Throwable) {
795 // ignored
796 }
797 }
798 }
799
800 /**
801 * Drops all elements in the database of the current connection.
802 */
803 public function dropDatabase(): void
804 {
805 $dropSchemaSql = $this->getDropDatabaseSQL();
806 $conn = $this->em->getConnection();
807
808 foreach ($dropSchemaSql as $sql) {
809 $conn->executeStatement($sql);
810 }
811 }
812
813 /**
814 * Gets the SQL needed to drop the database schema for the connections database.
815 *
816 * @return list<string>
817 */
818 public function getDropDatabaseSQL(): array
819 {
820 return $this->schemaManager
821 ->introspectSchema()
822 ->toDropSql($this->platform);
823 }
824
825 /**
826 * Gets SQL to drop the tables defined by the passed classes.
827 *
828 * @psalm-param list<ClassMetadata> $classes
829 *
830 * @return list<string>
831 */
832 public function getDropSchemaSQL(array $classes): array
833 {
834 $schema = $this->getSchemaFromMetadata($classes);
835
836 $deployedSchema = $this->schemaManager->introspectSchema();
837
838 foreach ($schema->getTables() as $table) {
839 if (! $deployedSchema->hasTable($table->getName())) {
840 $schema->dropTable($table->getName());
841 }
842 }
843
844 if ($this->platform->supportsSequences()) {
845 foreach ($schema->getSequences() as $sequence) {
846 if (! $deployedSchema->hasSequence($sequence->getName())) {
847 $schema->dropSequence($sequence->getName());
848 }
849 }
850
851 foreach ($schema->getTables() as $table) {
852 $primaryKey = $table->getPrimaryKey();
853 if ($primaryKey === null) {
854 continue;
855 }
856
857 $columns = $primaryKey->getColumns();
858 if (count($columns) === 1) {
859 $checkSequence = $table->getName() . '_' . $columns[0] . '_seq';
860 if ($deployedSchema->hasSequence($checkSequence) && ! $schema->hasSequence($checkSequence)) {
861 $schema->createSequence($checkSequence);
862 }
863 }
864 }
865 }
866
867 return $schema->toDropSql($this->platform);
868 }
869
870 /**
871 * Updates the database schema of the given classes by comparing the ClassMetadata
872 * instances to the current database schema that is inspected.
873 *
874 * @param mixed[] $classes
875 */
876 public function updateSchema(array $classes): void
877 {
878 $conn = $this->em->getConnection();
879
880 foreach ($this->getUpdateSchemaSql($classes) as $sql) {
881 $conn->executeStatement($sql);
882 }
883 }
884
885 /**
886 * Gets the sequence of SQL statements that need to be performed in order
887 * to bring the given class mappings in-synch with the relational schema.
888 *
889 * @param list<ClassMetadata> $classes The classes to consider.
890 *
891 * @return list<string> The sequence of SQL statements.
892 */
893 public function getUpdateSchemaSql(array $classes): array
894 {
895 $toSchema = $this->getSchemaFromMetadata($classes);
896 $fromSchema = $this->createSchemaForComparison($toSchema);
897 $comparator = $this->schemaManager->createComparator();
898 $schemaDiff = $comparator->compareSchemas($fromSchema, $toSchema);
899
900 return $this->platform->getAlterSchemaSQL($schemaDiff);
901 }
902
903 /**
904 * Creates the schema from the database, ensuring tables from the target schema are whitelisted for comparison.
905 */
906 private function createSchemaForComparison(Schema $toSchema): Schema
907 {
908 $connection = $this->em->getConnection();
909
910 // backup schema assets filter
911 $config = $connection->getConfiguration();
912 $previousFilter = $config->getSchemaAssetsFilter();
913
914 if ($previousFilter === null) {
915 return $this->schemaManager->introspectSchema();
916 }
917
918 // whitelist assets we already know about in $toSchema, use the existing filter otherwise
919 $config->setSchemaAssetsFilter(static function ($asset) use ($previousFilter, $toSchema): bool {
920 $assetName = $asset instanceof AbstractAsset ? $asset->getName() : $asset;
921
922 return $toSchema->hasTable($assetName) || $toSchema->hasSequence($assetName) || $previousFilter($asset);
923 });
924
925 try {
926 return $this->schemaManager->introspectSchema();
927 } finally {
928 // restore schema assets filter
929 $config->setSchemaAssetsFilter($previousFilter);
930 }
931 }
932}