AbstractTable   F
last analyzed

Complexity

Total Complexity 97

Size/Duplication

Total Lines 800
Duplicated Lines 0 %

Test Coverage

Coverage 93.28%

Importance

Changes 0
Metric Value
eloc 203
dl 0
loc 800
ccs 222
cts 238
cp 0.9328
rs 2
c 0
b 0
f 0
wmc 97

44 Methods

Rating   Name   Duplication   Size   Complexity  
A exists() 0 4 2
A renameIndex() 0 10 2
F normalizeSchema() 0 80 18
A hasChanges() 0 3 2
A getState() 0 6 1
A setName() 0 5 1
A prefixTableName() 0 3 1
A sanitizeColumnExpression() 0 3 1
A getIndexes() 0 3 1
A __construct() 0 20 3
A __toString() 0 3 1
B index() 0 47 8
A hasColumn() 0 3 1
A foreignKey() 0 28 6
A setPrimaryKeys() 0 9 1
A getPrefix() 0 3 1
A getPrimaryKeys() 0 3 1
A __debugInfo() 0 10 1
A createIdentifier() 0 19 3
A resetState() 0 5 1
A getColumns() 0 3 1
A setState() 0 10 2
A save() 0 32 6
A __get() 0 3 1
A getForeignKeys() 0 3 1
A isIndexColumnSortingSupported() 0 3 1
A declareDropped() 0 6 2
A getComparator() 0 3 1
A renameColumn() 0 10 2
A getStatus() 0 3 1
A column() 0 18 3
A __call() 0 5 1
A hasForeignKey() 0 3 1
A dropForeignKey() 0 12 2
A getFullName() 0 3 1
A getDriver() 0 3 1
A getName() 0 3 1
A hasIndex() 0 3 1
A __clone() 0 4 1
A initSchema() 0 15 4
A dropColumn() 0 9 2
A getDependencies() 0 8 2
A getInitialName() 0 3 1
A dropIndex() 0 11 2

How to fix   Complexity   

Complex Class

Complex classes like AbstractTable often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use AbstractTable, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/**
4
 * This file is part of Cycle ORM package.
5
 *
6
 * For the full copyright and license information, please view the LICENSE
7
 * file that was distributed with this source code.
8
 */
9
10
declare(strict_types=1);
11
12
namespace Cycle\Database\Schema;
13
14
use Cycle\Database\Driver\DriverInterface;
15
use Cycle\Database\Driver\HandlerInterface;
16
use Cycle\Database\Exception\DriverException;
17
use Cycle\Database\Exception\HandlerException;
18
use Cycle\Database\Exception\SchemaException;
19
use Cycle\Database\TableInterface;
20
21
/**
22
 * AbstractTable class used to describe and manage state of specified table. It provides ability to
23
 * get table introspection, update table schema and automatically generate set of diff operations.
24
 *
25
 * Most of table operation like column, index or foreign key creation/altering will be applied when
26
 * save() method will be called.
27
 *
28
 * Column configuration shortcuts:
29
 *
30
 * @method AbstractColumn primary($column)
31
 * @method AbstractColumn bigPrimary($column)
32
 * @method AbstractColumn enum($column, array $values)
33
 * @method AbstractColumn string($column, $length = 255)
34
 * @method AbstractColumn decimal($column, $precision, $scale)
35
 * @method AbstractColumn boolean($column)
36
 * @method AbstractColumn integer($column)
37
 * @method AbstractColumn tinyInteger($column)
38
 * @method AbstractColumn smallInteger($column)
39
 * @method AbstractColumn bigInteger($column)
40
 * @method AbstractColumn text($column)
41
 * @method AbstractColumn tinyText($column)
42
 * @method AbstractColumn mediumText($column)
43
 * @method AbstractColumn longText($column)
44
 * @method AbstractColumn json($column)
45
 * @method AbstractColumn double($column)
46
 * @method AbstractColumn float($column)
47
 * @method AbstractColumn datetime($column, $size = 0)
48
 * @method AbstractColumn date($column)
49
 * @method AbstractColumn time($column)
50
 * @method AbstractColumn timestamp($column)
51
 * @method AbstractColumn binary($column)
52
 * @method AbstractColumn tinyBinary($column)
53
 * @method AbstractColumn longBinary($column)
54
 * @method AbstractColumn snowflake($column)
55
 * @method AbstractColumn ulid($column)
56
 * @method AbstractColumn uuid($column)
57
 */
58
abstract class AbstractTable implements TableInterface, ElementInterface
59
{
60
    /**
61
     * Table states.
62
     */
63
    public const STATUS_NEW = 0;
64
65
    public const STATUS_EXISTS = 1;
66
    public const STATUS_DECLARED_DROPPED = 2;
67
68
    /**
69
     * Initial table state.
70
     *
71
     * @internal
72
     */
73
    protected State $initial;
74
75
    /**
76
     * Currently defined table state.
77
     *
78
     * @internal
79
     */
80
    protected State $current;
81
82
    /**
83
     * Indication that table is exists and current schema is fetched from database.
84
     */
85
    private int $status = self::STATUS_NEW;
86
87
    /**
88 1974
     * @param DriverInterface $driver Parent driver.
89
     *
90
     * @psalm-param non-empty-string $name Table name, must include table prefix.
91
     *
92
     * @param string $prefix Database specific table prefix. Required for table renames.
93
     */
94 1974
    public function __construct(
95 1974
        protected DriverInterface $driver,
96 1974
        string $name,
97
        private string $prefix,
98 1974
    ) {
99 1928
        //Initializing states
100
        $prefixedName = $this->prefixTableName($name);
101
        $this->initial = new State($prefixedName);
102 1974
        $this->current = new State($prefixedName);
103
104 1928
        if ($this->driver->getSchemaHandler()->hasTable($this->getFullName())) {
105
            $this->status = self::STATUS_EXISTS;
106
        }
107 1974
108 1974
        if ($this->exists()) {
109
            //Initiating table schema
110
            $this->initSchema($this->initial);
111
        }
112
113
        $this->setState($this->initial);
114
    }
115 14
116
    /**
117 14
     * Sanitize column expression for index name
118
     *
119
     * @psalm-param non-empty-string $column
120
     *
121
     * @psalm-return non-empty-string
122
     */
123
    public static function sanitizeColumnExpression(string $column): string
124
    {
125
        return \preg_replace(['/\(/', '/\)/', '/ /'], '__', \strtolower($column));
126
    }
127
128
    /**
129
     * Get instance of associated driver.
130
     */
131
    public function getDriver(): DriverInterface
132 1948
    {
133
        return $this->driver;
134
    }
135 1948
136 1948
    /**
137
     * Return database specific table prefix.
138
     */
139
    public function getPrefix(): string
140
    {
141
        return $this->prefix;
142
    }
143
144
    public function getComparator(): ComparatorInterface
145
    {
146
        return new Comparator($this->initial, $this->current);
147
    }
148 1950
149
    public function exists(): bool
150 1950
    {
151 1950
        // Declared as dropped != actually dropped
152 1950
        return $this->status === self::STATUS_EXISTS || $this->status === self::STATUS_DECLARED_DROPPED;
153
    }
154 8
155
    /**
156
     * Table status (see codes above).
157 8
     */
158 8
    public function getStatus(): int
159 8
    {
160 8
        return $this->status;
161 8
    }
162 8
163 8
    /**
164
     * Sets table name. Use this function in combination with save to rename table.
165
     *
166
     * @psalm-param non-empty-string $name
167
     *
168
     * @psalm-return non-empty-string Prefixed table name.
169
     */
170 112
    public function setName(string $name): string
171
    {
172 112
        $this->current->setName($this->prefixTableName($name));
173
174
        return $this->getFullName();
175
    }
176
177
    /**
178 240
     * @psalm-return non-empty-string
179
     */
180 240
    public function getName(): string
181
    {
182
        return $this->getFullName();
183 1950
    }
184
185 1950
    /**
186
     * @psalm-return non-empty-string
187
     */
188 1974
    public function getFullName(): string
189
    {
190
        return $this->current->getName();
191 1974
    }
192
193
    /**
194
     * Table name before rename.
195
     *
196
     * @psalm-return non-empty-string
197 112
     */
198
    public function getInitialName(): string
199 112
    {
200
        return $this->initial->getName();
201
    }
202
203
    /**
204
     * Declare table as dropped, you have to sync table using "save" method in order to apply this
205
     * change.
206
     *
207
     * Attention, method will flush declared FKs to ensure that table express no dependecies.
208 132
     */
209
    public function declareDropped(): void
210 132
    {
211
        $this->status === self::STATUS_NEW and throw new SchemaException('Unable to drop non existed table');
212 132
213
        //Declaring as dropped
214
        $this->status = self::STATUS_DECLARED_DROPPED;
215
    }
216
217
    /**
218 18
     * Set table primary keys. Operation can only be applied for newly created tables. Now every
219
     * database might support compound indexes.
220 18
     */
221
    public function setPrimaryKeys(array $columns): self
222
    {
223
        //Originally i were forcing an exception when primary key were changed, now we should
224
        //force it when table will be synced
225
226 1974
        //Updating primary keys in current state
227
        $this->current->setPrimaryKeys($columns);
228 1974
229
        return $this;
230
    }
231
232
    public function getPrimaryKeys(): array
233
    {
234
        return $this->current->getPrimaryKeys();
235
    }
236 1930
237
    public function hasColumn(string $name): bool
238 1930
    {
239
        return $this->current->hasColumn($name);
240
    }
241
242
    /**
243
     * @return AbstractColumn[]
244
     */
245
    public function getColumns(): array
246
    {
247 1938
        return $this->current->getColumns();
248
    }
249 1938
250
    public function hasIndex(array $columns = []): bool
251
    {
252 1930
        return $this->current->hasIndex($columns);
253 1930
    }
254
255
    /**
256
     * @return AbstractIndex[]
257
     */
258
    public function getIndexes(): array
259 16
    {
260
        return $this->current->getIndexes();
261
    }
262
263
    public function hasForeignKey(array $columns): bool
264
    {
265 16
        return $this->current->hasForeignKey($columns);
266
    }
267 16
268
    /**
269
     * @return AbstractForeignKey[]
270 1952
     */
271
    public function getForeignKeys(): array
272 1952
    {
273
        return $this->current->getForeignKeys();
274
    }
275 568
276
    public function getDependencies(): array
277 568
    {
278
        $tables = [];
279
        foreach ($this->current->getForeignKeys() as $foreignKey) {
280
            $tables[] = $foreignKey->getForeignTable();
281
        }
282
283 1956
        return $tables;
284
    }
285 1956
286
    /**
287
     * Get/create instance of AbstractColumn associated with current table.
288 458
     *
289
     * Attention, renamed column will be available by it's old name until being synced!
290 458
     *
291
     * @psalm-param non-empty-string $name
292
     *
293
     * Examples:
294
     * $table->column('name')->string();
295
     */
296 1952
    public function column(string $name): AbstractColumn
0 ignored issues
show
Bug introduced by
The type Cycle\Database\Schema\AbstractColumn was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
297
    {
298 1952
        if ($this->current->hasColumn($name)) {
299
            //Column already exists
300
            return $this->current->findColumn($name);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->current->findColumn($name) could return the type null which is incompatible with the type-hinted return Cycle\Database\Schema\AbstractColumn. Consider adding an additional type-check to rule them out.
Loading history...
301 224
        }
302
303 224
        if ($this->initial->hasColumn($name)) {
304
            //Fetch from initial state (this code is required to ensure column states after schema
305
            //flushing)
306
            $column = clone $this->initial->findColumn($name);
307
        } else {
308
            $column = $this->createColumn($name);
309 1954
        }
310
311 1954
        $this->current->registerColumn($column);
312
313
        return $column;
314 120
    }
315
316 120
    /**
317 120
     * Get/create instance of AbstractIndex associated with current table based on list of forming
318 104
     * column names.
319
     *
320
     * Example:
321 120
     * $table->index(['key']);
322
     * $table->index(['key', 'key2']);
323
     *
324
     * @param array $columns List of index columns
325
     *
326
     * @throws SchemaException
327
     * @throws DriverException
328
     */
329
    public function index(array $columns): AbstractIndex
330
    {
331
        $original = $columns;
332
        $normalized = [];
333
        $sort = [];
334 1950
335
        foreach ($columns as $expression) {
336 1950
            [$column, $order] = AbstractIndex::parseColumn($expression);
337
338 988
            // If expression like 'column DESC' was passed, we cast it to 'column' => 'DESC'
339
            if ($order !== null) {
340
                $this->isIndexColumnSortingSupported() or throw new DriverException(\sprintf(
341 1948
                    'Failed to create index with `%s` on `%s`, column sorting is not supported',
342
                    $expression,
343
                    $this->getFullName(),
344
                ));
345
346 1948
                $sort[$column] = $order;
347
            }
348
349 1948
            $normalized[] = $column;
350
        }
351 1948
        $columns = $normalized;
352
353
        foreach ($columns as $column) {
354
            $this->hasColumn($column) or throw new SchemaException(
355
                "Undefined column '{$column}' in '{$this->getFullName()}'",
356
            );
357
        }
358
359
        if ($this->hasIndex($original)) {
360
            return $this->current->findIndex($original);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->current->findIndex($original) could return the type null which is incompatible with the type-hinted return Cycle\Database\Schema\AbstractIndex. Consider adding an additional type-check to rule them out.
Loading history...
361
        }
362
363
        if ($this->initial->hasIndex($original)) {
364
            //Let's ensure that index name is always stays synced (not regenerated)
365
            $name = $this->initial->findIndex($original)->getName();
366
        } else {
367 458
            $name = $this->createIdentifier('index', $original);
368
        }
369 458
370 458
        $index = $this->createIndex($name)->columns($columns)->sort($sort);
371 458
372
        //Adding to current schema
373 458
        $this->current->registerIndex($index);
374 458
375
        return $index;
376
    }
377 458
378 16
    /**
379
     * Get/create instance of AbstractReference associated with current table based on local column
380
     * name.
381
     *
382
     * @throws SchemaException
383
     */
384 16
    public function foreignKey(array $columns, bool $indexCreate = true): AbstractForeignKey
385
    {
386
        foreach ($columns as $column) {
387 458
            $this->hasColumn($column) or throw new SchemaException(
388
                "Undefined column '{$column}' in '{$this->getFullName()}'",
389 458
            );
390
        }
391 458
392 458
        if ($this->hasForeignKey($columns)) {
393
            return $this->current->findForeignKey($columns);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->current->findForeignKey($columns) could return the type null which is incompatible with the type-hinted return Cycle\Database\Schema\AbstractForeignKey. Consider adding an additional type-check to rule them out.
Loading history...
394
        }
395
396
        if ($this->initial->hasForeignKey($columns)) {
397 458
            //Let's ensure that FK name is always stays synced (not regenerated)
398 24
            $name = $this->initial->findForeignKey($columns)->getName();
399
        } else {
400
            $name = $this->createIdentifier('foreign', $columns);
401 458
        }
402
403
        $foreign = $this->createForeign($name)->columns($columns);
404
405 458
        //Adding to current schema
406
        $this->current->registerForeignKey($foreign);
407
408 458
        //Let's ensure index existence to performance and compatibility reasons
409
        $indexCreate ? $this->index($columns) : $foreign->setIndex(false);
410
411 458
        return $foreign;
412
    }
413 458
414
    /**
415
     * Rename column (only if column exists).
416
     *
417
     * @psalm-param non-empty-string $column
418
     * @psalm-param non-empty-string $name New column name.
419
     *
420
     * @throws SchemaException
421
     */
422 224
    public function renameColumn(string $column, string $name): self
423
    {
424 224
        $this->hasColumn($column) or throw new SchemaException(
425 224
            "Undefined column '{$column}' in '{$this->getFullName()}'",
426
        );
427
428
        //Rename operation is simple about declaring new name
429
        $this->column($column)->setName($name);
430 224
431 64
        return $this;
432
    }
433
434 224
    /**
435
     * Rename index (only if index exists).
436
     *
437
     * @param array $columns Index forming columns.
438 224
     *
439
     * @psalm-param non-empty-string $name New index name.
440
     *
441 224
     * @throws SchemaException
442
     */
443
    public function renameIndex(array $columns, string $name): self
444 224
    {
445
        $this->hasIndex($columns) or throw new SchemaException(
446
            "Undefined index ['" . \implode("', '", $columns) . "'] in '{$this->getFullName()}'",
447 224
        );
448
449 224
        //Declaring new index name
450
        $this->index($columns)->setName($name);
451
452
        return $this;
453
    }
454
455
    /**
456
     * Drop column by it's name.
457
     *
458
     * @psalm-param non-empty-string $column
459
     *
460 64
     * @throws SchemaException
461
     */
462 64
    public function dropColumn(string $column): self
463
    {
464
        $schema = $this->current->findColumn($column);
465
        $schema === null and throw new SchemaException("Undefined column '{$column}' in '{$this->getFullName()}'");
466
467 64
        //Dropping column from current schema
468
        $this->current->forgetColumn($schema);
0 ignored issues
show
Bug introduced by
It seems like $schema can also be of type null; however, parameter $column of Cycle\Database\Schema\State::forgetColumn() does only seem to accept Cycle\Database\Schema\AbstractColumn, maybe add an additional type check? ( Ignorable by Annotation )

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

468
        $this->current->forgetColumn(/** @scrutinizer ignore-type */ $schema);
Loading history...
469 64
470
        return $this;
471
    }
472
473
    /**
474
     * Drop index by it's forming columns.
475
     *
476
     * @throws SchemaException
477
     */
478
    public function dropIndex(array $columns): self
479
    {
480 8
        $schema = $this->current->findIndex($columns);
481
        $schema === null and throw new SchemaException(
482 8
            "Undefined index ['" . \implode("', '", $columns) . "'] in '{$this->getFullName()}'",
483
        );
484
485
        //Dropping index from current schema
486
        $this->current->forgetIndex($schema);
0 ignored issues
show
Bug introduced by
It seems like $schema can also be of type null; however, parameter $index of Cycle\Database\Schema\State::forgetIndex() does only seem to accept Cycle\Database\Schema\AbstractIndex, maybe add an additional type check? ( Ignorable by Annotation )

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

486
        $this->current->forgetIndex(/** @scrutinizer ignore-type */ $schema);
Loading history...
487 8
488
        return $this;
489 8
    }
490
491
    /**
492
     * Drop foreign key by it's name.
493
     *
494
     * @throws SchemaException
495
     */
496
    public function dropForeignKey(array $columns): self
497
    {
498
        $schema = $this->current->findForeignKey($columns);
499 32
        if ($schema === null) {
500
            $names = \implode("','", $columns);
501 32
            throw new SchemaException("Undefined FK on '{$names}' in '{$this->getFullName()}'");
502 32
        }
503
504
        //Dropping foreign from current schema
505 32
        $this->current->forgetForeignKey($schema);
506
507 32
        return $this;
508
    }
509
510
    /**
511
     * Get current table state (detached).
512
     */
513
    public function getState(): State
514
    {
515 102
        $state = clone $this->current;
516
        $state->remountElements();
517 102
518 102
        return $state;
519
    }
520
521
    /**
522
     * Reset table state to new form.
523 102
     *
524
     * @param State $state Use null to flush table schema.
525 102
     */
526
    public function setState(?State $state = null): self
527
    {
528
        $this->current = new State($this->initial->getName());
529
530
        if ($state !== null) {
531
            $this->current->setName($state->getName());
532
            $this->current->syncState($state);
533 216
        }
534
535 216
        return $this;
536 216
    }
537
538
    /**
539
     * Reset table state to it initial form.
540
     */
541
    public function resetState(): self
542 216
    {
543
        $this->setState($this->initial);
544 216
545
        return $this;
546
    }
547
548
    /**
549
     * Save table schema including every column, index, foreign key creation/altering. If table
550 852
     * does not exist it must be created. If table declared as dropped it will be removed from
551
     * the database.
552 852
     *
553 852
     * @param int  $operation Operation to be performed while table being saved. In some cases
554
     *                        (when multiple tables are being updated) it is reasonable to drop
555 852
     *                        foreign keys and indexes prior to dropping related columns. See sync
556
     *                        bus class to get more details.
557
     * @param bool $reset     When true schema will be marked as synced.
558
     *
559
     * @throws HandlerException
560
     * @throws SchemaException
561
     */
562
    public function save(int $operation = HandlerInterface::DO_ALL, bool $reset = true): void
563 1974
    {
564
        // We need an instance of Handler of dbal operations
565 1974
        $handler = $this->driver->getSchemaHandler();
566
567 1974
        if ($this->status === self::STATUS_DECLARED_DROPPED && $operation & HandlerInterface::DO_DROP) {
568 1974
            //We don't need reflector for this operation
569 1974
            $handler->dropTable($this);
570
571
            //Flushing status
572 1974
            $this->status = self::STATUS_NEW;
573
574
            return;
575
        }
576
577
        // Ensure that columns references to valid indexes and et
578 116
        $prepared = $this->normalizeSchema(($operation & HandlerInterface::CREATE_FOREIGN_KEYS) !== 0);
579
580 116
        if ($this->status === self::STATUS_NEW) {
581
            //Executing table creation
582 116
            $handler->createTable($prepared);
583
        } else {
584
            //Executing table syncing
585
            if ($this->hasChanges()) {
586
                $handler->syncTable($prepared, $operation);
587
            }
588
        }
589
590
        // Syncing our schemas
591
        if ($reset) {
592
            $this->status = self::STATUS_EXISTS;
593
            $this->initial->syncState($prepared->current);
594
        }
595
    }
596
597
    /**
598
     * Shortcut for column() method.
599 1950
     *
600
     * @psalm-param non-empty-string $column
601
     */
602 1950
    public function __get(string $column): AbstractColumn
603
    {
604 1950
        return $this->column($column);
605
    }
606 1930
607
    /**
608
     * Column creation/altering shortcut, call chain is identical to:
609 1930
     * AbstractTable->column($name)->$type($arguments).
610
     *
611 1930
     * Example:
612
     * $table->string("name");
613
     * $table->text("some_column");
614
     *
615 1950
     * @psalm-param non-empty-string $type
616
     *
617 1950
     * @param array $arguments Type specific parameters.
618
     */
619 1948
    public function __call(string $type, array $arguments): AbstractColumn
620
    {
621
        return \call_user_func_array(
622 1836
            [$this->column($arguments[0]), $type],
623 646
            \array_slice($arguments, 1),
624
        );
625
    }
626
627
    public function __toString(): string
628 1944
    {
629 1944
        return $this->getFullName();
630 1944
    }
631
632 1944
    /**
633
     * Cloning schemas as well.
634
     */
635
    public function __clone()
636
    {
637
        $this->initial = clone $this->initial;
638
        $this->current = clone $this->current;
639
    }
640
641 458
    public function __debugInfo(): array
642
    {
643 458
        return [
644
            'status'      => $this->status,
645
            'full_name'   => $this->getFullName(),
646
            'name'        => $this->getName(),
647
            'primaryKeys' => $this->getPrimaryKeys(),
648
            'columns'     => \array_values($this->getColumns()),
649 1836
            'indexes'     => \array_values($this->getIndexes()),
650
            'foreignKeys' => \array_values($this->getForeignKeys()),
651 1836
        ];
652
    }
653
654
    /**
655
     * Check if table schema has been modified since synchronization.
656
     */
657
    protected function hasChanges(): bool
658
    {
659
        return $this->getComparator()->hasChanges() || $this->status === self::STATUS_DECLARED_DROPPED;
660
    }
661 1974
662
    /**
663 1974
     * Add prefix to a given table name
664
     *
665
     * @psalm-param non-empty-string $column
666
     *
667
     * @psalm-return non-empty-string
668
     */
669
    protected function prefixTableName(string $name): string
670 1950
    {
671
        return $this->prefix . $name;
672
    }
673 1950
674
    /**
675
     * Ensure that no wrong indexes left in table. This method will create AbstractTable
676 1950
     * copy in order to prevent cross modifications.
677 16
     */
678 8
    protected function normalizeSchema(bool $withForeignKeys = true): self
679
    {
680
        // To make sure that no pre-sync modifications will be reflected on current table
681
        $target = clone $this;
682
683
        // declare all FKs dropped on tables scheduled for removal
684
        if ($this->status === self::STATUS_DECLARED_DROPPED) {
685
            foreach ($target->getForeignKeys() as $fk) {
686 1950
                $target->current->forgetForeignKey($fk);
687 36
            }
688 12
        }
689
690
        /*
691
         * In cases where columns are removed we have to automatically remove related indexes and
692
         * foreign keys.
693 36
         */
694 12
        foreach ($this->getComparator()->droppedColumns() as $column) {
695
            foreach ($target->getIndexes() as $index) {
696
                if (\in_array($column->getName(), $index->getColumns(), true)) {
697
                    $target->current->forgetIndex($index);
698
                }
699
            }
700
701 1950
            foreach ($target->getForeignKeys() as $foreign) {
702
                if ($column->getName() === $foreign->getColumns()) {
703
                    $target->current->forgetForeignKey($foreign);
704
                }
705
            }
706 150
        }
707
708 150
        //We also have to adjusts indexes and foreign keys
709 24
        foreach ($this->getComparator()->alteredColumns() as $pair) {
710 16
            /**
711
             * @var AbstractColumn $initial
712
             * @var AbstractColumn $name
713 16
             */
714 16
            [$name, $initial] = $pair;
715 16
716
            foreach ($target->getIndexes() as $index) {
717
                if (\in_array($initial->getName(), $index->getColumns(), true)) {
718 16
                    $columns = $index->getColumns();
719
720 16
                    //Replacing column name
721
                    foreach ($columns as &$column) {
722 16
                        if ($column === $initial->getName()) {
723 16
                            $column = $name->getName();
724
                        }
725 16
726
                        unset($column);
727
                    }
728 16
                    unset($column);
729
730
                    $targetIndex = $target->initial->findIndex($index->getColumns());
731
                    if ($targetIndex !== null) {
732 150
                        //Target index got renamed or removed.
733 8
                        $targetIndex->columns($columns);
734 8
                    }
735 8
736 8
                    $index->columns($columns);
737
                }
738
            }
739
740
            foreach ($target->getForeignKeys() as $foreign) {
741
                $foreign->columns(
742 1950
                    \array_map(
743 1834
                        static fn($column) => $column === $initial->getName() ? $name->getName() : $column,
744
                        $foreign->getColumns(),
745 96
                    ),
746
                );
747
            }
748
        }
749 1950
750
        if (!$withForeignKeys) {
751
            foreach ($this->getComparator()->addedForeignKeys() as $foreign) {
752
                //Excluding from creation
753
                $target->current->forgetForeignKey($foreign);
754
            }
755 1928
        }
756
757 1928
        return $target;
758 1928
    }
759
760
    /**
761 1928
     * Populate table schema with values from database.
762 458
     */
763
    protected function initSchema(State $state): void
764
    {
765 1928
        foreach ($this->fetchColumns() as $column) {
766 224
            $state->registerColumn($column);
767
        }
768
769 1928
        foreach ($this->fetchIndexes() as $index) {
770
            $state->registerIndex($index);
771 1928
        }
772
773 12
        foreach ($this->fetchReferences() as $foreign) {
774
            $state->registerForeignKey($foreign);
775 12
        }
776
777
        $state->setPrimaryKeys($this->fetchPrimaryKeys());
778
        //DBMS specific initialization can be placed here
779
    }
780
781
    protected function isIndexColumnSortingSupported(): bool
782
    {
783
        return true;
784
    }
785
786
    /**
787
     * Fetch index declarations from database.
788
     *
789
     * @return AbstractColumn[]
790
     */
791
    abstract protected function fetchColumns(): array;
792
793
    /**
794
     * Fetch index declarations from database.
795
     *
796
     * @return AbstractIndex[]
797
     */
798
    abstract protected function fetchIndexes(): array;
799
800
    /**
801
     * Fetch references declaration from database.
802
     *
803
     * @return AbstractForeignKey[]
804
     */
805
    abstract protected function fetchReferences(): array;
806
807
    /**
808
     * Fetch names of primary keys from table.
809
     */
810
    abstract protected function fetchPrimaryKeys(): array;
811
812
    /**
813
     * Create column with a given name.
814
     *
815
     * @psalm-param non-empty-string $name
816
     *
817
     */
818
    abstract protected function createColumn(string $name): AbstractColumn;
819
820
    /**
821
     * Create index for a given set of columns.
822
     *
823
     * @psalm-param non-empty-string $name
824
     */
825
    abstract protected function createIndex(string $name): AbstractIndex;
826
827
    /**
828
     * Create reference on a given column set.
829
     *
830
     * @psalm-param non-empty-string $name
831
     */
832 458
    abstract protected function createForeign(string $name): AbstractForeignKey;
833
834
    /**
835 458
     * Generate unique name for indexes and foreign keys.
836 458
     *
837 458
     * @psalm-param non-empty-string $type
838
     */
839
    protected function createIdentifier(string $type, array $columns): string
840 458
    {
841 458
        // Sanitize columns in case they have expressions
842 458
        $sanitized = [];
843 458
        foreach ($columns as $column) {
844
            $sanitized[] = self::sanitizeColumnExpression($column);
845 458
        }
846
847 24
        $name = $this->getFullName()
848
            . '_' . $type
849
            . '_' . \implode('_', $sanitized)
850 458
            . '_' . \uniqid();
851
852
        if (\strlen($name) > 64) {
853
            //Many DBMS has limitations on identifier length
854
            $name = \md5($name);
855
        }
856
857
        return $name;
858
    }
859
}
860