Failed Conditions
Pull Request — 3.0.x (#3980)
by Guilherme
38:44
created

Table::hasUniqueConstraint()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
eloc 2
dl 0
loc 5
ccs 0
cts 3
cp 0
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 1
crap 2
1
<?php
2
3
namespace Doctrine\DBAL\Schema;
4
5
use Doctrine\DBAL\DBALException;
6
use Doctrine\DBAL\Schema\Visitor\Visitor;
7
use Doctrine\DBAL\Types\Type;
8
use const ARRAY_FILTER_USE_KEY;
9
use function array_filter;
10
use function array_keys;
11
use function array_merge;
12
use function in_array;
13
use function is_numeric;
14
use function is_string;
15
use function preg_match;
16
use function strlen;
17
use function strtolower;
18
19
/**
20
 * Object Representation of a table.
21
 */
22
class Table extends AbstractAsset
23
{
24
    /** @var Column[] */
25
    protected $_columns = [];
26
27
    /** @var Index[] */
28
    protected $_indexes = [];
29
30
    /** @var string|null */
31
    protected $_primaryKeyName = null;
32
33
    /** @var UniqueConstraint[] */
34
    protected $uniqueConstraints = [];
35
36
    /** @var ForeignKeyConstraint[] */
37
    protected $_fkConstraints = [];
38
39
    /** @var mixed[] */
40
    protected $_options = [
41
        'create_options' => [],
42
    ];
43
44
    /** @var SchemaConfig|null */
45
    protected $_schemaConfig = null;
46
47
    /** @var Index[] */
48
    private $implicitIndexes = [];
49
50
    /**
51
     * @param string                 $tableName
52
     * @param Column[]               $columns
53
     * @param Index[]                $indexes
54
     * @param UniqueConstraint[]     $uniqueConstraints
55
     * @param ForeignKeyConstraint[] $fkConstraints
56
     * @param mixed[]                $options
57
     *
58
     * @throws DBALException
59
     */
60 14477
    public function __construct(
61
        $tableName,
62
        array $columns = [],
63
        array $indexes = [],
64
        array $uniqueConstraints = [],
65
        array $fkConstraints = [],
66
        array $options = []
67
    ) {
68 14477
        if (strlen($tableName) === 0) {
69 22
            throw DBALException::invalidTableName($tableName);
70
        }
71
72 14455
        $this->_setName($tableName);
73
74 14455
        foreach ($columns as $column) {
75 2062
            $this->_addColumn($column);
76
        }
77
78 14433
        foreach ($indexes as $idx) {
79 654
            $this->_addIndex($idx);
80
        }
81
82 14389
        foreach ($uniqueConstraints as $uniqueConstraint) {
83
            $this->_addUniqueConstraint($uniqueConstraint);
84
        }
85
86 14389
        foreach ($fkConstraints as $constraint) {
87 253
            $this->_addForeignKeyConstraint($constraint);
88
        }
89
90 14389
        $this->_options = array_merge($this->_options, $options);
91 14389
    }
92
93
    /**
94
     * @return void
95
     */
96 1443
    public function setSchemaConfig(SchemaConfig $schemaConfig)
97
    {
98 1443
        $this->_schemaConfig = $schemaConfig;
99 1443
    }
100
101
    /**
102
     * Sets the Primary Key.
103
     *
104
     * @param string[]     $columnNames
105
     * @param string|false $indexName
106
     *
107
     * @return self
108
     */
109 5827
    public function setPrimaryKey(array $columnNames, $indexName = false)
110
    {
111 5827
        if ($indexName === false) {
112 5805
            $indexName = 'primary';
113
        }
114
115 5827
        $this->_addIndex($this->_createIndex($columnNames, $indexName, true, true));
116
117 5827
        foreach ($columnNames as $columnName) {
118 5827
            $column = $this->getColumn($columnName);
119 5827
            $column->setNotnull(true);
120
        }
121
122 5827
        return $this;
123
    }
124
125
    /**
126
     * @param string[] $columnNames
127
     * @param string[] $flags
128
     * @param mixed[]  $options
129
     *
130
     * @return self
131
     */
132 1815
    public function addIndex(array $columnNames, ?string $indexName = null, array $flags = [], array $options = [])
133
    {
134 1815
        if ($indexName === null) {
135 393
            $indexName = $this->_generateIdentifierName(
136 393
                array_merge([$this->getName()], $columnNames),
137 393
                'idx',
138 393
                $this->_getMaxIdentifierLength()
139
            );
140
        }
141
142 1815
        return $this->_addIndex($this->_createIndex($columnNames, $indexName, false, false, $flags, $options));
143
    }
144
145
    /**
146
     * @param string[] $columnNames
147
     * @param string[] $flags
148
     * @param mixed[]  $options
149
     *
150
     * @return self
151
     */
152
    public function addUniqueConstraint(array $columnNames, ?string $indexName = null, array $flags = [], array $options = []) : Table
153
    {
154
        if ($indexName === null) {
155
            $indexName = $this->_generateIdentifierName(
156
                array_merge([$this->getName()], $columnNames),
157
                'uniq',
158
                $this->_getMaxIdentifierLength()
159
            );
160
        }
161
162
        return $this->_addUniqueConstraint($this->_createUniqueConstraint($columnNames, $indexName, $flags, $options));
163
    }
164
165
    /**
166
     * Drops the primary key from this table.
167
     *
168
     * @return void
169
     */
170 398
    public function dropPrimaryKey()
171
    {
172 398
        if ($this->_primaryKeyName === null) {
173
            return;
174
        }
175
176 398
        $this->dropIndex($this->_primaryKeyName);
177 398
        $this->_primaryKeyName = null;
178 398
    }
179
180
    /**
181
     * Drops an index from this table.
182
     *
183
     * @param string $indexName The index name.
184
     *
185
     * @return void
186
     *
187
     * @throws SchemaException If the index does not exist.
188
     */
189 674
    public function dropIndex($indexName)
190
    {
191 674
        $indexName = $this->normalizeIdentifier($indexName);
192
193 674
        if (! $this->hasIndex($indexName)) {
194
            throw SchemaException::indexDoesNotExist($indexName, $this->_name);
195
        }
196
197 674
        unset($this->_indexes[$indexName]);
198 674
    }
199
200
    /**
201
     * @param string[]    $columnNames
202
     * @param string|null $indexName
203
     * @param mixed[]     $options
204
     *
205
     * @return self
206
     */
207 543
    public function addUniqueIndex(array $columnNames, $indexName = null, array $options = [])
208
    {
209 543
        if ($indexName === null) {
210 361
            $indexName = $this->_generateIdentifierName(
211 361
                array_merge([$this->getName()], $columnNames),
212 361
                'uniq',
213 361
                $this->_getMaxIdentifierLength()
214
            );
215
        }
216
217 543
        return $this->_addIndex($this->_createIndex($columnNames, $indexName, true, false, [], $options));
218
    }
219
220
    /**
221
     * Renames an index.
222
     *
223
     * @param string      $oldIndexName The name of the index to rename from.
224
     * @param string|null $newIndexName The name of the index to rename to.
225
     *                                  If null is given, the index name will be auto-generated.
226
     *
227
     * @return self This table instance.
228
     *
229
     * @throws SchemaException If no index exists for the given current name
230
     *                         or if an index with the given new name already exists on this table.
231
     */
232 307
    public function renameIndex($oldIndexName, $newIndexName = null)
233
    {
234 307
        $oldIndexName           = $this->normalizeIdentifier($oldIndexName);
235 307
        $normalizedNewIndexName = $this->normalizeIdentifier($newIndexName);
236
237 307
        if ($oldIndexName === $normalizedNewIndexName) {
238 198
            return $this;
239
        }
240
241 307
        if (! $this->hasIndex($oldIndexName)) {
242 22
            throw SchemaException::indexDoesNotExist($oldIndexName, $this->_name);
243
        }
244
245 285
        if ($this->hasIndex($normalizedNewIndexName)) {
246 22
            throw SchemaException::indexAlreadyExists($normalizedNewIndexName, $this->_name);
247
        }
248
249 263
        $oldIndex = $this->_indexes[$oldIndexName];
250
251 263
        if ($oldIndex->isPrimary()) {
252 22
            $this->dropPrimaryKey();
253
254 22
            return $this->setPrimaryKey($oldIndex->getColumns(), $newIndexName ?? false);
255
        }
256
257 263
        unset($this->_indexes[$oldIndexName]);
258
259 263
        if ($oldIndex->isUnique()) {
260 44
            return $this->addUniqueIndex($oldIndex->getColumns(), $newIndexName, $oldIndex->getOptions());
261
        }
262
263 241
        return $this->addIndex($oldIndex->getColumns(), $newIndexName, $oldIndex->getFlags(), $oldIndex->getOptions());
264
    }
265
266
    /**
267
     * Checks if an index begins in the order of the given columns.
268
     *
269
     * @param string[] $columnNames
270
     *
271
     * @return bool
272
     */
273 43
    public function columnsAreIndexed(array $columnNames)
274
    {
275 43
        foreach ($this->getIndexes() as $index) {
276
            /** @var $index Index */
277 43
            if ($index->spansColumns($columnNames)) {
278 43
                return true;
279
            }
280
        }
281
282
        return false;
283
    }
284
285
    /**
286
     * @param string  $columnName
287
     * @param string  $typeName
288
     * @param mixed[] $options
289
     *
290
     * @return Column
291
     */
292 11636
    public function addColumn($columnName, $typeName, array $options = [])
293
    {
294 11636
        $column = new Column($columnName, Type::getType($typeName), $options);
295
296 11636
        $this->_addColumn($column);
297
298 11636
        return $column;
299
    }
300
301
    /**
302
     * Renames a Column.
303
     *
304
     * @deprecated
305
     *
306
     * @param string $oldColumnName
307
     * @param string $newColumnName
308
     *
309
     * @return void
310
     *
311
     * @throws DBALException
312
     */
313
    public function renameColumn($oldColumnName, $newColumnName)
0 ignored issues
show
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

313
    public function renameColumn($oldColumnName, /** @scrutinizer ignore-unused */ $newColumnName)

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 $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

313
    public function renameColumn(/** @scrutinizer ignore-unused */ $oldColumnName, $newColumnName)

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...
314
    {
315
        throw new DBALException(
316
            'Table#renameColumn() was removed, because it drops and recreates ' .
317
            'the column instead. There is no fix available, because a schema diff cannot reliably detect if a ' .
318
            'column was renamed or one column was created and another one dropped.'
319
        );
320
    }
321
322
    /**
323
     * Change Column Details.
324
     *
325
     * @param string  $columnName
326
     * @param mixed[] $options
327
     *
328
     * @return self
329
     */
330 265
    public function changeColumn($columnName, array $options)
331
    {
332 265
        $column = $this->getColumn($columnName);
333
334 265
        $column->setOptions($options);
335
336 265
        return $this;
337
    }
338
339
    /**
340
     * Drops a Column from the Table.
341
     *
342
     * @param string $columnName
343
     *
344
     * @return self
345
     */
346 198
    public function dropColumn($columnName)
347
    {
348 198
        $columnName = $this->normalizeIdentifier($columnName);
349
350 198
        unset($this->_columns[$columnName]);
351
352 198
        return $this;
353
    }
354
355
    /**
356
     * Adds a foreign key constraint.
357
     *
358
     * Name is inferred from the local columns.
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
     * @param string|null  $constraintName
365
     *
366
     * @return self
367
     */
368 1812
    public function addForeignKeyConstraint($foreignTable, array $localColumnNames, array $foreignColumnNames, array $options = [], $constraintName = null)
369
    {
370 1812
        if ($constraintName === null) {
371 954
            $constraintName = $this->_generateIdentifierName(array_merge((array) $this->getName(), $localColumnNames), 'fk', $this->_getMaxIdentifierLength());
372
        }
373
374 1812
        return $this->addNamedForeignKeyConstraint($constraintName, $foreignTable, $localColumnNames, $foreignColumnNames, $options);
375
    }
376
377
    /**
378
     * Adds a foreign key constraint.
379
     *
380
     * Name is to be generated by the database itself.
381
     *
382
     * @deprecated Use {@link addForeignKeyConstraint}
383
     *
384
     * @param Table|string $foreignTable       Table schema instance or table name
385
     * @param string[]     $localColumnNames
386
     * @param string[]     $foreignColumnNames
387
     * @param mixed[]      $options
388
     *
389
     * @return self
390
     */
391 71
    public function addUnnamedForeignKeyConstraint($foreignTable, array $localColumnNames, array $foreignColumnNames, array $options = [])
392
    {
393 71
        return $this->addForeignKeyConstraint($foreignTable, $localColumnNames, $foreignColumnNames, $options);
394
    }
395
396
    /**
397
     * Adds a foreign key constraint with a given name.
398
     *
399
     * @deprecated Use {@link addForeignKeyConstraint}
400
     *
401
     * @param string       $name
402
     * @param Table|string $foreignTable       Table schema instance or table name
403
     * @param string[]     $localColumnNames
404
     * @param string[]     $foreignColumnNames
405
     * @param mixed[]      $options
406
     *
407
     * @return self
408
     *
409
     * @throws SchemaException
410
     */
411 1834
    public function addNamedForeignKeyConstraint($name, $foreignTable, array $localColumnNames, array $foreignColumnNames, array $options = [])
412
    {
413 1834
        if ($foreignTable instanceof Table) {
414 923
            foreach ($foreignColumnNames as $columnName) {
415 923
                if (! $foreignTable->hasColumn($columnName)) {
416 22
                    throw SchemaException::columnDoesNotExist($columnName, $foreignTable->getName());
417
                }
418
            }
419
        }
420
421 1812
        foreach ($localColumnNames as $columnName) {
422 1812
            if (! $this->hasColumn($columnName)) {
423 22
                throw SchemaException::columnDoesNotExist($columnName, $this->_name);
424
            }
425
        }
426
427 1790
        $constraint = new ForeignKeyConstraint(
428 1790
            $localColumnNames,
429
            $foreignTable,
430
            $foreignColumnNames,
431
            $name,
432
            $options
433
        );
434
435 1790
        return $this->_addForeignKeyConstraint($constraint);
436
    }
437
438
    /**
439
     * @param string $name
440
     * @param mixed  $value
441
     *
442
     * @return self
443
     */
444 1737
    public function addOption($name, $value)
445
    {
446 1737
        $this->_options[$name] = $value;
447
448 1737
        return $this;
449
    }
450
451
    /**
452
     * Returns whether this table has a foreign key constraint with the given name.
453
     *
454
     * @param string $constraintName
455
     *
456
     * @return bool
457
     */
458 242
    public function hasForeignKey($constraintName)
459
    {
460 242
        $constraintName = $this->normalizeIdentifier($constraintName);
461
462 242
        return isset($this->_fkConstraints[$constraintName]);
463
    }
464
465
    /**
466
     * Returns the foreign key constraint with the given name.
467
     *
468
     * @param string $constraintName The constraint name.
469
     *
470
     * @return ForeignKeyConstraint
471
     *
472
     * @throws SchemaException If the foreign key does not exist.
473
     */
474 177
    public function getForeignKey($constraintName)
475
    {
476 177
        $constraintName = $this->normalizeIdentifier($constraintName);
477
478 177
        if (! $this->hasForeignKey($constraintName)) {
479
            throw SchemaException::foreignKeyDoesNotExist($constraintName, $this->_name);
480
        }
481
482 177
        return $this->_fkConstraints[$constraintName];
483
    }
484
485
    /**
486
     * Removes the foreign key constraint with the given name.
487
     *
488
     * @param string $constraintName The constraint name.
489
     *
490
     * @return void
491
     *
492
     * @throws SchemaException
493
     */
494 220
    public function removeForeignKey($constraintName)
495
    {
496 220
        $constraintName = $this->normalizeIdentifier($constraintName);
497
498 220
        if (! $this->hasForeignKey($constraintName)) {
499
            throw SchemaException::foreignKeyDoesNotExist($constraintName, $this->_name);
500
        }
501
502 220
        unset($this->_fkConstraints[$constraintName]);
503 220
    }
504
505
    /**
506
     * Returns whether this table has a unique constraint with the given name.
507
     */
508
    public function hasUniqueConstraint(string $name) : bool
509
    {
510
        $name = $this->normalizeIdentifier($name);
511
512
        return isset($this->uniqueConstraints[$name]);
513
    }
514
515
    /**
516
     * Returns the unique constraint with the given name.
517
     *
518
     * @throws SchemaException If the unique constraint does not exist.
519
     */
520
    public function getUniqueConstraint(string $name) : UniqueConstraint
521
    {
522
        $name = $this->normalizeIdentifier($name);
523
524
        if (! $this->hasUniqueConstraint($name)) {
525
            throw SchemaException::uniqueConstraintDoesNotExist($name, $this->_name);
526
        }
527
528
        return $this->uniqueConstraints[$name];
529
    }
530
531
    /**
532
     * Removes the unique constraint with the given name.
533
     *
534
     * @throws SchemaException If the unique constraint does not exist.
535
     */
536
    public function removeUniqueConstraint(string $name) : void
537
    {
538
        $name = $this->normalizeIdentifier($name);
539
540
        if (! $this->hasForeignKey($name)) {
541
            throw SchemaException::uniqueConstraintDoesNotExist($name, $this->_name);
542
        }
543
544
        unset($this->uniqueConstraints[$name]);
545
    }
546
547
    /**
548
     * Returns ordered list of columns (primary keys are first, then foreign keys, then the rest)
549
     *
550
     * @return Column[]
551
     */
552 9221
    public function getColumns()
553
    {
554 9221
        $primaryKeyColumns = $this->hasPrimaryKey() ? $this->getPrimaryKeyColumns() : [];
555 9221
        $foreignKeyColumns = $this->getForeignKeyColumns();
556 9221
        $remainderColumns  = $this->filterColumns(
557 9221
            array_merge(array_keys($primaryKeyColumns), array_keys($foreignKeyColumns)),
558 9221
            true
559
        );
560
561 9221
        return array_merge($primaryKeyColumns, $foreignKeyColumns, $remainderColumns);
562
    }
563
564
    /**
565
     * Returns whether this table has a Column with the given name.
566
     *
567
     * @param string $columnName The column name.
568
     *
569
     * @return bool
570
     */
571 10055
    public function hasColumn($columnName)
572
    {
573 10055
        $columnName = $this->normalizeIdentifier($columnName);
574
575 10055
        return isset($this->_columns[$columnName]);
576
    }
577
578
    /**
579
     * Returns the Column with the given name.
580
     *
581
     * @param string $columnName The column name.
582
     *
583
     * @return Column
584
     *
585
     * @throws SchemaException If the column does not exist.
586
     */
587 7958
    public function getColumn($columnName)
588
    {
589 7958
        $columnName = $this->normalizeIdentifier($columnName);
590
591 7958
        if (! $this->hasColumn($columnName)) {
592 22
            throw SchemaException::columnDoesNotExist($columnName, $this->_name);
593
        }
594
595 7936
        return $this->_columns[$columnName];
596
    }
597
598
    /**
599
     * Returns the primary key.
600
     *
601
     * @return Index|null The primary key, or null if this Table has no primary key.
602
     */
603 4446
    public function getPrimaryKey()
604
    {
605 4446
        return $this->_primaryKeyName !== null
606 4446
            ? $this->getIndex($this->_primaryKeyName)
607 4446
            : null;
608
    }
609
610
    /**
611
     * Returns the primary key columns.
612
     *
613
     * @return Column[]
614
     *
615
     * @throws DBALException
616
     */
617 4336
    public function getPrimaryKeyColumns()
618
    {
619 4336
        $primaryKey = $this->getPrimaryKey();
620
621 4336
        if ($primaryKey === null) {
622
            throw new DBALException('Table ' . $this->getName() . ' has no primary key.');
623
        }
624
625 4336
        return $this->filterColumns($primaryKey->getColumns());
626
    }
627
628
    /**
629
     * Returns the foreign key columns
630
     *
631
     * @return Column[]
632
     */
633 9221
    public function getForeignKeyColumns()
634
    {
635 9221
        $foreignKeyColumns = [];
636
637 9221
        foreach ($this->getForeignKeys() as $foreignKey) {
638 894
            $foreignKeyColumns = array_merge($foreignKeyColumns, $foreignKey->getLocalColumns());
639
        }
640
641 9221
        return $this->filterColumns($foreignKeyColumns);
642
    }
643
644
    /**
645
     * Returns whether this table has a primary key.
646
     *
647
     * @return bool
648
     */
649 9309
    public function hasPrimaryKey()
650
    {
651 9309
        return $this->_primaryKeyName !== null && $this->hasIndex($this->_primaryKeyName);
652
    }
653
654
    /**
655
     * Returns whether this table has an Index with the given name.
656
     *
657
     * @param string $indexName The index name.
658
     *
659
     * @return bool
660
     */
661 5619
    public function hasIndex($indexName)
662
    {
663 5619
        $indexName = $this->normalizeIdentifier($indexName);
664
665 5619
        return isset($this->_indexes[$indexName]);
666
    }
667
668
    /**
669
     * Returns the Index with the given name.
670
     *
671
     * @param string $indexName The index name.
672
     *
673
     * @return Index
674
     *
675
     * @throws SchemaException If the index does not exist.
676
     */
677 5135
    public function getIndex($indexName)
678
    {
679 5135
        $indexName = $this->normalizeIdentifier($indexName);
680 5135
        if (! $this->hasIndex($indexName)) {
681 22
            throw SchemaException::indexDoesNotExist($indexName, $this->_name);
682
        }
683
684 5113
        return $this->_indexes[$indexName];
685
    }
686
687
    /**
688
     * @return Index[]
689
     */
690 9023
    public function getIndexes()
691
    {
692 9023
        return $this->_indexes;
693
    }
694
695
    /**
696
     * Returns the unique constraints.
697
     *
698
     * @return UniqueConstraint[]
699
     */
700 6731
    public function getUniqueConstraints()
701
    {
702 6731
        return $this->uniqueConstraints;
703
    }
704
705
    /**
706
     * Returns the foreign key constraints.
707
     *
708
     * @return ForeignKeyConstraint[]
709
     */
710 9617
    public function getForeignKeys()
711
    {
712 9617
        return $this->_fkConstraints;
713
    }
714
715
    /**
716
     * @param string $name
717
     *
718
     * @return bool
719
     */
720 4399
    public function hasOption($name)
721
    {
722 4399
        return isset($this->_options[$name]);
723
    }
724
725
    /**
726
     * @param string $name
727
     *
728
     * @return mixed
729
     */
730 221
    public function getOption($name)
731
    {
732 221
        return $this->_options[$name];
733
    }
734
735
    /**
736
     * @return mixed[]
737
     */
738 6995
    public function getOptions()
739
    {
740 6995
        return $this->_options;
741
    }
742
743
    /**
744
     * @return void
745
     */
746 342
    public function visit(Visitor $visitor)
747
    {
748 342
        $visitor->acceptTable($this);
749
750 342
        foreach ($this->getColumns() as $column) {
751 276
            $visitor->acceptColumn($this, $column);
752
        }
753
754 342
        foreach ($this->getIndexes() as $index) {
755 192
            $visitor->acceptIndex($this, $index);
756
        }
757
758 342
        foreach ($this->getForeignKeys() as $constraint) {
759 88
            $visitor->acceptForeignKey($this, $constraint);
760
        }
761 342
    }
762
763
    /**
764
     * Clone of a Table triggers a deep clone of all affected assets.
765
     *
766
     * @return void
767
     */
768 1052
    public function __clone()
769
    {
770 1052
        foreach ($this->_columns as $k => $column) {
771 1030
            $this->_columns[$k] = clone $column;
772
        }
773 1052
        foreach ($this->_indexes as $k => $index) {
774 742
            $this->_indexes[$k] = clone $index;
775
        }
776 1052
        foreach ($this->_fkConstraints as $k => $fk) {
777 175
            $this->_fkConstraints[$k] = clone $fk;
778 175
            $this->_fkConstraints[$k]->setLocalTable($this);
779
        }
780 1052
    }
781
782 44
    public function setComment(?string $comment) : self
783
    {
784
        // For keeping backward compatibility with MySQL in previous releases, table comments are stored as options.
785 44
        $this->addOption('comment', $comment);
786
787 44
        return $this;
788
    }
789
790 44
    public function getComment() : ?string
791
    {
792 44
        return $this->_options['comment'] ?? null;
793
    }
794
795
    /**
796
     * @return int
797
     */
798 2628
    protected function _getMaxIdentifierLength()
799
    {
800 2628
        if ($this->_schemaConfig instanceof SchemaConfig) {
801 227
            return $this->_schemaConfig->getMaxIdentifierLength();
802
        }
803
804 2443
        return 63;
805
    }
806
807
    /**
808
     * @return void
809
     *
810
     * @throws SchemaException
811
     */
812 12617
    protected function _addColumn(Column $column)
813
    {
814 12617
        $columnName = $column->getName();
815 12617
        $columnName = $this->normalizeIdentifier($columnName);
816
817 12617
        if (isset($this->_columns[$columnName])) {
818 22
            throw SchemaException::columnAlreadyExists($this->getName(), $columnName);
819
        }
820
821 12617
        $this->_columns[$columnName] = $column;
822 12617
    }
823
824
    /**
825
     * Adds an index to the table.
826
     *
827
     * @return self
828
     *
829
     * @throws SchemaException
830
     */
831 8386
    protected function _addIndex(Index $indexCandidate)
832
    {
833 8386
        $indexName               = $indexCandidate->getName();
834 8386
        $indexName               = $this->normalizeIdentifier($indexName);
835 8386
        $replacedImplicitIndexes = [];
836
837 8386
        foreach ($this->implicitIndexes as $name => $implicitIndex) {
838 550
            if (! $implicitIndex->isFullfilledBy($indexCandidate) || ! isset($this->_indexes[$name])) {
839 462
                continue;
840
            }
841
842 88
            $replacedImplicitIndexes[] = $name;
843
        }
844
845 8386
        if ((isset($this->_indexes[$indexName]) && ! in_array($indexName, $replacedImplicitIndexes, true)) ||
846 8386
            ($this->_primaryKeyName !== null && $indexCandidate->isPrimary())
847
        ) {
848 44
            throw SchemaException::indexAlreadyExists($indexName, $this->_name);
849
        }
850
851 8386
        foreach ($replacedImplicitIndexes as $name) {
852 88
            unset($this->_indexes[$name], $this->implicitIndexes[$name]);
853
        }
854
855 8386
        if ($indexCandidate->isPrimary()) {
856 5893
            $this->_primaryKeyName = $indexName;
857
        }
858
859 8386
        $this->_indexes[$indexName] = $indexCandidate;
860
861 8386
        return $this;
862
    }
863
864
    /**
865
     * @return self
866
     */
867
    protected function _addUniqueConstraint(UniqueConstraint $constraint) : Table
868
    {
869
        $mergedNames = array_merge([$this->getName()], $constraint->getColumns());
870
        $name        = strlen($constraint->getName()) > 0
871
            ? $constraint->getName()
872
            : $this->_generateIdentifierName($mergedNames, 'fk', $this->_getMaxIdentifierLength());
873
874
        $name = $this->normalizeIdentifier($name);
875
876
        $this->uniqueConstraints[$name] = $constraint;
877
878
        // If there is already an index that fulfills this requirements drop the request. In the case of __construct
879
        // calling this method during hydration from schema-details all the explicitly added indexes lead to duplicates.
880
        // This creates computation overhead in this case, however no duplicate indexes are ever added (column based).
881
        $indexName = $this->_generateIdentifierName($mergedNames, 'idx', $this->_getMaxIdentifierLength());
882
883
        $indexCandidate = $this->_createIndex($constraint->getColumns(), $indexName, true, false);
884
885
        foreach ($this->_indexes as $existingIndex) {
886
            if ($indexCandidate->isFullfilledBy($existingIndex)) {
887
                return $this;
888
            }
889
        }
890
891
        $this->implicitIndexes[$this->normalizeIdentifier($indexName)] = $indexCandidate;
892
893
        return $this;
894
    }
895
896
    /**
897
     * @return self
898
     */
899 1893
    protected function _addForeignKeyConstraint(ForeignKeyConstraint $constraint)
900
    {
901 1893
        $constraint->setLocalTable($this);
902
903 1893
        if (strlen($constraint->getName()) > 0) {
904 1870
            $name = $constraint->getName();
905
        } else {
906 23
            $name = $this->_generateIdentifierName(
907 23
                array_merge([$this->getName()], $constraint->getLocalColumns()),
908 23
                'fk',
909 23
                $this->_getMaxIdentifierLength()
910
            );
911
        }
912
913 1893
        $name = $this->normalizeIdentifier($name);
914
915 1893
        $this->_fkConstraints[$name] = $constraint;
916
917
        // add an explicit index on the foreign key columns. If there is already an index that fulfils this requirements drop the request.
918
        // In the case of __construct calling this method during hydration from schema-details all the explicitly added indexes
919
        // lead to duplicates. This creates computation overhead in this case, however no duplicate indexes are ever added (based on columns).
920 1893
        $indexName      = $this->_generateIdentifierName(
921 1893
            array_merge([$this->getName()], $constraint->getColumns()),
922 1893
            'idx',
923 1893
            $this->_getMaxIdentifierLength()
924
        );
925 1893
        $indexCandidate = $this->_createIndex($constraint->getColumns(), $indexName, false, false);
926
927 1893
        foreach ($this->_indexes as $existingIndex) {
928 1314
            if ($indexCandidate->isFullfilledBy($existingIndex)) {
929 831
                return $this;
930
            }
931
        }
932
933 1424
        $this->_addIndex($indexCandidate);
934 1424
        $this->implicitIndexes[$this->normalizeIdentifier($indexName)] = $indexCandidate;
935
936 1424
        return $this;
937
    }
938
939
    /**
940
     * @param string[] $columnNames
941
     * @param string   $indexName
942
     * @param bool     $isUnique
943
     * @param bool     $isPrimary
944
     * @param string[] $flags
945
     * @param mixed[]  $options
946
     *
947
     * @return Index
948
     *
949
     * @throws SchemaException
950
     */
951 8259
    private function _createIndex(array $columnNames, $indexName, $isUnique, $isPrimary, array $flags = [], array $options = [])
952
    {
953 8259
        if (preg_match('(([^a-zA-Z0-9_]+))', $this->normalizeIdentifier($indexName)) === 1) {
954 22
            throw SchemaException::indexNameInvalid($indexName);
955
        }
956
957 8237
        foreach ($columnNames as $columnName) {
958 8215
            if (! $this->hasColumn($columnName)) {
959 22
                throw SchemaException::columnDoesNotExist($columnName, $this->_name);
960
            }
961
        }
962
963 8215
        return new Index($indexName, $columnNames, $isUnique, $isPrimary, $flags, $options);
964
    }
965
966
    /**
967
     * @param string[] $columnNames
968
     * @param string[] $flags
969
     * @param mixed[]  $options
970
     *
971
     * @throws SchemaException
972
     */
973
    private function _createUniqueConstraint(array $columnNames, string $indexName, array $flags = [], array $options = []) : UniqueConstraint
974
    {
975
        if (preg_match('(([^a-zA-Z0-9_]+))', $this->normalizeIdentifier($indexName)) === 1) {
976
            throw SchemaException::indexNameInvalid($indexName);
977
        }
978
979
        foreach ($columnNames as $columnName => $indexColOptions) {
980
            if (is_numeric($columnName) && is_string($indexColOptions)) {
981
                $columnName = $indexColOptions;
982
            }
983
984
            if (! $this->hasColumn($columnName)) {
985
                throw SchemaException::columnDoesNotExist($columnName, $this->_name);
986
            }
987
        }
988
989
        return new UniqueConstraint($indexName, $columnNames, $flags, $options);
990
    }
991
992
    /**
993
     * Normalizes a given identifier.
994
     *
995
     * Trims quotes and lowercases the given identifier.
996
     *
997
     * @return string The normalized identifier.
998
     */
999 12705
    private function normalizeIdentifier(?string $identifier) : string
1000
    {
1001 12705
        if ($identifier === null) {
1002 22
            return '';
1003
        }
1004
1005 12705
        return $this->trimQuotes(strtolower($identifier));
1006
    }
1007
1008
    /**
1009
     * Returns only columns that have specified names
1010
     *
1011
     * @param string[] $columnNames
1012
     *
1013
     * @return Column[]
1014
     */
1015 9221
    private function filterColumns(array $columnNames, bool $reverse = false) : array
1016
    {
1017
        return array_filter($this->_columns, static function ($columnName) use ($columnNames, $reverse) : bool {
1018 8825
            return in_array($columnName, $columnNames, true) !== $reverse;
1019 9221
        }, ARRAY_FILTER_USE_KEY);
1020
    }
1021
}
1022