Completed
Push — master ( c4e3cd...c5cae6 )
by Anton
05:38
created

AbstractTable::removeDependent()   A

Complexity

Conditions 4
Paths 6

Size

Total Lines 13
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 13
rs 9.2
cc 4
eloc 6
nc 6
nop 1
1
<?php
2
/**
3
 * Spiral Framework.
4
 *
5
 * @license   MIT
6
 * @author    Anton Titov (Wolfy-J)
7
 */
8
namespace Spiral\Database\Entities\Schemas;
9
10
use Psr\Log\LoggerAwareInterface;
11
use Spiral\Database\Entities\Driver;
12
use Spiral\Database\Schemas\ColumnInterface;
13
use Spiral\Database\Schemas\TableInterface;
14
use Spiral\Debug\Traits\LoggerTrait;
15
use Spiral\ODM\Exceptions\SchemaException;
16
17
/**
18
 * AbstractTable class used to describe and manage state of specified table. It provides ability to
19
 * get table introspection, update table schema and automatically generate set of diff operations.
20
 *
21
 * Most of table operation like column, index or foreign key creation/altering will be applied when
22
 * save() method will be called.
23
 *
24
 * Column configuration shortcuts:
25
 * @method AbstractColumn primary($column)
26
 * @method AbstractColumn bigPrimary($column)
27
 * @method AbstractColumn enum($column, array $values)
28
 * @method AbstractColumn string($column, $length = 255)
29
 * @method AbstractColumn decimal($column, $precision, $scale)
30
 * @method AbstractColumn boolean($column)
31
 * @method AbstractColumn integer($column)
32
 * @method AbstractColumn tinyInteger($column)
33
 * @method AbstractColumn bigInteger($column)
34
 * @method AbstractColumn text($column)
35
 * @method AbstractColumn tinyText($column)
36
 * @method AbstractColumn longText($column)
37
 * @method AbstractColumn double($column)
38
 * @method AbstractColumn float($column)
39
 * @method AbstractColumn datetime($column)
40
 * @method AbstractColumn date($column)
41
 * @method AbstractColumn time($column)
42
 * @method AbstractColumn timestamp($column)
43
 * @method AbstractColumn binary($column)
44
 * @method AbstractColumn tinyBinary($column)
45
 * @method AbstractColumn longBinary($column)
46
 */
47
abstract class AbstractTable extends TableState implements TableInterface, LoggerAwareInterface
48
{
49
    /**
50
     * Some operation better to be logged.
51
     */
52
    use LoggerTrait;
53
54
    /**
55
     * Indication that table is exists and current schema is fetched from database.
56
     *
57
     * @var bool
58
     */
59
    private $exists = false;
60
61
    /**
62
     * Database specific tablePrefix. Required for table renames.
63
     *
64
     * @var string
65
     */
66
    private $prefix = '';
67
68
    /**
69
     * We have to remember original schema state to create set of diff based commands.
70
     *
71
     * @invisible
72
     * @var TableState
73
     */
74
    protected $initial = null;
75
76
    /**
77
     * Compares current and original states.
78
     *
79
     * @invisible
80
     * @var Comparator
81
     */
82
    protected $comparator = null;
83
84
    /**
85
     * @invisible
86
     * @var Driver
87
     */
88
    protected $driver = null;
89
90
    /**
91
     * Executes table operations.
92
     *
93
     * @var AbstractCommander
94
     */
95
    protected $commander = null;
96
97
    /**
98
     * @param Driver            $driver Parent driver.
99
     * @param AbstractCommander $commander
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, AbstractCommander $commander, $name, $prefix)
104
    {
105
        parent::__construct($name);
106
107
        $this->driver = $driver;
108
        $this->commander = $commander;
109
110
        $this->prefix = $prefix;
111
112
        //Locking down initial table state
113
        $this->initial = new TableState($name);
114
115
        //Needed to compare schemas
116
        $this->comparator = new Comparator($this->initial, $this);
117
118
        if (!$this->driver->hasTable($this->getName())) {
119
            //There is no need to load table schema when table does not exist
120
            return;
121
        }
122
123
        //Loading table information
124
        $this->loadColumns()->loadIndexes()->loadReferences();
125
126
        //Syncing schemas
127
        $this->initial->syncSchema($this);
128
129
        $this->exists = true;
130
    }
131
132
    /**
133
     * Get associated table driver.
134
     *
135
     * @return Driver
136
     */
137
    public function driver()
138
    {
139
        return $this->driver;
140
    }
141
142
    /**
143
     * Get table comparator.
144
     *
145
     * @return Comparator
146
     */
147
    public function comparator()
148
    {
149
        return $this->comparator;
150
    }
151
152
    /**
153
     * {@inheritdoc}
154
     */
155
    public function exists()
156
    {
157
        return $this->exists;
158
    }
159
160
    /**
161
     * {@inheritdoc}
162
     *
163
     * Automatically forces prefix value.
164
     */
165
    public function setName($name)
166
    {
167
        parent::setName($this->prefix . $name);
168
    }
169
170
    /**
171
     * {@inheritdoc}
172
     *
173
     * @param bool $quoted Quote name.
174
     */
175
    public function getName($quoted = false)
176
    {
177
        if (!$quoted) {
178
            return parent::getName();
179
        }
180
181
        return $this->driver->identifier(parent::getName());
182
    }
183
184
    /**
185
     * Return database specific table prefix.
186
     *
187
     * @return string
188
     */
189
    public function getPrefix()
190
    {
191
        return $this->prefix;
192
    }
193
194
    /**
195
     * {@inheritdoc}
196
     */
197
    public function getDependencies()
198
    {
199
        $tables = [];
200
        foreach ($this->getForeigns() as $foreign) {
201
            $tables[] = substr($foreign->getForeignTable(), strlen($this->prefix));
202
        }
203
204
        return $tables;
205
    }
206
207
    /**
208
     * Set table primary keys. Operation can only be applied for newly created tables. Now every
209
     * database might support compound indexes.
210
     *
211
     * @param array $columns
212
     * @return $this
213
     * @throws SchemaException
214
     */
215
    public function setPrimaryKeys(array $columns)
216
    {
217
        if ($this->exists() && $this->getPrimaryKeys() != $columns) {
218
            throw new SchemaException("Unable to change primary keys for already exists table.");
219
        }
220
221
        parent::setPrimaryKeys($columns);
222
223
        return $this;
224
    }
225
226
    /**
227
     * Get/create instance of AbstractColumn associated with current table.
228
     *
229
     * Examples:
230
     * $table->column('name')->string();
231
     *
232
     * @param string $name
233
     * @return AbstractColumn
234
     */
235
    public function column($name)
236
    {
237
        if (!empty($column = $this->findColumn($name))) {
238
            return $column->declared(true);
239
        }
240
241
        $column = $this->columnSchema($name)->declared(true);
242
243
        //Registering (without adding to initial schema)
244
        return $this->registerColumn($column);
245
    }
246
247
    /**
248
     * Get/create instance of AbstractIndex associated with current table based on list of forming
249
     * column names.
250
     *
251
     * Example:
252
     * $table->index('key');
253
     * $table->index('key', 'key2');
254
     * $table->index(['key', 'key2']);
255
     *
256
     * @param mixed $columns Column name, or array of columns.
257
     * @return AbstractIndex
258
     */
259
    public function index($columns)
260
    {
261
        $columns = is_array($columns) ? $columns : func_get_args();
262
        if (!empty($index = $this->findIndex($columns))) {
263
            return $index->declared(true);
264
        }
265
266
        $index = $this->indexSchema(null)->declared(true);
267
        $index->columns($columns)->unique(false);
268
269
        return $this->registerIndex($index);
270
    }
271
272
    /**
273
     * Get/create instance of AbstractIndex associated with current table based on list of forming
274
     * column names. Index type must be forced as UNIQUE.
275
     *
276
     * Example:
277
     * $table->unique('key');
278
     * $table->unique('key', 'key2');
279
     * $table->unique(['key', 'key2']);
280
     *
281
     * @param mixed $columns Column name, or array of columns.
282
     * @return AbstractColumn|null
283
     */
284
    public function unique($columns)
285
    {
286
        $columns = is_array($columns) ? $columns : func_get_args();
287
288
        return $this->index($columns)->unique(true);
289
    }
290
291
    /**
292
     * Get/create instance of AbstractReference associated with current table based on local column
293
     * name.
294
     *
295
     * @param string $column Column name.
296
     * @return AbstractReference|null
297
     */
298
    public function foreign($column)
299
    {
300
        if (!empty($foreign = $this->findForeign($column))) {
301
            return $foreign->declared(true);
302
        }
303
304
        $foreign = $this->referenceSchema(null)->declared(true);
305
        $foreign->column($column);
306
307
        return $this->registerReference($foreign);
308
    }
309
310
    /**
311
     * Rename column (only if column exists).
312
     *
313
     * @param string $column
314
     * @param string $name New column name.
315
     * @return $this
316
     */
317
    public function renameColumn($column, $name)
318
    {
319
        if (empty($column = $this->findColumn($column))) {
320
            return $this;
321
        }
322
323
        //Renaming automatically declares column
324
        $column->declared(true)->setName($name);
325
326
        return $this;
327
    }
328
329
    /**
330
     * Rename index (only if index exists).
331
     *
332
     * @param array  $columns Index forming columns.
333
     * @param string $name    New index name.
334
     * @return $this
335
     */
336
    public function renameIndex(array $columns, $name)
337
    {
338
        if (empty($index = $this->findIndex($columns))) {
339
            return $this;
340
        }
341
342
        //Renaming automatically declares index
343
        $index->declared(true)->setName($name);
344
345
        return $this;
346
    }
347
348
    /**
349
     * Drop column by it's name.
350
     *
351
     * @param string $column
352
     * @return $this
353
     */
354
    public function dropColumn($column)
355
    {
356
        if (!empty($column = $this->findColumn($column))) {
357
            $this->forgetColumn($column);
358
            $this->removeDependent($column);
359
        }
360
361
        return $this;
362
    }
363
364
    /**
365
     * Drop index by it's forming columns.
366
     *
367
     * @param array $columns
368
     * @return $this
369
     */
370
    public function dropIndex(array $columns)
371
    {
372
        if (!empty($index = $this->findIndex($columns))) {
373
            $this->forgetIndex($index);
374
        }
375
376
        return $this;
377
    }
378
379
    /**
380
     * Drop foreign key by it's name.
381
     *
382
     * @param string $column
383
     * @return $this
384
     */
385
    public function dropForeign($column)
386
    {
387
        if (!empty($foreign = $this->findForeign($column))) {
388
            $this->forgetForeign($foreign);
389
        }
390
391
        return $this;
392
    }
393
394
    /**
395
     * Shortcut for column() method.
396
     *
397
     * @param string $column
398
     * @return AbstractColumn
399
     */
400
    public function __get($column)
401
    {
402
        return $this->column($column);
403
    }
404
405
    /**
406
     * Column creation/altering shortcut, call chain is identical to:
407
     * AbstractTable->column($name)->$type($arguments)
408
     *
409
     * Example:
410
     * $table->string("name");
411
     * $table->text("some_column");
412
     *
413
     * @param string $type
414
     * @param array  $arguments Type specific parameters.
415
     * @return AbstractColumn
416
     */
417
    public function __call($type, array $arguments)
418
    {
419
        return call_user_func_array(
420
            [$this->column($arguments[0]), $type],
421
            array_slice($arguments, 1)
422
        );
423
    }
424
425
    /**
426
     * Declare every existed element. Method has to be called if table modification applied to
427
     * existed table to prevent dropping of existed elements.
428
     *
429
     * @return $this
430
     */
431
    public function declareExisted()
432
    {
433
        foreach ($this->getColumns() as $column) {
434
            $column->declared(true);
435
        }
436
437
        foreach ($this->getIndexes() as $index) {
438
            $index->declared(true);
439
        }
440
441
        foreach ($this->getForeigns() as $foreign) {
442
            $foreign->declared(true);
443
        }
444
445
        return $this;
446
    }
447
448
    /**
449
     * Save table schema including every column, index, foreign key creation/altering. If table does
450
     * not exist it must be created.
451
     *
452
     * @param bool $forgetColumns  Drop all non declared columns.
453
     * @param bool $forgetIndexes  Drop all non declared indexes.
454
     * @param bool $forgetForeigns Drop all non declared foreign keys.
455
     */
456
    public function save($forgetColumns = true, $forgetIndexes = true, $forgetForeigns = true)
457
    {
458
        if (!$this->exists()) {
459
            $this->createSchema();
460
        } else {
461
            //Let's remove from schema elements which wasn't declared
462
            $this->forgetUndeclared($forgetColumns, $forgetIndexes, $forgetForeigns);
463
464
            if ($this->hasChanges()) {
465
                $this->synchroniseSchema();
466
            }
467
        }
468
469
        //Syncing internal states
470
        $this->initial->syncSchema($this);
471
        $this->exists = true;
472
    }
473
474
    /**
475
     * Drop table schema in database. This operation must be applied immediately.
476
     */
477
    public function drop()
478
    {
479
        $this->forgetElements();
480
481
        //Re-syncing initial state
482
        $this->initial->syncSchema($this->forgetElements());
483
484
        if ($this->exists()) {
485
            $this->commander->dropTable($this->getName());
486
        }
487
488
        $this->exists = false;
489
    }
490
491
    /**
492
     * @return AbstractColumn|string
493
     */
494
    public function __toString()
495
    {
496
        return $this->getName();
497
    }
498
499
    /**
500
     * @return object
501
     */
502
    public function __debugInfo()
503
    {
504
        return (object)[
505
            'name'        => $this->getName(),
506
            'primaryKeys' => $this->getPrimaryKeys(),
507
            'columns'     => array_values($this->getColumns()),
508
            'indexes'     => array_values($this->getIndexes()),
509
            'references'  => array_values($this->getForeigns())
510
        ];
511
    }
512
513
    /**
514
     * Create table.
515
     */
516
    protected function createSchema()
517
    {
518
        $this->logger()->debug("Creating new table {table}.", ['table' => $this->getName(true)]);
519
520
        $this->commander->createTable($this);
521
    }
522
523
    /**
524
     * Execute schema update.
525
     */
526
    protected function synchroniseSchema()
527
    {
528
        if ($this->getName() != $this->initial->getName()) {
529
            //Executing renaming
530
            $this->commander->renameTable($this->initial->getName(), $this->getName());
531
        }
532
533
        //Some data has to be dropped before column updates
534
        $this->dropForeigns()->dropIndexes();
535
536
        //Generate update flow
537
        $this->synchroniseColumns()->synchroniseIndexes()->synchroniseForeigns();
538
    }
539
540
    /**
541
     * Synchronise columns.
542
     *
543
     * @return $this
544
     */
545
    protected function synchroniseColumns()
546
    {
547
        foreach ($this->comparator->droppedColumns() as $column) {
548
            $this->logger()->debug("Dropping column [{statement}] from table {table}.", [
549
                'statement' => $column->sqlStatement(),
550
                'table'     => $this->getName(true)
551
            ]);
552
553
            $this->commander->dropColumn($this, $column);
554
        }
555
556
        foreach ($this->comparator->addedColumns() as $column) {
557
            $this->logger()->debug("Adding column [{statement}] into table {table}.", [
558
                'statement' => $column->sqlStatement(),
559
                'table'     => $this->getName(true)
560
            ]);
561
562
            $this->commander->addColumn($this, $column);
563
        }
564
565
        foreach ($this->comparator->alteredColumns() as $pair) {
566
            /**
567
             * @var AbstractColumn $initial
568
             * @var AbstractColumn $current
569
             */
570
            list($current, $initial) = $pair;
571
572
            $this->logger()->debug("Altering column [{statement}] to [{new}] in table {table}.", [
573
                'statement' => $initial->sqlStatement(),
574
                'new'       => $current->sqlStatement(),
575
                'table'     => $this->getName(true)
576
            ]);
577
578
            $this->commander->alterColumn($this, $initial, $current);
579
        }
580
581
        return $this;
582
    }
583
584
    /**
585
     * Drop needed indexes.
586
     *
587
     * @return $this
588
     */
589 View Code Duplication
    protected function dropIndexes()
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...
590
    {
591
        foreach ($this->comparator->droppedIndexes() as $index) {
592
            $this->logger()->debug("Dropping index [{statement}] from table {table}.", [
593
                'statement' => $index->sqlStatement(),
594
                'table'     => $this->getName(true)
595
            ]);
596
597
            $this->commander->dropIndex($this, $index);
598
        }
599
600
        return $this;
601
    }
602
603
    /**
604
     * Synchronise indexes.
605
     *
606
     * @return $this
607
     */
608 View Code Duplication
    protected function synchroniseIndexes()
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...
609
    {
610
        foreach ($this->comparator->addedIndexes() as $index) {
611
            $this->logger()->debug("Adding index [{statement}] into table {table}.", [
612
                'statement' => $index->sqlStatement(),
613
                'table'     => $this->getName(true)
614
            ]);
615
616
            $this->commander->addIndex($this, $index);
617
        }
618
619
        foreach ($this->comparator->alteredIndexes() as $pair) {
620
            /**
621
             * @var AbstractIndex $initial
622
             * @var AbstractIndex $current
623
             */
624
            list($current, $initial) = $pair;
625
626
            $this->logger()->debug("Altering index [{statement}] to [{new}] in table {table}.", [
627
                'statement' => $initial->sqlStatement(),
628
                'new'       => $current->sqlStatement(),
629
                'table'     => $this->getName(true)
630
            ]);
631
632
            $this->commander->alterIndex($this, $initial, $current);
633
        }
634
635
        return $this;
636
    }
637
638
    /**
639
     * Drop needed foreign keys.
640
     *
641
     * @return $this
642
     */
643 View Code Duplication
    protected function dropForeigns()
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...
644
    {
645
        foreach ($this->comparator->droppedForeigns() as $foreign) {
646
            $this->logger()->debug("Dropping foreign key [{statement}] from table {table}.", [
647
                'statement' => $foreign->sqlStatement(),
648
                'table'     => $this->getName(true)
649
            ]);
650
651
            $this->commander->dropForeign($this, $foreign);
652
        }
653
654
        return $this;
655
    }
656
657
    /**
658
     * Synchronise foreign keys.
659
     *
660
     * @return $this
661
     */
662 View Code Duplication
    protected function synchroniseForeigns()
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...
663
    {
664
        foreach ($this->comparator->addedForeigns() as $foreign) {
665
            $this->logger()->debug("Adding foreign key [{statement}] into table {table}.", [
666
                'statement' => $foreign->sqlStatement(),
667
                'table'     => $this->getName(true)
668
            ]);
669
670
            $this->commander->addForeign($this, $foreign);
671
        }
672
673
        foreach ($this->comparator->alteredForeigns() as $pair) {
674
            /**
675
             * @var AbstractReference $initial
676
             * @var AbstractReference $current
677
             */
678
            list($current, $initial) = $pair;
679
680
            $this->logger()->debug("Altering foreign key [{statement}] to [{new}] in {table}.", [
681
                'statement' => $initial->sqlStatement(),
682
                'table'     => $this->getName(true)
683
            ]);
684
685
            $this->commander->alterForeign($this, $initial, $current);
686
        }
687
688
        return $this;
689
    }
690
691
    /**
692
     * Driver specific column schema.
693
     *
694
     * @param string $name
695
     * @param mixed  $schema
696
     * @return AbstractColumn
697
     */
698
    abstract protected function columnSchema($name, $schema = null);
699
700
    /**
701
     * Driver specific index schema.
702
     *
703
     * @param string $name
704
     * @param mixed  $schema
705
     * @return AbstractIndex
706
     */
707
    abstract protected function indexSchema($name, $schema = null);
708
709
    /**
710
     * Driver specific reference schema.
711
     *
712
     * @param string $name
713
     * @param mixed  $schema
714
     * @return AbstractReference
715
     */
716
    abstract protected function referenceSchema($name, $schema = null);
717
718
719
    /**
720
     * Must load table columns.
721
     *
722
     * @see registerColumn()
723
     * @return self
724
     */
725
    abstract protected function loadColumns();
726
727
    /**
728
     * Must load table indexes.
729
     *
730
     * @see registerIndex()
731
     * @return self
732
     */
733
    abstract protected function loadIndexes();
734
735
    /**
736
     * Must load table references.
737
     *
738
     * @see registerReference()
739
     * @return self
740
     */
741
    abstract protected function loadReferences();
742
743
    /**
744
     * Check if table schema has been modified. Attention, you have to execute dropUndeclared first
745
     * to get valid results.
746
     *
747
     * @return bool
748
     */
749
    protected function hasChanges()
750
    {
751
        return $this->comparator->hasChanges();
752
    }
753
754
    /**
755
     * Calculate difference (removed columns, indexes and foreign keys).
756
     *
757
     * @param bool $forgetColumns
758
     * @param bool $forgetIndexes
759
     * @param bool $forgetForeigns
760
     */
761
    protected function forgetUndeclared($forgetColumns, $forgetIndexes, $forgetForeigns)
762
    {
763
        //We don't need to worry about changed or created columns, indexes and foreign keys here
764
        //as it already handled, we only have to drop columns which were not listed in schema
765
766
        foreach ($this->getColumns() as $column) {
767
            if ($forgetColumns && !$column->isDeclared()) {
768
                $this->forgetColumn($column);
769
                $this->removeDependent($column);
770
            }
771
        }
772
773
        foreach ($this->getIndexes() as $index) {
774
            if ($forgetIndexes && !$index->isDeclared()) {
775
                $this->forgetIndex($index);
776
            }
777
        }
778
779
        foreach ($this->getForeigns() as $foreign) {
780
            if ($forgetForeigns && !$foreign->isDeclared()) {
781
                $this->forgetForeign($foreign);
782
            }
783
        }
784
    }
785
786
    /**
787
     * Remove dependent indexes and foreign keys.
788
     *
789
     * @param ColumnInterface $column
790
     */
791
    private function removeDependent(ColumnInterface $column)
792
    {
793
        if ($this->hasForeign($column->getName())) {
794
            $this->forgetForeign($this->foreign($column->getName()));
795
        }
796
797
        foreach ($this->getIndexes() as $index) {
798
            if (in_array($column->getName(), $index->getColumns())) {
799
                //Dropping related indexes
800
                $this->forgetIndex($index);
801
            }
802
        }
803
    }
804
805
    /**
806
     * Forget all elements.
807
     *
808
     * @return $this
809
     */
810
    private function forgetElements()
811
    {
812
        foreach ($this->getColumns() as $column) {
813
            $this->forgetColumn($column);
814
        }
815
816
        foreach ($this->getIndexes() as $index) {
817
            $this->forgetIndex($index);
818
        }
819
820
        foreach ($this->getForeigns() as $foreign) {
821
            $this->forgetForeign($foreign);
822
        }
823
824
        return $this;
825
    }
826
}