Failed Conditions
Pull Request — develop (#3348)
by Sergei
60:44
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
     * Renames a Column.
290
     *
291
     * @deprecated
292
     *
293
     * @throws DBALException
294 18934
     */
295
    public function renameColumn(string $oldColumnName, string $newColumnName) : void
0 ignored issues
show
Unused Code introduced by
The parameter $oldColumnName is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

295
    public function renameColumn(/** @scrutinizer ignore-unused */ string $oldColumnName, string $newColumnName) : void

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $newColumnName is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

295
    public function renameColumn(string $oldColumnName, /** @scrutinizer ignore-unused */ string $newColumnName) : void

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
296 18934
    {
297
        throw new DBALException(
298 18934
            'Table#renameColumn() was removed, because it drops and recreates the column instead. ' .
299
            'There is no fix available, because a schema diff cannot reliably detect if a column ' .
300 18934
            'was renamed or one column was created and another one dropped.'
301
        );
302
    }
303
304
    /**
305
     * Change Column Details.
306
     *
307
     * @param mixed[] $options
308
     */
309
    public function changeColumn(string $columnName, array $options) : self
310
    {
311
        $column = $this->getColumn($columnName);
312
313
        $column->setOptions($options);
314
315
        return $this;
316
    }
317
318
    /**
319
     * Drops a Column from the Table.
320
     */
321
    public function dropColumn(string $columnName) : self
322
    {
323
        $columnName = $this->normalizeIdentifier($columnName);
324
325
        unset($this->_columns[$columnName]);
326
327
        return $this;
328
    }
329
330 15439
    /**
331
     * Adds a foreign key constraint.
332 15439
     *
333
     * Name is inferred from the local columns.
334 15439
     *
335
     * @param Table|string $foreignTable       Table schema instance or table name
336 15439
     * @param string[]     $localColumnNames
337
     * @param string[]     $foreignColumnNames
338
     * @param mixed[]      $options
339
     */
340
    public function addForeignKeyConstraint($foreignTable, array $localColumnNames, array $foreignColumnNames, array $options = [], ?string $constraintName = null) : self
341
    {
342
        if (! $constraintName) {
343
            $constraintName = $this->_generateIdentifierName(
344
                array_merge((array) $this->getName(), $localColumnNames),
345
                'fk',
346 1518
                $this->_getMaxIdentifierLength()
347
            );
348 1518
        }
349
350 1518
        return $this->addNamedForeignKeyConstraint($constraintName, $foreignTable, $localColumnNames, $foreignColumnNames, $options);
1 ignored issue
show
Deprecated Code introduced by
The function Doctrine\DBAL\Schema\Tab...dForeignKeyConstraint() has been deprecated: Use {@link addForeignKeyConstraint} ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

350
        return /** @scrutinizer ignore-deprecated */ $this->addNamedForeignKeyConstraint($constraintName, $foreignTable, $localColumnNames, $foreignColumnNames, $options);

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
351
    }
352 1518
353
    /**
354
     * Adds a foreign key constraint.
355
     *
356
     * Name is to be generated by the database itself.
357
     *
358
     * @deprecated Use {@link addForeignKeyConstraint}
359
     *
360
     * @param Table|string $foreignTable       Table schema instance or table name
361
     * @param string[]     $localColumnNames
362
     * @param string[]     $foreignColumnNames
363
     * @param mixed[]      $options
364
     */
365
    public function addUnnamedForeignKeyConstraint($foreignTable, array $localColumnNames, array $foreignColumnNames, array $options = []) : self
366
    {
367
        return $this->addForeignKeyConstraint($foreignTable, $localColumnNames, $foreignColumnNames, $options);
368 17641
    }
369
370 17641
    /**
371 17549
     * Adds a foreign key constraint with a given name.
372 17549
     *
373 17549
     * @deprecated Use {@link addForeignKeyConstraint}
374 17549
     *
375
     * @param Table|string $foreignTable       Table schema instance or table name
376
     * @param string[]     $localColumnNames
377
     * @param string[]     $foreignColumnNames
378 17641
     * @param mixed[]      $options
379
     *
380
     * @throws SchemaException
381
     */
382
    public function addNamedForeignKeyConstraint(string $name, $foreignTable, array $localColumnNames, array $foreignColumnNames, array $options = []) : self
383
    {
384
        if ($foreignTable instanceof Table) {
385
            foreach ($foreignColumnNames as $columnName) {
386
                if (! $foreignTable->hasColumn($columnName)) {
387
                    throw ColumnDoesNotExist::new($columnName, $foreignTable->getName());
388
                }
389
            }
390
        }
391
392
        foreach ($localColumnNames as $columnName) {
393
            if (! $this->hasColumn($columnName)) {
394
                throw ColumnDoesNotExist::new($columnName, $this->_name);
395 10058
            }
396
        }
397 10058
398
        $constraint = new ForeignKeyConstraint(
399
            $localColumnNames,
400
            $foreignTable,
401
            $foreignColumnNames,
402
            $name,
403
            $options
404
        );
405
406
        return $this->_addForeignKeyConstraint($constraint);
407
    }
408
409
    /**
410
     * @param mixed $value
411
     */
412
    public function addOption(string $name, $value) : self
413
    {
414
        $this->_options[$name] = $value;
415 17643
416
        return $this;
417 17643
    }
418 17529
419 17529
    /**
420 1142
     * Returns whether this table has a foreign key constraint with the given name.
421
     */
422
    public function hasForeignKey(string $constraintName) : bool
423
    {
424
        $constraintName = $this->normalizeIdentifier($constraintName);
425 17641
426 17641
        return isset($this->_fkConstraints[$constraintName]);
427 1255
    }
428
429
    /**
430
     * Returns the foreign key constraint with the given name.
431 17639
     *
432 17639
     * @param string $constraintName The constraint name.
433 178
     *
434 178
     * @throws SchemaException If the foreign key does not exist.
435 178
     */
436 178
    public function getForeignKey(string $constraintName) : ForeignKeyConstraint
437
    {
438
        $constraintName = $this->normalizeIdentifier($constraintName);
439 17639
440
        if (! $this->hasForeignKey($constraintName)) {
441
            throw ForeignKeyDoesNotExist::new($constraintName, $this->_name);
442
        }
443
444
        return $this->_fkConstraints[$constraintName];
445
    }
446
447
    /**
448 16311
     * Removes the foreign key constraint with the given name.
449
     *
450 16311
     * @param string $constraintName The constraint name.
451
     *
452 16311
     * @throws SchemaException
453
     */
454
    public function removeForeignKey(string $constraintName) : void
455
    {
456
        $constraintName = $this->normalizeIdentifier($constraintName);
457
458
        if (! $this->hasForeignKey($constraintName)) {
459
            throw ForeignKeyDoesNotExist::new($constraintName, $this->_name);
460
        }
461
462 15006
        unset($this->_fkConstraints[$constraintName]);
463
    }
464 15006
465
    /**
466 15006
     * Returns whether this table has a unique constraint with the given name.
467
     */
468
    public function hasUniqueConstraint(string $constraintName) : bool
469
    {
470
        $constraintName = $this->normalizeIdentifier($constraintName);
471
472
        return isset($this->_uniqueConstraints[$constraintName]);
473
    }
474
475
    /**
476
     * Returns the unique constraint with the given name.
477
     *
478 368
     * @param string $constraintName The constraint name.
479
     *
480 368
     * @throws SchemaException If the foreign key does not exist.
481
     */
482 368
    public function getUniqueConstraint(string $constraintName) : UniqueConstraint
483
    {
484
        $constraintName = $this->normalizeIdentifier($constraintName);
485
486 368
        if (! $this->hasUniqueConstraint($constraintName)) {
487
            throw UniqueConstraintDoesNotExist::new($constraintName, $this->_name);
488
        }
489
490
        return $this->_uniqueConstraints[$constraintName];
491
    }
492
493
    /**
494
     * Removes the unique constraint with the given name.
495
     *
496
     * @param string $constraintName The constraint name.
497
     *
498 370
     * @throws SchemaException
499
     */
500 370
    public function removeUniqueConstraint(string $constraintName) : void
501
    {
502 370
        $constraintName = $this->normalizeIdentifier($constraintName);
503
504
        if (! $this->hasUniqueConstraint($constraintName)) {
505
            throw UniqueConstraintDoesNotExist::new($constraintName, $this->_name);
506 370
        }
507 370
508
        unset($this->_uniqueConstraints[$constraintName]);
509
    }
510
511
    /**
512
     * Returns ordered list of columns (primary keys are first, then foreign keys, then the rest)
513
     *
514
     * @return Column[]
515
     */
516
    public function getColumns() : array
517
    {
518
        $columns = $this->_columns;
519
        $pkCols  = [];
520
        $fkCols  = [];
521
522
        $primaryKey = $this->getPrimaryKey();
523
524
        if ($primaryKey !== null) {
525
            $pkCols = $primaryKey->getColumns();
526
        }
527
528
        foreach ($this->getForeignKeys() as $fk) {
529
            /** @var ForeignKeyConstraint $fk */
530
            $fkCols = array_merge($fkCols, $fk->getColumns());
531
        }
532
533
        $colNames = array_unique(array_merge($pkCols, $fkCols, array_keys($columns)));
534
535
        uksort($columns, static function ($a, $b) use ($colNames) : bool {
536
            return array_search($a, $colNames) >= array_search($b, $colNames);
537
        });
538
539
        return $columns;
540
    }
541
542
    /**
543
     * Returns whether this table has a Column with the given name.
544
     *
545
     * @param string $columnName The column name.
546
     */
547
    public function hasColumn(string $columnName) : bool
548
    {
549
        $columnName = $this->normalizeIdentifier($columnName);
550
551
        return isset($this->_columns[$columnName]);
552
    }
553
554
    /**
555
     * Returns the Column with the given name.
556
     *
557
     * @param string $columnName The column name.
558
     *
559
     * @throws SchemaException If the column does not exist.
560
     */
561
    public function getColumn(string $columnName) : Column
562
    {
563
        $columnName = $this->normalizeIdentifier($columnName);
564
565
        if (! $this->hasColumn($columnName)) {
566
            throw ColumnDoesNotExist::new($columnName, $this->_name);
567
        }
568 18688
569
        return $this->_columns[$columnName];
570 18688
    }
571 18688
572 18688
    /**
573
     * Returns the primary key.
574 18688
     *
575
     * @return Index|null The primary key, or null if this Table has no primary key.
576 18688
     */
577 18212
    public function getPrimaryKey() : ?Index
578
    {
579
        return $this->hasPrimaryKey()
580 18688
            ? $this->getIndex($this->_primaryKeyName)
581
            : null;
582 17628
    }
583
584
    /**
585 18688
     * Returns the primary key columns.
586
     *
587
     * @return string[]
588 18366
     *
589 18688
     * @throws DBALException
590
     */
591 18688
    public function getPrimaryKeyColumns() : array
592
    {
593
        $primaryKey = $this->getPrimaryKey();
594
595
        if ($primaryKey === null) {
596
            throw new DBALException(sprintf('Table "%s" has no primary key.', $this->getName()));
597
        }
598
599
        return $primaryKey->getColumns();
600
    }
601 18744
602
    /**
603 18744
     * Returns whether this table has a primary key.
604
     */
605 18744
    public function hasPrimaryKey() : bool
606
    {
607
        return $this->_primaryKeyName && $this->hasIndex($this->_primaryKeyName);
608
    }
609
610
    /**
611
     * Returns whether this table has an Index with the given name.
612
     *
613
     * @param string $indexName The index name.
614
     */
615
    public function hasIndex(string $indexName) : bool
616
    {
617 18534
        $indexName = $this->normalizeIdentifier($indexName);
618
619 18534
        return isset($this->_indexes[$indexName]);
620
    }
621 18534
622 1477
    /**
623
     * Returns the Index with the given name.
624
     *
625 18532
     * @param string $indexName The index name.
626
     *
627
     * @throws SchemaException If the index does not exist.
628
     */
629
    public function getIndex(string $indexName) : Index
630
    {
631
        $indexName = $this->normalizeIdentifier($indexName);
632
633 18698
        if (! $this->hasIndex($indexName)) {
634
            throw IndexDoesNotExist::new($indexName, $this->_name);
635 18698
        }
636 18222
637 18698
        return $this->_indexes[$indexName];
638
    }
639
640
    /**
641
     * @return Index[]
642
     */
643
    public function getIndexes() : array
644
    {
645
        return $this->_indexes;
646
    }
647 16150
648
    /**
649 16150
     * Returns the unique constraints.
650
     *
651 16150
     * @return UniqueConstraint[]
652
     */
653
    public function getUniqueConstraints() : array
654
    {
655 16150
        return $this->_uniqueConstraints;
656
    }
657
658
    /**
659
     * Returns the foreign key constraints.
660
     *
661
     * @return ForeignKeyConstraint[]
662
     */
663 18704
    public function getForeignKeys() : array
664
    {
665 18704
        return $this->_fkConstraints;
666
    }
667
668
    public function hasOption(string $name) : bool
669
    {
670
        return isset($this->_options[$name]);
671
    }
672
673
    /**
674
     * @return mixed
675 18326
     */
676
    public function getOption(string $name)
677 18326
    {
678
        return $this->_options[$name];
679 18326
    }
680
681
    /**
682
     * @return mixed[]
683
     */
684
    public function getOptions() : array
685
    {
686
        return $this->_options;
687
    }
688
689
    public function visit(Visitor $visitor) : void
690
    {
691 18282
        $visitor->acceptTable($this);
692
693 18282
        foreach ($this->getColumns() as $column) {
694
            $visitor->acceptColumn($this, $column);
695 18282
        }
696 1352
697
        foreach ($this->getIndexes() as $index) {
698
            $visitor->acceptIndex($this, $index);
699 18280
        }
700
701
        foreach ($this->getForeignKeys() as $constraint) {
702
            $visitor->acceptForeignKey($this, $constraint);
703
        }
704
    }
705 18662
706
    /**
707 18662
     * Clone of a Table triggers a deep clone of all affected assets.
708
     */
709
    public function __clone()
710
    {
711
        foreach ($this->_columns as $k => $column) {
712
            $this->_columns[$k] = clone $column;
713
        }
714
715 18442
        foreach ($this->_indexes as $k => $index) {
716
            $this->_indexes[$k] = clone $index;
717 18442
        }
718
719
        foreach ($this->_fkConstraints as $k => $fk) {
720
            $this->_fkConstraints[$k] = clone $fk;
721
            $this->_fkConstraints[$k]->setLocalTable($this);
722
        }
723
    }
724
725 18724
    protected function _getMaxIdentifierLength() : int
726
    {
727 18724
        return $this->_schemaConfig instanceof SchemaConfig
728
            ? $this->_schemaConfig->getMaxIdentifierLength()
729
            : 63;
730
    }
731
732
    /**
733
     * @throws SchemaException
734
     */
735 16290
    protected function _addColumn(Column $column) : void
736
    {
737 16290
        $columnName = $column->getName();
738
        $columnName = $this->normalizeIdentifier($columnName);
739
740
        if (isset($this->_columns[$columnName])) {
741
            throw ColumnAlreadyExists::new($this->getName(), $columnName);
742
        }
743
744
        $this->_columns[$columnName] = $column;
745 15731
    }
746
747 15731
    /**
748
     * Adds an index to the table.
749
     *
750
     * @throws SchemaException
751
     */
752
    protected function _addIndex(Index $indexCandidate) : self
753 18468
    {
754
        $indexName               = $indexCandidate->getName();
755 18468
        $indexName               = $this->normalizeIdentifier($indexName);
756
        $replacedImplicitIndexes = [];
757
758
        foreach ($this->implicitIndexes as $name => $implicitIndex) {
759
            if (! $implicitIndex->isFullfilledBy($indexCandidate) || ! isset($this->_indexes[$name])) {
760
                continue;
761 17534
            }
762
763 17534
            $replacedImplicitIndexes[] = $name;
764
        }
765 17534
766 17528
        if ((isset($this->_indexes[$indexName]) && ! in_array($indexName, $replacedImplicitIndexes, true)) ||
767
            ($this->_primaryKeyName !== false && $indexCandidate->isPrimary())
768
        ) {
769 17534
            throw IndexAlreadyExists::new($indexName, $this->_name);
770 17514
        }
771
772
        foreach ($replacedImplicitIndexes as $name) {
773 17534
            unset($this->_indexes[$name], $this->implicitIndexes[$name]);
774 158
        }
775
776 17534
        if ($indexCandidate->isPrimary()) {
777
            $this->_primaryKeyName = $indexName;
778
        }
779
780
        $this->_indexes[$indexName] = $indexCandidate;
781
782
        return $this;
783 16832
    }
784
785 16832
    protected function _addUniqueConstraint(UniqueConstraint $constraint) : self
786 16830
    {
787
        $name = $constraint->getName() ?? $this->_generateIdentifierName(
788
            array_merge([$this->getName()], $constraint->getColumns()),
789 16832
            'fk',
790 16808
            $this->_getMaxIdentifierLength()
791
        );
792
793 16832
        $name = $this->normalizeIdentifier($name);
794 15479
795 15479
        $this->_uniqueConstraints[$name] = $constraint;
796
797 16832
        // If there is already an index that fulfills this requirements drop the request. In the case of __construct
798
        // calling this method during hydration from schema-details all the explicitly added indexes lead to duplicates.
799
        // This creates computation overhead in this case, however no duplicate indexes are ever added (column based).
800
        $indexName = $this->_generateIdentifierName(
801
            array_merge([$this->getName()], $constraint->getColumns()),
802 17849
            'idx',
803
            $this->_getMaxIdentifierLength()
804 17849
        );
805 17437
806 17849
        $indexCandidate = $this->_createIndex($constraint->getColumns(), $indexName, true, false);
807
808
        foreach ($this->_indexes as $existingIndex) {
809
            if ($indexCandidate->isFullfilledBy($existingIndex)) {
810
                return $this;
811
            }
812
        }
813
814 18994
        $this->implicitIndexes[$this->normalizeIdentifier($indexName)] = $indexCandidate;
815
816 18994
        return $this;
817 18994
    }
818
819 18994
    protected function _addForeignKeyConstraint(ForeignKeyConstraint $constraint) : self
820 1452
    {
821
        $constraint->setLocalTable($this);
822
823 18994
        $name = $constraint->getName() ?? $this->_generateIdentifierName(
824 18994
            array_merge([$this->getName()], $constraint->getLocalColumns()),
825
            'fk',
826
            $this->_getMaxIdentifierLength()
827
        );
828
829
        $name = $this->normalizeIdentifier($name);
830
831
        $this->_fkConstraints[$name] = $constraint;
832
833 18622
        // add an explicit index on the foreign key columns.
834
        // If there is already an index that fulfills this requirements drop the request. In the case of __construct
835 18622
        // calling this method during hydration from schema-details all the explicitly added indexes lead to duplicates.
836 18622
        // This creates computation overhead in this case, however no duplicate indexes are ever added (column based).
837 18622
        $indexName = $this->_generateIdentifierName(
838
            array_merge([$this->getName()], $constraint->getColumns()),
839 18622
            'idx',
840 13346
            $this->_getMaxIdentifierLength()
841 13338
        );
842
843
        $indexCandidate = $this->_createIndex($constraint->getColumns(), $indexName, false, false);
844 758
845
        foreach ($this->_indexes as $existingIndex) {
846
            if ($indexCandidate->isFullfilledBy($existingIndex)) {
847 18622
                return $this;
848 18622
            }
849
        }
850 1329
851
        $this->_addIndex($indexCandidate);
852
        $this->implicitIndexes[$this->normalizeIdentifier($indexName)] = $indexCandidate;
853 18622
854 758
        return $this;
855
    }
856
857 18622
    /**
858 18378
     * Normalizes a given identifier.
859
     *
860
     * Trims quotes and lowercases the given identifier.
861 18622
     *
862
     * @param string|null $identifier The identifier to normalize.
863 18622
     *
864
     * @return string The normalized identifier.
865
     */
866
    private function normalizeIdentifier(?string $identifier) : string
867
    {
868
        if ($identifier === null) {
869 202
            return '';
870
        }
871 202
872
        return $this->trimQuotes(strtolower($identifier));
873 202
    }
874 202
875 202
    /**
876 202
     * @param mixed[] $columns
877
     * @param mixed[] $flags
878
     * @param mixed[] $options
879 202
     *
880
     * @throws SchemaException
881 202
     */
882
    private function _createUniqueConstraint(array $columns, string $indexName, array $flags = [], array $options = []) : UniqueConstraint
883
    {
884
        if (preg_match('(([^a-zA-Z0-9_]+))', $this->normalizeIdentifier($indexName))) {
885
            throw IndexNameInvalid::new($indexName);
886 202
        }
887 202
888 202
        foreach ($columns as $index => $value) {
889 202
            if (is_string($index)) {
890
                $columnName = $index;
891
            } else {
892 202
                $columnName = $value;
893
            }
894 202
895
            if (! $this->hasColumn($columnName)) {
896
                throw ColumnDoesNotExist::new($columnName, $this->_name);
897
            }
898
        }
899
900 202
        return new UniqueConstraint($indexName, $columns, $flags, $options);
901
    }
902 202
903
    /**
904
     * @param mixed[]  $columns
905
     * @param string[] $flags
906
     * @param mixed[]  $options
907
     *
908 17728
     * @throws SchemaException
909
     */
910 17728
    private function _createIndex(array $columns, string $indexName, bool $isUnique, bool $isPrimary, array $flags = [], array $options = []) : Index
911
    {
912 17728
        if (preg_match('(([^a-zA-Z0-9_]+))', $this->normalizeIdentifier($indexName))) {
913 17647
            throw IndexNameInvalid::new($indexName);
914 1848
        }
915 1848
916 1848
        foreach ($columns as $index => $value) {
917 17728
            if (is_string($index)) {
918
                $columnName = $index;
919
            } else {
920 17728
                $columnName = $value;
921
            }
922 17728
923
            if (! $this->hasColumn($columnName)) {
924
                throw ColumnDoesNotExist::new($columnName, $this->_name);
925
            }
926
        }
927
928 17728
        return new Index($indexName, $columns, $isUnique, $isPrimary, $flags, $options);
929 17728
    }
930
}
931