diff options
author | polo <ordipolo@gmx.fr> | 2024-08-13 23:45:21 +0200 |
---|---|---|
committer | polo <ordipolo@gmx.fr> | 2024-08-13 23:45:21 +0200 |
commit | bf6655a534a6775d30cafa67bd801276bda1d98d (patch) | |
tree | c6381e3f6c81c33eab72508f410b165ba05f7e9c /vendor/doctrine/orm/src/Tools/SchemaTool.php | |
parent | 94d67a4b51f8e62e7d518cce26a526ae1ec48278 (diff) | |
download | AppliGestionPHP-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.php | 932 |
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 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\ORM\Tools; | ||
6 | |||
7 | use BackedEnum; | ||
8 | use Doctrine\DBAL\Platforms\AbstractPlatform; | ||
9 | use Doctrine\DBAL\Schema\AbstractAsset; | ||
10 | use Doctrine\DBAL\Schema\AbstractSchemaManager; | ||
11 | use Doctrine\DBAL\Schema\Index; | ||
12 | use Doctrine\DBAL\Schema\Schema; | ||
13 | use Doctrine\DBAL\Schema\Table; | ||
14 | use Doctrine\ORM\EntityManagerInterface; | ||
15 | use Doctrine\ORM\Mapping\AssociationMapping; | ||
16 | use Doctrine\ORM\Mapping\ClassMetadata; | ||
17 | use Doctrine\ORM\Mapping\DiscriminatorColumnMapping; | ||
18 | use Doctrine\ORM\Mapping\FieldMapping; | ||
19 | use Doctrine\ORM\Mapping\JoinColumnMapping; | ||
20 | use Doctrine\ORM\Mapping\ManyToManyOwningSideMapping; | ||
21 | use Doctrine\ORM\Mapping\MappingException; | ||
22 | use Doctrine\ORM\Mapping\QuoteStrategy; | ||
23 | use Doctrine\ORM\Tools\Event\GenerateSchemaEventArgs; | ||
24 | use Doctrine\ORM\Tools\Event\GenerateSchemaTableEventArgs; | ||
25 | use Doctrine\ORM\Tools\Exception\MissingColumnException; | ||
26 | use Doctrine\ORM\Tools\Exception\NotSupported; | ||
27 | use Throwable; | ||
28 | |||
29 | use function array_diff; | ||
30 | use function array_diff_key; | ||
31 | use function array_filter; | ||
32 | use function array_flip; | ||
33 | use function array_intersect_key; | ||
34 | use function assert; | ||
35 | use function count; | ||
36 | use function current; | ||
37 | use function implode; | ||
38 | use function in_array; | ||
39 | use function is_numeric; | ||
40 | use 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 | */ | ||
48 | class 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 | } | ||