Completed
Branch feature/pre-split (92bc7d)
by Anton
04:30
created

AbstractTable::save()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 17
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 9
nc 3
nop 3
dl 0
loc 17
rs 9.4285
c 0
b 0
f 0
1
<?php
2
/**
3
 * Spiral Framework.
4
 *
5
 * @license   MIT
6
 * @author    Anton Titov (Wolfy-J)
7
 */
8
9
namespace Spiral\Database\Schemas;
10
11
use Psr\Log\LoggerAwareInterface;
12
use Spiral\Database\Entities\Driver;
13
use Spiral\Debug\Traits\LoggerTrait;
14
use Spiral\ODM\Exceptions\SchemaException;
15
16
/**
17
 * AbstractTable class used to describe and manage state of specified table. It provides ability to
18
 * get table introspection, update table schema and automatically generate set of diff operations.
19
 *
20
 * Most of table operation like column, index or foreign key creation/altering will be applied when
21
 * save() method will be called.
22
 *
23
 * @todo Split operations and state representation.
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 double($column)
40
 * @method AbstractColumn float($column)
41
 * @method AbstractColumn datetime($column)
42
 * @method AbstractColumn date($column)
43
 * @method AbstractColumn time($column)
44
 * @method AbstractColumn timestamp($column)
45
 * @method AbstractColumn binary($column)
46
 * @method AbstractColumn tinyBinary($column)
47
 * @method AbstractColumn longBinary($column)
48
 */
49
abstract class AbstractTable extends TableState implements TableInterface, LoggerAwareInterface
50
{
51
    use LoggerTrait;
52
53
    /**
54
     * Indication that table is exists and current schema is fetched from database.
55
     *
56
     * @var bool
57
     */
58
    private $exists = false;
59
60
    /**
61
     * Database specific tablePrefix. Required for table renames.
62
     *
63
     * @var string
64
     */
65
    private $prefix = '';
66
67
    /**
68
     * We have to remember original schema state to create set of diff based commands.
69
     *
70
     * @invisible
71
     *
72
     * @var TableState
73
     */
74
    protected $initial = null;
75
76
    /**
77
     * Compares current and original states.
78
     *
79
     * @invisible
80
     *
81
     * @var Comparator
82
     */
83
    protected $comparator = null;
84
85
    /**
86
     * @invisible
87
     *
88
     * @var Driver
89
     */
90
    protected $driver = null;
91
92
    /**
93
     * Executes table operations.
94
     *
95
     * @var AbstractCommander
96
     */
97
    protected $commander = null;
98
99
    /**
100
     * @param Driver            $driver Parent driver.
101
     * @param AbstractCommander $commander
102
     * @param string            $name   Table name, must include table prefix.
103
     * @param string            $prefix Database specific table prefix.
104
     */
105
    public function __construct(
106
        Driver $driver,
107
        AbstractCommander $commander,
108
        string $name,
109
        string $prefix
110
    ) {
111
        parent::__construct($name);
112
113
        $this->driver = $driver;
114
        $this->commander = $commander;
115
116
        $this->prefix = $prefix;
117
118
        //Locking down initial table state
119
        $this->initial = new TableState($name);
120
121
        //Needed to compare schemas
122
        $this->comparator = new Comparator($this->initial, $this);
123
124
        if (!$this->driver->hasTable($this->getName())) {
125
            //There is no need to load table schema when table does not exist
126
            return;
127
        }
128
129
        //Loading table information
130
        $this->loadColumns()->loadIndexes()->loadReferences();
131
132
        //Syncing schemas
133
        $this->initial->syncSchema($this);
134
135
        $this->exists = true;
136
    }
137
138
    /**
139
     * Get associated table driver.
140
     *
141
     * @return Driver
142
     */
143
    public function getDriver(): Driver
144
    {
145
        return $this->driver;
146
    }
147
148
    /**
149
     * Get table comparator.
150
     *
151
     * @return Comparator
152
     */
153
    public function getComparator(): Comparator
154
    {
155
        return $this->comparator;
156
    }
157
158
    /**
159
     * {@inheritdoc}
160
     */
161
    public function exists(): bool
162
    {
163
        return $this->exists;
164
    }
165
166
    /**
167
     * {@inheritdoc}
168
     *
169
     * Automatically forces prefix value.
170
     */
171
    public function setName(string $name)
172
    {
173
        parent::setName($this->prefix . $name);
174
    }
175
176
    /**
177
     * {@inheritdoc}
178
     *
179
     * @param bool $quoted Quote name.
180
     */
181
    public function getName(bool $quoted = false): string
182
    {
183
        if (!$quoted) {
184
            return parent::getName();
185
        }
186
187
        return $this->driver->identifier(parent::getName());
188
    }
189
190
    /**
191
     * Return database specific table prefix.
192
     *
193
     * @return string
194
     */
195
    public function getPrefix(): string
196
    {
197
        return $this->prefix;
198
    }
199
200
    /**
201
     * {@inheritdoc}
202
     */
203
    public function getDependencies(): array
204
    {
205
        $tables = [];
206
        foreach ($this->getForeigns() as $foreign) {
207
            $tables[] = $foreign->getForeignTable();
208
        }
209
210
        return $tables;
211
    }
212
213
    /**
214
     * Set table primary keys. Operation can only be applied for newly created tables. Now every
215
     * database might support compound indexes.
216
     *
217
     * @param array $columns
218
     *
219
     * @return self
220
     *
221
     * @throws SchemaException
222
     */
223
    public function setPrimaryKeys(array $columns): AbstractTable
224
    {
225
        if ($this->exists() && $this->getPrimaryKeys() != $columns) {
226
            throw new SchemaException('Unable to change primary keys for already exists table');
227
        }
228
229
        parent::setPrimaryKeys($columns);
230
231
        return $this;
232
    }
233
234
    /**
235
     * Get/create instance of AbstractColumn associated with current table.
236
     *
237
     * Examples:
238
     * $table->column('name')->string();
239
     *
240
     * @param string $name
241
     *
242
     * @return AbstractColumn
243
     */
244
    public function column($name): AbstractColumn
245
    {
246
        if (!empty($column = $this->findColumn($name))) {
247
            return $column->declared(true);
248
        }
249
250
        $column = $this->columnSchema($name)->declared(true);
251
252
        //Registering (without adding to initial schema)
253
        return $this->registerColumn($column);
254
    }
255
256
    /**
257
     * Get/create instance of AbstractIndex associated with current table based on list of forming
258
     * column names.
259
     *
260
     * Example:
261
     * $table->index('key');
262
     * $table->index('key', 'key2');
263
     * $table->index(['key', 'key2']);
264
     *
265
     * @param mixed $columns Column name, or array of columns.
266
     *
267
     * @return AbstractIndex
268
     */
269
    public function index($columns): AbstractIndex
270
    {
271
        $columns = is_array($columns) ? $columns : func_get_args();
272
        if (!empty($index = $this->findIndex($columns))) {
273
            return $index->declared(true);
274
        }
275
276
        $index = $this->indexSchema(null)->declared(true);
277
        $index->columns($columns)->unique(false);
278
279
        return $this->registerIndex($index);
280
    }
281
282
    /**
283
     * Get/create instance of AbstractIndex associated with current table based on list of forming
284
     * column names. Index type must be forced as UNIQUE.
285
     *
286
     * Example:
287
     * $table->unique('key');
288
     * $table->unique('key', 'key2');
289
     * $table->unique(['key', 'key2']);
290
     *
291
     * @param mixed $columns Column name, or array of columns.
292
     *
293
     * @return AbstractIndex
294
     */
295
    public function unique($columns): AbstractIndex
296
    {
297
        $columns = is_array($columns) ? $columns : func_get_args();
298
299
        return $this->index($columns)->unique(true);
300
    }
301
302
    /**
303
     * Get/create instance of AbstractReference associated with current table based on local column
304
     * name.
305
     *
306
     * @param string $column Column name.
307
     *
308
     * @return AbstractReference
309
     */
310
    public function foreign($column): AbstractReference
311
    {
312
        if (!empty($foreign = $this->findForeign($column))) {
313
            return $foreign->declared(true);
314
        }
315
316
        $foreign = $this->referenceSchema(null)->declared(true);
317
        $foreign->column($column);
318
319
        return $this->registerReference($foreign);
320
    }
321
322
    /**
323
     * Rename column (only if column exists).
324
     *
325
     * @param string $column
326
     * @param string $name New column name.
327
     *
328
     * @return self
329
     */
330
    public function renameColumn(string $column, string $name): AbstractTable
331
    {
332
        if (empty($column = $this->findColumn($column))) {
333
            return $this;
334
        }
335
336
        //Renaming automatically declares column
337
        $column->declared(true)->setName($name);
338
339
        return $this;
340
    }
341
342
    /**
343
     * Rename index (only if index exists).
344
     *
345
     * @param array  $columns Index forming columns.
346
     * @param string $name    New index name.
347
     *
348
     * @return self
349
     */
350
    public function renameIndex(array $columns, string $name): AbstractTable
351
    {
352
        if (empty($index = $this->findIndex($columns))) {
353
            return $this;
354
        }
355
356
        //Renaming automatically declares index
357
        $index->declared(true)->setName($name);
358
359
        return $this;
360
    }
361
362
    /**
363
     * Drop column by it's name.
364
     *
365
     * @param string $column
366
     *
367
     * @return self
368
     */
369
    public function dropColumn(string $column): AbstractTable
370
    {
371
        if (!empty($column = $this->findColumn($column))) {
372
            $this->forgetColumn($column);
373
            $this->removeDependent($column);
374
        }
375
376
        return $this;
377
    }
378
379
    /**
380
     * Drop index by it's forming columns.
381
     *
382
     * @param array $columns
383
     *
384
     * @return self
385
     */
386
    public function dropIndex(array $columns): AbstractTable
387
    {
388
        if (!empty($index = $this->findIndex($columns))) {
389
            $this->forgetIndex($index);
390
        }
391
392
        return $this;
393
    }
394
395
    /**
396
     * Drop foreign key by it's name.
397
     *
398
     * @param string $column
399
     *
400
     * @return self
401
     */
402
    public function dropForeign($column): AbstractTable
403
    {
404
        if (!empty($foreign = $this->findForeign($column))) {
405
            $this->forgetForeign($foreign);
406
        }
407
408
        return $this;
409
    }
410
411
    /**
412
     * Shortcut for column() method.
413
     *
414
     * @param string $column
415
     *
416
     * @return AbstractColumn
417
     */
418
    public function __get($column)
419
    {
420
        return $this->column($column);
421
    }
422
423
    /**
424
     * Column creation/altering shortcut, call chain is identical to:
425
     * AbstractTable->column($name)->$type($arguments).
426
     *
427
     * Example:
428
     * $table->string("name");
429
     * $table->text("some_column");
430
     *
431
     * @param string $type
432
     * @param array  $arguments Type specific parameters.
433
     *
434
     * @return AbstractColumn
435
     */
436
    public function __call($type, array $arguments)
437
    {
438
        return call_user_func_array(
439
            [$this->column($arguments[0]), $type],
440
            array_slice($arguments, 1)
441
        );
442
    }
443
444
    /**
445
     * Declare every existed element. Method has to be called if table modification applied to
446
     * existed table to prevent dropping of existed elements.
447
     *
448
     * @return self
449
     */
450
    public function declareExisted(): AbstractTable
451
    {
452
        foreach ($this->getColumns() as $column) {
453
            $column->declared(true);
454
        }
455
456
        foreach ($this->getIndexes() as $index) {
457
            $index->declared(true);
458
        }
459
460
        foreach ($this->getForeigns() as $foreign) {
461
            $foreign->declared(true);
462
        }
463
464
        return $this;
465
    }
466
467
    /**
468
     * Calculate difference (removed columns, indexes and foreign keys).
469
     *
470
     * @param bool $forgetColumns
471
     * @param bool $forgetIndexes
472
     * @param bool $forgetForeigns
473
     */
474
    public function forgetUndeclared($forgetColumns, $forgetIndexes, $forgetForeigns)
475
    {
476
        //We don't need to worry about changed or created columns, indexes and foreign keys here
477
        //as it already handled, we only have to drop columns which were not listed in schema
478
479
        foreach ($this->getColumns() as $column) {
480
            if ($forgetColumns && !$column->isDeclared()) {
481
                $this->forgetColumn($column);
482
                $this->removeDependent($column);
483
            }
484
        }
485
486
        foreach ($this->getIndexes() as $index) {
487
            if ($forgetIndexes && !$index->isDeclared()) {
488
                $this->forgetIndex($index);
489
            }
490
        }
491
492
        foreach ($this->getForeigns() as $foreign) {
493
            if ($forgetForeigns && !$foreign->isDeclared()) {
494
                $this->forgetForeign($foreign);
495
            }
496
        }
497
    }
498
499
    /**
500
     * Save table schema including every column, index, foreign key creation/altering. If table does
501
     * not exist it must be created.
502
     *
503
     * @param bool $forgetColumns  Drop all non declared columns.
504
     * @param bool $forgetIndexes  Drop all non declared indexes.
505
     * @param bool $forgetForeigns Drop all non declared foreign keys.
506
     */
507
    public function save($forgetColumns = true, $forgetIndexes = true, $forgetForeigns = true)
508
    {
509
        if (!$this->exists()) {
510
            $this->createSchema();
511
        } else {
512
            //Let's remove from schema elements which wasn't declared
513
            $this->forgetUndeclared($forgetColumns, $forgetIndexes, $forgetForeigns);
514
515
            if ($this->hasChanges()) {
516
                $this->synchroniseSchema();
517
            }
518
        }
519
520
        //Syncing internal states
521
        $this->initial->syncSchema($this);
522
        $this->exists = true;
523
    }
524
525
    /**
526
     * Drop table schema in database. This operation must be applied immediately.
527
     */
528
    public function drop()
529
    {
530
        $this->forgetElements();
531
532
        //Re-syncing initial state
533
        $this->initial->syncSchema($this->forgetElements());
534
535
        if ($this->exists()) {
536
            $this->commander->dropTable($this->getName());
537
        }
538
539
        $this->exists = false;
540
    }
541
542
    /**
543
     * @return AbstractColumn|string
544
     */
545
    public function __toString(): string
546
    {
547
        return $this->getName();
548
    }
549
550
    /**
551
     * @return array
552
     */
553
    public function __debugInfo()
554
    {
555
        return [
556
            'name'        => $this->getName(),
557
            'primaryKeys' => $this->getPrimaryKeys(),
558
            'columns'     => array_values($this->getColumns()),
559
            'indexes'     => array_values($this->getIndexes()),
560
            'references'  => array_values($this->getForeigns()),
561
        ];
562
    }
563
564
    /**
565
     * Create table.
566
     */
567
    protected function createSchema()
568
    {
569
        $this->logger()->debug('Creating new table {table}.', ['table' => $this->getName(true)]);
570
571
        $this->commander->createTable($this);
572
    }
573
574
    /**
575
     * Execute schema update.
576
     */
577
    protected function synchroniseSchema()
578
    {
579
        if ($this->getName() != $this->initial->getName()) {
580
            //Executing renaming
581
            $this->commander->renameTable($this->initial->getName(), $this->getName());
582
        }
583
584
        //Some data has to be dropped before column updates
585
        $this->dropForeigns()->dropIndexes();
586
587
        //Generate update flow
588
        $this->synchroniseColumns()->synchroniseIndexes()->synchroniseForeigns();
589
    }
590
591
    /**
592
     * Synchronise columns.
593
     *
594
     * @todo Split or isolate.
595
     * @return self
596
     */
597
    protected function synchroniseColumns(): AbstractTable
598
    {
599
        foreach ($this->comparator->droppedColumns() as $column) {
600
            $this->logger()->debug('Dropping column [{statement}] from table {table}.', [
601
                'statement' => $column->sqlStatement(),
602
                'table'     => $this->getName(true),
603
            ]);
604
605
            $this->commander->dropColumn($this, $column);
606
        }
607
608
        foreach ($this->comparator->addedColumns() as $column) {
609
            $this->logger()->debug('Adding column [{statement}] into table {table}.', [
610
                'statement' => $column->sqlStatement(),
611
                'table'     => $this->getName(true),
612
            ]);
613
614
            $this->commander->addColumn($this, $column);
615
        }
616
617
        foreach ($this->comparator->alteredColumns() as $pair) {
618
            /**
619
             * @var AbstractColumn $initial
620
             * @var AbstractColumn $current
621
             */
622
            list($current, $initial) = $pair;
623
624
            $this->logger()->debug('Altering column [{statement}] to [{new}] in table {table}.', [
625
                'statement' => $initial->sqlStatement(),
626
                'new'       => $current->sqlStatement(),
627
                'table'     => $this->getName(true),
628
            ]);
629
630
            $this->commander->alterColumn($this, $initial, $current);
631
        }
632
633
        return $this;
634
    }
635
636
    /**
637
     * Drop needed indexes.
638
     *
639
     * @return self
640
     */
641 View Code Duplication
    protected function dropIndexes(): AbstractTable
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
642
    {
643
        foreach ($this->comparator->droppedIndexes() as $index) {
644
            $this->logger()->debug('Dropping index [{statement}] from table {table}.', [
645
                'statement' => $index->sqlStatement(),
646
                'table'     => $this->getName(true),
647
            ]);
648
649
            $this->commander->dropIndex($this, $index);
650
        }
651
652
        return $this;
653
    }
654
655
    /**
656
     * Synchronise indexes.
657
     *
658
     * @return self
659
     */
660 View Code Duplication
    protected function synchroniseIndexes(): AbstractTable
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
661
    {
662
        foreach ($this->comparator->addedIndexes() as $index) {
663
            $this->logger()->debug('Adding index [{statement}] into table {table}.', [
664
                'statement' => $index->sqlStatement(),
665
                'table'     => $this->getName(true),
666
            ]);
667
668
            $this->commander->addIndex($this, $index);
669
        }
670
671
        foreach ($this->comparator->alteredIndexes() as $pair) {
672
            /**
673
             * @var AbstractIndex $initial
674
             * @var AbstractIndex $current
675
             */
676
            list($current, $initial) = $pair;
677
678
            $this->logger()->debug('Altering index [{statement}] to [{new}] in table {table}.', [
679
                'statement' => $initial->sqlStatement(),
680
                'new'       => $current->sqlStatement(),
681
                'table'     => $this->getName(true),
682
            ]);
683
684
            $this->commander->alterIndex($this, $initial, $current);
685
        }
686
687
        return $this;
688
    }
689
690
    /**
691
     * Drop needed foreign keys.
692
     *
693
     * @return self
694
     */
695 View Code Duplication
    protected function dropForeigns(): AbstractTable
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
696
    {
697
        foreach ($this->comparator->droppedForeigns() as $foreign) {
698
            $this->logger()->debug('Dropping foreign key [{statement}] from table {table}.', [
699
                'statement' => $foreign->sqlStatement(),
700
                'table'     => $this->getName(true),
701
            ]);
702
703
            $this->commander->dropForeign($this, $foreign);
704
        }
705
706
        return $this;
707
    }
708
709
    /**
710
     * Synchronise foreign keys.
711
     *
712
     * @return self
713
     */
714 View Code Duplication
    protected function synchroniseForeigns(): AbstractTable
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
715
    {
716
        foreach ($this->comparator->addedForeigns() as $foreign) {
717
            $this->logger()->debug('Adding foreign key [{statement}] into table {table}.', [
718
                'statement' => $foreign->sqlStatement(),
719
                'table'     => $this->getName(true),
720
            ]);
721
722
            $this->commander->addForeign($this, $foreign);
723
        }
724
725
        foreach ($this->comparator->alteredForeigns() as $pair) {
726
            /**
727
             * @var AbstractReference $initial
728
             * @var AbstractReference $current
729
             */
730
            list($current, $initial) = $pair;
731
732
            $this->logger()->debug('Altering foreign key [{statement}] to [{new}] in {table}.', [
733
                'statement' => $initial->sqlStatement(),
734
                'table'     => $this->getName(true),
735
            ]);
736
737
            $this->commander->alterForeign($this, $initial, $current);
738
        }
739
740
        return $this;
741
    }
742
743
    /**
744
     * Driver specific column schema.
745
     *
746
     * @param string $name
747
     * @param mixed  $schema
748
     *
749
     * @return AbstractColumn
750
     */
751
    abstract protected function columnSchema($name, $schema = null): AbstractColumn;
752
753
    /**
754
     * Driver specific index schema.
755
     *
756
     * @param string $name
757
     * @param mixed  $schema
758
     *
759
     * @return AbstractIndex
760
     */
761
    abstract protected function indexSchema($name, $schema = null): AbstractIndex;
762
763
    /**
764
     * Driver specific reference schema.
765
     *
766
     * @param string $name
767
     * @param mixed  $schema
768
     *
769
     * @return AbstractReference
770
     */
771
    abstract protected function referenceSchema($name, $schema = null): AbstractReference;
772
773
    /**
774
     * Must load table columns.
775
     *
776
     * @see registerColumn()
777
     *
778
     * @return self
779
     */
780
    abstract protected function loadColumns(): AbstractTable;
781
782
    /**
783
     * Must load table indexes.
784
     *
785
     * @see registerIndex()
786
     *
787
     * @return self
788
     */
789
    abstract protected function loadIndexes(): AbstractTable;
790
791
    /**
792
     * Must load table references.
793
     *
794
     * @see registerReference()
795
     *
796
     * @return self
797
     */
798
    abstract protected function loadReferences(): AbstractTable;
799
800
    /**
801
     * Check if table schema has been modified. Attention, you have to execute dropUndeclared first
802
     * to get valid results.
803
     *
804
     * @return bool
805
     */
806
    protected function hasChanges(): bool
807
    {
808
        return $this->comparator->hasChanges();
809
    }
810
811
    /**
812
     * Remove dependent indexes and foreign keys.
813
     *
814
     * @param ColumnInterface $column
815
     */
816
    private function removeDependent(ColumnInterface $column)
817
    {
818
        if ($this->hasForeign($column->getName())) {
819
            $this->forgetForeign($this->foreign($column->getName()));
820
        }
821
822
        foreach ($this->getIndexes() as $index) {
823
            if (in_array($column->getName(), $index->getColumns())) {
824
                //Dropping related indexes
825
                $this->forgetIndex($index);
826
            }
827
        }
828
    }
829
830
    /**
831
     * Forget all elements.
832
     *
833
     * @return self
834
     */
835
    private function forgetElements(): AbstractTable
836
    {
837
        foreach ($this->getColumns() as $column) {
838
            $this->forgetColumn($column);
839
        }
840
841
        foreach ($this->getIndexes() as $index) {
842
            $this->forgetIndex($index);
843
        }
844
845
        foreach ($this->getForeigns() as $foreign) {
846
            $this->forgetForeign($foreign);
847
        }
848
849
        return $this;
850
    }
851
}
852