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