Failed Conditions
Push — master ( 01c22b...e42c1f )
by Marco
79:13 queued 10s
created

Table   F

Complexity

Total Complexity 114

Size/Duplication

Total Lines 829
Duplicated Lines 0 %

Test Coverage

Coverage 96.85%

Importance

Changes 3
Bugs 0 Features 0
Metric Value
wmc 114
eloc 261
dl 0
loc 829
ccs 246
cts 254
cp 0.9685
rs 2
c 3
b 0
f 0

47 Methods

Rating   Name   Duplication   Size   Complexity  
A getIndexes() 0 3 1
A hasPrimaryKey() 0 3 2
A getPrimaryKey() 0 5 2
A addColumn() 0 7 1
A hasColumn() 0 5 1
A getOption() 0 3 1
A getForeignKeys() 0 3 1
A getComment() 0 3 1
A setComment() 0 6 1
A hasUniqueConstraint() 0 5 1
A getUniqueConstraints() 0 3 1
A getPrimaryKeyColumns() 0 9 2
A getForeignKey() 0 9 2
A __construct() 0 31 6
A addUniqueConstraint() 0 11 2
A renameIndex() 0 32 6
A addIndex() 0 11 2
A removeForeignKey() 0 9 2
A hasIndex() 0 5 1
A removeUniqueConstraint() 0 9 2
B _addIndex() 0 31 10
A _addForeignKeyConstraint() 0 38 4
A setPrimaryKey() 0 10 3
A _getMaxIdentifierLength() 0 5 2
A hasForeignKey() 0 5 1
A getOptions() 0 3 1
A visit() 0 14 4
A columnsAreIndexed() 0 10 3
A normalizeIdentifier() 0 7 2
A _addColumn() 0 10 2
A getColumns() 0 24 3
A dropPrimaryKey() 0 4 1
A _createUniqueConstraint() 0 19 5
A addOption() 0 5 1
A getUniqueConstraint() 0 9 2
A getColumn() 0 9 2
A dropColumn() 0 7 1
A getIndex() 0 9 2
A changeColumn() 0 7 1
A hasOption() 0 3 1
A addUniqueIndex() 0 11 2
A _createIndex() 0 19 5
B addForeignKeyConstraint() 0 33 7
A _addUniqueConstraint() 0 34 4
A dropIndex() 0 9 2
A setSchemaConfig() 0 3 1
A __clone() 0 13 4

How to fix   Complexity   

Complex Class

Complex classes like Table often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Table, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Doctrine\DBAL\Schema;
6
7
use Doctrine\DBAL\DBALException;
8
use Doctrine\DBAL\Schema\Exception\ColumnAlreadyExists;
9
use Doctrine\DBAL\Schema\Exception\ColumnDoesNotExist;
10
use Doctrine\DBAL\Schema\Exception\ForeignKeyDoesNotExist;
11
use Doctrine\DBAL\Schema\Exception\IndexAlreadyExists;
12
use Doctrine\DBAL\Schema\Exception\IndexDoesNotExist;
13
use Doctrine\DBAL\Schema\Exception\IndexNameInvalid;
14
use Doctrine\DBAL\Schema\Exception\InvalidTableName;
15
use Doctrine\DBAL\Schema\Exception\UniqueConstraintDoesNotExist;
16
use Doctrine\DBAL\Schema\Visitor\Visitor;
17
use Doctrine\DBAL\Types\Type;
18
use function array_keys;
19
use function array_merge;
20
use function array_search;
21
use function array_unique;
22
use function in_array;
23
use function is_string;
24
use function preg_match;
25
use function sprintf;
26
use function strlen;
27
use function strtolower;
28
use function uksort;
29
30
/**
31
 * Object Representation of a table.
32
 */
33
class Table extends AbstractAsset
34
{
35
    /** @var Column[] */
36
    protected $_columns = [];
37
38
    /** @var Index[] */
39
    private $implicitIndexes = [];
40
41
    /** @var Index[] */
42
    protected $_indexes = [];
43
44
    /** @var string */
45
    protected $_primaryKeyName = false;
46
47
    /** @var UniqueConstraint[] */
48
    protected $_uniqueConstraints = [];
49
50
    /** @var ForeignKeyConstraint[] */
51
    protected $_fkConstraints = [];
52
53
    /** @var mixed[] */
54 24764
    protected $_options = [
55
        'create_options' => [],
56 24764
    ];
57 1563
58
    /** @var SchemaConfig|null */
59
    protected $_schemaConfig = null;
60 24762
61
    /**
62 24762
     * @param array<Column>               $columns
63 22214
     * @param array<Index>                $indexes
64
     * @param array<UniqueConstraint>     $uniqueConstraints
65
     * @param array<ForeignKeyConstraint> $fkConstraints
66 24760
     * @param array<string, mixed>        $options
67 21982
     *
68
     * @throws DBALException
69
     */
70 24756
    public function __construct(
71 21051
        string $tableName,
72
        array $columns = [],
73
        array $indexes = [],
74 24756
        array $uniqueConstraints = [],
75 24756
        array $fkConstraints = [],
76
        array $options = []
77
    ) {
78
        if (strlen($tableName) === 0) {
79
            throw InvalidTableName::new($tableName);
80 22711
        }
81
82 22711
        $this->_setName($tableName);
83 22711
84
        foreach ($columns as $column) {
85
            $this->_addColumn($column);
86
        }
87
88 22972
        foreach ($indexes as $idx) {
89
            $this->_addIndex($idx);
90 22972
        }
91 22507
92
        foreach ($uniqueConstraints as $uniqueConstraint) {
93
            $this->_addUniqueConstraint($uniqueConstraint);
94 22906
        }
95
96
        foreach ($fkConstraints as $fkConstraint) {
97
            $this->_addForeignKeyConstraint($fkConstraint);
98
        }
99
100
        $this->_options = array_merge($this->_options, $options);
101
    }
102
103
    public function setSchemaConfig(SchemaConfig $schemaConfig) : void
104
    {
105 23682
        $this->_schemaConfig = $schemaConfig;
106
    }
107 23682
108
    /**
109 23682
     * Sets the Primary Key.
110 23682
     *
111 23682
     * @param array<int, string> $columnNames
112
     */
113
    public function setPrimaryKey(array $columnNames, ?string $indexName = null) : self
114 23682
    {
115
        $this->_addIndex($this->_createIndex($columnNames, $indexName ?: 'primary', true, true));
116
117
        foreach ($columnNames as $columnName) {
118
            $column = $this->getColumn($columnName);
119
            $column->setNotnull(true);
120
        }
121
122
        return $this;
123
    }
124
125 21509
    /**
126
     * @param array<int, string>   $columnNames
127 21509
     * @param array<int, string>   $flags
128 20980
     * @param array<string, mixed> $options
129 20980
     */
130 20980
    public function addUniqueConstraint(array $columnNames, ?string $indexName = null, array $flags = [], array $options = []) : self
131 20980
    {
132
        if ($indexName === null) {
133
            $indexName = $this->_generateIdentifierName(
134
                array_merge([$this->getName()], $columnNames),
135 21509
                'uniq',
136
                $this->_getMaxIdentifierLength()
137
            );
138
        }
139
140
        return $this->_addUniqueConstraint($this->_createUniqueConstraint($columnNames, $indexName, $flags, $options));
141
    }
142
143 20243
    /**
144
     * @param array<int, string>   $columnNames
145 20243
     * @param array<int, string>   $flags
146 20243
     * @param array<string, mixed> $options
147 20243
     */
148
    public function addIndex(array $columnNames, ?string $indexName = null, array $flags = [], array $options = []) : self
149
    {
150
        if ($indexName === null) {
151
            $indexName = $this->_generateIdentifierName(
152
                array_merge([$this->getName()], $columnNames),
153
                'idx',
154
                $this->_getMaxIdentifierLength()
155
            );
156
        }
157
158 20280
        return $this->_addIndex($this->_createIndex($columnNames, $indexName, false, false, $flags, $options));
159
    }
160 20280
161 20280
    /**
162
     * Drops the primary key from this table.
163
     */
164 20280
    public function dropPrimaryKey() : void
165 20280
    {
166
        $this->dropIndex($this->_primaryKeyName);
167
        $this->_primaryKeyName = false;
0 ignored issues
show
Documentation Bug introduced by
The property $_primaryKeyName was declared of type string, but false is of type false. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
168
    }
169
170
    /**
171
     * Drops an index from this table.
172
     *
173
     * @throws SchemaException If the index does not exist.
174 22565
     */
175
    public function dropIndex(string $indexName) : void
176 22565
    {
177 22208
        $indexName = $this->normalizeIdentifier($indexName);
178 22208
179 22208
        if (! $this->hasIndex($indexName)) {
180 22208
            throw IndexDoesNotExist::new($indexName, $this->_name);
181
        }
182
183
        unset($this->_indexes[$indexName]);
184 22565
    }
185
186
    /**
187
     * @param array<int, string>   $columnNames
188
     * @param array<string, mixed> $options
189
     */
190
    public function addUniqueIndex(array $columnNames, ?string $indexName = null, array $options = []) : self
191
    {
192
        if ($indexName === null) {
193
            $indexName = $this->_generateIdentifierName(
194
                array_merge([$this->getName()], $columnNames),
195
                'uniq',
196
                $this->_getMaxIdentifierLength()
197
            );
198
        }
199 19869
200
        return $this->_addIndex($this->_createIndex($columnNames, $indexName, true, false, [], $options));
201 19869
    }
202 19869
203
    /**
204 19869
     * Renames an index.
205 429
     *
206
     * @param string      $oldIndexName The name of the index to rename from.
207
     * @param string|null $newIndexName The name of the index to rename to.
208 19869
     *                                  If null is given, the index name will be auto-generated.
209 338
     *
210
     * @throws SchemaException If no index exists for the given current name
211
     *                         or if an index with the given new name already exists on this table.
212 19867
     */
213 313
    public function renameIndex(string $oldIndexName, ?string $newIndexName = null) : self
214
    {
215
        $oldIndexName           = $this->normalizeIdentifier($oldIndexName);
216 19865
        $normalizedNewIndexName = $this->normalizeIdentifier($newIndexName);
217
218 19865
        if ($oldIndexName === $normalizedNewIndexName) {
219 413
            return $this;
220
        }
221 413
222
        if (! $this->hasIndex($oldIndexName)) {
223
            throw IndexDoesNotExist::new($oldIndexName, $this->_name);
224 19865
        }
225
226 19865
        if ($this->hasIndex($normalizedNewIndexName)) {
227 415
            throw IndexAlreadyExists::new($normalizedNewIndexName, $this->_name);
228
        }
229
230 19863
        $oldIndex = $this->_indexes[$oldIndexName];
231
232
        if ($oldIndex->isPrimary()) {
233
            $this->dropPrimaryKey();
234
235
            return $this->setPrimaryKey($oldIndex->getColumns(), $newIndexName ?? null);
236
        }
237
238
        unset($this->_indexes[$oldIndexName]);
239
240 20044
        if ($oldIndex->isUnique()) {
241
            return $this->addUniqueIndex($oldIndex->getColumns(), $newIndexName, $oldIndex->getOptions());
242 20044
        }
243
244 20044
        return $this->addIndex($oldIndex->getColumns(), $newIndexName, $oldIndex->getFlags(), $oldIndex->getOptions());
245 20044
    }
246
247
    /**
248
     * Checks if an index begins in the order of the given columns.
249
     *
250
     * @param array<int, string> $columnNames
251
     */
252
    public function columnsAreIndexed(array $columnNames) : bool
253
    {
254
        foreach ($this->getIndexes() as $index) {
255
            /** @var $index Index */
256
            if ($index->spansColumns($columnNames)) {
257
                return true;
258
            }
259
        }
260
261
        return false;
262
    }
263
264 23980
    /**
265
     * @param array<string, mixed> $options
266 23980
     */
267 1088
    public function addColumn(string $columnName, string $typeName, array $options = []) : Column
268
    {
269
        $column = new Column($columnName, Type::getType($typeName), $options);
270 23978
271 23976
        $this->_addColumn($column);
272 2059
273
        return $column;
274
    }
275
276 23976
    /**
277
     * Change Column Details.
278
     *
279
     * @param array<string, mixed> $options
280
     */
281
    public function changeColumn(string $columnName, array $options) : self
282
    {
283
        $column = $this->getColumn($columnName);
284
285
        $column->setOptions($options);
286 24410
287
        return $this;
288 24410
    }
289
290 24410
    /**
291
     * Drops a Column from the Table.
292 24410
     */
293
    public function dropColumn(string $columnName) : self
294
    {
295
        $columnName = $this->normalizeIdentifier($columnName);
296
297
        unset($this->_columns[$columnName]);
298
299
        return $this;
300
    }
301
302
    /**
303
     * Adds a foreign key constraint.
304
     *
305
     * Name is inferred from the local columns.
306
     *
307
     * @param Table|string         $foreignTable       Table schema instance or table name
308
     * @param array<int, string>   $localColumnNames
309
     * @param array<int, string>   $foreignColumnNames
310
     * @param array<string, mixed> $options
311
     */
312
    public function addForeignKeyConstraint($foreignTable, array $localColumnNames, array $foreignColumnNames, array $options = [], ?string $name = null) : self
313
    {
314
        if (! $name) {
315
            $name = $this->_generateIdentifierName(
316
                array_merge((array) $this->getName(), $localColumnNames),
317
                'fk',
318
                $this->_getMaxIdentifierLength()
319
            );
320 20497
        }
321
322 20497
        if ($foreignTable instanceof Table) {
323 20497
            foreach ($foreignColumnNames as $columnName) {
324
                if (! $foreignTable->hasColumn($columnName)) {
325 20497
                    throw ColumnDoesNotExist::new($columnName, $foreignTable->getName());
326
                }
327
            }
328
        }
329
330
        foreach ($localColumnNames as $columnName) {
331
            if (! $this->hasColumn($columnName)) {
332
                throw ColumnDoesNotExist::new($columnName, $this->_name);
333
            }
334
        }
335 1454
336
        $constraint = new ForeignKeyConstraint(
337 1454
            $localColumnNames,
338 1454
            $foreignTable,
339
            $foreignColumnNames,
340 1454
            $name,
341
            $options
342
        );
343
344
        return $this->_addForeignKeyConstraint($constraint);
345
    }
346
347
    /**
348
     * @param mixed $value
349
     */
350
    public function addOption(string $name, $value) : self
351
    {
352
        $this->_options[$name] = $value;
353
354
        return $this;
355
    }
356 22729
357
    /**
358 22729
     * Returns whether this table has a foreign key constraint with the given name.
359
     */
360 22729
    public function hasForeignKey(string $constraintName) : bool
361
    {
362
        $constraintName = $this->normalizeIdentifier($constraintName);
363
364
        return isset($this->_fkConstraints[$constraintName]);
365
    }
366
367
    /**
368
     * Returns the foreign key constraint with the given name.
369
     *
370
     * @throws SchemaException If the foreign key does not exist.
371
     */
372
    public function getForeignKey(string $constraintName) : ForeignKeyConstraint
373
    {
374
        $constraintName = $this->normalizeIdentifier($constraintName);
375
376
        if (! $this->hasForeignKey($constraintName)) {
377 15986
            throw ForeignKeyDoesNotExist::new($constraintName, $this->_name);
378
        }
379 15986
380
        return $this->_fkConstraints[$constraintName];
381
    }
382
383
    /**
384
     * Removes the foreign key constraint with the given name.
385
     *
386
     * @throws SchemaException
387
     */
388
    public function removeForeignKey(string $constraintName) : void
389
    {
390
        $constraintName = $this->normalizeIdentifier($constraintName);
391
392
        if (! $this->hasForeignKey($constraintName)) {
393
            throw ForeignKeyDoesNotExist::new($constraintName, $this->_name);
394
        }
395
396
        unset($this->_fkConstraints[$constraintName]);
397 22731
    }
398
399 22731
    /**
400 22241
     * Returns whether this table has a unique constraint with the given name.
401 22241
     */
402 1102
    public function hasUniqueConstraint(string $constraintName) : bool
403
    {
404
        $constraintName = $this->normalizeIdentifier($constraintName);
405
406
        return isset($this->_uniqueConstraints[$constraintName]);
407 22729
    }
408 22729
409 1237
    /**
410
     * Returns the unique constraint with the given name.
411
     *
412
     * @throws SchemaException If the foreign key does not exist.
413 22727
     */
414 22727
    public function getUniqueConstraint(string $constraintName) : UniqueConstraint
415 224
    {
416 224
        $constraintName = $this->normalizeIdentifier($constraintName);
417 224
418 224
        if (! $this->hasUniqueConstraint($constraintName)) {
419
            throw UniqueConstraintDoesNotExist::new($constraintName, $this->_name);
420 22727
        }
421
422 22727
        return $this->_uniqueConstraints[$constraintName];
423
    }
424
425
    /**
426
     * Removes the unique constraint with the given name.
427
     *
428
     * @throws SchemaException
429
     */
430
    public function removeUniqueConstraint(string $constraintName) : void
431 22048
    {
432
        $constraintName = $this->normalizeIdentifier($constraintName);
433 22048
434
        if (! $this->hasUniqueConstraint($constraintName)) {
435 22048
            throw UniqueConstraintDoesNotExist::new($constraintName, $this->_name);
436
        }
437
438
        unset($this->_uniqueConstraints[$constraintName]);
439
    }
440
441
    /**
442
     * Returns ordered list of columns (primary keys are first, then foreign keys, then the rest)
443 24514
     *
444
     * @return array<string, Column>
445 24514
     */
446 24514
    public function getColumns() : array
447
    {
448 24514
        $columns = $this->_columns;
449 1388
        $pkCols  = [];
450
        $fkCols  = [];
451
452 24514
        $primaryKey = $this->getPrimaryKey();
453 24514
454
        if ($primaryKey !== null) {
455
            $pkCols = $primaryKey->getColumns();
456
        }
457
458
        foreach ($this->getForeignKeys() as $fk) {
459
            /** @var ForeignKeyConstraint $fk */
460
            $fkCols = array_merge($fkCols, $fk->getColumns());
461
        }
462 23990
463
        $colNames = array_unique(array_merge($pkCols, $fkCols, array_keys($columns)));
464 23990
465 23990
        uksort($columns, static function ($a, $b) use ($colNames) : int {
466 23990
            return array_search($a, $colNames) <=> array_search($b, $colNames);
467
        });
468 23990
469 19007
        return $columns;
470 18999
    }
471
472
    /**
473 694
     * Returns whether this table has a Column with the given name.
474
     */
475
    public function hasColumn(string $columnName) : bool
476 23990
    {
477 23990
        $columnName = $this->normalizeIdentifier($columnName);
478
479 1265
        return isset($this->_columns[$columnName]);
480
    }
481
482 23990
    /**
483 694
     * Returns the Column with the given name.
484
     *
485
     * @throws SchemaException If the column does not exist.
486 23990
     */
487 23686
    public function getColumn(string $columnName) : Column
488
    {
489
        $columnName = $this->normalizeIdentifier($columnName);
490 23990
491
        if (! $this->hasColumn($columnName)) {
492 23990
            throw ColumnDoesNotExist::new($columnName, $this->_name);
493
        }
494
495
        return $this->_columns[$columnName];
496
    }
497
498 22818
    /**
499
     * Returns the primary key.
500 22818
     */
501
    public function getPrimaryKey() : ?Index
502 22818
    {
503 22735
        return $this->hasPrimaryKey()
504
            ? $this->getIndex($this->_primaryKeyName)
505 1988
            : null;
506 1988
    }
507 1988
508 1988
    /**
509
     * Returns the primary key columns.
510
     *
511 22818
     * @return array<int, string>
512
     *
513 22818
     * @throws DBALException
514
     */
515
    public function getPrimaryKeyColumns() : array
516
    {
517
        $primaryKey = $this->getPrimaryKey();
518 22818
519 22818
        if ($primaryKey === null) {
520 22818
            throw new DBALException(sprintf('Table "%s" has no primary key.', $this->getName()));
521 22818
        }
522
523 22818
        return $primaryKey->getColumns();
524
    }
525 22818
526 22766
    /**
527 21128
     * Returns whether this table has a primary key.
528
     */
529
    public function hasPrimaryKey() : bool
530
    {
531 22758
        return $this->_primaryKeyName && $this->hasIndex($this->_primaryKeyName);
532 22758
    }
533 22758
534
    /**
535
     * Returns whether this table has an Index with the given name.
536
     */
537
    public function hasIndex(string $indexName) : bool
538
    {
539
        $indexName = $this->normalizeIdentifier($indexName);
540
541
        return isset($this->_indexes[$indexName]);
542 19860
    }
543
544 19860
    /**
545
     * Returns the Index with the given name.
546 19860
     *
547
     * @throws SchemaException If the index does not exist.
548
     */
549
    public function getIndex(string $indexName) : Index
550
    {
551
        $indexName = $this->normalizeIdentifier($indexName);
552
553
        if (! $this->hasIndex($indexName)) {
554
            throw IndexDoesNotExist::new($indexName, $this->_name);
555
        }
556
557
        return $this->_indexes[$indexName];
558 304
    }
559
560 304
    /**
561 304
     * @return array<string, Index>
562
     */
563
    public function getIndexes() : array
564
    {
565 304
        return $this->_indexes;
566
    }
567
568
    /**
569
     * Returns the unique constraints.
570
     *
571
     * @return array<string, UniqueConstraint>
572
     */
573
    public function getUniqueConstraints() : array
574
    {
575
        return $this->_uniqueConstraints;
576
    }
577 306
578
    /**
579 306
     * Returns the foreign key constraints.
580 306
     *
581
     * @return array<string, ForeignKeyConstraint>
582
     */
583
    public function getForeignKeys() : array
584 306
    {
585 306
        return $this->_fkConstraints;
586
    }
587
588
    public function hasOption(string $name) : bool
589
    {
590
        return isset($this->_options[$name]);
591
    }
592 24064
593
    /**
594 24064
     * @return mixed
595 24064
     */
596
    public function getOption(string $name)
597 24064
    {
598 23446
        return $this->_options[$name];
599
    }
600
601 24064
    /**
602
     * @return array<string, mixed>
603
     */
604
    public function getOptions() : array
605
    {
606
        return $this->_options;
607
    }
608
609 24064
    public function visit(Visitor $visitor) : void
610
    {
611 24064
        $visitor->acceptTable($this);
612 24064
613 22694
        foreach ($this->getColumns() as $column) {
614
            $visitor->acceptColumn($this, $column);
615
        }
616 24064
617
        foreach ($this->getIndexes() as $index) {
618
            $visitor->acceptIndex($this, $index);
619
        }
620
621
        foreach ($this->getForeignKeys() as $constraint) {
622
            $visitor->acceptForeignKey($this, $constraint);
623
        }
624
    }
625
626 24064
    /**
627
     * Clone of a Table triggers a deep clone of all affected assets.
628
     */
629 24012
    public function __clone()
630 24064
    {
631
        foreach ($this->_columns as $k => $column) {
632
            $this->_columns[$k] = clone $column;
633
        }
634
635
        foreach ($this->_indexes as $k => $index) {
636
            $this->_indexes[$k] = clone $index;
637
        }
638
639
        foreach ($this->_fkConstraints as $k => $fk) {
640 24178
            $this->_fkConstraints[$k] = clone $fk;
641
            $this->_fkConstraints[$k]->setLocalTable($this);
642 24178
        }
643
    }
644 24178
645
    protected function _getMaxIdentifierLength() : int
646
    {
647
        return $this->_schemaConfig instanceof SchemaConfig
648
            ? $this->_schemaConfig->getMaxIdentifierLength()
649
            : 63;
650
    }
651
652
    /**
653
     * @throws SchemaException
654
     */
655
    protected function _addColumn(Column $column) : void
656 23910
    {
657
        $columnName = $column->getName();
658 23910
        $columnName = $this->normalizeIdentifier($columnName);
659 23910
660 1413
        if (isset($this->_columns[$columnName])) {
661
            throw ColumnAlreadyExists::new($this->getName(), $columnName);
662
        }
663 23908
664
        $this->_columns[$columnName] = $column;
665
    }
666
667
    /**
668
     * Adds an index to the table.
669
     *
670
     * @throws SchemaException
671 24074
     */
672
    protected function _addIndex(Index $indexCandidate) : self
673 24074
    {
674 23296
        $indexName               = $indexCandidate->getName();
675
        $indexName               = $this->normalizeIdentifier($indexName);
676
        $replacedImplicitIndexes = [];
677 23456
678
        foreach ($this->implicitIndexes as $name => $implicitIndex) {
679
            if (! $implicitIndex->isFullfilledBy($indexCandidate) || ! isset($this->_indexes[$name])) {
680
                continue;
681
            }
682
683
            $replacedImplicitIndexes[] = $name;
684
        }
685
686
        if ((isset($this->_indexes[$indexName]) && ! in_array($indexName, $replacedImplicitIndexes, true)) ||
687 20233
            ($this->_primaryKeyName !== false && $indexCandidate->isPrimary())
688
        ) {
689 20233
            throw IndexAlreadyExists::new($indexName, $this->_name);
690
        }
691 20233
692
        foreach ($replacedImplicitIndexes as $name) {
693
            unset($this->_indexes[$name], $this->implicitIndexes[$name]);
694
        }
695 20233
696
        if ($indexCandidate->isPrimary()) {
697
            $this->_primaryKeyName = $indexName;
698
        }
699
700
        $this->_indexes[$indexName] = $indexCandidate;
701
702
        return $this;
703 24080
    }
704
705 24080
    protected function _addUniqueConstraint(UniqueConstraint $constraint) : self
706
    {
707
        $name = strlen($constraint->getName())
708
            ? $constraint->getName()
709
            : $this->_generateIdentifierName(
710
                array_merge((array) $this->getName(), $constraint->getColumns()),
711
                'fk',
712
                $this->_getMaxIdentifierLength()
713
            );
714
715 23560
        $name = $this->normalizeIdentifier($name);
716
717 23560
        $this->_uniqueConstraints[$name] = $constraint;
718
719 23560
        // If there is already an index that fulfills this requirements drop the request. In the case of __construct
720
        // calling this method during hydration from schema-details all the explicitly added indexes lead to duplicates.
721
        // This creates computation overhead in this case, however no duplicate indexes are ever added (column based).
722
        $indexName = $this->_generateIdentifierName(
723
            array_merge([$this->getName()], $constraint->getColumns()),
724
            'idx',
725
            $this->_getMaxIdentifierLength()
726
        );
727
728
        $indexCandidate = $this->_createIndex($constraint->getColumns(), $indexName, true, false);
729
730
        foreach ($this->_indexes as $existingIndex) {
731 23516
            if ($indexCandidate->isFullfilledBy($existingIndex)) {
732
                return $this;
733 23516
            }
734 23516
        }
735 1288
736
        $this->implicitIndexes[$this->normalizeIdentifier($indexName)] = $indexCandidate;
737
738 23514
        return $this;
739
    }
740
741
    protected function _addForeignKeyConstraint(ForeignKeyConstraint $constraint) : self
742
    {
743
        $constraint->setLocalTable($this);
744 24008
745
        $name = strlen($constraint->getName())
746 24008
            ? $constraint->getName()
747
            : $this->_generateIdentifierName(
748
                array_merge((array) $this->getName(), $constraint->getLocalColumns()),
749
                'fk',
750
                $this->_getMaxIdentifierLength()
751
            );
752
753
        $name = $this->normalizeIdentifier($name);
754 24100
755
        $this->_fkConstraints[$name] = $constraint;
756 24100
757
        // add an explicit index on the foreign key columns.
758
        // If there is already an index that fulfills this requirements drop the request. In the case of __construct
759
        // calling this method during hydration from schema-details all the explicitly added indexes lead to duplicates.
760
        // This creates computation overhead in this case, however no duplicate indexes are ever added (column based).
761
        $indexName = $this->_generateIdentifierName(
762
            array_merge([$this->getName()], $constraint->getColumns()),
763
            'idx',
764 22769
            $this->_getMaxIdentifierLength()
765
        );
766 22769
767
        $indexCandidate = $this->_createIndex($constraint->getColumns(), $indexName, false, false);
768
769
        foreach ($this->_indexes as $existingIndex) {
770
            if ($indexCandidate->isFullfilledBy($existingIndex)) {
771
                return $this;
772
            }
773
        }
774 20609
775
        $this->_addIndex($indexCandidate);
776 20609
        $this->implicitIndexes[$this->normalizeIdentifier($indexName)] = $indexCandidate;
777
778
        return $this;
779
    }
780
781
    /**
782 23794
     * Normalizes a given identifier.
783
     *
784 23794
     * Trims quotes and lowercases the given identifier.
785
     */
786
    private function normalizeIdentifier(?string $identifier) : string
787
    {
788
        if ($identifier === null) {
789
            return '';
790 22428
        }
791
792 22428
        return $this->trimQuotes(strtolower($identifier));
793
    }
794 22428
795 22422
    public function setComment(?string $comment) : self
796
    {
797
        // For keeping backward compatibility with MySQL in previous releases, table comments are stored as options.
798 22428
        $this->addOption('comment', $comment);
799 20265
800
        return $this;
801
    }
802 22428
803 158
    public function getComment() : ?string
804
    {
805 22428
        return $this->_options['comment'] ?? null;
806
    }
807
808
    /**
809
     * @param array<string|int, string> $columns
810
     * @param array<int, string>        $flags
811
     * @param array<string, mixed>      $options
812 21694
     *
813
     * @throws SchemaException
814 21694
     */
815 21692
    private function _createUniqueConstraint(array $columns, string $indexName, array $flags = [], array $options = []) : UniqueConstraint
816
    {
817 21694
        if (preg_match('(([^a-zA-Z0-9_]+))', $this->normalizeIdentifier($indexName))) {
818 21670
            throw IndexNameInvalid::new($indexName);
819
        }
820 21694
821 20535
        foreach ($columns as $index => $value) {
822 20535
            if (is_string($index)) {
823
                $columnName = $index;
824 21694
            } else {
825
                $columnName = $value;
826
            }
827
828
            if (! $this->hasColumn($columnName)) {
829
                throw ColumnDoesNotExist::new($columnName, $this->_name);
830
            }
831
        }
832
833
        return new UniqueConstraint($indexName, $columns, $flags, $options);
834
    }
835 24522
836
    /**
837 24522
     * @param array<string|int, string> $columns
838 413
     * @param array<int, string>        $flags
839
     * @param array<string, mixed>      $options
840
     *
841 24522
     * @throws SchemaException
842
     */
843
    private function _createIndex(array $columns, string $indexName, bool $isUnique, bool $isPrimary, array $flags = [], array $options = []) : Index
844 20045
    {
845
        if (preg_match('(([^a-zA-Z0-9_]+))', $this->normalizeIdentifier($indexName))) {
846
            throw IndexNameInvalid::new($indexName);
847 20045
        }
848
849 20045
        foreach ($columns as $index => $value) {
850
            if (is_string($index)) {
851
                $columnName = $index;
852 20045
            } else {
853
                $columnName = $value;
854 20045
            }
855
856
            if (! $this->hasColumn($columnName)) {
857
                throw ColumnDoesNotExist::new($columnName, $this->_name);
858
            }
859
        }
860
861
        return new Index($indexName, $columns, $isUnique, $isPrimary, $flags, $options);
862
    }
863
}
864