Table::setComment()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 0
Metric Value
eloc 2
dl 0
loc 6
ccs 3
cts 3
cp 1
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 1
crap 1
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|null */
45
    protected $_primaryKeyName;
46
47
    /** @var UniqueConstraint[] */
48
    protected $_uniqueConstraints = [];
49
50
    /** @var ForeignKeyConstraint[] */
51
    protected $_fkConstraints = [];
52
53
    /** @var mixed[] */
54
    protected $_options = [
55
        'create_options' => [],
56
    ];
57
58
    /** @var SchemaConfig|null */
59
    protected $_schemaConfig = null;
60
61
    /**
62
     * @param array<Column>               $columns
63
     * @param array<Index>                $indexes
64
     * @param array<UniqueConstraint>     $uniqueConstraints
65
     * @param array<ForeignKeyConstraint> $fkConstraints
66
     * @param array<string, mixed>        $options
67
     *
68
     * @throws DBALException
69
     */
70 14493
    public function __construct(
71
        string $tableName,
72
        array $columns = [],
73
        array $indexes = [],
74
        array $uniqueConstraints = [],
75
        array $fkConstraints = [],
76
        array $options = []
77
    ) {
78 14493
        if (strlen($tableName) === 0) {
79 22
            throw InvalidTableName::new($tableName);
80
        }
81
82 14471
        $this->_setName($tableName);
83
84 14471
        foreach ($columns as $column) {
85 2168
            $this->_addColumn($column);
86
        }
87
88 14449
        foreach ($indexes as $idx) {
89 668
            $this->_addIndex($idx);
90
        }
91
92 14405
        foreach ($uniqueConstraints as $uniqueConstraint) {
93 22
            $this->_addUniqueConstraint($uniqueConstraint);
94
        }
95
96 14405
        foreach ($fkConstraints as $fkConstraint) {
97 297
            $this->_addForeignKeyConstraint($fkConstraint);
98
        }
99
100 14405
        $this->_options = array_merge($this->_options, $options);
101 14405
    }
102
103 1479
    public function setSchemaConfig(SchemaConfig $schemaConfig) : void
104
    {
105 1479
        $this->_schemaConfig = $schemaConfig;
106 1479
    }
107
108
    /**
109
     * Sets the Primary Key.
110
     *
111
     * @param array<int, string> $columnNames
112
     */
113 5757
    public function setPrimaryKey(array $columnNames, ?string $indexName = null) : self
114
    {
115 5757
        if ($indexName === null) {
116 5735
            $indexName = 'primary';
117
        }
118
119 5757
        $this->_addIndex($this->_createIndex($columnNames, $indexName, true, true));
120
121 5757
        foreach ($columnNames as $columnName) {
122 5757
            $column = $this->getColumn($columnName);
123 5757
            $column->setNotnull(true);
124
        }
125
126 5757
        return $this;
127
    }
128
129
    /**
130
     * @param array<int, string>   $columnNames
131
     * @param array<int, string>   $flags
132
     * @param array<string, mixed> $options
133
     */
134
    public function addUniqueConstraint(array $columnNames, ?string $indexName = null, array $flags = [], array $options = []) : self
135
    {
136
        if ($indexName === null) {
137
            $indexName = $this->_generateIdentifierName(
138
                array_merge([$this->getName()], $columnNames),
139
                'uniq',
140
                $this->_getMaxIdentifierLength()
141
            );
142
        }
143
144
        return $this->_addUniqueConstraint($this->_createUniqueConstraint($columnNames, $indexName, $flags, $options));
145
    }
146
147
    /**
148
     * @param array<int, string>   $columnNames
149
     * @param array<int, string>   $flags
150
     * @param array<string, mixed> $options
151
     */
152 1806
    public function addIndex(array $columnNames, ?string $indexName = null, array $flags = [], array $options = []) : self
153
    {
154 1806
        if ($indexName === null) {
155 381
            $indexName = $this->_generateIdentifierName(
156 381
                array_merge([$this->getName()], $columnNames),
157 381
                'idx',
158 381
                $this->_getMaxIdentifierLength()
159
            );
160
        }
161
162 1806
        return $this->_addIndex($this->_createIndex($columnNames, $indexName, false, false, $flags, $options));
163
    }
164
165
    /**
166
     * Drops the primary key from this table.
167
     */
168 398
    public function dropPrimaryKey() : void
169
    {
170 398
        if ($this->_primaryKeyName === null) {
171
            return;
172
        }
173
174 398
        $this->dropIndex($this->_primaryKeyName);
175 398
        $this->_primaryKeyName = null;
176 398
    }
177
178
    /**
179
     * Drops an index from this table.
180
     *
181
     * @throws SchemaException If the index does not exist.
182
     */
183 674
    public function dropIndex(string $indexName) : void
184
    {
185 674
        $indexName = $this->normalizeIdentifier($indexName);
186
187 674
        if (! $this->hasIndex($indexName)) {
188
            throw IndexDoesNotExist::new($indexName, $this->_name);
189
        }
190
191 674
        unset($this->_indexes[$indexName]);
192 674
    }
193
194
    /**
195
     * @param array<int, string>   $columnNames
196
     * @param array<string, mixed> $options
197
     */
198 531
    public function addUniqueIndex(array $columnNames, ?string $indexName = null, array $options = []) : self
199
    {
200 531
        if ($indexName === null) {
201 349
            $indexName = $this->_generateIdentifierName(
202 349
                array_merge([$this->getName()], $columnNames),
203 349
                'uniq',
204 349
                $this->_getMaxIdentifierLength()
205
            );
206
        }
207
208 531
        return $this->_addIndex($this->_createIndex($columnNames, $indexName, true, false, [], $options));
209
    }
210
211
    /**
212
     * Renames an index.
213
     *
214
     * @param string      $oldIndexName The name of the index to rename from.
215
     * @param string|null $newIndexName The name of the index to rename to.
216
     *                                  If null is given, the index name will be auto-generated.
217
     *
218
     * @throws SchemaException If no index exists for the given current name
219
     *                         or if an index with the given new name already exists on this table.
220
     */
221 308
    public function renameIndex(string $oldIndexName, ?string $newIndexName = null) : self
222
    {
223 308
        $oldIndexName           = $this->normalizeIdentifier($oldIndexName);
224 308
        $normalizedNewIndexName = $this->normalizeIdentifier($newIndexName);
225
226 308
        if ($oldIndexName === $normalizedNewIndexName) {
227 198
            return $this;
228
        }
229
230 308
        if (! $this->hasIndex($oldIndexName)) {
231 22
            throw IndexDoesNotExist::new($oldIndexName, $this->_name);
232
        }
233
234 286
        if ($this->hasIndex($normalizedNewIndexName)) {
235 22
            throw IndexAlreadyExists::new($normalizedNewIndexName, $this->_name);
236
        }
237
238 264
        $oldIndex = $this->_indexes[$oldIndexName];
239
240 264
        if ($oldIndex->isPrimary()) {
241 22
            $this->dropPrimaryKey();
242
243 22
            return $this->setPrimaryKey($oldIndex->getColumns(), $newIndexName ?? null);
244
        }
245
246 264
        unset($this->_indexes[$oldIndexName]);
247
248 264
        if ($oldIndex->isUnique()) {
249 44
            return $this->addUniqueIndex($oldIndex->getColumns(), $newIndexName, $oldIndex->getOptions());
250
        }
251
252 242
        return $this->addIndex($oldIndex->getColumns(), $newIndexName, $oldIndex->getFlags(), $oldIndex->getOptions());
253
    }
254
255
    /**
256
     * Checks if an index begins in the order of the given columns.
257
     *
258
     * @param array<int, string> $columnNames
259
     */
260 44
    public function columnsAreIndexed(array $columnNames) : bool
261
    {
262 44
        foreach ($this->getIndexes() as $index) {
263 44
            if ($index->spansColumns($columnNames)) {
264 44
                return true;
265
            }
266
        }
267
268
        return false;
269
    }
270
271
    /**
272
     * @param array<string, mixed> $options
273
     */
274 11410
    public function addColumn(string $columnName, string $typeName, array $options = []) : Column
275
    {
276 11410
        $column = new Column($columnName, Type::getType($typeName), $options);
277
278 11410
        $this->_addColumn($column);
279
280 11410
        return $column;
281
    }
282
283
    /**
284
     * Change Column Details.
285
     *
286
     * @param array<string, mixed> $options
287
     */
288 67
    public function changeColumn(string $columnName, array $options) : self
289
    {
290 67
        $column = $this->getColumn($columnName);
291
292 67
        $column->setOptions($options);
293
294 67
        return $this;
295
    }
296
297
    /**
298
     * Drops a Column from the Table.
299
     */
300 198
    public function dropColumn(string $columnName) : self
301
    {
302 198
        $columnName = $this->normalizeIdentifier($columnName);
303
304 198
        unset($this->_columns[$columnName]);
305
306 198
        return $this;
307
    }
308
309
    /**
310
     * Adds a foreign key constraint.
311
     *
312
     * Name is inferred from the local columns.
313
     *
314
     * @param Table|string         $foreignTable       Table schema instance or table name
315
     * @param array<int, string>   $localColumnNames
316
     * @param array<int, string>   $foreignColumnNames
317
     * @param array<string, mixed> $options
318
     */
319 1886
    public function addForeignKeyConstraint($foreignTable, array $localColumnNames, array $foreignColumnNames, array $options = [], ?string $name = null) : self
320
    {
321 1886
        if ($name === null) {
322 1005
            $name = $this->_generateIdentifierName(
323 1005
                array_merge((array) $this->getName(), $localColumnNames),
324 1005
                'fk',
325 1005
                $this->_getMaxIdentifierLength()
326
            );
327
        }
328
329 1886
        if ($foreignTable instanceof Table) {
330 949
            foreach ($foreignColumnNames as $columnName) {
331 949
                if (! $foreignTable->hasColumn($columnName)) {
332 22
                    throw ColumnDoesNotExist::new($columnName, $foreignTable->getName());
333
                }
334
            }
335
        }
336
337 1864
        foreach ($localColumnNames as $columnName) {
338 1864
            if (! $this->hasColumn($columnName)) {
339 22
                throw ColumnDoesNotExist::new($columnName, $this->_name);
340
            }
341
        }
342
343 1842
        $constraint = new ForeignKeyConstraint(
344 1842
            $localColumnNames,
345
            $foreignTable,
346
            $foreignColumnNames,
347
            $name,
348
            $options
349
        );
350
351 1842
        return $this->_addForeignKeyConstraint($constraint);
352
    }
353
354
    /**
355
     * @param mixed $value
356
     */
357 1801
    public function addOption(string $name, $value) : self
358
    {
359 1801
        $this->_options[$name] = $value;
360
361 1801
        return $this;
362
    }
363
364
    /**
365
     * Returns whether this table has a foreign key constraint with the given name.
366
     */
367 243
    public function hasForeignKey(string $constraintName) : bool
368
    {
369 243
        $constraintName = $this->normalizeIdentifier($constraintName);
370
371 243
        return isset($this->_fkConstraints[$constraintName]);
372
    }
373
374
    /**
375
     * Returns the foreign key constraint with the given name.
376
     *
377
     * @throws SchemaException If the foreign key does not exist.
378
     */
379 1
    public function getForeignKey(string $constraintName) : ForeignKeyConstraint
380
    {
381 1
        $constraintName = $this->normalizeIdentifier($constraintName);
382
383 1
        if (! $this->hasForeignKey($constraintName)) {
384
            throw ForeignKeyDoesNotExist::new($constraintName, $this->_name);
385
        }
386
387 1
        return $this->_fkConstraints[$constraintName];
388
    }
389
390
    /**
391
     * Removes the foreign key constraint with the given name.
392
     *
393
     * @throws SchemaException
394
     */
395 220
    public function removeForeignKey(string $constraintName) : void
396
    {
397 220
        $constraintName = $this->normalizeIdentifier($constraintName);
398
399 220
        if (! $this->hasForeignKey($constraintName)) {
400
            throw ForeignKeyDoesNotExist::new($constraintName, $this->_name);
401
        }
402
403 220
        unset($this->_fkConstraints[$constraintName]);
404 220
    }
405
406
    /**
407
     * Returns whether this table has a unique constraint with the given name.
408
     */
409
    public function hasUniqueConstraint(string $constraintName) : bool
410
    {
411
        $constraintName = $this->normalizeIdentifier($constraintName);
412
413
        return isset($this->_uniqueConstraints[$constraintName]);
414
    }
415
416
    /**
417
     * Returns the unique constraint with the given name.
418
     *
419
     * @throws SchemaException If the foreign key does not exist.
420
     */
421
    public function getUniqueConstraint(string $constraintName) : UniqueConstraint
422
    {
423
        $constraintName = $this->normalizeIdentifier($constraintName);
424
425
        if (! $this->hasUniqueConstraint($constraintName)) {
426
            throw UniqueConstraintDoesNotExist::new($constraintName, $this->_name);
427
        }
428
429
        return $this->_uniqueConstraints[$constraintName];
430
    }
431
432
    /**
433
     * Removes the unique constraint with the given name.
434
     *
435
     * @throws SchemaException
436
     */
437
    public function removeUniqueConstraint(string $constraintName) : void
438
    {
439
        $constraintName = $this->normalizeIdentifier($constraintName);
440
441
        if (! $this->hasUniqueConstraint($constraintName)) {
442
            throw UniqueConstraintDoesNotExist::new($constraintName, $this->_name);
443
        }
444
445
        unset($this->_uniqueConstraints[$constraintName]);
446
    }
447
448
    /**
449
     * Returns ordered list of columns (primary keys are first, then foreign keys, then the rest)
450
     *
451
     * @return array<string, Column>
452
     */
453 8995
    public function getColumns() : array
454
    {
455 8995
        $columns = $this->_columns;
456 8995
        $pkCols  = [];
457 8995
        $fkCols  = [];
458
459 8995
        $primaryKey = $this->getPrimaryKey();
460
461 8995
        if ($primaryKey !== null) {
462 4266
            $pkCols = $primaryKey->getColumns();
463
        }
464
465 8995
        foreach ($this->getForeignKeys() as $fk) {
466
            /** @var ForeignKeyConstraint $fk */
467 948
            $fkCols = array_merge($fkCols, $fk->getColumns());
468
        }
469
470 8995
        $colNames = array_unique(array_merge($pkCols, $fkCols, array_keys($columns)));
471
472
        uksort($columns, static function ($a, $b) use ($colNames) : int {
473 5634
            return array_search($a, $colNames, true) <=> array_search($b, $colNames, true);
474 8995
        });
475
476 8995
        return $columns;
477
    }
478
479
    /**
480
     * Returns whether this table has a Column with the given name.
481
     */
482 10099
    public function hasColumn(string $columnName) : bool
483
    {
484 10099
        $columnName = $this->normalizeIdentifier($columnName);
485
486 10099
        return isset($this->_columns[$columnName]);
487
    }
488
489
    /**
490
     * Returns the Column with the given name.
491
     *
492
     * @throws SchemaException If the column does not exist.
493
     */
494 7804
    public function getColumn(string $columnName) : Column
495
    {
496 7804
        $columnName = $this->normalizeIdentifier($columnName);
497
498 7804
        if (! $this->hasColumn($columnName)) {
499 22
            throw ColumnDoesNotExist::new($columnName, $this->_name);
500
        }
501
502 7782
        return $this->_columns[$columnName];
503
    }
504
505
    /**
506
     * Returns the primary key.
507
     */
508 9105
    public function getPrimaryKey() : ?Index
509
    {
510 9105
        if ($this->_primaryKeyName !== null) {
511 4376
            return $this->getIndex($this->_primaryKeyName);
512
        }
513
514 5130
        return null;
515
    }
516
517
    /**
518
     * Returns the primary key columns.
519
     *
520
     * @return array<int, string>
521
     *
522
     * @throws DBALException
523
     */
524 310
    public function getPrimaryKeyColumns() : array
525
    {
526 310
        $primaryKey = $this->getPrimaryKey();
527
528 310
        if ($primaryKey === null) {
529
            throw new DBALException(sprintf('Table "%s" has no primary key.', $this->getName()));
530
        }
531
532 310
        return $primaryKey->getColumns();
533
    }
534
535
    /**
536
     * Returns whether this table has a primary key.
537
     */
538 950
    public function hasPrimaryKey() : bool
539
    {
540 950
        return $this->_primaryKeyName !== null && $this->hasIndex($this->_primaryKeyName);
541
    }
542
543
    /**
544
     * Returns whether this table has an Index with the given name.
545
     */
546 5549
    public function hasIndex(string $indexName) : bool
547
    {
548 5549
        $indexName = $this->normalizeIdentifier($indexName);
549
550 5549
        return isset($this->_indexes[$indexName]);
551
    }
552
553
    /**
554
     * Returns the Index with the given name.
555
     *
556
     * @throws SchemaException If the index does not exist.
557
     */
558 4889
    public function getIndex(string $indexName) : Index
559
    {
560 4889
        $indexName = $this->normalizeIdentifier($indexName);
561
562 4889
        if (! $this->hasIndex($indexName)) {
563 22
            throw IndexDoesNotExist::new($indexName, $this->_name);
564
        }
565
566 4867
        return $this->_indexes[$indexName];
567
    }
568
569
    /**
570
     * @return array<string, Index>
571
     */
572 8797
    public function getIndexes() : array
573
    {
574 8797
        return $this->_indexes;
575
    }
576
577
    /**
578
     * Returns the unique constraints.
579
     *
580
     * @return array<string, UniqueConstraint>
581
     */
582 6532
    public function getUniqueConstraints() : array
583
    {
584 6532
        return $this->_uniqueConstraints;
585
    }
586
587
    /**
588
     * Returns the foreign key constraints.
589
     *
590
     * @return array<string, ForeignKeyConstraint>
591
     */
592 9391
    public function getForeignKeys() : array
593
    {
594 9391
        return $this->_fkConstraints;
595
    }
596
597 4274
    public function hasOption(string $name) : bool
598
    {
599 4274
        return isset($this->_options[$name]);
600
    }
601
602
    /**
603
     * @return mixed
604
     */
605 221
    public function getOption(string $name)
606
    {
607 221
        return $this->_options[$name];
608
    }
609
610
    /**
611
     * @return array<string, mixed>
612
     */
613 6774
    public function getOptions() : array
614
    {
615 6774
        return $this->_options;
616
    }
617
618 364
    public function visit(Visitor $visitor) : void
619
    {
620 364
        $visitor->acceptTable($this);
621
622 364
        foreach ($this->getColumns() as $column) {
623 298
            $visitor->acceptColumn($this, $column);
624
        }
625
626 364
        foreach ($this->getIndexes() as $index) {
627 214
            $visitor->acceptIndex($this, $index);
628
        }
629
630 364
        foreach ($this->getForeignKeys() as $constraint) {
631 110
            $visitor->acceptForeignKey($this, $constraint);
632
        }
633 364
    }
634
635
    /**
636
     * Clone of a Table triggers a deep clone of all affected assets.
637
     */
638 1156
    public function __clone()
639
    {
640 1156
        foreach ($this->_columns as $k => $column) {
641 1134
            $this->_columns[$k] = clone $column;
642
        }
643
644 1156
        foreach ($this->_indexes as $k => $index) {
645 745
            $this->_indexes[$k] = clone $index;
646
        }
647
648 1156
        foreach ($this->_fkConstraints as $k => $fk) {
649 177
            $this->_fkConstraints[$k] = clone $fk;
650 177
            $this->_fkConstraints[$k]->setLocalTable($this);
651
        }
652 1156
    }
653
654 2709
    protected function _getMaxIdentifierLength() : int
655
    {
656 2709
        return $this->_schemaConfig instanceof SchemaConfig
657 239
            ? $this->_schemaConfig->getMaxIdentifierLength()
658 2709
            : 63;
659
    }
660
661
    /**
662
     * @throws SchemaException
663
     */
664 12611
    protected function _addColumn(Column $column) : void
665
    {
666 12611
        $columnName = $column->getName();
667 12611
        $columnName = $this->normalizeIdentifier($columnName);
668
669 12611
        if (isset($this->_columns[$columnName])) {
670 22
            throw ColumnAlreadyExists::new($this->getName(), $columnName);
671
        }
672
673 12611
        $this->_columns[$columnName] = $column;
674 12611
    }
675
676
    /**
677
     * Adds an index to the table.
678
     *
679
     * @throws SchemaException
680
     */
681 8316
    protected function _addIndex(Index $indexCandidate) : self
682
    {
683 8316
        $indexName               = $indexCandidate->getName();
684 8316
        $indexName               = $this->normalizeIdentifier($indexName);
685 8316
        $replacedImplicitIndexes = [];
686
687 8316
        foreach ($this->implicitIndexes as $name => $implicitIndex) {
688 552
            if (! $implicitIndex->isFullfilledBy($indexCandidate) || ! isset($this->_indexes[$name])) {
689 464
                continue;
690
            }
691
692 88
            $replacedImplicitIndexes[] = $name;
693
        }
694
695 8316
        if ((isset($this->_indexes[$indexName]) && ! in_array($indexName, $replacedImplicitIndexes, true)) ||
696 8316
            ($this->_primaryKeyName !== null && $indexCandidate->isPrimary())
697
        ) {
698 44
            throw IndexAlreadyExists::new($indexName, $this->_name);
699
        }
700
701 8316
        foreach ($replacedImplicitIndexes as $name) {
702 88
            unset($this->_indexes[$name], $this->implicitIndexes[$name]);
703
        }
704
705 8316
        if ($indexCandidate->isPrimary()) {
706 5823
            $this->_primaryKeyName = $indexName;
707
        }
708
709 8316
        $this->_indexes[$indexName] = $indexCandidate;
710
711 8316
        return $this;
712
    }
713
714 22
    protected function _addUniqueConstraint(UniqueConstraint $constraint) : self
715
    {
716 22
        $name = $constraint->getName() !== ''
717
            ? $constraint->getName()
718 22
            : $this->_generateIdentifierName(
719 22
                array_merge((array) $this->getName(), $constraint->getColumns()),
720 22
                'fk',
721 22
                $this->_getMaxIdentifierLength()
722
            );
723
724 22
        $name = $this->normalizeIdentifier($name);
725
726 22
        $this->_uniqueConstraints[$name] = $constraint;
727
728
        // If there is already an index that fulfills this requirements drop the request. In the case of __construct
729
        // calling this method during hydration from schema-details all the explicitly added indexes lead to duplicates.
730
        // This creates computation overhead in this case, however no duplicate indexes are ever added (column based).
731 22
        $indexName = $this->_generateIdentifierName(
732 22
            array_merge([$this->getName()], $constraint->getColumns()),
733 22
            'idx',
734 22
            $this->_getMaxIdentifierLength()
735
        );
736
737 22
        $indexCandidate = $this->_createIndex($constraint->getColumns(), $indexName, true, false);
738
739 22
        foreach ($this->_indexes as $existingIndex) {
740
            if ($indexCandidate->isFullfilledBy($existingIndex)) {
741
                return $this;
742
            }
743
        }
744
745 22
        $this->implicitIndexes[$this->normalizeIdentifier($indexName)] = $indexCandidate;
746
747 22
        return $this;
748
    }
749
750 1964
    protected function _addForeignKeyConstraint(ForeignKeyConstraint $constraint) : self
751
    {
752 1964
        $constraint->setLocalTable($this);
753
754 1964
        $name = $constraint->getName() !== ''
755 1942
            ? $constraint->getName()
756 23
            : $this->_generateIdentifierName(
757 23
                array_merge((array) $this->getName(), $constraint->getLocalColumns()),
758 23
                'fk',
759 1964
                $this->_getMaxIdentifierLength()
760
            );
761
762 1964
        $name = $this->normalizeIdentifier($name);
763
764 1964
        $this->_fkConstraints[$name] = $constraint;
765
766
        // add an explicit index on the foreign key columns.
767
        // If there is already an index that fulfills this requirements drop the request. In the case of __construct
768
        // calling this method during hydration from schema-details all the explicitly added indexes lead to duplicates.
769
        // This creates computation overhead in this case, however no duplicate indexes are ever added (column based).
770 1964
        $indexName = $this->_generateIdentifierName(
771 1964
            array_merge([$this->getName()], $constraint->getColumns()),
772 1964
            'idx',
773 1964
            $this->_getMaxIdentifierLength()
774
        );
775
776 1964
        $indexCandidate = $this->_createIndex($constraint->getColumns(), $indexName, false, false);
777
778 1964
        foreach ($this->_indexes as $existingIndex) {
779 1363
            if ($indexCandidate->isFullfilledBy($existingIndex)) {
780 874
                return $this;
781
            }
782
        }
783
784 1480
        $this->_addIndex($indexCandidate);
785 1480
        $this->implicitIndexes[$this->normalizeIdentifier($indexName)] = $indexCandidate;
786
787 1480
        return $this;
788
    }
789
790
    /**
791
     * Normalizes a given identifier.
792
     *
793
     * Trims quotes and lowercases the given identifier.
794
     */
795 12699
    private function normalizeIdentifier(?string $identifier) : string
796
    {
797 12699
        if ($identifier === null) {
798 22
            return '';
799
        }
800
801 12699
        return $this->trimQuotes(strtolower($identifier));
802
    }
803
804 44
    public function setComment(string $comment) : self
805
    {
806
        // For keeping backward compatibility with MySQL in previous releases, table comments are stored as options.
807 44
        $this->addOption('comment', $comment);
808
809 44
        return $this;
810
    }
811
812 44
    public function getComment() : ?string
813
    {
814 44
        return $this->_options['comment'] ?? null;
815
    }
816
817
    /**
818
     * @param array<string|int, string> $columns
819
     * @param array<int, string>        $flags
820
     * @param array<string, mixed>      $options
821
     *
822
     * @throws SchemaException
823
     */
824
    private function _createUniqueConstraint(array $columns, string $indexName, array $flags = [], array $options = []) : UniqueConstraint
825
    {
826
        if (preg_match('(([^a-zA-Z0-9_]+))', $this->normalizeIdentifier($indexName)) === 1) {
827
            throw IndexNameInvalid::new($indexName);
828
        }
829
830
        foreach ($columns as $index => $value) {
831
            if (is_string($index)) {
832
                $columnName = $index;
833
            } else {
834
                $columnName = $value;
835
            }
836
837
            if (! $this->hasColumn($columnName)) {
838
                throw ColumnDoesNotExist::new($columnName, $this->_name);
839
            }
840
        }
841
842
        return new UniqueConstraint($indexName, $columns, $flags, $options);
843
    }
844
845
    /**
846
     * @param array<int, string>   $columns
847
     * @param array<int, string>   $flags
848
     * @param array<string, mixed> $options
849
     *
850
     * @throws SchemaException
851
     */
852 8211
    private function _createIndex(array $columns, string $indexName, bool $isUnique, bool $isPrimary, array $flags = [], array $options = []) : Index
853
    {
854 8211
        if (preg_match('(([^a-zA-Z0-9_]+))', $this->normalizeIdentifier($indexName)) === 1) {
855 22
            throw IndexNameInvalid::new($indexName);
856
        }
857
858 8189
        foreach ($columns as $columnName) {
859 8167
            if (! $this->hasColumn($columnName)) {
860 22
                throw ColumnDoesNotExist::new($columnName, $this->_name);
861
            }
862
        }
863
864 8167
        return new Index($indexName, $columns, $isUnique, $isPrimary, $flags, $options);
865
    }
866
}
867