Completed
Branch feature/pre-split (ce4b6b)
by Anton
05:23
created

AbstractTable::fetchReferences()

Size

Total Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
nc 1
dl 0
loc 1
c 0
b 0
f 0
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
 * @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_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_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_DROPPED;
225
    }
226
227
    /**
228
     * Set table primary keys. Operation can only be applied for newly created tables. Now every
229
     * database might support compound indexes.
230
     *
231
     * @param array $columns
232
     *
233
     * @return self
234
     */
235
    public function setPrimaryKeys(array $columns): AbstractTable
236
    {
237
        //Originally i were forcing an exception when primary key were changed, now we should
238
        //force it when table will be synced
239
240
        //Updating primary keys in current state
241
        $this->current->setPrimaryKeys($columns);
242
243
        return $this;
244
    }
245
246
    /**
247
     * {@inheritdoc}
248
     */
249
    public function getPrimaryKeys(): array
250
    {
251
        return $this->current->getPrimaryKeys();
252
    }
253
254
    /**
255
     * {@inheritdoc}
256
     */
257
    public function hasColumn(string $name): bool
258
    {
259
        return $this->current->hasColumn($name);
260
    }
261
262
    /**
263
     * {@inheritdoc}
264
     *
265
     * @return AbstractColumn[]
266
     */
267
    public function getColumns(): array
268
    {
269
        return $this->current->getColumns();
270
    }
271
272
    /**
273
     * {@inheritdoc}
274
     */
275
    public function hasIndex(array $columns = []): bool
276
    {
277
        return $this->current->hasIndex($columns);
278
    }
279
280
    /**
281
     * {@inheritdoc}
282
     *
283
     * @return AbstractIndex[]
284
     */
285
    public function getIndexes(): array
286
    {
287
        return $this->current->getIndexes();
288
    }
289
290
    /**
291
     * {@inheritdoc}
292
     */
293
    public function hasForeign(string $column): bool
294
    {
295
        return $this->current->hasForeign($column);
296
    }
297
298
    /**
299
     * {@inheritdoc}
300
     *
301
     * @return AbstractReference[]
302
     */
303
    public function getForeigns(): array
304
    {
305
        return $this->current->getForeigns();
306
    }
307
308
    /**
309
     * {@inheritdoc}
310
     */
311
    public function getDependencies(): array
312
    {
313
        $tables = [];
314
        foreach ($this->current->getForeigns() as $foreign) {
315
            $tables[] = $foreign->getForeignTable();
316
        }
317
318
        return $tables;
319
    }
320
321
    /**
322
     * Get/create instance of AbstractColumn associated with current table.
323
     *
324
     * Attention, renamed column will be available by it's old name until being synced!
325
     *
326
     * Examples:
327
     * $table->column('name')->string();
328
     *
329
     * @param string $name
330
     *
331
     * @return AbstractColumn
332
     */
333
    public function column(string $name): AbstractColumn
334
    {
335
        if ($this->current->hasColumn($name)) {
336
            //Column already exists
337
            return $this->current->findColumn($name);
338
        }
339
340
        $column = $this->createColumn($name);
341
        $this->current->registerColumn($column);
342
343
        return $column;
344
    }
345
346
    /**
347
     * Shortcut for column() method.
348
     *
349
     * @param string $column
350
     *
351
     * @return AbstractColumn
352
     */
353
    public function __get(string $column)
354
    {
355
        return $this->column($column);
356
    }
357
358
    /**
359
     * Column creation/altering shortcut, call chain is identical to:
360
     * AbstractTable->column($name)->$type($arguments).
361
     *
362
     * Example:
363
     * $table->string("name");
364
     * $table->text("some_column");
365
     *
366
     * @param string $type
367
     * @param array  $arguments Type specific parameters.
368
     *
369
     * @return AbstractColumn
370
     */
371
    public function __call(string $type, array $arguments)
372
    {
373
        return call_user_func_array(
374
            [$this->column($arguments[0]), $type],
375
            array_slice($arguments, 1)
376
        );
377
    }
378
379
    /**
380
     * Get/create instance of AbstractIndex associated with current table based on list of forming
381
     * column names.
382
     *
383
     * Example:
384
     * $table->index(['key']);
385
     * $table->index(['key', 'key2']);
386
     *
387
     * @param array $columns List of index columns.
388
     *
389
     * @return AbstractIndex
390
     *
391
     * @throws SchemaException
392
     */
393
    public function index(array $columns): AbstractIndex
394
    {
395
        foreach ($columns as $column) {
396
            if (!$this->hasColumn($column)) {
397
                throw new SchemaException("Undefined column '{$column}' in '{$this->getName()}'");
398
            }
399
        }
400
401
        if ($this->hasIndex($columns)) {
402
            return $this->current->findIndex($columns);
403
        }
404
405
        if ($this->initial->hasIndex($columns)) {
406
            //Let's ensure that index name is always stays synced (not regenerated)
407
            $name = $this->initial->findIndex($columns)->getName();
408
        } else {
409
            $name = $this->createIdentifier('index', $columns);
410
        }
411
412
        $index = $this->createIndex($name)->columns($columns);
413
414
        //Adding to current schema
415
        $this->current->registerIndex($index);
416
417
        return $index;
418
    }
419
420
    /**
421
     * Get/create instance of AbstractReference associated with current table based on local column
422
     * name.
423
     *
424
     * @param string $column
425
     *
426
     * @return AbstractReference
427
     *
428
     * @throws SchemaException
429
     */
430
    public function foreign(string $column): AbstractReference
431
    {
432
        if (!$this->hasColumn($column)) {
433
            throw new SchemaException("Undefined column '{$column}' in '{$this->getName()}'");
434
        }
435
436
        if ($this->hasForeign($column)) {
437
            return $this->current->findForeign($column);
438
        }
439
440
        if ($this->initial->hasForeign($column)) {
441
            //Let's ensure that FK name is always stays synced (not regenerated)
442
            $name = $this->initial->findForeign($column)->getName();
443
        } else {
444
            $name = $this->createIdentifier('foreign', [$column]);
445
        }
446
447
        $foreign = $this->createForeign($name)->column($column);
448
449
        //Adding to current schema
450
        $this->current->registerForeign($foreign);
451
452
        //Let's ensure index existence to performance and compatibility reasons
453
        $this->index([$column]);
454
455
        return $foreign;
456
    }
457
458
    /**
459
     * Rename column (only if column exists).
460
     *
461
     * @param string $column
462
     * @param string $name New column name.
463
     *
464
     * @return self
465
     *
466
     * @throws SchemaException
467
     */
468
    public function renameColumn(string $column, string $name): AbstractTable
469
    {
470
        if (!$this->hasColumn($column)) {
471
            throw new SchemaException("Undefined column '{$column}' in '{$this->getName()}'");
472
        }
473
474
        //Rename operation is simple about declaring new name
475
        $this->column($column)->setName($name);
476
477
        return $this;
478
    }
479
480
    /**
481
     * Rename index (only if index exists).
482
     *
483
     * @param array  $columns Index forming columns.
484
     * @param string $name    New index name.
485
     *
486
     * @return self
487
     *
488
     * @throws SchemaException
489
     */
490
    public function renameIndex(array $columns, string $name): AbstractTable
491
    {
492
        if ($this->hasIndex($columns)) {
493
            throw new SchemaException(
494
                "Undefined index ['" . join("', '", $columns) . "'] in '{$this->getName()}'"
495
            );
496
        }
497
498
        //Declaring new index name
499
        $this->index($columns)->setName($name);
500
501
        return $this;
502
    }
503
504
    /**
505
     * Drop column by it's name.
506
     *
507
     * @param string $column
508
     *
509
     * @return self
510
     *
511
     * @throws SchemaException
512
     */
513
    public function dropColumn(string $column): AbstractTable
514
    {
515
        if (empty($schema = $this->current->findColumn($column))) {
516
            throw new SchemaException("Undefined column '{$column}' in '{$this->getName()}'");
517
        }
518
519
        //Dropping column from current schema
520
        $this->current->forgetColumn($schema);
521
522
        return $this;
523
    }
524
525
    /**
526
     * Drop index by it's forming columns.
527
     *
528
     * @param array $columns
529
     *
530
     * @return self
531
     *
532
     * @throws SchemaException
533
     */
534
    public function dropIndex(array $columns): AbstractTable
535
    {
536
        if (empty($schema = $this->current->findIndex($columns))) {
537
            throw new SchemaException(
538
                "Undefined index ['" . join("', '", $columns) . "'] in '{$this->getName()}'"
539
            );
540
        }
541
542
        //Dropping index from current schema
543
        $this->current->forgetIndex($schema);
544
545
        return $this;
546
    }
547
548
    /**
549
     * Drop foreign key by it's name.
550
     *
551
     * @param string $column
552
     *
553
     * @return self
554
     *
555
     * @throws SchemaException
556
     */
557
    public function dropForeign($column): AbstractTable
558
    {
559
        if (empty($schema = $this->current->findForeign($column))) {
560
            throw new SchemaException(
561
                "Undefined FK on '{$column}' in '{$this->getName()}'"
562
            );
563
        }
564
565
        //Dropping foreign from current schema
566
        $this->current->forgetForeign($schema);
567
568
        return $this;
569
    }
570
571
    /**
572
     * Get current table state (detached).
573
     *
574
     * @return TableState
575
     */
576
    public function getState(): TableState
577
    {
578
        $state = clone $this->current;
579
        $state->remountElements();
580
581
        return $state;
582
    }
583
584
    /**
585
     * Reset table state to new form.
586
     *
587
     * @param TableState $state Use null to flush table schema.
588
     *
589
     * @return self|$this
590
     */
591
    public function setState(TableState $state = null): AbstractTable
592
    {
593
        $this->current = new TableState($this->initial->getName());
594
595
        if (!empty($state)) {
596
            $this->current->setName($state->getName());
597
            $this->current->syncState($state);
598
        }
599
600
        return $this;
601
    }
602
603
    /**
604
     * Reset table state to it initial form.
605
     *
606
     * @return self|$this
607
     */
608
    public function resetState(): AbstractTable
609
    {
610
        $this->setState($this->initial);
611
612
        return $this;
613
    }
614
615
    /**
616
     * Save table schema including every column, index, foreign key creation/altering. If table
617
     * does not exist it must be created. If table declared as dropped it will be removed from
618
     * the database.
619
     *
620
     * @param int             $behaviour Operation to be performed while table being saved. In some
621
     *                                   cases (when multiple tables are being updated) it is
622
     *                                   reasonable to drop foreing keys and indexes prior to
623
     *                                   dropping related columns. See sync bus class to get more
624
     *                                   details.
625
     * @param LoggerInterface $logger    Optional, aggregates messages for data syncing.
626
     * @param bool            $reset     When true schema will be marked as synced.
627
     *
628
     * @throws HandlerException
629
     *
630
     * @throws SchemaException
631
     */
632
    public function save(
633
        int $behaviour = Behaviour::DO_ALL,
634
        LoggerInterface $logger = null,
635
        bool $reset = true
636
    ) {
637
        //We need an instance of Handler of dbal operations
638
        $handler = $this->driver->getHandler($logger);
639
640
        if ($this->status == self::STATUS_DROPPED) {
641
            //We don't need syncer for this operation
642
            $handler->dropTable($this);
643
644
            //Flushing status
645
            $this->status = self::STATUS_NEW;
646
647
            return;
648
        }
649
650
        //Ensure that columns references to valid indexes and et
651
        $prepared = $this->prepareSchema($behaviour & Behaviour::CREATE_FOREIGNS);
652
653
        if ($this->status == self::STATUS_NEW) {
654
            //Executing table creation
655
            $handler->createTable($prepared);
656
        } else {
657
            //Executing table syncing
658
            if ($this->hasChanges()) {
659
                $handler->syncTable($prepared, $behaviour);
660
            }
661
        }
662
663
        //Syncing our schemas
664
        if ($reset) {
665
            $this->status = self::STATUS_EXISTS;
666
            $this->initial->syncState($prepared->current);
667
        }
668
    }
669
670
    /**
671
     * Ensure that no wrong indexes left in table.
672
     *
673
     * @param bool $withForeigns
674
     *
675
     * @return AbstractTable
676
     */
677
    protected function prepareSchema(bool $withForeigns = true)
678
    {
679
        //To make sure that no pre-sync modifications will be reflected on current table
680
        $target = clone $this;
681
682
        /*
683
         * In cases where columns are removed we have to automatically remove related indexes and
684
         * foreign keys.
685
         */
686
        foreach ($this->getComparator()->droppedColumns() as $column) {
687
            foreach ($target->getIndexes() as $index) {
688
                if (in_array($column->getName(), $index->getColumns())) {
689
                    $target->current->forgetIndex($index);
690
                }
691
            }
692
693
            foreach ($target->getForeigns() as $foreign) {
694
                if ($column->getName() == $foreign->getColumn()) {
695
                    $target->current->forgetForeign($foreign);
696
                }
697
            }
698
        }
699
700
        //We also have to adjusts indexes and foreign keys
701
        foreach ($this->getComparator()->alteredColumns() as $pair) {
702
            /**
703
             * @var AbstractColumn $initial
704
             * @var AbstractColumn $name
705
             */
706
            list($name, $initial) = $pair;
707
708
            foreach ($target->getIndexes() as $index) {
709
                if (in_array($initial->getName(), $index->getColumns())) {
710
                    $columns = $index->getColumns();
711
712
                    //Replacing column name
713
                    foreach ($columns as &$column) {
714
                        if ($column == $initial->getName()) {
715
                            $column = $name->getName();
716
                        }
717
718
                        unset($column);
719
                    }
720
721
                    $index->columns($columns);
722
                }
723
            }
724
725
            foreach ($target->getForeigns() as $foreign) {
726
                if ($initial->getName() == $foreign->getColumn()) {
727
                    $foreign->column($name->getName());
728
                }
729
            }
730
        }
731
732
        if (!$withForeigns) {
733
            foreach ($this->getComparator()->addedForeigns() as $foreign) {
734
                //Excluding from creation
735
                $target->current->forgetForeign($foreign);
736
            }
737
        }
738
739
        return $target;
740
    }
741
742
    /**
743
     * @return AbstractColumn|string
744
     */
745
    public function __toString(): string
746
    {
747
        return $this->getName();
748
    }
749
750
    /**
751
     * Cloning schemas as well.
752
     */
753
    public function __clone()
754
    {
755
        $this->initial = clone $this->initial;
756
        $this->current = clone $this->current;
757
    }
758
759
    /**
760
     * @return array
761
     */
762
    public function __debugInfo()
763
    {
764
        return [
765
            'name'        => $this->getName(),
766
            'primaryKeys' => $this->getPrimaryKeys(),
767
            'columns'     => array_values($this->getColumns()),
768
            'indexes'     => array_values($this->getIndexes()),
769
            'references'  => array_values($this->getForeigns()),
770
        ];
771
    }
772
773
    /**
774
     * Populate table schema with values from database.
775
     *
776
     * @param TableState $state
777
     */
778
    protected function initSchema(TableState $state)
779
    {
780
        foreach ($this->fetchColumns() as $column) {
781
            $state->registerColumn($column);
782
        }
783
784
        foreach ($this->fetchIndexes() as $index) {
785
            $state->registerIndex($index);
786
        }
787
788
        foreach ($this->fetchReferences() as $foreign) {
789
            $state->registerForeign($foreign);
790
        }
791
792
        $state->setPrimaryKeys($this->fetchPrimaryKeys());
793
794
        //DBMS specific initialization can be placed here
795
    }
796
797
    /**
798
     * Fetch index declarations from database.
799
     *
800
     * @return AbstractColumn[]
801
     */
802
    abstract protected function fetchColumns(): array;
803
804
    /**
805
     * Fetch index declarations from database.
806
     *
807
     * @return AbstractIndex[]
808
     */
809
    abstract protected function fetchIndexes(): array;
810
811
    /**
812
     * Fetch references declaration from database.
813
     *
814
     * @return AbstractReference[]
815
     */
816
    abstract protected function fetchReferences(): array;
817
818
    /**
819
     * Fetch names of primary keys from table.
820
     *
821
     * @return array
822
     */
823
    abstract protected function fetchPrimaryKeys(): array;
824
825
    /**
826
     * Create column with a given name.
827
     *
828
     * @param string $name
829
     *
830
     * @return AbstractColumn
831
     */
832
    abstract protected function createColumn(string $name): AbstractColumn;
833
834
    /**
835
     * Create index for a given set of columns.
836
     *
837
     * @param string $name
838
     *
839
     * @return AbstractIndex
840
     */
841
    abstract protected function createIndex(string $name): AbstractIndex;
842
843
    /**
844
     * Create reference on a given column set.
845
     *
846
     * @param string $name
847
     *
848
     * @return AbstractReference
849
     */
850
    abstract protected function createForeign(string $name): AbstractReference;
851
852
    /**
853
     * Generate unique name for indexes and foreign keys.
854
     *
855
     * @param string $type
856
     * @param array  $columns
857
     *
858
     * @return string
859
     */
860
    protected function createIdentifier(string $type, array $columns): string
861
    {
862
        $name = $this->getName() . '_' . $type . '_' . join('_', $columns) . '_' . uniqid();
863
864
        if (strlen($name) > 64) {
865
            //Many DBMS has limitations on identifier length
866
            $name = md5($name);
867
        }
868
869
        return $name;
870
    }
871
}
872