Completed
Pull Request — 3.0.x (#3980)
by Guilherme
65:33
created

Table::addUniqueConstraint()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 11
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 2

Importance

Changes 0
Metric Value
eloc 6
dl 0
loc 11
ccs 3
cts 3
cp 1
rs 10
c 0
b 0
f 0
cc 2
nc 2
nop 4
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 14477
     * @param UniqueConstraint[]     $uniqueConstraints
55
     * @param ForeignKeyConstraint[] $fkConstraints
56 14477
     * @param mixed[]                $options
57 22
     *
58
     * @throws DBALException
59
     */
60 14455
    public function __construct(
61
        $tableName,
62 14455
        array $columns = [],
63 2062
        array $indexes = [],
64
        array $uniqueConstraints = [],
65
        array $fkConstraints = [],
66 14433
        array $options = []
67 654
    ) {
68
        if (strlen($tableName) === 0) {
69
            throw DBALException::invalidTableName($tableName);
70 14389
        }
71 253
72
        $this->_setName($tableName);
73
74 14389
        foreach ($columns as $column) {
75 14389
            $this->_addColumn($column);
76
        }
77
78
        foreach ($indexes as $idx) {
79
            $this->_addIndex($idx);
80 1443
        }
81
82 1443
        foreach ($uniqueConstraints as $uniqueConstraint) {
83 1443
            $this->_addUniqueConstraint($uniqueConstraint);
84
        }
85
86
        foreach ($fkConstraints as $constraint) {
87
            $this->_addForeignKeyConstraint($constraint);
88 2628
        }
89
90 2628
        $this->_options = array_merge($this->_options, $options);
91 227
    }
92
93
    /**
94 2443
     * @return void
95
     */
96
    public function setSchemaConfig(SchemaConfig $schemaConfig)
97
    {
98
        $this->_schemaConfig = $schemaConfig;
99
    }
100
101
    /**
102
     * Sets the Primary Key.
103
     *
104
     * @param string[]     $columnNames
105 5827
     * @param string|false $indexName
106
     *
107 5827
     * @return self
108 5805
     */
109
    public function setPrimaryKey(array $columnNames, $indexName = false)
110
    {
111 5827
        if ($indexName === false) {
112
            $indexName = 'primary';
113 5827
        }
114 5827
115 5827
        $this->_addIndex($this->_createIndex($columnNames, $indexName, true, true));
116
117
        foreach ($columnNames as $columnName) {
118 5827
            $column = $this->getColumn($columnName);
119
            $column->setNotnull(true);
120
        }
121
122
        return $this;
123
    }
124
125
    /**
126
     * @param string[] $columnNames
127
     * @param string[] $flags
128
     * @param mixed[]  $options
129 1815
     *
130
     * @return self
131 1815
     */
132 393
    public function addIndex(array $columnNames, ?string $indexName = null, array $flags = [], array $options = [])
133 393
    {
134 393
        if ($indexName === null) {
135 393
            $indexName = $this->_generateIdentifierName(
136
                array_merge([$this->getName()], $columnNames),
137
                'idx',
138
                $this->_getMaxIdentifierLength()
139 1815
            );
140
        }
141
142
        return $this->_addIndex($this->_createIndex($columnNames, $indexName, false, false, $flags, $options));
143
    }
144
145
    /**
146
     * @param string[] $columnNames
147 398
     * @param string[] $flags
148
     * @param mixed[]  $options
149 398
     *
150
     * @return self
151
     */
152
    public function addUniqueConstraint(array $columnNames, ?string $indexName = null, array $flags = [], array $options = [])
153 398
    {
154 398
        if ($indexName === null) {
155 398
            $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 674
     * Drops the primary key from this table.
167
     *
168 674
     * @return void
169 674
     */
170
    public function dropPrimaryKey()
171
    {
172 674
        if ($this->_primaryKeyName === null) {
173 674
            return;
174
        }
175
176
        $this->dropIndex($this->_primaryKeyName);
177
        $this->_primaryKeyName = null;
178
    }
179
180
    /**
181
     * Drops an index from this table.
182 543
     *
183
     * @param string $indexName The index name.
184 543
     *
185 361
     * @return void
186 361
     *
187 361
     * @throws SchemaException If the index does not exist.
188 361
     */
189
    public function dropIndex($indexName)
190
    {
191
        $indexName = $this->normalizeIdentifier($indexName);
192 543
193
        if (! $this->hasIndex($indexName)) {
194
            throw SchemaException::indexDoesNotExist($indexName, $this->_name);
195
        }
196
197
        unset($this->_indexes[$indexName]);
198
    }
199
200
    /**
201
     * @param string[]    $columnNames
202
     * @param string|null $indexName
203
     * @param mixed[]     $options
204
     *
205
     * @return self
206
     */
207 307
    public function addUniqueIndex(array $columnNames, $indexName = null, array $options = [])
208
    {
209 307
        if ($indexName === null) {
210 307
            $indexName = $this->_generateIdentifierName(
211
                array_merge([$this->getName()], $columnNames),
212 307
                'uniq',
213 198
                $this->_getMaxIdentifierLength()
214
            );
215
        }
216 307
217 22
        return $this->_addIndex($this->_createIndex($columnNames, $indexName, true, false, [], $options));
218
    }
219
220 285
    /**
221 22
     * Renames an index.
222
     *
223
     * @param string      $oldIndexName The name of the index to rename from.
224 263
     * @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 263
     *
227 22
     * @return self This table instance.
228
     *
229 22
     * @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 263
    public function renameIndex($oldIndexName, $newIndexName = null)
233
    {
234 263
        $oldIndexName           = $this->normalizeIdentifier($oldIndexName);
235 44
        $normalizedNewIndexName = $this->normalizeIdentifier($newIndexName);
236
237
        if ($oldIndexName === $normalizedNewIndexName) {
238 241
            return $this;
239
        }
240
241
        if (! $this->hasIndex($oldIndexName)) {
242
            throw SchemaException::indexDoesNotExist($oldIndexName, $this->_name);
243
        }
244
245
        if ($this->hasIndex($normalizedNewIndexName)) {
246
            throw SchemaException::indexAlreadyExists($normalizedNewIndexName, $this->_name);
247
        }
248 43
249
        $oldIndex = $this->_indexes[$oldIndexName];
250 43
251
        if ($oldIndex->isPrimary()) {
252 43
            $this->dropPrimaryKey();
253 43
254
            return $this->setPrimaryKey($oldIndex->getColumns(), $newIndexName ?? false);
255
        }
256
257
        unset($this->_indexes[$oldIndexName]);
258
259
        if ($oldIndex->isUnique()) {
260
            return $this->addUniqueIndex($oldIndex->getColumns(), $newIndexName, $oldIndex->getOptions());
261
        }
262
263
        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 8259
     */
273
    public function columnsAreIndexed(array $columnNames)
274 8259
    {
275 22
        foreach ($this->getIndexes() as $index) {
276
            /** @var $index Index */
277
            if ($index->spansColumns($columnNames)) {
278 8237
                return true;
279 8215
            }
280 22
        }
281
282
        return false;
283
    }
284 8215
285
    /**
286
     * @param string  $columnName
287
     * @param string  $typeName
288
     * @param mixed[] $options
289
     *
290
     * @return Column
291
     */
292
    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 11636
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 265
334
        $column->setOptions($options);
335 265
336
        return $this;
337
    }
338
339
    /**
340
     * Drops a Column from the Table.
341
     *
342
     * @param string $columnName
343
     *
344
     * @return self
345 198
     */
346
    public function dropColumn($columnName)
347 198
    {
348 198
        $columnName = $this->normalizeIdentifier($columnName);
349
350 198
        unset($this->_columns[$columnName]);
351
352
        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 1812
     * @return self
367
     */
368 1812
    public function addForeignKeyConstraint($foreignTable, array $localColumnNames, array $foreignColumnNames, array $options = [], $constraintName = null)
369 954
    {
370
        if ($constraintName === null) {
371
            $constraintName = $this->_generateIdentifierName(array_merge((array) $this->getName(), $localColumnNames), 'fk', $this->_getMaxIdentifierLength());
372 1812
        }
373
374
        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 71
     * @return self
390
     */
391 71
    public function addUnnamedForeignKeyConstraint($foreignTable, array $localColumnNames, array $foreignColumnNames, array $options = [])
392
    {
393
        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 1834
     * @throws SchemaException
410
     */
411 1834
    public function addNamedForeignKeyConstraint($name, $foreignTable, array $localColumnNames, array $foreignColumnNames, array $options = [])
412 923
    {
413 923
        if ($foreignTable instanceof Table) {
414 22
            foreach ($foreignColumnNames as $columnName) {
415
                if (! $foreignTable->hasColumn($columnName)) {
416
                    throw SchemaException::columnDoesNotExist($columnName, $foreignTable->getName());
417
                }
418
            }
419 1812
        }
420 1812
421 22
        foreach ($localColumnNames as $columnName) {
422
            if (! $this->hasColumn($columnName)) {
423
                throw SchemaException::columnDoesNotExist($columnName, $this->_name);
424
            }
425 1790
        }
426 1790
427
        $constraint = new ForeignKeyConstraint(
428
            $localColumnNames,
429
            $foreignTable,
430
            $foreignColumnNames,
431
            $name,
432 1790
            $options
433
        );
434 1790
435
        return $this->_addForeignKeyConstraint($constraint);
436
    }
437
438
    /**
439
     * @param string $name
440
     * @param mixed  $value
441
     *
442
     * @return self
443 1737
     */
444
    public function addOption($name, $value)
445 1737
    {
446
        $this->_options[$name] = $value;
447 1737
448
        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 12617
     *
456
     * @return bool
457 12617
     */
458 12617
    public function hasForeignKey($constraintName)
459
    {
460 12617
        $constraintName = $this->normalizeIdentifier($constraintName);
461 22
462
        return isset($this->_fkConstraints[$constraintName]);
463
    }
464 12617
465 12617
    /**
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 8386
    public function getForeignKey($constraintName)
475
    {
476 8386
        $constraintName = $this->normalizeIdentifier($constraintName);
477 8386
478 8386
        if (! $this->hasForeignKey($constraintName)) {
479
            throw SchemaException::foreignKeyDoesNotExist($constraintName, $this->_name);
480 8386
        }
481 550
482 462
        return $this->_fkConstraints[$constraintName];
483
    }
484
485 88
    /**
486
     * Removes the foreign key constraint with the given name.
487
     *
488 8386
     * @param string $constraintName The constraint name.
489 8386
     *
490
     * @return void
491 44
     *
492
     * @throws SchemaException
493
     */
494 8386
    public function removeForeignKey($constraintName)
495 88
    {
496
        $constraintName = $this->normalizeIdentifier($constraintName);
497
498 8386
        if (! $this->hasForeignKey($constraintName)) {
499 5893
            throw SchemaException::foreignKeyDoesNotExist($constraintName, $this->_name);
500
        }
501
502 8386
        unset($this->_fkConstraints[$constraintName]);
503
    }
504 8386
505
    /**
506
     * Returns whether this table has a unique constraint with the given name.
507
     *
508
     * @param string $constraintName
509
     *
510 1893
     * @return bool
511
     */
512 1893
    public function hasUniqueConstraint($constraintName)
513
    {
514 1893
        $constraintName = $this->normalizeIdentifier($constraintName);
515 1870
516
        return isset($this->_uniqueConstraints[$constraintName]);
517 23
    }
518 23
519 23
    /**
520 23
     * Returns the unique constraint with the given name.
521
     *
522
     * @param string $constraintName The constraint name.
523 1893
     *
524
     * @return UniqueConstraint
525 1893
     *
526
     * @throws SchemaException If the unique constraint does not exist.
527
     */
528
    public function getUniqueConstraint($constraintName)
529
    {
530 1893
        $constraintName = $this->normalizeIdentifier($constraintName);
531 1893
532 1893
        if (! $this->hasForeignKey($constraintName)) {
533 1893
            throw SchemaException::uniqueConstraintDoesNotExist($constraintName, $this->_name);
534
        }
535 1893
536
        return $this->_uniqueConstraints[$constraintName];
537 1893
    }
538 1314
539 831
    /**
540
     * Removes the unique constraint with the given name.
541
     *
542
     * @param string $constraintName The constraint name.
543 1424
     *
544 1424
     * @return void
545 1424
     *
546
     * @throws SchemaException
547
     */
548
    public function removeUniqueConstraint($constraintName)
549
    {
550
        $constraintName = $this->normalizeIdentifier($constraintName);
551
552
        if (! $this->hasForeignKey($constraintName)) {
553
            throw SchemaException::uniqueConstraintDoesNotExist($constraintName, $this->_name);
554 242
        }
555
556 242
        unset($this->_uniqueConstraints[$constraintName]);
557
    }
558 242
559
    /**
560
     * Returns ordered list of columns (primary keys are first, then foreign keys, then the rest)
561
     *
562
     * @return Column[]
563
     */
564
    public function getColumns()
565
    {
566
        $primaryKeyColumns = $this->hasPrimaryKey() ? $this->getPrimaryKeyColumns() : [];
567
        $foreignKeyColumns = $this->getForeignKeyColumns();
568
        $remainderColumns  = $this->filterColumns(
569
            array_merge(array_keys($primaryKeyColumns), array_keys($foreignKeyColumns)),
570 177
            true
571
        );
572 177
573 177
        return array_merge($primaryKeyColumns, $foreignKeyColumns, $remainderColumns);
574
    }
575
576
    /**
577 177
     * Returns whether this table has a Column with the given name.
578
     *
579
     * @param string $columnName The column name.
580
     *
581
     * @return bool
582
     */
583
    public function hasColumn($columnName)
584
    {
585
        $columnName = $this->normalizeIdentifier($columnName);
586
587
        return isset($this->_columns[$columnName]);
588
    }
589 220
590
    /**
591 220
     * Returns the Column with the given name.
592 220
     *
593
     * @param string $columnName The column name.
594
     *
595
     * @return Column
596 220
     *
597 220
     * @throws SchemaException If the column does not exist.
598
     */
599
    public function getColumn($columnName)
600
    {
601
        $columnName = $this->normalizeIdentifier($columnName);
602
603
        if (! $this->hasColumn($columnName)) {
604 9221
            throw SchemaException::columnDoesNotExist($columnName, $this->_name);
605
        }
606 9221
607 9221
        return $this->_columns[$columnName];
608
    }
609 9221
610 4336
    /**
611
     * Returns the primary key.
612
     *
613 9221
     * @return Index|null The primary key, or null if this Table has no primary key.
614
     */
615
    public function getPrimaryKey()
616
    {
617
        return $this->_primaryKeyName !== null
618
            ? $this->getIndex($this->_primaryKeyName)
619
            : null;
620
    }
621 9221
622
    /**
623 9221
     * Returns the primary key columns.
624 9221
     *
625 894
     * @return Column[]
626
     *
627
     * @throws DBALException
628 9221
     */
629
    public function getPrimaryKeyColumns()
630
    {
631
        $primaryKey = $this->getPrimaryKey();
632
633
        if ($primaryKey === null) {
634
            throw new DBALException('Table ' . $this->getName() . ' has no primary key.');
635
        }
636
637
        return $this->filterColumns($primaryKey->getColumns());
638 9221
    }
639
640
    /**
641 8825
     * Returns the foreign key columns
642 9221
     *
643
     * @return Column[]
644
     */
645
    public function getForeignKeyColumns()
646
    {
647
        $foreignKeyColumns = [];
648
649
        foreach ($this->getForeignKeys() as $foreignKey) {
650
            $foreignKeyColumns = array_merge($foreignKeyColumns, $foreignKey->getLocalColumns());
651
        }
652 10055
653
        return $this->filterColumns($foreignKeyColumns);
654 10055
    }
655
656 10055
    /**
657
     * Returns whether this table has a primary key.
658
     *
659
     * @return bool
660
     */
661
    public function hasPrimaryKey()
662
    {
663
        return $this->_primaryKeyName !== null && $this->hasIndex($this->_primaryKeyName);
664
    }
665
666
    /**
667
     * Returns whether this table has an Index with the given name.
668 7958
     *
669
     * @param string $indexName The index name.
670 7958
     *
671 7958
     * @return bool
672 22
     */
673
    public function hasIndex($indexName)
674
    {
675 7936
        $indexName = $this->normalizeIdentifier($indexName);
676
677
        return isset($this->_indexes[$indexName]);
678
    }
679
680
    /**
681
     * Returns the Index with the given name.
682
     *
683 9331
     * @param string $indexName The index name.
684
     *
685 9331
     * @return Index
686 4446
     *
687
     * @throws SchemaException If the index does not exist.
688
     */
689 5262
    public function getIndex($indexName)
690
    {
691
        $indexName = $this->normalizeIdentifier($indexName);
692
        if (! $this->hasIndex($indexName)) {
693
            throw SchemaException::indexDoesNotExist($indexName, $this->_name);
694
        }
695
696
        return $this->_indexes[$indexName];
697
    }
698
699 310
    /**
700
     * @return Index[]
701 310
     */
702
    public function getIndexes()
703 310
    {
704
        return $this->_indexes;
705
    }
706
707 310
    /**
708
     * Returns the unique constraints.
709
     *
710
     * @return UniqueConstraint[]
711
     */
712
    public function getUniqueConstraints()
713
    {
714
        return $this->_uniqueConstraints;
715 939
    }
716
717 939
    /**
718
     * Returns the foreign key constraints.
719
     *
720
     * @return ForeignKeyConstraint[]
721
     */
722
    public function getForeignKeys()
723
    {
724
        return $this->_fkConstraints;
725
    }
726
727 5619
    /**
728
     * @param string $name
729 5619
     *
730
     * @return bool
731 5619
     */
732
    public function hasOption($name)
733
    {
734
        return isset($this->_options[$name]);
735
    }
736
737
    /**
738
     * @param string $name
739
     *
740
     * @return mixed
741
     */
742
    public function getOption($name)
743 5135
    {
744
        return $this->_options[$name];
745 5135
    }
746 5135
747 22
    /**
748
     * @return mixed[]
749
     */
750 5113
    public function getOptions()
751
    {
752
        return $this->_options;
753
    }
754
755
    /**
756 9023
     * @return void
757
     */
758 9023
    public function visit(Visitor $visitor)
759
    {
760
        $visitor->acceptTable($this);
761
762
        foreach ($this->getColumns() as $column) {
763
            $visitor->acceptColumn($this, $column);
764
        }
765
766 9617
        foreach ($this->getIndexes() as $index) {
767
            $visitor->acceptIndex($this, $index);
768 9617
        }
769
770
        foreach ($this->getForeignKeys() as $constraint) {
771
            $visitor->acceptForeignKey($this, $constraint);
772
        }
773
    }
774
775
    /**
776 4399
     * Clone of a Table triggers a deep clone of all affected assets.
777
     *
778 4399
     * @return void
779
     */
780
    public function __clone()
781
    {
782
        foreach ($this->_columns as $k => $column) {
783
            $this->_columns[$k] = clone $column;
784
        }
785
        foreach ($this->_indexes as $k => $index) {
786 221
            $this->_indexes[$k] = clone $index;
787
        }
788 221
        foreach ($this->_fkConstraints as $k => $fk) {
789
            $this->_fkConstraints[$k] = clone $fk;
790
            $this->_fkConstraints[$k]->setLocalTable($this);
791
        }
792
    }
793
794 6995
    public function setComment(?string $comment) : self
795
    {
796 6995
        // For keeping backward compatibility with MySQL in previous releases, table comments are stored as options.
797
        $this->addOption('comment', $comment);
798
799
        return $this;
800
    }
801
802 342
    public function getComment() : ?string
803
    {
804 342
        return $this->_options['comment'] ?? null;
805
    }
806 342
807 276
    /**
808
     * @return int
809
     */
810 342
    protected function _getMaxIdentifierLength()
811 192
    {
812
        if ($this->_schemaConfig instanceof SchemaConfig) {
813
            return $this->_schemaConfig->getMaxIdentifierLength();
814 342
        }
815 88
816
        return 63;
817 342
    }
818
819
    /**
820
     * @return void
821
     *
822
     * @throws SchemaException
823
     */
824 1052
    protected function _addColumn(Column $column)
825
    {
826 1052
        $columnName = $column->getName();
827 1030
        $columnName = $this->normalizeIdentifier($columnName);
828
829 1052
        if (isset($this->_columns[$columnName])) {
830 742
            throw SchemaException::columnAlreadyExists($this->getName(), $columnName);
831
        }
832 1052
833 175
        $this->_columns[$columnName] = $column;
834 175
    }
835
836 1052
    /**
837
     * Adds an index to the table.
838
     *
839
     * @return self
840
     *
841
     * @throws SchemaException
842
     */
843
    protected function _addIndex(Index $indexCandidate)
844
    {
845
        $indexName               = $indexCandidate->getName();
846
        $indexName               = $this->normalizeIdentifier($indexName);
847 12705
        $replacedImplicitIndexes = [];
848
849 12705
        foreach ($this->implicitIndexes as $name => $implicitIndex) {
850 22
            if (! $implicitIndex->isFullfilledBy($indexCandidate) || ! isset($this->_indexes[$name])) {
851
                continue;
852
            }
853 12705
854
            $replacedImplicitIndexes[] = $name;
855
        }
856 44
857
        if ((isset($this->_indexes[$indexName]) && ! in_array($indexName, $replacedImplicitIndexes, true)) ||
858
            ($this->_primaryKeyName !== null && $indexCandidate->isPrimary())
859 44
        ) {
860
            throw SchemaException::indexAlreadyExists($indexName, $this->_name);
861 44
        }
862
863
        foreach ($replacedImplicitIndexes as $name) {
864 44
            unset($this->_indexes[$name], $this->implicitIndexes[$name]);
865
        }
866 44
867
        if ($indexCandidate->isPrimary()) {
868
            $this->_primaryKeyName = $indexName;
869
        }
870
871
        $this->_indexes[$indexName] = $indexCandidate;
872
873
        return $this;
874
    }
875
876
    /**
877
     * @return self
878
     */
879
    protected function _addUniqueConstraint(UniqueConstraint $constraint)
880
    {
881
        $mergedNames = array_merge([$this->getName()], $constraint->getColumns());
882
        $name        = strlen($constraint->getName()) > 0
883
            ? $constraint->getName()
884
            : $this->_generateIdentifierName($mergedNames, 'fk', $this->_getMaxIdentifierLength());
885
886
        $name = $this->normalizeIdentifier($name);
887
888
        $this->_uniqueConstraints[$name] = $constraint;
889
890
        // If there is already an index that fulfills this requirements drop the request. In the case of __construct
891
        // calling this method during hydration from schema-details all the explicitly added indexes lead to duplicates.
892
        // This creates computation overhead in this case, however no duplicate indexes are ever added (column based).
893
        $indexName = $this->_generateIdentifierName($mergedNames, 'idx', $this->_getMaxIdentifierLength());
894
895
        $indexCandidate = $this->_createIndex($constraint->getColumns(), $indexName, true, false);
896
897
        foreach ($this->_indexes as $existingIndex) {
898
            if ($indexCandidate->isFullfilledBy($existingIndex)) {
899
                return $this;
900
            }
901
        }
902
903
        $this->implicitIndexes[$this->normalizeIdentifier($indexName)] = $indexCandidate;
904
905
        return $this;
906
    }
907
908
    /**
909
     * @return self
910
     */
911
    protected function _addForeignKeyConstraint(ForeignKeyConstraint $constraint)
912
    {
913
        $constraint->setLocalTable($this);
914
915
        if (strlen($constraint->getName()) > 0) {
916
            $name = $constraint->getName();
917
        } else {
918
            $name = $this->_generateIdentifierName(
919
                array_merge([$this->getName()], $constraint->getLocalColumns()),
920
                'fk',
921
                $this->_getMaxIdentifierLength()
922
            );
923
        }
924
925
        $name = $this->normalizeIdentifier($name);
926
927
        $this->_fkConstraints[$name] = $constraint;
928
929
        // add an explicit index on the foreign key columns. If there is already an index that fulfils this requirements drop the request.
930
        // In the case of __construct calling this method during hydration from schema-details all the explicitly added indexes
931
        // lead to duplicates. This creates computation overhead in this case, however no duplicate indexes are ever added (based on columns).
932
        $indexName      = $this->_generateIdentifierName(
933
            array_merge([$this->getName()], $constraint->getColumns()),
934
            'idx',
935
            $this->_getMaxIdentifierLength()
936
        );
937
        $indexCandidate = $this->_createIndex($constraint->getColumns(), $indexName, false, false);
938
939
        foreach ($this->_indexes as $existingIndex) {
940
            if ($indexCandidate->isFullfilledBy($existingIndex)) {
941
                return $this;
942
            }
943
        }
944
945
        $this->_addIndex($indexCandidate);
946
        $this->implicitIndexes[$this->normalizeIdentifier($indexName)] = $indexCandidate;
947
948
        return $this;
949
    }
950
951
    /**
952
     * @param string[] $columnNames
953
     * @param string   $indexName
954
     * @param bool     $isUnique
955
     * @param bool     $isPrimary
956
     * @param string[] $flags
957
     * @param mixed[]  $options
958
     *
959
     * @return Index
960
     *
961
     * @throws SchemaException
962
     */
963
    private function _createIndex(array $columnNames, $indexName, $isUnique, $isPrimary, array $flags = [], array $options = [])
964
    {
965
        if (preg_match('(([^a-zA-Z0-9_]+))', $this->normalizeIdentifier($indexName)) === 1) {
966
            throw SchemaException::indexNameInvalid($indexName);
967
        }
968
969
        foreach ($columnNames as $columnName) {
970
            if (! $this->hasColumn($columnName)) {
971
                throw SchemaException::columnDoesNotExist($columnName, $this->_name);
972
            }
973
        }
974
975
        return new Index($indexName, $columnNames, $isUnique, $isPrimary, $flags, $options);
976
    }
977
978
    /**
979
     * @param string[] $columnNames
980
     * @param string[] $flags
981
     * @param mixed[]  $options
982
     *
983
     * @return UniqueConstraint
984
     *
985
     * @throws SchemaException
986
     */
987
    private function _createUniqueConstraint(array $columnNames, string $indexName, array $flags = [], array $options = [])
988
    {
989
        if (preg_match('(([^a-zA-Z0-9_]+))', $this->normalizeIdentifier($indexName)) === 1) {
990
            throw SchemaException::indexNameInvalid($indexName);
991
        }
992
993
        foreach ($columnNames as $columnName => $indexColOptions) {
994
            if (is_numeric($columnName) && is_string($indexColOptions)) {
995
                $columnName = $indexColOptions;
996
            }
997
998
            if (! $this->hasColumn($columnName)) {
999
                throw SchemaException::columnDoesNotExist($columnName, $this->_name);
1000
            }
1001
        }
1002
1003
        return new UniqueConstraint($indexName, $columnNames, $flags, $options);
1004
    }
1005
1006
    /**
1007
     * Normalizes a given identifier.
1008
     *
1009
     * Trims quotes and lowercases the given identifier.
1010
     *
1011
     * @return string The normalized identifier.
1012
     */
1013
    private function normalizeIdentifier(?string $identifier)
1014
    {
1015
        if ($identifier === null) {
1016
            return '';
1017
        }
1018
1019
        return $this->trimQuotes(strtolower($identifier));
1020
    }
1021
1022
    /**
1023
     * Returns only columns that have specified names
1024
     *
1025
     * @param string[] $columnNames
1026
     *
1027
     * @return Column[]
1028
     */
1029
    private function filterColumns(array $columnNames, bool $reverse = false)
1030
    {
1031
        return array_filter($this->_columns, static function ($columnName) use ($columnNames, $reverse) : bool {
1032
            return in_array($columnName, $columnNames, true) !== $reverse;
1033
        }, ARRAY_FILTER_USE_KEY);
1034
    }
1035
}
1036