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