Completed
Push — master ( 9728d9...4d9a08 )
by Sergei
27s queued 16s
created

Table::_addIndex()   B

Complexity

Conditions 10
Paths 15

Size

Total Lines 31
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 17
CRAP Score 10

Importance

Changes 0
Metric Value
eloc 16
dl 0
loc 31
ccs 17
cts 17
cp 1
rs 7.6666
c 0
b 0
f 0
cc 10
nc 15
nop 1
crap 10

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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