Completed
Pull Request — develop (#3565)
by Jonathan
13:02
created

Table::addOption()   A

Complexity

Conditions 1
Paths 1

Size

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