Failed Conditions
Pull Request — develop (#3525)
by Jonathan
12:46
created

Table::__construct()   A

Complexity

Conditions 6
Paths 17

Size

Total Lines 31
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 6

Importance

Changes 0
Metric Value
eloc 12
dl 0
loc 31
rs 9.2222
c 0
b 0
f 0
ccs 13
cts 13
cp 1
cc 6
nc 17
nop 6
crap 6
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Doctrine\DBAL\Schema;
6
7
use Doctrine\DBAL\DBALException;
8
use Doctrine\DBAL\Schema\Exception\ColumnAlreadyExists;
9
use Doctrine\DBAL\Schema\Exception\ColumnDoesNotExist;
10
use Doctrine\DBAL\Schema\Exception\ForeignKeyDoesNotExist;
11
use Doctrine\DBAL\Schema\Exception\IndexAlreadyExists;
12
use Doctrine\DBAL\Schema\Exception\IndexDoesNotExist;
13
use Doctrine\DBAL\Schema\Exception\IndexNameInvalid;
14
use Doctrine\DBAL\Schema\Exception\InvalidTableName;
15
use Doctrine\DBAL\Schema\Exception\UniqueConstraintDoesNotExist;
16
use Doctrine\DBAL\Schema\Visitor\Visitor;
17
use Doctrine\DBAL\Types\Type;
18
use function array_keys;
19
use function array_merge;
20
use function array_search;
21
use function array_unique;
22
use function in_array;
23
use function is_string;
24
use function preg_match;
25
use function strlen;
26
use function strtolower;
27
use function uksort;
28
29
/**
30
 * Object Representation of a table.
31
 */
32
class Table extends AbstractAsset
33
{
34
    /** @var Column[] */
35
    protected $_columns = [];
36
37
    /** @var Index[] */
38
    private $implicitIndexes = [];
39
40
    /** @var Index[] */
41
    protected $_indexes = [];
42
43
    /** @var string */
44
    protected $_primaryKeyName = false;
45
46
    /** @var UniqueConstraint[] */
47
    protected $_uniqueConstraints = [];
48
49
    /** @var ForeignKeyConstraint[] */
50
    protected $_fkConstraints = [];
51
52
    /** @var mixed[] */
53
    protected $_options = [];
54
55
    /** @var SchemaConfig|null */
56
    protected $_schemaConfig = null;
57
58
    /**
59
     * @param string                 $tableName
60
     * @param Column[]               $columns
61
     * @param Index[]                $indexes
62
     * @param UniqueConstraint[]     $uniqueConstraints
63
     * @param ForeignKeyConstraint[] $fkConstraints
64
     * @param mixed[]                $options
65
     *
66
     * @throws DBALException
67
     */
68 19143
    public function __construct(
69
        $tableName,
70
        array $columns = [],
71
        array $indexes = [],
72
        array $uniqueConstraints = [],
73
        array $fkConstraints = [],
74
        array $options = []
75
    ) {
76 19143
        if (strlen($tableName) === 0) {
77 1627
            throw InvalidTableName::new($tableName);
78
        }
79
80 19141
        $this->_setName($tableName);
81
82 19141
        foreach ($columns as $column) {
83 17019
            $this->_addColumn($column);
84
        }
85
86 19139
        foreach ($indexes as $idx) {
87 16921
            $this->_addIndex($idx);
88
        }
89
90 19135
        foreach ($uniqueConstraints as $uniqueConstraint) {
91 202
            $this->_addUniqueConstraint($uniqueConstraint);
92
        }
93
94 19135
        foreach ($fkConstraints as $fkConstraint) {
95 15904
            $this->_addForeignKeyConstraint($fkConstraint);
96
        }
97
98 19135
        $this->_options = $options;
99 19135
    }
100
101
    /**
102
     * @return void
103
     */
104 17596
    public function setSchemaConfig(SchemaConfig $schemaConfig)
105
    {
106 17596
        $this->_schemaConfig = $schemaConfig;
107 17596
    }
108
109
    /**
110
     * Sets the Primary Key.
111
     *
112
     * @param string[]     $columnNames
113
     * @param string|false $indexName
114
     *
115
     * @return self
116
     */
117 18341
    public function setPrimaryKey(array $columnNames, $indexName = false)
118
    {
119 18341
        $this->_addIndex($this->_createIndex($columnNames, $indexName ?: 'primary', true, true));
120
121 18341
        foreach ($columnNames as $columnName) {
122 18341
            $column = $this->getColumn($columnName);
123 18341
            $column->setNotnull(true);
124
        }
125
126 18341
        return $this;
127
    }
128
129
    /**
130
     * @param mixed[]     $columnNames
131
     * @param string|null $indexName
132
     * @param string[]    $flags
133
     * @param mixed[]     $options
134
     *
135
     * @return self
136
     */
137
    public function addUniqueConstraint(array $columnNames, $indexName = null, array $flags = [], array $options = [])
138
    {
139
        if ($indexName === null) {
140
            $indexName = $this->_generateIdentifierName(
141
                array_merge([$this->getName()], $columnNames),
142
                'uniq',
143
                $this->_getMaxIdentifierLength()
144
            );
145
        }
146
147
        return $this->_addUniqueConstraint($this->_createUniqueConstraint($columnNames, $indexName, $flags, $options));
148
    }
149
150
    /**
151
     * @param string[]    $columnNames
152
     * @param string|null $indexName
153
     * @param string[]    $flags
154
     * @param mixed[]     $options
155
     *
156
     * @return self
157
     */
158 16544
    public function addIndex(array $columnNames, $indexName = null, array $flags = [], array $options = [])
159
    {
160 16544
        if ($indexName === null) {
161 16161
            $indexName = $this->_generateIdentifierName(
162 16161
                array_merge([$this->getName()], $columnNames),
163 16161
                'idx',
164 16161
                $this->_getMaxIdentifierLength()
165
            );
166
        }
167
168 16544
        return $this->_addIndex($this->_createIndex($columnNames, $indexName, false, false, $flags, $options));
169
    }
170
171
    /**
172
     * Drops the primary key from this table.
173
     *
174
     * @return void
175
     */
176 15982
    public function dropPrimaryKey()
177
    {
178 15982
        $this->dropIndex($this->_primaryKeyName);
179 15982
        $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...
180 15982
    }
181
182
    /**
183
     * Drops an index from this table.
184
     *
185
     * @param string $indexName The index name.
186
     *
187
     * @return void
188
     *
189
     * @throws SchemaException If the index does not exist.
190
     */
191 16014
    public function dropIndex($indexName)
192
    {
193 16014
        $indexName = $this->normalizeIdentifier($indexName);
194
195 16014
        if (! $this->hasIndex($indexName)) {
196
            throw IndexDoesNotExist::new($indexName, $this->_name);
197
        }
198
199 16014
        unset($this->_indexes[$indexName]);
200 16014
    }
201
202
    /**
203
     * @param string[]    $columnNames
204
     * @param string|null $indexName
205
     * @param mixed[]     $options
206
     *
207
     * @return self
208
     */
209 17441
    public function addUniqueIndex(array $columnNames, $indexName = null, array $options = [])
210
    {
211 17441
        if ($indexName === null) {
212 17423
            $indexName = $this->_generateIdentifierName(
213 17423
                array_merge([$this->getName()], $columnNames),
214 17423
                'uniq',
215 17423
                $this->_getMaxIdentifierLength()
216
            );
217
        }
218
219 17441
        return $this->_addIndex($this->_createIndex($columnNames, $indexName, true, false, [], $options));
220
    }
221
222
    /**
223
     * Renames an index.
224
     *
225
     * @param string      $oldIndexName The name of the index to rename from.
226
     * @param string|null $newIndexName The name of the index to rename to.
227
     *                                  If null is given, the index name will be auto-generated.
228
     *
229
     * @return self This table instance.
230
     *
231
     * @throws SchemaException If no index exists for the given current name
232
     *                         or if an index with the given new name already exists on this table.
233
     */
234 14411
    public function renameIndex($oldIndexName, $newIndexName = null)
235
    {
236 14411
        $oldIndexName           = $this->normalizeIdentifier($oldIndexName);
237 14411
        $normalizedNewIndexName = $this->normalizeIdentifier($newIndexName);
238
239 14411
        if ($oldIndexName === $normalizedNewIndexName) {
240 493
            return $this;
241
        }
242
243 14411
        if (! $this->hasIndex($oldIndexName)) {
244 402
            throw IndexDoesNotExist::new($oldIndexName, $this->_name);
245
        }
246
247 14409
        if ($this->hasIndex($normalizedNewIndexName)) {
248 377
            throw IndexAlreadyExists::new($normalizedNewIndexName, $this->_name);
249
        }
250
251 14407
        $oldIndex = $this->_indexes[$oldIndexName];
252
253 14407
        if ($oldIndex->isPrimary()) {
254 477
            $this->dropPrimaryKey();
255
256 477
            return $this->setPrimaryKey($oldIndex->getColumns(), $newIndexName ?? false);
257
        }
258
259 14407
        unset($this->_indexes[$oldIndexName]);
260
261 14407
        if ($oldIndex->isUnique()) {
262 479
            return $this->addUniqueIndex($oldIndex->getColumns(), $newIndexName, $oldIndex->getOptions());
263
        }
264
265 14405
        return $this->addIndex($oldIndex->getColumns(), $newIndexName, $oldIndex->getFlags(), $oldIndex->getOptions());
266
    }
267
268
    /**
269
     * Checks if an index begins in the order of the given columns.
270
     *
271
     * @param string[] $columnNames
272
     *
273
     * @return bool
274
     */
275 14581
    public function columnsAreIndexed(array $columnNames)
276
    {
277 14581
        foreach ($this->getIndexes() as $index) {
278
            /** @var $index Index */
279 14581
            if ($index->spansColumns($columnNames)) {
280 14581
                return true;
281
            }
282
        }
283
284
        return false;
285
    }
286
287
    /**
288
     * @param string  $columnName
289
     * @param string  $typeName
290
     * @param mixed[] $options
291
     *
292
     * @return Column
293
     */
294 18901
    public function addColumn($columnName, $typeName, array $options = [])
295
    {
296 18901
        $column = new Column($columnName, Type::getType($typeName), $options);
297
298 18901
        $this->_addColumn($column);
299
300 18901
        return $column;
301
    }
302
303
    /**
304
     * Renames a Column.
305
     *
306
     * @deprecated
307
     *
308
     * @param string $oldColumnName
309
     * @param string $newColumnName
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 the column instead. ' .
317
            'There is no fix available, because a schema diff cannot reliably detect if a column ' .
318
            '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 15437
    public function changeColumn($columnName, array $options)
331
    {
332 15437
        $column = $this->getColumn($columnName);
333
334 15437
        $column->setOptions($options);
335
336 15437
        return $this;
337
    }
338
339
    /**
340
     * Drops a Column from the Table.
341
     *
342
     * @param string $columnName
343
     *
344
     * @return self
345
     */
346 1518
    public function dropColumn($columnName)
347
    {
348 1518
        $columnName = $this->normalizeIdentifier($columnName);
349
350 1518
        unset($this->_columns[$columnName]);
351
352 1518
        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 17479
    public function addForeignKeyConstraint($foreignTable, array $localColumnNames, array $foreignColumnNames, array $options = [], $constraintName = null)
369
    {
370 17479
        if (! $constraintName) {
371 17387
            $constraintName = $this->_generateIdentifierName(
372 17387
                array_merge((array) $this->getName(), $localColumnNames),
373 17387
                'fk',
374 17387
                $this->_getMaxIdentifierLength()
375
            );
376
        }
377
378 17479
        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

378
        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...
379
    }
380
381
    /**
382
     * Adds a foreign key constraint.
383
     *
384
     * Name is to be generated by the database itself.
385
     *
386
     * @deprecated Use {@link addForeignKeyConstraint}
387
     *
388
     * @param Table|string $foreignTable       Table schema instance or table name
389
     * @param string[]     $localColumnNames
390
     * @param string[]     $foreignColumnNames
391
     * @param mixed[]      $options
392
     *
393
     * @return self
394
     */
395 10057
    public function addUnnamedForeignKeyConstraint($foreignTable, array $localColumnNames, array $foreignColumnNames, array $options = [])
396
    {
397 10057
        return $this->addForeignKeyConstraint($foreignTable, $localColumnNames, $foreignColumnNames, $options);
398
    }
399
400
    /**
401
     * Adds a foreign key constraint with a given name.
402
     *
403
     * @deprecated Use {@link addForeignKeyConstraint}
404
     *
405
     * @param string       $name
406
     * @param Table|string $foreignTable       Table schema instance or table name
407
     * @param string[]     $localColumnNames
408
     * @param string[]     $foreignColumnNames
409
     * @param mixed[]      $options
410
     *
411
     * @return self
412
     *
413
     * @throws SchemaException
414
     */
415 17481
    public function addNamedForeignKeyConstraint($name, $foreignTable, array $localColumnNames, array $foreignColumnNames, array $options = [])
416
    {
417 17481
        if ($foreignTable instanceof Table) {
418 17343
            foreach ($foreignColumnNames as $columnName) {
419 17343
                if (! $foreignTable->hasColumn($columnName)) {
420 1142
                    throw ColumnDoesNotExist::new($columnName, $foreignTable->getName());
421
                }
422
            }
423
        }
424
425 17479
        foreach ($localColumnNames as $columnName) {
426 17479
            if (! $this->hasColumn($columnName)) {
427 1255
                throw ColumnDoesNotExist::new($columnName, $this->_name);
428
            }
429
        }
430
431 17477
        $constraint = new ForeignKeyConstraint(
432 17477
            $localColumnNames,
433 178
            $foreignTable,
434 178
            $foreignColumnNames,
435 178
            $name,
436 178
            $options
437
        );
438
439 17477
        return $this->_addForeignKeyConstraint($constraint);
440
    }
441
442
    /**
443
     * @param string $name
444
     * @param mixed  $value
445
     *
446
     * @return self
447
     */
448 16255
    public function addOption($name, $value)
449
    {
450 16255
        $this->_options[$name] = $value;
451
452 16255
        return $this;
453
    }
454
455
    /**
456
     * Returns whether this table has a foreign key constraint with the given name.
457
     *
458
     * @param string $constraintName
459
     *
460
     * @return bool
461
     */
462 14397
    public function hasForeignKey($constraintName)
463
    {
464 14397
        $constraintName = $this->normalizeIdentifier($constraintName);
465
466 14397
        return isset($this->_fkConstraints[$constraintName]);
467
    }
468
469
    /**
470
     * Returns the foreign key constraint with the given name.
471
     *
472
     * @param string $constraintName The constraint name.
473
     *
474
     * @return ForeignKeyConstraint
475
     *
476
     * @throws SchemaException If the foreign key does not exist.
477
     */
478 368
    public function getForeignKey($constraintName)
479
    {
480 368
        $constraintName = $this->normalizeIdentifier($constraintName);
481
482 368
        if (! $this->hasForeignKey($constraintName)) {
483
            throw ForeignKeyDoesNotExist::new($constraintName, $this->_name);
484
        }
485
486 368
        return $this->_fkConstraints[$constraintName];
487
    }
488
489
    /**
490
     * Removes the foreign key constraint with the given name.
491
     *
492
     * @param string $constraintName The constraint name.
493
     *
494
     * @return void
495
     *
496
     * @throws SchemaException
497
     */
498 370
    public function removeForeignKey($constraintName)
499
    {
500 370
        $constraintName = $this->normalizeIdentifier($constraintName);
501
502 370
        if (! $this->hasForeignKey($constraintName)) {
503
            throw ForeignKeyDoesNotExist::new($constraintName, $this->_name);
504
        }
505
506 370
        unset($this->_fkConstraints[$constraintName]);
507 370
    }
508
509
    /**
510
     * Returns whether this table has a unique constraint with the given name.
511
     *
512
     * @param string $constraintName
513
     *
514
     * @return bool
515
     */
516
    public function hasUniqueConstraint($constraintName)
517
    {
518
        $constraintName = $this->normalizeIdentifier($constraintName);
519
520
        return isset($this->_uniqueConstraints[$constraintName]);
521
    }
522
523
    /**
524
     * Returns the unique constraint with the given name.
525
     *
526
     * @param string $constraintName The constraint name.
527
     *
528
     * @return UniqueConstraint
529
     *
530
     * @throws SchemaException If the foreign key does not exist.
531
     */
532
    public function getUniqueConstraint($constraintName)
533
    {
534
        $constraintName = $this->normalizeIdentifier($constraintName);
535
536
        if (! $this->hasUniqueConstraint($constraintName)) {
537
            throw UniqueConstraintDoesNotExist::new($constraintName, $this->_name);
538
        }
539
540
        return $this->_uniqueConstraints[$constraintName];
541
    }
542
543
    /**
544
     * Removes the unique constraint with the given name.
545
     *
546
     * @param string $constraintName The constraint name.
547
     *
548
     * @return void
549
     *
550
     * @throws SchemaException
551
     */
552
    public function removeUniqueConstraint($constraintName)
553
    {
554
        $constraintName = $this->normalizeIdentifier($constraintName);
555
556
        if (! $this->hasUniqueConstraint($constraintName)) {
557
            throw UniqueConstraintDoesNotExist::new($constraintName, $this->_name);
558
        }
559
560
        unset($this->_uniqueConstraints[$constraintName]);
561
    }
562
563
    /**
564
     * Returns ordered list of columns (primary keys are first, then foreign keys, then the rest)
565
     *
566
     * @return Column[]
567
     */
568 18655
    public function getColumns()
569
    {
570 18655
        $columns = $this->_columns;
571 18655
        $pkCols  = [];
572 18655
        $fkCols  = [];
573
574 18655
        $primaryKey = $this->getPrimaryKey();
575
576 18655
        if ($primaryKey !== null) {
577 18179
            $pkCols = $primaryKey->getColumns();
578
        }
579
580 18655
        foreach ($this->getForeignKeys() as $fk) {
581
            /** @var ForeignKeyConstraint $fk */
582 17546
            $fkCols = array_merge($fkCols, $fk->getColumns());
583
        }
584
585 18655
        $colNames = array_unique(array_merge($pkCols, $fkCols, array_keys($columns)));
586
587
        uksort($columns, static function ($a, $b) use ($colNames) {
588 18333
            return array_search($a, $colNames) >= array_search($b, $colNames);
589 18655
        });
590
591 18655
        return $columns;
592
    }
593
594
    /**
595
     * Returns whether this table has a Column with the given name.
596
     *
597
     * @param string $columnName The column name.
598
     *
599
     * @return bool
600
     */
601 18711
    public function hasColumn($columnName)
602
    {
603 18711
        $columnName = $this->normalizeIdentifier($columnName);
604
605 18711
        return isset($this->_columns[$columnName]);
606
    }
607
608
    /**
609
     * Returns the Column with the given name.
610
     *
611
     * @param string $columnName The column name.
612
     *
613
     * @return Column
614
     *
615
     * @throws SchemaException If the column does not exist.
616
     */
617 18501
    public function getColumn($columnName)
618
    {
619 18501
        $columnName = $this->normalizeIdentifier($columnName);
620
621 18501
        if (! $this->hasColumn($columnName)) {
622 1477
            throw ColumnDoesNotExist::new($columnName, $this->_name);
623
        }
624
625 18499
        return $this->_columns[$columnName];
626
    }
627
628
    /**
629
     * Returns the primary key.
630
     *
631
     * @return Index|null The primary key, or null if this Table has no primary key.
632
     */
633 18665
    public function getPrimaryKey()
634
    {
635 18665
        return $this->hasPrimaryKey()
636 18189
            ? $this->getIndex($this->_primaryKeyName)
637 18665
            : null;
638
    }
639
640
    /**
641
     * Returns the primary key columns.
642
     *
643
     * @return string[]
644
     *
645
     * @throws DBALException
646
     */
647 15972
    public function getPrimaryKeyColumns()
648
    {
649 15972
        $primaryKey = $this->getPrimaryKey();
650
651 15972
        if ($primaryKey === null) {
652
            throw new DBALException('Table ' . $this->getName() . ' has no primary key.');
653
        }
654
655 15972
        return $primaryKey->getColumns();
656
    }
657
658
    /**
659
     * Returns whether this table has a primary key.
660
     *
661
     * @return bool
662
     */
663 18671
    public function hasPrimaryKey()
664
    {
665 18671
        return $this->_primaryKeyName && $this->hasIndex($this->_primaryKeyName);
666
    }
667
668
    /**
669
     * Returns whether this table has an Index with the given name.
670
     *
671
     * @param string $indexName The index name.
672
     *
673
     * @return bool
674
     */
675 18293
    public function hasIndex($indexName)
676
    {
677 18293
        $indexName = $this->normalizeIdentifier($indexName);
678
679 18293
        return isset($this->_indexes[$indexName]);
680
    }
681
682
    /**
683
     * Returns the Index with the given name.
684
     *
685
     * @param string $indexName The index name.
686
     *
687
     * @return Index
688
     *
689
     * @throws SchemaException If the index does not exist.
690
     */
691 18249
    public function getIndex($indexName)
692
    {
693 18249
        $indexName = $this->normalizeIdentifier($indexName);
694
695 18249
        if (! $this->hasIndex($indexName)) {
696 1352
            throw IndexDoesNotExist::new($indexName, $this->_name);
697
        }
698
699 18247
        return $this->_indexes[$indexName];
700
    }
701
702
    /**
703
     * @return Index[]
704
     */
705 18629
    public function getIndexes()
706
    {
707 18629
        return $this->_indexes;
708
    }
709
710
    /**
711
     * Returns the unique constraints.
712
     *
713
     * @return UniqueConstraint[]
714
     */
715 18409
    public function getUniqueConstraints()
716
    {
717 18409
        return $this->_uniqueConstraints;
718
    }
719
720
    /**
721
     * Returns the foreign key constraints.
722
     *
723
     * @return ForeignKeyConstraint[]
724
     */
725 18691
    public function getForeignKeys()
726
    {
727 18691
        return $this->_fkConstraints;
728
    }
729
730
    /**
731
     * @param string $name
732
     *
733
     * @return bool
734
     */
735 16116
    public function hasOption($name)
736
    {
737 16116
        return isset($this->_options[$name]);
738
    }
739
740
    /**
741
     * @param string $name
742
     *
743
     * @return mixed
744
     */
745 15564
    public function getOption($name)
746
    {
747 15564
        return $this->_options[$name];
748
    }
749
750
    /**
751
     * @return mixed[]
752
     */
753 18435
    public function getOptions()
754
    {
755 18435
        return $this->_options;
756
    }
757
758
    /**
759
     * @return void
760
     */
761 17504
    public function visit(Visitor $visitor)
762
    {
763 17504
        $visitor->acceptTable($this);
764
765 17504
        foreach ($this->getColumns() as $column) {
766 17498
            $visitor->acceptColumn($this, $column);
767
        }
768
769 17504
        foreach ($this->getIndexes() as $index) {
770 17484
            $visitor->acceptIndex($this, $index);
771
        }
772
773 17504
        foreach ($this->getForeignKeys() as $constraint) {
774 158
            $visitor->acceptForeignKey($this, $constraint);
775
        }
776 17504
    }
777
778
    /**
779
     * Clone of a Table triggers a deep clone of all affected assets.
780
     *
781
     * @return void
782
     */
783 16775
    public function __clone()
784
    {
785 16775
        foreach ($this->_columns as $k => $column) {
786 16773
            $this->_columns[$k] = clone $column;
787
        }
788
789 16775
        foreach ($this->_indexes as $k => $index) {
790 16751
            $this->_indexes[$k] = clone $index;
791
        }
792
793 16775
        foreach ($this->_fkConstraints as $k => $fk) {
794 15351
            $this->_fkConstraints[$k] = clone $fk;
795 15351
            $this->_fkConstraints[$k]->setLocalTable($this);
796
        }
797 16775
    }
798
799
    /**
800
     * @return int
801
     */
802 17812
    protected function _getMaxIdentifierLength()
803
    {
804 17812
        return $this->_schemaConfig instanceof SchemaConfig
805 17407
            ? $this->_schemaConfig->getMaxIdentifierLength()
806 17812
            : 63;
807
    }
808
809
    /**
810
     * @return void
811
     *
812
     * @throws SchemaException
813
     */
814 18961
    protected function _addColumn(Column $column)
815
    {
816 18961
        $columnName = $column->getName();
817 18961
        $columnName = $this->normalizeIdentifier($columnName);
818
819 18961
        if (isset($this->_columns[$columnName])) {
820 1452
            throw ColumnAlreadyExists::new($this->getName(), $columnName);
821
        }
822
823 18961
        $this->_columns[$columnName] = $column;
824 18961
    }
825
826
    /**
827
     * Adds an index to the table.
828
     *
829
     * @return self
830
     *
831
     * @throws SchemaException
832
     */
833 18589
    protected function _addIndex(Index $indexCandidate)
834
    {
835 18589
        $indexName               = $indexCandidate->getName();
836 18589
        $indexName               = $this->normalizeIdentifier($indexName);
837 18589
        $replacedImplicitIndexes = [];
838
839 18589
        foreach ($this->implicitIndexes as $name => $implicitIndex) {
840 13345
            if (! $implicitIndex->isFullfilledBy($indexCandidate) || ! isset($this->_indexes[$name])) {
841 13337
                continue;
842
            }
843
844 758
            $replacedImplicitIndexes[] = $name;
845
        }
846
847 18589
        if ((isset($this->_indexes[$indexName]) && ! in_array($indexName, $replacedImplicitIndexes, true)) ||
848 18589
            ($this->_primaryKeyName !== false && $indexCandidate->isPrimary())
849
        ) {
850 1329
            throw IndexAlreadyExists::new($indexName, $this->_name);
851
        }
852
853 18589
        foreach ($replacedImplicitIndexes as $name) {
854 758
            unset($this->_indexes[$name], $this->implicitIndexes[$name]);
855
        }
856
857 18589
        if ($indexCandidate->isPrimary()) {
858 18345
            $this->_primaryKeyName = $indexName;
859
        }
860
861 18589
        $this->_indexes[$indexName] = $indexCandidate;
862
863 18589
        return $this;
864
    }
865
866
    /**
867
     * @return self
868
     */
869 202
    protected function _addUniqueConstraint(UniqueConstraint $constraint)
870
    {
871 202
        $name = strlen($constraint->getName())
872
            ? $constraint->getName()
873 202
            : $this->_generateIdentifierName(
874 202
                array_merge((array) $this->getName(), $constraint->getColumns()),
875 202
                'fk',
876 202
                $this->_getMaxIdentifierLength()
877
            );
878
879 202
        $name = $this->normalizeIdentifier($name);
880
881 202
        $this->_uniqueConstraints[$name] = $constraint;
882
883
        // If there is already an index that fulfills this requirements drop the request. In the case of __construct
884
        // calling this method during hydration from schema-details all the explicitly added indexes lead to duplicates.
885
        // This creates computation overhead in this case, however no duplicate indexes are ever added (column based).
886 202
        $indexName = $this->_generateIdentifierName(
887 202
            array_merge([$this->getName()], $constraint->getColumns()),
888 202
            'idx',
889 202
            $this->_getMaxIdentifierLength()
890
        );
891
892 202
        $indexCandidate = $this->_createIndex($constraint->getColumns(), $indexName, true, false);
893
894 202
        foreach ($this->_indexes as $existingIndex) {
895
            if ($indexCandidate->isFullfilledBy($existingIndex)) {
896
                return $this;
897
            }
898
        }
899
900 202
        $this->implicitIndexes[$this->normalizeIdentifier($indexName)] = $indexCandidate;
901
902 202
        return $this;
903
    }
904
905
    /**
906
     * @return self
907
     */
908 17646
    protected function _addForeignKeyConstraint(ForeignKeyConstraint $constraint)
909
    {
910 17646
        $constraint->setLocalTable($this);
911
912 17646
        $name = strlen($constraint->getName())
913 17485
            ? $constraint->getName()
914 2420
            : $this->_generateIdentifierName(
915 2420
                array_merge((array) $this->getName(), $constraint->getLocalColumns()),
916 2420
                'fk',
917 17646
                $this->_getMaxIdentifierLength()
918
            );
919
920 17646
        $name = $this->normalizeIdentifier($name);
921
922 17646
        $this->_fkConstraints[$name] = $constraint;
923
924
        // add an explicit index on the foreign key columns.
925
        // If there is already an index that fulfills this requirements drop the request. In the case of __construct
926
        // calling this method during hydration from schema-details all the explicitly added indexes lead to duplicates.
927
        // This creates computation overhead in this case, however no duplicate indexes are ever added (column based).
928 17646
        $indexName = $this->_generateIdentifierName(
929 17646
            array_merge([$this->getName()], $constraint->getColumns()),
930 17646
            'idx',
931 17646
            $this->_getMaxIdentifierLength()
932
        );
933
934 17646
        $indexCandidate = $this->_createIndex($constraint->getColumns(), $indexName, false, false);
935
936 17646
        foreach ($this->_indexes as $existingIndex) {
937 17594
            if ($indexCandidate->isFullfilledBy($existingIndex)) {
938 15857
                return $this;
939
            }
940
        }
941
942 17598
        $this->_addIndex($indexCandidate);
943 17598
        $this->implicitIndexes[$this->normalizeIdentifier($indexName)] = $indexCandidate;
944
945 17598
        return $this;
946
    }
947
948
    /**
949
     * Normalizes a given identifier.
950
     *
951
     * Trims quotes and lowercases the given identifier.
952
     *
953
     * @param string|null $identifier The identifier to normalize.
954
     *
955
     * @return string The normalized identifier.
956
     */
957 18969
    private function normalizeIdentifier($identifier)
958
    {
959 18969
        if ($identifier === null) {
960 477
            return '';
961
        }
962
963 18969
        return $this->trimQuotes(strtolower($identifier));
964
    }
965
966
    /**
967
     * @param mixed[] $columns
968
     * @param string  $indexName
969
     * @param mixed[] $flags
970
     * @param mixed[] $options
971
     *
972
     * @return UniqueConstraint
973
     *
974
     * @throws SchemaException
975
     */
976
    private function _createUniqueConstraint(array $columns, $indexName, array $flags = [], array $options = [])
977
    {
978
        if (preg_match('(([^a-zA-Z0-9_]+))', $this->normalizeIdentifier($indexName))) {
979
            throw IndexNameInvalid::new($indexName);
980
        }
981
982
        foreach ($columns as $index => $value) {
983
            if (is_string($index)) {
984
                $columnName = $index;
985
            } else {
986
                $columnName = $value;
987
            }
988
989
            if (! $this->hasColumn($columnName)) {
990
                throw ColumnDoesNotExist::new($columnName, $this->_name);
991
            }
992
        }
993
994
        return new UniqueConstraint($indexName, $columns, $flags, $options);
995
    }
996
997
    /**
998
     * @param mixed[]  $columns
999
     * @param string   $indexName
1000
     * @param bool     $isUnique
1001
     * @param bool     $isPrimary
1002
     * @param string[] $flags
1003
     * @param mixed[]  $options
1004
     *
1005
     * @return Index
1006
     *
1007
     * @throws SchemaException
1008
     */
1009 18581
    private function _createIndex(array $columns, $indexName, $isUnique, $isPrimary, array $flags = [], array $options = [])
1010
    {
1011 18581
        if (preg_match('(([^a-zA-Z0-9_]+))', $this->normalizeIdentifier($indexName))) {
1012 1152
            throw IndexNameInvalid::new($indexName);
1013
        }
1014
1015 18579
        foreach ($columns as $index => $value) {
1016 18577
            if (is_string($index)) {
1017
                $columnName = $index;
1018
            } else {
1019 18577
                $columnName = $value;
1020
            }
1021
1022 18577
            if (! $this->hasColumn($columnName)) {
1023 1921
                throw ColumnDoesNotExist::new($columnName, $this->_name);
1024
            }
1025
        }
1026
1027 18577
        return new Index($indexName, $columns, $isUnique, $isPrimary, $flags, $options);
1028
    }
1029
}
1030