Failed Conditions
Pull Request — master (#3535)
by Jonathan
60:43
created

Table::dropIndexIfExists()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
eloc 4
dl 0
loc 9
rs 10
c 0
b 0
f 0
ccs 0
cts 0
cp 0
cc 2
nc 2
nop 1
crap 6
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_merge;
11
use function in_array;
12
use function preg_match;
13
use function strlen;
14
use function strtolower;
15
16
/**
17
 * Object Representation of a table.
18
 */
19
class Table extends AbstractAsset
20
{
21
    /** @var Column[] */
22
    protected $_columns = [];
23
24
    /** @var Index[] */
25
    private $implicitIndexes = [];
26
27
    /** @var Index[] */
28
    protected $_indexes = [];
29
30
    /** @var string */
31
    protected $_primaryKeyName = false;
32
33
    /** @var ForeignKeyConstraint[] */
34
    protected $_fkConstraints = [];
35
36
    /** @var mixed[] */
37
    protected $_options = [];
38
39
    /** @var SchemaConfig|null */
40
    protected $_schemaConfig = null;
41
42
    /**
43
     * @param string                 $tableName
44
     * @param Column[]               $columns
45
     * @param Index[]                $indexes
46
     * @param ForeignKeyConstraint[] $fkConstraints
47
     * @param int                    $idGeneratorType
48
     * @param mixed[]                $options
49
     *
50
     * @throws DBALException
51
     */
52 23035
    public function __construct($tableName, array $columns = [], array $indexes = [], array $fkConstraints = [], $idGeneratorType = 0, array $options = [])
0 ignored issues
show
Unused Code introduced by
The parameter $idGeneratorType 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

52
    public function __construct($tableName, array $columns = [], array $indexes = [], array $fkConstraints = [], /** @scrutinizer ignore-unused */ $idGeneratorType = 0, array $options = [])

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...
53
    {
54 23035
        if (strlen($tableName) === 0) {
55 1625
            throw DBALException::invalidTableName($tableName);
56
        }
57
58 23035
        $this->_setName($tableName);
59
60 23035
        foreach ($columns as $column) {
61 22173
            $this->_addColumn($column);
62
        }
63
64 23035
        foreach ($indexes as $idx) {
65 22009
            $this->_addIndex($idx);
66
        }
67
68 23035
        foreach ($fkConstraints as $constraint) {
69 21051
            $this->_addForeignKeyConstraint($constraint);
70
        }
71
72 23035
        $this->_options = $options;
73 23035
    }
74
75
    /**
76
     * @return void
77
     */
78 22607
    public function setSchemaConfig(SchemaConfig $schemaConfig)
79
    {
80 22607
        $this->_schemaConfig = $schemaConfig;
81 22607
    }
82
83
    /**
84
     * @return int
85
     */
86 22659
    protected function _getMaxIdentifierLength()
87
    {
88 22659
        if ($this->_schemaConfig instanceof SchemaConfig) {
89 22512
            return $this->_schemaConfig->getMaxIdentifierLength();
90
        }
91
92 22659
        return 63;
93
    }
94
95
    /**
96
     * Sets the Primary Key.
97
     *
98
     * @param string[]     $columnNames
99
     * @param string|false $indexName
100
     *
101
     * @return self
102
     */
103 23035
    public function setPrimaryKey(array $columnNames, $indexName = false)
104
    {
105 23035
        $this->_addIndex($this->_createIndex($columnNames, $indexName ?: 'primary', true, true));
106
107 23035
        foreach ($columnNames as $columnName) {
108 23035
            $column = $this->getColumn($columnName);
109 23035
            $column->setNotnull(true);
110
        }
111
112 23035
        return $this;
113
    }
114
115
    /**
116
     * @param string[]    $columnNames
117
     * @param string|null $indexName
118
     * @param string[]    $flags
119
     * @param mixed[]     $options
120
     *
121
     * @return self
122
     */
123 21316
    public function addIndex(array $columnNames, $indexName = null, array $flags = [], array $options = [])
124
    {
125 21316
        if ($indexName === null) {
126 20923
            $indexName = $this->generateNormalIndexName($columnNames);
127 20923
        }
128 20923
129 20923
        return $this->_addIndex($this->_createIndex($columnNames, $indexName, false, false, $flags, $options));
130
    }
131
132
    /**
133 21316
     * @param array<int, string>   $columnNames
134
     * @param array<int, string>   $flags
135
     * @param array<string, mixed> $options
136
     */
137
    public function addIndexIfNotExists(
138
        array $columnNames,
139
        ?string $indexName = null,
140
        array $flags = [],
141 20287
        array $options = []
142
    ) : self {
143 20287
        if ($indexName === null) {
144 20287
            $indexName = $this->generateNormalIndexName($columnNames);
145 20287
        }
146
147
        if ($this->hasIndex($indexName)) {
148
            return $this;
149
        }
150
151
        return $this->_addIndex($this->_createIndex($columnNames, $indexName, false, false, $flags, $options));
152
    }
153
154
    /**
155
     * Drops the primary key from this table.
156 20298
     *
157
     * @return void
158 20298
     */
159 20298
    public function dropPrimaryKey()
160
    {
161
        $this->dropIndex($this->_primaryKeyName);
162 20298
        $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...
163 20298
    }
164
165
    /**
166
     * Drops an index from this table.
167
     *
168
     * @param string $indexName The index name.
169
     *
170
     * @return void
171
     *
172 22528
     * @throws SchemaException If the index does not exist.
173
     */
174 22528
    public function dropIndex($indexName)
175 22065
    {
176 22065
        $indexName = $this->normalizeIdentifier($indexName);
177 22065
        if (! $this->hasIndex($indexName)) {
178 22065
            throw SchemaException::indexDoesNotExist($indexName, $this->_name);
179
        }
180
        unset($this->_indexes[$indexName]);
181
    }
182 22528
183
    public function dropIndexIfExists(string $indexName) : void
184
    {
185
        $indexName = $this->normalizeIdentifier($indexName);
186
187
        if (! $this->hasIndex($indexName)) {
188
            return;
189
        }
190
191
        unset($this->_indexes[$indexName]);
192
    }
193
194
    /**
195
     * @param string[]    $columnNames
196
     * @param string|null $indexName
197 20664
     * @param mixed[]     $options
198
     *
199 20664
     * @return self
200 20664
     */
201
    public function addUniqueIndex(array $columnNames, $indexName = null, array $options = [])
202 20664
    {
203 450
        if ($indexName === null) {
204
            $indexName = $this->generateUniqueIndexName($columnNames);
205
        }
206 20664
207 375
        return $this->_addIndex($this->_createIndex($columnNames, $indexName, true, false, [], $options));
208
    }
209
210 20664
    /**
211 350
     * @param array<int, string>   $columnNames
212
     * @param array<string, mixed> $options
213
     */
214 20664
    public function addUniqueIndexIfNotExists(array $columnNames, $indexName = null, array $options = []) : self
215
    {
216 20664
        if ($indexName === null) {
217 450
            $indexName = $this->generateUniqueIndexName($columnNames);
218
        }
219 450
220
        if ($this->hasIndex($indexName)) {
221
            return $this;
222 20664
        }
223
224 20664
        return $this->_addIndex($this->_createIndex($columnNames, $indexName, true, false, [], $options));
225 450
    }
226
227
    /**
228 20664
     * Renames an index.
229
     *
230
     * @param string      $oldIndexName The name of the index to rename from.
231
     * @param string|null $newIndexName The name of the index to rename to.
232
     *                                  If null is given, the index name will be auto-generated.
233
     *
234
     * @return self This table instance.
235
     *
236
     * @throws SchemaException If no index exists for the given current name
237
     *                         or if an index with the given new name already exists on this table.
238 20866
     */
239
    public function renameIndex($oldIndexName, $newIndexName = null)
240 20866
    {
241
        $oldIndexName           = $this->normalizeIdentifier($oldIndexName);
242 20866
        $normalizedNewIndexName = $this->normalizeIdentifier($newIndexName);
243 20866
244
        if ($oldIndexName === $normalizedNewIndexName) {
245
            return $this;
246
        }
247
248
        if (! $this->hasIndex($oldIndexName)) {
249
            throw SchemaException::indexDoesNotExist($oldIndexName, $this->_name);
250
        }
251
252
        if ($this->hasIndex($normalizedNewIndexName)) {
253
            throw SchemaException::indexAlreadyExists($normalizedNewIndexName, $this->_name);
254
        }
255
256
        $oldIndex = $this->_indexes[$oldIndexName];
257
258
        if ($oldIndex->isPrimary()) {
259
            $this->dropPrimaryKey();
260
261
            return $this->setPrimaryKey($oldIndex->getColumns(), $newIndexName ?? false);
262 23035
        }
263
264 23035
        unset($this->_indexes[$oldIndexName]);
265 1150
266
        if ($oldIndex->isUnique()) {
267
            return $this->addUniqueIndex($oldIndex->getColumns(), $newIndexName, $oldIndex->getOptions());
268 23035
        }
269 23035
270 1125
        return $this->addIndex($oldIndex->getColumns(), $newIndexName, $oldIndex->getFlags(), $oldIndex->getOptions());
271
    }
272
273
    /**
274 23035
     * Checks if an index begins in the order of the given columns.
275
     *
276
     * @param string[] $columnNames
277
     *
278
     * @return bool
279
     */
280
    public function columnsAreIndexed(array $columnNames)
281
    {
282
        foreach ($this->getIndexes() as $index) {
283
            /** @var $index Index */
284 23035
            if ($index->spansColumns($columnNames)) {
285
                return true;
286 23035
            }
287
        }
288 23035
289
        return false;
290 23035
    }
291
292
    /**
293
     * @param string[] $columnNames
294
     * @param string   $indexName
295
     * @param bool     $isUnique
296
     * @param bool     $isPrimary
297
     * @param string[] $flags
298
     * @param mixed[]  $options
299
     *
300
     * @return Index
301
     *
302
     * @throws SchemaException
303
     */
304
    private function _createIndex(array $columnNames, $indexName, $isUnique, $isPrimary, array $flags = [], array $options = [])
305
    {
306
        if (preg_match('(([^a-zA-Z0-9_]+))', $this->normalizeIdentifier($indexName))) {
307
            throw SchemaException::indexNameInvalid($indexName);
308
        }
309
310
        foreach ($columnNames as $columnName) {
311
            if (! $this->hasColumn($columnName)) {
312
                throw SchemaException::columnDoesNotExist($columnName, $this->_name);
313
            }
314
        }
315
316
        return new Index($indexName, $columnNames, $isUnique, $isPrimary, $flags, $options);
317
    }
318 20489
319
    /**
320 20489
     * @param string  $columnName
321 20489
     * @param string  $typeName
322
     * @param mixed[] $options
323 20489
     *
324
     * @return Column
325
     */
326
    public function addColumn($columnName, $typeName, array $options = [])
327
    {
328
        $column = new Column($columnName, Type::getType($typeName), $options);
329
330
        $this->_addColumn($column);
331
332
        return $column;
333 1500
    }
334
335 1500
    /**
336 1500
     * @param array<string, mixed> $options
337
     */
338 1500
    public function addColumnIfNotExists(string $columnName, string $typeName, array $options = []) : Column
339
    {
340
        if ($this->hasColumn($columnName)) {
341
            return $this->getColumn($columnName);
342
        }
343
344
        return $this->addColumn($columnName, $typeName, $options);
345
    }
346
347
    /**
348
     * Renames a Column.
349
     *
350
     * @deprecated
351
     *
352
     * @param string $oldColumnName
353
     * @param string $newColumnName
354 22659
     *
355
     * @throws DBALException
356 22659
     */
357
    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

357
    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

357
    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...
358 22659
    {
359
        throw new DBALException('Table#renameColumn() was removed, because it drops and recreates ' .
360
            'the column instead. There is no fix available, because a schema diff cannot reliably detect if a ' .
361
            'column was renamed or one column was created and another one dropped.');
362
    }
363
364
    /**
365
     * Change Column Details.
366
     *
367
     * @param string  $columnName
368
     * @param mixed[] $options
369
     *
370
     * @return self
371
     */
372
    public function changeColumn($columnName, array $options)
373
    {
374
        $column = $this->getColumn($columnName);
375 15993
        $column->setOptions($options);
376
377 15993
        return $this;
378
    }
379
380
    /**
381
     * Drops a Column from the Table.
382
     *
383
     * @param string $columnName
384
     *
385
     * @return self
386
     */
387
    public function dropColumn($columnName)
388
    {
389
        $columnName = $this->normalizeIdentifier($columnName);
390
        unset($this->_columns[$columnName]);
391
392
        return $this;
393
    }
394
395 22659
    public function dropColumnIfExists(string $columnName) : self
396
    {
397 22659
        if (! $this->hasColumn($columnName)) {
398 22176
            return $this;
399 22176
        }
400 1050
401
        return $this->dropColumn($columnName);
402
    }
403
404
    /**
405 22659
     * Adds a foreign key constraint.
406 22659
     *
407 1075
     * Name is inferred from the local columns.
408
     *
409
     * @param Table|string $foreignTable       Table schema instance or table name
410
     * @param string[]     $localColumnNames
411 22659
     * @param string[]     $foreignColumnNames
412 22659
     * @param mixed[]      $options
413
     * @param string|null  $constraintName
414
     *
415
     * @return self
416
     */
417
    public function addForeignKeyConstraint($foreignTable, array $localColumnNames, array $foreignColumnNames, array $options = [], $constraintName = null)
418 22659
    {
419
        $constraintName = $constraintName ?: $this->generateForeignKeyName($localColumnNames);
420 22659
421
        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

421
        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...
422
    }
423
424
    /**
425
     * @param array<int, string>   $localColumnNames
426
     * @param array<int, string>   $foreignColumnNames
427
     * @param array<string, mixed> $options
428
     */
429 20320
    public function addForeignKeyConstraintIfNotExists(
430
        $foreignTable,
431 20320
        array $localColumnNames,
432
        array $foreignColumnNames,
433 20320
        array $options = [],
434
        $constraintName = null
435
    ) : self {
436
        $constraintName = $constraintName ?: $this->generateForeignKeyName($localColumnNames);
437
438
        if ($this->hasForeignKey($constraintName)) {
439
            return $this;
440
        }
441 23035
442
        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

442
        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...
443 23035
    }
444 23035
445
    /**
446 23035
     * Adds a foreign key constraint.
447 1450
     *
448
     * Name is to be generated by the database itself.
449
     *
450 23035
     * @deprecated Use {@link addForeignKeyConstraint}
451 23035
     *
452
     * @param Table|string $foreignTable       Table schema instance or table name
453
     * @param string[]     $localColumnNames
454
     * @param string[]     $foreignColumnNames
455
     * @param mixed[]      $options
456
     *
457
     * @return self
458
     */
459
    public function addUnnamedForeignKeyConstraint($foreignTable, array $localColumnNames, array $foreignColumnNames, array $options = [])
460 23035
    {
461
        return $this->addForeignKeyConstraint($foreignTable, $localColumnNames, $foreignColumnNames, $options);
462 23035
    }
463 23035
464 23035
    /**
465
     * Adds a foreign key constraint with a given name.
466 23035
     *
467 18961
     * @deprecated Use {@link addForeignKeyConstraint}
468 18961
     *
469
     * @param string       $name
470
     * @param Table|string $foreignTable       Table schema instance or table name
471 750
     * @param string[]     $localColumnNames
472
     * @param string[]     $foreignColumnNames
473
     * @param mixed[]      $options
474 23035
     *
475 23035
     * @return self
476
     *
477 1325
     * @throws SchemaException
478
     */
479
    public function addNamedForeignKeyConstraint($name, $foreignTable, array $localColumnNames, array $foreignColumnNames, array $options = [])
480 23035
    {
481 750
        if ($foreignTable instanceof Table) {
482
            foreach ($foreignColumnNames as $columnName) {
483
                if (! $foreignTable->hasColumn($columnName)) {
484 23035
                    throw SchemaException::columnDoesNotExist($columnName, $foreignTable->getName());
485 23035
                }
486
            }
487
        }
488 23035
489
        foreach ($localColumnNames as $columnName) {
490 23035
            if (! $this->hasColumn($columnName)) {
491
                throw SchemaException::columnDoesNotExist($columnName, $this->_name);
492
            }
493
        }
494
495
        $constraint = new ForeignKeyConstraint(
496 22659
            $localColumnNames,
497
            $foreignTable,
498 22659
            $foreignColumnNames,
499
            $name,
500 22659
            $options
501 22659
        );
502
        $this->_addForeignKeyConstraint($constraint);
503 1275
504 1275
        return $this;
505 1275
    }
506 1275
507
    /**
508
     * @param string $name
509 22659
     * @param mixed  $value
510
     *
511 22659
     * @return self
512
     */
513
    public function addOption($name, $value)
514
    {
515
        $this->_options[$name] = $value;
516 22659
517 22659
        return $this;
518 22659
    }
519 22659
520
    /**
521 22659
     * @return void
522
     *
523 22659
     * @throws SchemaException
524 22659
     */
525 21051
    protected function _addColumn(Column $column)
526
    {
527
        $columnName = $column->getName();
528
        $columnName = $this->normalizeIdentifier($columnName);
529 22659
530 22659
        if (isset($this->_columns[$columnName])) {
531 22659
            throw SchemaException::columnAlreadyExists($this->getName(), $columnName);
532
        }
533
534
        $this->_columns[$columnName] = $column;
535
    }
536
537
    /**
538
     * Adds an index to the table.
539
     *
540 20664
     * @return self
541
     *
542 20664
     * @throws SchemaException
543
     */
544 20664
    protected function _addIndex(Index $indexCandidate)
545
    {
546
        $indexName               = $indexCandidate->getName();
547
        $indexName               = $this->normalizeIdentifier($indexName);
548
        $replacedImplicitIndexes = [];
549
550
        foreach ($this->implicitIndexes as $name => $implicitIndex) {
551
            if (! $implicitIndex->isFullfilledBy($indexCandidate) || ! isset($this->_indexes[$name])) {
552
                continue;
553
            }
554
555
            $replacedImplicitIndexes[] = $name;
556 325
        }
557
558 325
        if ((isset($this->_indexes[$indexName]) && ! in_array($indexName, $replacedImplicitIndexes, true)) ||
559 325
            ($this->_primaryKeyName !== false && $indexCandidate->isPrimary())
560
        ) {
561
            throw SchemaException::indexAlreadyExists($indexName, $this->_name);
562
        }
563 325
564
        foreach ($replacedImplicitIndexes as $name) {
565
            unset($this->_indexes[$name], $this->implicitIndexes[$name]);
566
        }
567
568
        if ($indexCandidate->isPrimary()) {
569
            $this->_primaryKeyName = $indexName;
570
        }
571
572
        $this->_indexes[$indexName] = $indexCandidate;
573
574
        return $this;
575 325
    }
576
577 325
    /**
578 325
     * @return void
579
     */
580
    protected function _addForeignKeyConstraint(ForeignKeyConstraint $constraint)
581
    {
582 325
        $constraint->setLocalTable($this);
583 325
584
        if (strlen($constraint->getName())) {
585
            $name = $constraint->getName();
586
        } else {
587
            $name = $this->generateForeignKeyName($constraint->getLocalColumns());
588
        }
589
        $name = $this->normalizeIdentifier($name);
590 23035
591
        $this->_fkConstraints[$name] = $constraint;
592 23035
593 23035
        // add an explicit index on the foreign key columns. If there is already an index that fulfils this requirements drop the request.
594
        // In the case of __construct calling this method during hydration from schema-details all the explicitly added indexes
595 23035
        // lead to duplicates. This creates computation overhead in this case, however no duplicate indexes are ever added (based on columns).
596 23035
        $indexName      = $this->generateNormalIndexName($constraint->getColumns());
597
        $indexCandidate = $this->_createIndex($constraint->getColumns(), $indexName, false, false);
598
599 23035
        foreach ($this->_indexes as $existingIndex) {
600
            if ($indexCandidate->isFullfilledBy($existingIndex)) {
601
                return;
602
            }
603
        }
604
605
        $this->_addIndex($indexCandidate);
606
        $this->implicitIndexes[$this->normalizeIdentifier($indexName)] = $indexCandidate;
607 23035
    }
608
609 23035
    /**
610 23035
     * Returns whether this table has a foreign key constraint with the given name.
611 22659
     *
612
     * @param string $constraintName
613
     *
614 23035
     * @return bool
615
     */
616
    public function hasForeignKey($constraintName)
617
    {
618
        $constraintName = $this->normalizeIdentifier($constraintName);
619
620
        return isset($this->_fkConstraints[$constraintName]);
621
    }
622
623
    /**
624 23035
     * Returns the foreign key constraint with the given name.
625
     *
626
     * @param string $constraintName The constraint name.
627 23035
     *
628 23035
     * @return ForeignKeyConstraint
629
     *
630
     * @throws SchemaException If the foreign key does not exist.
631
     */
632
    public function getForeignKey($constraintName)
633
    {
634
        $constraintName = $this->normalizeIdentifier($constraintName);
635
        if (! $this->hasForeignKey($constraintName)) {
636
            throw SchemaException::foreignKeyDoesNotExist($constraintName, $this->_name);
637
        }
638 23035
639
        return $this->_fkConstraints[$constraintName];
640 23035
    }
641
642 23035
    /**
643
     * Removes the foreign key constraint with the given name.
644
     *
645
     * @param string $constraintName The constraint name.
646
     *
647
     * @return void
648
     *
649
     * @throws SchemaException
650
     */
651
    public function removeForeignKey($constraintName)
652
    {
653
        $constraintName = $this->normalizeIdentifier($constraintName);
654 23035
        if (! $this->hasForeignKey($constraintName)) {
655
            throw SchemaException::foreignKeyDoesNotExist($constraintName, $this->_name);
656 23035
        }
657 23035
658 1475
        unset($this->_fkConstraints[$constraintName]);
659
    }
660
661 23035
    public function removeForeignKeyIfExists(string $constraintName) : void
662
    {
663
        if (! $this->hasForeignKey($constraintName)) {
664
            return;
665
        }
666
667
        $this->removeForeignKey($constraintName);
668
    }
669 23035
670
    /**
671 23035
     * Returns ordered list of columns (primary keys are first, then foreign keys, then the rest)
672 22707
     *
673
     * @return Column[]
674
     */
675 23035
    public function getColumns()
676
    {
677
        $primaryKey        = $this->getPrimaryKey();
678
        $primaryKeyColumns = [];
679
680
        if ($primaryKey !== null) {
681
            $primaryKeyColumns = $this->filterColumns($primaryKey->getColumns());
682
        }
683
684
        return array_merge($primaryKeyColumns, $this->getForeignKeyColumns(), $this->_columns);
685 20287
    }
686
687 20287
    /**
688
     * Returns foreign key columns
689 20287
     *
690
     * @return Column[]
691
     */
692
    private function getForeignKeyColumns()
693 20287
    {
694
        $foreignKeyColumns = [];
695
        foreach ($this->getForeignKeys() as $foreignKey) {
696
            $foreignKeyColumns = array_merge($foreignKeyColumns, $foreignKey->getColumns());
697
        }
698
699
        return $this->filterColumns($foreignKeyColumns);
700
    }
701 23035
702
    /**
703 23035
     * Returns only columns that have specified names
704
     *
705
     * @param string[] $columnNames
706
     *
707
     * @return Column[]
708
     */
709
    private function filterColumns(array $columnNames)
710
    {
711
        return array_filter($this->_columns, static function ($columnName) use ($columnNames) {
712
            return in_array($columnName, $columnNames, true);
713 23035
        }, ARRAY_FILTER_USE_KEY);
714
    }
715 23035
716
    /**
717 23035
     * Returns whether this table has a Column with the given name.
718
     *
719
     * @param string $columnName The column name.
720
     *
721
     * @return bool
722
     */
723
    public function hasColumn($columnName)
724
    {
725
        $columnName = $this->normalizeIdentifier($columnName);
726
727
        return isset($this->_columns[$columnName]);
728
    }
729 23035
730
    /**
731 23035
     * Returns the Column with the given name.
732 23035
     *
733 1350
     * @param string $columnName The column name.
734
     *
735
     * @return Column
736 23035
     *
737
     * @throws SchemaException If the column does not exist.
738
     */
739
    public function getColumn($columnName)
740
    {
741
        $columnName = $this->normalizeIdentifier($columnName);
742 23035
        if (! $this->hasColumn($columnName)) {
743
            throw SchemaException::columnDoesNotExist($columnName, $this->_name);
744 23035
        }
745
746
        return $this->_columns[$columnName];
747
    }
748
749
    /**
750
     * Returns the primary key.
751
     *
752 23035
     * @return Index|null The primary key, or null if this Table has no primary key.
753
     */
754 23035
    public function getPrimaryKey()
755
    {
756
        if (! $this->hasPrimaryKey()) {
757
            return null;
758
        }
759
760
        return $this->getIndex($this->_primaryKeyName);
761
    }
762 20331
763
    /**
764 20331
     * Returns the primary key columns.
765
     *
766
     * @return string[]
767
     *
768
     * @throws DBALException
769
     */
770
    public function getPrimaryKeyColumns()
771
    {
772 19732
        $primaryKey = $this->getPrimaryKey();
773
774 19732
        if ($primaryKey === null) {
775
            throw new DBALException('Table ' . $this->getName() . ' has no primary key.');
776
        }
777
778
        return $primaryKey->getColumns();
779
    }
780 23035
781
    /**
782 23035
     * Returns whether this table has a primary key.
783
     *
784
     * @return bool
785
     */
786
    public function hasPrimaryKey()
787
    {
788 22342
        return $this->_primaryKeyName && $this->hasIndex($this->_primaryKeyName);
789
    }
790 22342
791
    /**
792 22342
     * Returns whether this table has an Index with the given name.
793 22342
     *
794
     * @param string $indexName The index name.
795
     *
796 22342
     * @return bool
797 19488
     */
798
    public function hasIndex($indexName)
799
    {
800 22342
        $indexName = $this->normalizeIdentifier($indexName);
801 150
802
        return isset($this->_indexes[$indexName]);
803 22342
    }
804
805
    /**
806
     * Returns the Index with the given name.
807
     *
808
     * @param string $indexName The index name.
809
     *
810 21674
     * @return Index
811
     *
812 21674
     * @throws SchemaException If the index does not exist.
813 21674
     */
814
    public function getIndex($indexName)
815 21674
    {
816 21674
        $indexName = $this->normalizeIdentifier($indexName);
817
        if (! $this->hasIndex($indexName)) {
818 21674
            throw SchemaException::indexDoesNotExist($indexName, $this->_name);
819 20664
        }
820 20664
821
        return $this->_indexes[$indexName];
822 21674
    }
823
824
    /**
825
     * @return Index[]
826
     */
827
    public function getIndexes()
828
    {
829
        return $this->_indexes;
830
    }
831
832
    /**
833 23035
     * Returns the foreign key constraints.
834
     *
835 23035
     * @return ForeignKeyConstraint[]
836 450
     */
837
    public function getForeignKeys()
838
    {
839 23035
        return $this->_fkConstraints;
840
    }
841
842
    /**
843
     * @param string $name
844
     *
845
     * @return bool
846
     */
847
    public function hasOption($name)
848
    {
849
        return isset($this->_options[$name]);
850
    }
851
852
    /**
853
     * @param string $name
854
     *
855
     * @return mixed
856
     */
857
    public function getOption($name)
858
    {
859
        return $this->_options[$name];
860
    }
861
862
    /**
863
     * @return mixed[]
864
     */
865
    public function getOptions()
866
    {
867
        return $this->_options;
868
    }
869
870
    /**
871
     * @return void
872
     */
873
    public function visit(Visitor $visitor)
874
    {
875
        $visitor->acceptTable($this);
876
877
        foreach ($this->getColumns() as $column) {
878
            $visitor->acceptColumn($this, $column);
879
        }
880
881
        foreach ($this->getIndexes() as $index) {
882
            $visitor->acceptIndex($this, $index);
883
        }
884
885
        foreach ($this->getForeignKeys() as $constraint) {
886
            $visitor->acceptForeignKey($this, $constraint);
887
        }
888
    }
889
890
    /**
891
     * Clone of a Table triggers a deep clone of all affected assets.
892
     *
893
     * @return void
894
     */
895
    public function __clone()
896
    {
897
        foreach ($this->_columns as $k => $column) {
898
            $this->_columns[$k] = clone $column;
899
        }
900
        foreach ($this->_indexes as $k => $index) {
901
            $this->_indexes[$k] = clone $index;
902
        }
903
        foreach ($this->_fkConstraints as $k => $fk) {
904
            $this->_fkConstraints[$k] = clone $fk;
905
            $this->_fkConstraints[$k]->setLocalTable($this);
906
        }
907
    }
908
909
    /**
910
     * Normalizes a given identifier.
911
     *
912
     * Trims quotes and lowercases the given identifier.
913
     *
914
     * @param string|null $identifier The identifier to normalize.
915
     *
916
     * @return string The normalized identifier.
917
     */
918
    private function normalizeIdentifier($identifier)
919
    {
920
        if ($identifier === null) {
921
            return '';
922
        }
923
924
        return $this->trimQuotes(strtolower($identifier));
925
    }
926
927
    /**
928
     * @param array<int, string> $columnNames
929
     */
930
    private function generateIndexName(string $suffix, array $columnNames) : string
931
    {
932
        return $this->_generateIdentifierName(
933
            array_merge([$this->getName()], $columnNames),
934
            $suffix,
935
            $this->_getMaxIdentifierLength()
936
        );
937
    }
938
939
    /**
940
     * @param array<int, string> $columnNames
941
     */
942
    private function generateNormalIndexName(array $columnNames) : string
943
    {
944
        return $this->generateIndexName('idx', $columnNames);
945
    }
946
947
    /**
948
     * @param array<int, string> $columnNames
949
     */
950
    private function generateUniqueIndexName(array $columnNames) : string
951
    {
952
        return $this->generateIndexName('uniq', $columnNames);
953
    }
954
955
    /**
956
     * @param array<int, string> $columnNames
957
     */
958
    private function generateForeignKeyName(array $columnNames) : string
959
    {
960
        return $this->generateIndexName('fk', $columnNames);
961
    }
962
}
963