Failed Conditions
Pull Request — develop (#3348)
by Sergei
91:36 queued 30:26
created

Table::getName()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

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