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