Completed
Branch feature/pre-split (c6befc)
by Anton
03:31
created

AbstractTable::prepareSchema()   D

Complexity

Conditions 15
Paths 320

Size

Total Lines 64
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 15
eloc 26
nc 320
nop 1
dl 0
loc 64
rs 4.4547
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * Spiral Framework, Core Components
4
 *
5
 * @author    Wolfy-J
6
 */
7
namespace Spiral\Database\Schemas\Prototypes;
8
9
use Psr\Log\LoggerInterface;
10
use Spiral\Database\Entities\AbstractHandler as Behaviour;
11
use Spiral\Database\Entities\Driver;
12
use Spiral\Database\Exceptions\SchemaException;
13
use Spiral\Database\Exceptions\SchemaHandlerException;
14
use Spiral\Database\Schemas\StateComparator;
15
use Spiral\Database\Schemas\TableInterface;
16
use Spiral\Database\Schemas\TableState;
17
18
/**
19
 * AbstractTable class used to describe and manage state of specified table. It provides ability to
20
 * get table introspection, update table schema and automatically generate set of diff operations.
21
 *
22
 * Most of table operation like column, index or foreign key creation/altering will be applied when
23
 * save() method will be called.
24
 *
25
 * Column configuration shortcuts:
26
 *
27
 * @method AbstractColumn primary($column)
28
 * @method AbstractColumn bigPrimary($column)
29
 * @method AbstractColumn enum($column, array $values)
30
 * @method AbstractColumn string($column, $length = 255)
31
 * @method AbstractColumn decimal($column, $precision, $scale)
32
 * @method AbstractColumn boolean($column)
33
 * @method AbstractColumn integer($column)
34
 * @method AbstractColumn tinyInteger($column)
35
 * @method AbstractColumn bigInteger($column)
36
 * @method AbstractColumn text($column)
37
 * @method AbstractColumn tinyText($column)
38
 * @method AbstractColumn longText($column)
39
 * @method AbstractColumn json($column)
40
 * @method AbstractColumn double($column)
41
 * @method AbstractColumn float($column)
42
 * @method AbstractColumn datetime($column)
43
 * @method AbstractColumn date($column)
44
 * @method AbstractColumn time($column)
45
 * @method AbstractColumn timestamp($column)
46
 * @method AbstractColumn binary($column)
47
 * @method AbstractColumn tinyBinary($column)
48
 * @method AbstractColumn longBinary($column)
49
 *
50
 * @todo add AbstractView?
51
 */
52
abstract class AbstractTable implements TableInterface
53
{
54
    /**
55
     * Table states.
56
     */
57
    const STATUS_NEW              = 0;
58
    const STATUS_EXISTS           = 1;
59
    const STATUS_DECLARED_DROPPED = 2;
60
61
    /**
62
     * Indication that table is exists and current schema is fetched from database.
63
     *
64
     * @var int
65
     */
66
    private $status = self::STATUS_NEW;
67
68
    /**
69
     * Database specific tablePrefix. Required for table renames.
70
     *
71
     * @var string
72
     */
73
    private $prefix = '';
74
75
    /**
76
     * @invisible
77
     *
78
     * @var Driver
79
     */
80
    protected $driver = null;
81
82
    /**
83
     * Initial table state.
84
     *
85
     * @invisible
86
     * @var TableState
87
     */
88
    protected $initial = null;
89
90
    /**
91
     * Currently defined table state.
92
     *
93
     * @invisible
94
     * @var TableState
95
     */
96
    protected $current = null;
97
98
    /**
99
     * @param Driver $driver Parent driver.
100
     * @param string $name   Table name, must include table prefix.
101
     * @param string $prefix Database specific table prefix.
102
     */
103
    public function __construct(Driver $driver, string $name, string $prefix)
104
    {
105
        $this->driver = $driver;
106
        $this->prefix = $prefix;
107
108
        //Initializing states
109
        $this->initial = new TableState($this->prefix . $name);
110
        $this->current = new TableState($this->prefix . $name);
111
112
        if ($this->driver->hasTable($this->getName())) {
113
            $this->status = self::STATUS_EXISTS;
114
        }
115
116
        if ($this->exists()) {
117
            //Initiating table schema
118
            $this->initSchema($this->initial);
119
        }
120
121
        $this->setState($this->initial);
122
    }
123
124
    /**
125
     * Get instance of associated driver.
126
     *
127
     * @return Driver
128
     */
129
    public function getDriver(): Driver
130
    {
131
        return $this->driver;
132
    }
133
134
    /**
135
     * Return database specific table prefix.
136
     *
137
     * @return string
138
     */
139
    public function getPrefix(): string
140
    {
141
        return $this->prefix;
142
    }
143
144
    /**
145
     * @return StateComparator
146
     */
147
    public function getComparator(): StateComparator
148
    {
149
        return new StateComparator($this->initial, $this->current);
150
    }
151
152
    /**
153
     * Check if table schema has been modified since synchronization.
154
     *
155
     * @return bool
156
     */
157
    protected function hasChanges(): bool
158
    {
159
        return $this->getComparator()->hasChanges();
160
    }
161
162
    /**
163
     * {@inheritdoc}
164
     */
165
    public function exists(): bool
166
    {
167
        //Derlared as dropped != actually dropped
168
        return $this->status == self::STATUS_EXISTS || $this->status == self::STATUS_DECLARED_DROPPED;
169
    }
170
171
    /**
172
     * Table status (see codes above).
173
     *
174
     * @return int
175
     */
176
    public function getStatus(): int
177
    {
178
        return $this->status;
179
    }
180
181
    /**
182
     * Sets table name. Use this function in combination with save to rename table.
183
     *
184
     * @param string $name
185
     *
186
     * @return string Prefixed table name.
187
     */
188
    public function setName(string $name): string
189
    {
190
        $this->current->setName($this->prefix . $name);
191
192
        return $this->getName();
193
    }
194
195
    /**
196
     * {@inheritdoc}
197
     */
198
    public function getName(): string
199
    {
200
        return $this->current->getName();
201
    }
202
203
    /**
204
     * Table name before rename.
205
     *
206
     * @return string
207
     */
208
    public function getInitialName(): string
209
    {
210
        return $this->initial->getName();
211
    }
212
213
    /**
214
     * Declare table as dropped, you have to sync table using "save" method in order to apply this
215
     * change.
216
     */
217
    public function declareDropped()
218
    {
219
        if ($this->status == self::STATUS_NEW) {
220
            throw new SchemaException("Unable to drop non existed table");
221
        }
222
223
        //Declaring as dropped
224
        $this->status = self::STATUS_DECLARED_DROPPED;
225
226
        foreach ($this->current->getForeigns() as $foreign) {
227
            //Remove all FK keys
228
            $this->current->forgetForeign($foreign);
229
        }
230
    }
231
232
    /**
233
     * Set table primary keys. Operation can only be applied for newly created tables. Now every
234
     * database might support compound indexes.
235
     *
236
     * @param array $columns
237
     *
238
     * @return self
239
     */
240
    public function setPrimaryKeys(array $columns): AbstractTable
241
    {
242
        //Originally i were forcing an exception when primary key were changed, now we should
243
        //force it when table will be synced
244
245
        //Updating primary keys in current state
246
        $this->current->setPrimaryKeys($columns);
247
248
        return $this;
249
    }
250
251
    /**
252
     * {@inheritdoc}
253
     */
254
    public function getPrimaryKeys(): array
255
    {
256
        return $this->current->getPrimaryKeys();
257
    }
258
259
    /**
260
     * {@inheritdoc}
261
     */
262
    public function hasColumn(string $name): bool
263
    {
264
        return $this->current->hasColumn($name);
265
    }
266
267
    /**
268
     * {@inheritdoc}
269
     *
270
     * @return AbstractColumn[]
271
     */
272
    public function getColumns(): array
273
    {
274
        return $this->current->getColumns();
275
    }
276
277
    /**
278
     * {@inheritdoc}
279
     */
280
    public function hasIndex(array $columns = []): bool
281
    {
282
        return $this->current->hasIndex($columns);
283
    }
284
285
    /**
286
     * {@inheritdoc}
287
     *
288
     * @return AbstractIndex[]
289
     */
290
    public function getIndexes(): array
291
    {
292
        return $this->current->getIndexes();
293
    }
294
295
    /**
296
     * {@inheritdoc}
297
     */
298
    public function hasForeign(string $column): bool
299
    {
300
        return $this->current->hasForeign($column);
301
    }
302
303
    /**
304
     * {@inheritdoc}
305
     *
306
     * @return AbstractReference[]
307
     */
308
    public function getForeigns(): array
309
    {
310
        return $this->current->getForeigns();
311
    }
312
313
    /**
314
     * {@inheritdoc}
315
     */
316
    public function getDependencies(): array
317
    {
318
        $tables = [];
319
        foreach ($this->current->getForeigns() as $foreign) {
320
            $tables[] = $foreign->getForeignTable();
321
        }
322
323
        return $tables;
324
    }
325
326
    /**
327
     * Get/create instance of AbstractColumn associated with current table.
328
     *
329
     * Attention, renamed column will be available by it's old name until being synced!
330
     *
331
     * Examples:
332
     * $table->column('name')->string();
333
     *
334
     * @param string $name
335
     *
336
     * @return AbstractColumn
337
     */
338
    public function column(string $name): AbstractColumn
339
    {
340
        if ($this->current->hasColumn($name)) {
341
            //Column already exists
342
            return $this->current->findColumn($name);
343
        }
344
345
        if ($this->initial->hasColumn($name)) {
346
            //Fetch from initial state (this code is required to ensure column states after schema
347
            //flushing)
348
            $column = clone $this->initial->findColumn($name);
349
        } else {
350
            $column = $this->createColumn($name);
351
        }
352
353
        $this->current->registerColumn($column);
354
355
        return $column;
356
    }
357
358
    /**
359
     * Shortcut for column() method.
360
     *
361
     * @param string $column
362
     *
363
     * @return AbstractColumn
364
     */
365
    public function __get(string $column)
366
    {
367
        return $this->column($column);
368
    }
369
370
    /**
371
     * Column creation/altering shortcut, call chain is identical to:
372
     * AbstractTable->column($name)->$type($arguments).
373
     *
374
     * Example:
375
     * $table->string("name");
376
     * $table->text("some_column");
377
     *
378
     * @param string $type
379
     * @param array  $arguments Type specific parameters.
380
     *
381
     * @return AbstractColumn
382
     */
383
    public function __call(string $type, array $arguments)
384
    {
385
        return call_user_func_array(
386
            [$this->column($arguments[0]), $type],
387
            array_slice($arguments, 1)
388
        );
389
    }
390
391
    /**
392
     * Get/create instance of AbstractIndex associated with current table based on list of forming
393
     * column names.
394
     *
395
     * Example:
396
     * $table->index(['key']);
397
     * $table->index(['key', 'key2']);
398
     *
399
     * @param array $columns List of index columns.
400
     *
401
     * @return AbstractIndex
402
     *
403
     * @throws SchemaException
404
     */
405
    public function index(array $columns): AbstractIndex
406
    {
407
        foreach ($columns as $column) {
408
            if (!$this->hasColumn($column)) {
409
                throw new SchemaException("Undefined column '{$column}' in '{$this->getName()}'");
410
            }
411
        }
412
413
        if ($this->hasIndex($columns)) {
414
            return $this->current->findIndex($columns);
415
        }
416
417
        if ($this->initial->hasIndex($columns)) {
418
            //Let's ensure that index name is always stays synced (not regenerated)
419
            $name = $this->initial->findIndex($columns)->getName();
420
        } else {
421
            $name = $this->createIdentifier('index', $columns);
422
        }
423
424
        $index = $this->createIndex($name)->columns($columns);
425
426
        //Adding to current schema
427
        $this->current->registerIndex($index);
428
429
        return $index;
430
    }
431
432
    /**
433
     * Get/create instance of AbstractReference associated with current table based on local column
434
     * name.
435
     *
436
     * @param string $column
437
     *
438
     * @return AbstractReference
439
     *
440
     * @throws SchemaException
441
     */
442
    public function foreign(string $column): AbstractReference
443
    {
444
        if (!$this->hasColumn($column)) {
445
            throw new SchemaException("Undefined column '{$column}' in '{$this->getName()}'");
446
        }
447
448
        if ($this->hasForeign($column)) {
449
            return $this->current->findForeign($column);
450
        }
451
452
        if ($this->initial->hasForeign($column)) {
453
            //Let's ensure that FK name is always stays synced (not regenerated)
454
            $name = $this->initial->findForeign($column)->getName();
455
        } else {
456
            $name = $this->createIdentifier('foreign', [$column]);
457
        }
458
459
        $foreign = $this->createForeign($name)->column($column);
460
461
        //Adding to current schema
462
        $this->current->registerForeign($foreign);
463
464
        //Let's ensure index existence to performance and compatibility reasons
465
        $this->index([$column]);
466
467
        return $foreign;
468
    }
469
470
    /**
471
     * Rename column (only if column exists).
472
     *
473
     * @param string $column
474
     * @param string $name New column name.
475
     *
476
     * @return self
477
     *
478
     * @throws SchemaException
479
     */
480
    public function renameColumn(string $column, string $name): AbstractTable
481
    {
482
        if (!$this->hasColumn($column)) {
483
            throw new SchemaException("Undefined column '{$column}' in '{$this->getName()}'");
484
        }
485
486
        //Rename operation is simple about declaring new name
487
        $this->column($column)->setName($name);
488
489
        return $this;
490
    }
491
492
    /**
493
     * Rename index (only if index exists).
494
     *
495
     * @param array  $columns Index forming columns.
496
     * @param string $name    New index name.
497
     *
498
     * @return self
499
     *
500
     * @throws SchemaException
501
     */
502
    public function renameIndex(array $columns, string $name): AbstractTable
503
    {
504
        if ($this->hasIndex($columns)) {
505
            throw new SchemaException(
506
                "Undefined index ['" . join("', '", $columns) . "'] in '{$this->getName()}'"
507
            );
508
        }
509
510
        //Declaring new index name
511
        $this->index($columns)->setName($name);
512
513
        return $this;
514
    }
515
516
    /**
517
     * Drop column by it's name.
518
     *
519
     * @param string $column
520
     *
521
     * @return self
522
     *
523
     * @throws SchemaException
524
     */
525
    public function dropColumn(string $column): AbstractTable
526
    {
527
        if (empty($schema = $this->current->findColumn($column))) {
528
            throw new SchemaException("Undefined column '{$column}' in '{$this->getName()}'");
529
        }
530
531
        //Dropping column from current schema
532
        $this->current->forgetColumn($schema);
533
534
        return $this;
535
    }
536
537
    /**
538
     * Drop index by it's forming columns.
539
     *
540
     * @param array $columns
541
     *
542
     * @return self
543
     *
544
     * @throws SchemaException
545
     */
546
    public function dropIndex(array $columns): AbstractTable
547
    {
548
        if (empty($schema = $this->current->findIndex($columns))) {
549
            throw new SchemaException(
550
                "Undefined index ['" . join("', '", $columns) . "'] in '{$this->getName()}'"
551
            );
552
        }
553
554
        //Dropping index from current schema
555
        $this->current->forgetIndex($schema);
556
557
        return $this;
558
    }
559
560
    /**
561
     * Drop foreign key by it's name.
562
     *
563
     * @param string $column
564
     *
565
     * @return self
566
     *
567
     * @throws SchemaException
568
     */
569
    public function dropForeign($column): AbstractTable
570
    {
571
        if (empty($schema = $this->current->findForeign($column))) {
572
            throw new SchemaException(
573
                "Undefined FK on '{$column}' in '{$this->getName()}'"
574
            );
575
        }
576
577
        //Dropping foreign from current schema
578
        $this->current->forgetForeign($schema);
579
580
        return $this;
581
    }
582
583
    /**
584
     * Get current table state (detached).
585
     *
586
     * @return TableState
587
     */
588
    public function getState(): TableState
589
    {
590
        $state = clone $this->current;
591
        $state->remountElements();
592
593
        return $state;
594
    }
595
596
    /**
597
     * Reset table state to new form.
598
     *
599
     * @param TableState $state Use null to flush table schema.
600
     *
601
     * @return self|$this
602
     */
603
    public function setState(TableState $state = null): AbstractTable
604
    {
605
        $this->current = new TableState($this->initial->getName());
606
607
        if (!empty($state)) {
608
            $this->current->setName($state->getName());
609
            $this->current->syncState($state);
610
        }
611
612
        return $this;
613
    }
614
615
    /**
616
     * Reset table state to it initial form.
617
     *
618
     * @return self|$this
619
     */
620
    public function resetState(): AbstractTable
621
    {
622
        $this->setState($this->initial);
623
624
        return $this;
625
    }
626
627
    /**
628
     * Save table schema including every column, index, foreign key creation/altering. If table
629
     * does not exist it must be created. If table declared as dropped it will be removed from
630
     * the database.
631
     *
632
     * @param int             $behaviour Operation to be performed while table being saved. In some
633
     *                                   cases (when multiple tables are being updated) it is
634
     *                                   reasonable to drop foreing keys and indexes prior to
635
     *                                   dropping related columns. See sync bus class to get more
636
     *                                   details.
637
     * @param LoggerInterface $logger    Optional, aggregates messages for data syncing.
638
     * @param bool            $reset     When true schema will be marked as synced.
639
     *
640
     * @throws SchemaHandlerException
641
     *
642
     * @throws SchemaException
643
     */
644
    public function save(
645
        int $behaviour = Behaviour::DO_ALL,
646
        LoggerInterface $logger = null,
647
        bool $reset = true
648
    ) {
649
        //We need an instance of Handler of dbal operations
650
        $handler = $this->driver->getHandler($logger);
651
652
        if ($this->status == self::STATUS_DECLARED_DROPPED && $behaviour & Behaviour::DO_DROP) {
653
            //We don't need syncer for this operation
654
            $handler->dropTable($this);
655
656
            //Flushing status
657
            $this->status = self::STATUS_NEW;
658
659
            return;
660
        }
661
662
        //Ensure that columns references to valid indexes and et
663
        $prepared = $this->normalizeSchema($behaviour & Behaviour::CREATE_FOREIGNS);
664
665
        if ($this->status == self::STATUS_NEW) {
666
            //Executing table creation
667
            $handler->createTable($prepared);
668
        } else {
669
            //Executing table syncing
670
            if ($this->hasChanges()) {
671
                $handler->syncTable($prepared, $behaviour);
672
            }
673
        }
674
675
        //Syncing our schemas
676
        if ($reset) {
677
            $this->status = self::STATUS_EXISTS;
678
            $this->initial->syncState($prepared->current);
679
        }
680
    }
681
682
    /**
683
     * Ensure that no wrong indexes left in table. This method will create AbstracTable
684
     * copy in order to prevent cross modifications.
685
     *
686
     * @param bool $withForeigns
687
     *
688
     * @return AbstractTable
689
     */
690
    protected function normalizeSchema(bool $withForeigns = true)
691
    {
692
        //To make sure that no pre-sync modifications will be reflected on current table
693
        $target = clone $this;
694
695
        /*
696
         * In cases where columns are removed we have to automatically remove related indexes and
697
         * foreign keys.
698
         */
699
        foreach ($this->getComparator()->droppedColumns() as $column) {
700
            foreach ($target->getIndexes() as $index) {
701
                if (in_array($column->getName(), $index->getColumns())) {
702
                    $target->current->forgetIndex($index);
703
                }
704
            }
705
706
            foreach ($target->getForeigns() as $foreign) {
707
                if ($column->getName() == $foreign->getColumn()) {
708
                    $target->current->forgetForeign($foreign);
709
                }
710
            }
711
        }
712
713
        //We also have to adjusts indexes and foreign keys
714
        foreach ($this->getComparator()->alteredColumns() as $pair) {
715
            /**
716
             * @var AbstractColumn $initial
717
             * @var AbstractColumn $name
718
             */
719
            list($name, $initial) = $pair;
720
721
            foreach ($target->getIndexes() as $index) {
722
                if (in_array($initial->getName(), $index->getColumns())) {
723
                    $columns = $index->getColumns();
724
725
                    //Replacing column name
726
                    foreach ($columns as &$column) {
727
                        if ($column == $initial->getName()) {
728
                            $column = $name->getName();
729
                        }
730
731
                        unset($column);
732
                    }
733
734
                    //We must not register column change as index change!
735
                    $target->initial->findIndex($index->getColumns())->columns($columns);
736
                    $index->columns($columns);
737
                }
738
            }
739
740
            foreach ($target->getForeigns() as $foreign) {
741
                if ($initial->getName() == $foreign->getColumn()) {
742
                    $foreign->column($name->getName());
743
                }
744
            }
745
        }
746
747
        if (!$withForeigns) {
748
            foreach ($this->getComparator()->addedForeigns() as $foreign) {
749
                //Excluding from creation
750
                $target->current->forgetForeign($foreign);
751
            }
752
        }
753
754
        return $target;
755
    }
756
757
    /**
758
     * @return AbstractColumn|string
759
     */
760
    public function __toString(): string
761
    {
762
        return $this->getName();
763
    }
764
765
    /**
766
     * Cloning schemas as well.
767
     */
768
    public function __clone()
769
    {
770
        $this->initial = clone $this->initial;
771
        $this->current = clone $this->current;
772
    }
773
774
    /**
775
     * @return array
776
     */
777
    public function __debugInfo()
778
    {
779
        return [
780
            'status'      => $this->status,
781
            'name'        => $this->getName(),
782
            'primaryKeys' => $this->getPrimaryKeys(),
783
            'columns'     => array_values($this->getColumns()),
784
            'indexes'     => array_values($this->getIndexes()),
785
            'references'  => array_values($this->getForeigns()),
786
        ];
787
    }
788
789
    /**
790
     * Populate table schema with values from database.
791
     *
792
     * @param TableState $state
793
     */
794
    protected function initSchema(TableState $state)
795
    {
796
        foreach ($this->fetchColumns() as $column) {
797
            $state->registerColumn($column);
798
        }
799
800
        foreach ($this->fetchIndexes() as $index) {
801
            $state->registerIndex($index);
802
        }
803
804
        foreach ($this->fetchReferences() as $foreign) {
805
            $state->registerForeign($foreign);
806
        }
807
808
        $state->setPrimaryKeys($this->fetchPrimaryKeys());
809
810
        //DBMS specific initialization can be placed here
811
    }
812
813
    /**
814
     * Fetch index declarations from database.
815
     *
816
     * @return AbstractColumn[]
817
     */
818
    abstract protected function fetchColumns(): array;
819
820
    /**
821
     * Fetch index declarations from database.
822
     *
823
     * @return AbstractIndex[]
824
     */
825
    abstract protected function fetchIndexes(): array;
826
827
    /**
828
     * Fetch references declaration from database.
829
     *
830
     * @return AbstractReference[]
831
     */
832
    abstract protected function fetchReferences(): array;
833
834
    /**
835
     * Fetch names of primary keys from table.
836
     *
837
     * @return array
838
     */
839
    abstract protected function fetchPrimaryKeys(): array;
840
841
    /**
842
     * Create column with a given name.
843
     *
844
     * @param string $name
845
     *
846
     * @return AbstractColumn
847
     */
848
    abstract protected function createColumn(string $name): AbstractColumn;
849
850
    /**
851
     * Create index for a given set of columns.
852
     *
853
     * @param string $name
854
     *
855
     * @return AbstractIndex
856
     */
857
    abstract protected function createIndex(string $name): AbstractIndex;
858
859
    /**
860
     * Create reference on a given column set.
861
     *
862
     * @param string $name
863
     *
864
     * @return AbstractReference
865
     */
866
    abstract protected function createForeign(string $name): AbstractReference;
867
868
    /**
869
     * Generate unique name for indexes and foreign keys.
870
     *
871
     * @param string $type
872
     * @param array  $columns
873
     *
874
     * @return string
875
     */
876
    protected function createIdentifier(string $type, array $columns): string
877
    {
878
        $name = $this->getName() . '_' . $type . '_' . join('_', $columns) . '_' . uniqid();
879
880
        if (strlen($name) > 64) {
881
            //Many DBMS has limitations on identifier length
882
            $name = md5($name);
883
        }
884
885
        return $name;
886
    }
887
}
888