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