Completed
Branch feature/pre-split (bbd802)
by Anton
02:54
created

AbstractTable::index()   A

Complexity

Conditions 4
Paths 5

Size

Total Lines 18
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 10
nc 5
nop 1
dl 0
loc 18
rs 9.2
c 0
b 0
f 0
1
<?php
2
/**
3
 * components
4
 *
5
 * @author    Wolfy-J
6
 */
7
namespace Spiral\Database\Schemas\Prototypes;
8
9
use Interop\Container\ContainerInterface;
10
use Psr\Log\LoggerInterface;
11
use Spiral\Database\Entities\AbstractHandler;
12
use Spiral\Database\Entities\Driver;
13
use Spiral\Database\Exceptions\HandlerException;
14
use Spiral\Database\Exceptions\SchemaException;
15
use Spiral\Database\Schemas\ColumnInterface;
16
use Spiral\Database\Schemas\IndexInterface;
17
use Spiral\Database\Schemas\ReferenceInterface;
18
use Spiral\Database\Schemas\StateComparator;
19
use Spiral\Database\Schemas\TableInterface;
20
use Spiral\Database\Schemas\TableState;
21
22
/**
23
 * AbstractTable class used to describe and manage state of specified table. It provides ability to
24
 * get table introspection, update table schema and automatically generate set of diff operations.
25
 *
26
 * Most of table operation like column, index or foreign key creation/altering will be applied when
27
 * save() method will be called.
28
 *
29
 * Column configuration shortcuts:
30
 *
31
 * @method AbstractColumn primary($column)
32
 * @method AbstractColumn bigPrimary($column)
33
 * @method AbstractColumn enum($column, array $values)
34
 * @method AbstractColumn string($column, $length = 255)
35
 * @method AbstractColumn decimal($column, $precision, $scale)
36
 * @method AbstractColumn boolean($column)
37
 * @method AbstractColumn integer($column)
38
 * @method AbstractColumn tinyInteger($column)
39
 * @method AbstractColumn bigInteger($column)
40
 * @method AbstractColumn text($column)
41
 * @method AbstractColumn tinyText($column)
42
 * @method AbstractColumn longText($column)
43
 * @method AbstractColumn double($column)
44
 * @method AbstractColumn float($column)
45
 * @method AbstractColumn datetime($column)
46
 * @method AbstractColumn date($column)
47
 * @method AbstractColumn time($column)
48
 * @method AbstractColumn timestamp($column)
49
 * @method AbstractColumn binary($column)
50
 * @method AbstractColumn tinyBinary($column)
51
 * @method AbstractColumn longBinary($column)
52
 */
53
abstract class AbstractTable implements TableInterface
54
{
55
    /**
56
     * Table states.
57
     */
58
    const STATUS_NEW     = 0;
59
    const STATUS_EXISTS  = 1;
60
    const STATUS_DROPPED = 2;
61
62
    /**
63
     * Indication that table is exists and current schema is fetched from database.
64
     *
65
     * @var int
66
     */
67
    private $status = self::STATUS_NEW;
68
69
    /**
70
     * Database specific tablePrefix. Required for table renames.
71
     *
72
     * @var string
73
     */
74
    private $prefix = '';
75
76
    /**
77
     * @invisible
78
     *
79
     * @var Driver
80
     */
81
    protected $driver = null;
82
83
    /**
84
     * Initial table state.
85
     *
86
     * @invisible
87
     * @var TableState
88
     */
89
    protected $initial = null;
90
91
    /**
92
     * Currently defined table state.
93
     *
94
     * @invisible
95
     * @var TableState
96
     */
97
    protected $current = null;
98
99
    /**
100
     * @param Driver $driver Parent driver.
101
     * @param string $name   Table name, must include table prefix.
102
     * @param string $prefix Database specific table prefix.
103
     */
104
    public function __construct(Driver $driver, string $name, string $prefix)
105
    {
106
        $this->driver = $driver;
107
        $this->prefix = $prefix;
108
109
        //Initializing states
110
        $this->initial = new TableState($this->prefix . $name);
111
        $this->current = new TableState($this->prefix . $name);
112
113
        if ($this->driver->hasTable($this->getName())) {
114
            $this->status = self::STATUS_EXISTS;
115
        }
116
117
        if ($this->exists()) {
118
            //Initiating table schema
119
            $this->initSchema($this->initial);
120
        }
121
122
        $this->setState($this->initial);
123
    }
124
125
    /**
126
     * Get instance of associated driver.
127
     *
128
     * @return Driver
129
     */
130
    public function getDriver(): Driver
131
    {
132
        return $this->driver;
133
    }
134
135
    /**
136
     * Return database specific table prefix.
137
     *
138
     * @return string
139
     */
140
    public function getPrefix(): string
141
    {
142
        return $this->prefix;
143
    }
144
145
    /**
146
     * @return StateComparator
147
     */
148
    public function getComparator(): StateComparator
149
    {
150
        return new StateComparator($this->initial, $this->current);
151
    }
152
153
    /**
154
     * Check if table schema has been modified since synchronization.
155
     *
156
     * @return bool
157
     */
158
    protected function hasChanges(): bool
159
    {
160
        return $this->getComparator()->hasChanges();
161
    }
162
163
    /**
164
     * {@inheritdoc}
165
     */
166
    public function exists(): bool
167
    {
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 dropper
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
        $index = $this->createIndex($this->createIdentifier('index', $columns));
406
        $index->columns($columns);
407
        $this->current->registerIndex($index);
408
409
        return $index;
410
    }
411
412
    /**
413
     * Get/create instance of AbstractReference associated with current table based on local column
414
     * name.
415
     *
416
     * @param string $column
417
     *
418
     * @return AbstractReference
419
     *
420
     * @throws SchemaException
421
     */
422
    public function foreign(string $column): AbstractReference
423
    {
424
        if (!$this->hasColumn($column)) {
425
            throw new SchemaException("Undefined column '{$column}' in '{$this->getName()}'");
426
        }
427
428
        if ($this->hasForeign($column)) {
429
            return $this->current->findForeign($column);
430
        }
431
432
        $foreign = $this->createForeign($this->createIdentifier('foreign', [$column]));
433
        $foreign->column($column);
434
435
        $this->current->registerReference($foreign);
436
437
        //Let's ensure index existence to performance and compatibility reasons
438
        $this->index($column);
0 ignored issues
show
Documentation introduced by
$column is of type string, but the function expects a array.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
439
440
        return $foreign;
441
    }
442
443
    /**
444
     * Rename column (only if column exists).
445
     *
446
     * @param string $column
447
     * @param string $name New column name.
448
     *
449
     * @return self
450
     *
451
     * @throws SchemaException
452
     */
453
    public function renameColumn(string $column, string $name): AbstractTable
454
    {
455
        if (!$this->hasColumn($column)) {
456
            throw new SchemaException("Undefined column '{$column}' in '{$this->getName()}'");
457
        }
458
459
        //Rename operation is simple about declaring new name
460
        $this->column($column)->setName($name);
461
462
        return $this;
463
    }
464
465
    /**
466
     * Rename index (only if index exists).
467
     *
468
     * @param array  $columns Index forming columns.
469
     * @param string $name    New index name.
470
     *
471
     * @return self
472
     *
473
     * @throws SchemaException
474
     */
475
    public function renameIndex(array $columns, string $name): AbstractTable
476
    {
477
        if ($this->hasIndex($columns)) {
478
            throw new SchemaException(
479
                "Undefined index ['" . join("', '", $columns) . "'] in '{$this->getName()}'"
480
            );
481
        }
482
483
        //Declaring new index name
484
        $this->index($columns)->setName($name);
485
486
        return $this;
487
    }
488
489
    /**
490
     * Drop column by it's name.
491
     *
492
     * @param string $column
493
     *
494
     * @return self
495
     *
496
     * @throws SchemaException
497
     */
498
    public function dropColumn(string $column): AbstractTable
499
    {
500
        if (empty($schema = $this->current->findColumn($column))) {
501
            throw new SchemaException("Undefined column '{$column}' in '{$this->getName()}'");
502
        }
503
504
        //Dropping column from current schema
505
        $this->current->forgetColumn($schema);
506
507
        return $this;
508
    }
509
510
    /**
511
     * Drop index by it's forming columns.
512
     *
513
     * @param array $columns
514
     *
515
     * @return self
516
     *
517
     * @throws SchemaException
518
     */
519
    public function dropIndex(array $columns): AbstractTable
520
    {
521
        if (empty($schema = $this->current->findIndex($columns))) {
522
            throw new SchemaException(
523
                "Undefined index ['" . join("', '", $columns) . "'] in '{$this->getName()}'"
524
            );
525
        }
526
527
        //Dropping index from current schema
528
        $this->current->forgetIndex($schema);
529
530
        return $this;
531
    }
532
533
    /**
534
     * Drop foreign key by it's name.
535
     *
536
     * @param string $column
537
     *
538
     * @return self
539
     *
540
     * @throws SchemaException
541
     */
542
    public function dropForeign($column): AbstractTable
543
    {
544
        if (empty($schema = $this->current->findForeign($column))) {
545
            throw new SchemaException(
546
                "Undefined FK on '{$column}' in '{$this->getName()}'"
547
            );
548
        }
549
550
        //Dropping foreign from current schema
551
        $this->current->forgetForeign($schema);
552
553
        return $this;
554
    }
555
556
    /**
557
     * Reset table state to new form.
558
     *
559
     * @param TableState $state Use null to flush table schema.
560
     *
561
     * @return self|$this
562
     */
563
    public function setState(TableState $state = null): AbstractTable
564
    {
565
        $this->current = new TableState($this->initial->getName());
566
567
        if (!empty($state)) {
568
            $this->current->setName($state->getName());
569
            $this->current->syncState($state);
570
        }
571
572
        return $this;
573
    }
574
575
    /**
576
     * Reset table state to it initial form.
577
     *
578
     * @return self|$this
579
     */
580
    public function resetState(): AbstractTable
581
    {
582
        $this->setState($this->initial);
583
584
        return $this;
585
    }
586
587
    /**
588
     * Save table schema including every column, index, foreign key creation/altering. If table
589
     * does not exist it must be created. If table declared as dropped it will be removed from
590
     * the database.
591
     *
592
     * @param int             $behaviour Operation to be performed while table being saved. In some
593
     *                                   cases (when multiple tables are being updated) it is
594
     *                                   reasonable to drop foreing keys and indexes prior to
595
     *                                   dropping related columns. See sync bus class to get more
596
     *                                   details.
597
     * @param LoggerInterface $logger    Optional, aggregates messages for data syncing.
598
     *
599
     * @throws HandlerException
600
     */
601
    public function save(int $behaviour = AbstractHandler::DO_ALL, LoggerInterface $logger = null)
602
    {
603
        //We need an instance of Handler of dbal operations
604
        $handler = $this->driver->getHandler($logger);
605
606
        if ($this->status == self::STATUS_DROPPED) {
607
            //We don't need syncer for this operation
608
            $handler->dropTable($this);
609
610
            //Flushing status
611
            $this->status = self::STATUS_NEW;
612
613
            return;
614
        }
615
616
        //Ensure that columns references to valid indexes and et
617
        $prepared = $this->prepareSchema();
618
619
        if ($this->status == self::STATUS_NEW) {
620
            //Executing table creation
621
            $handler->createTable($prepared);
622
            $this->status = self::STATUS_EXISTS;
623
        } else {
624
            //Executing table syncing
625
            if ($this->hasChanges()) {
626
                $handler->syncTable($prepared, $behaviour);
627
            }
628
629
            $prepared->status = self::STATUS_EXISTS;
630
        }
631
632
        //Syncing our schemas
633
        $this->initial->syncState($prepared->current);
634
    }
635
636
    /**
637
     * Ensure that no wrong indexes left in table.
638
     *
639
     * @return AbstractTable
640
     */
641
    protected function prepareSchema()
642
    {
643
        //To make sure that no pre-sync modifications will be reflected on current table
644
        $target = clone $this;
645
646
        /*
647
         * In cases where columns are removed we have to automatically remove related indexes and
648
         * foreign keys.
649
         */
650
        foreach ($this->getComparator()->droppedColumns() as $column) {
651
            foreach ($target->getIndexes() as $index) {
652
                if (in_array($column->getName(), $index->getColumns())) {
653
                    $target->current->forgetIndex($index);
654
                }
655
            }
656
657
            foreach ($target->getForeigns() as $foreign) {
658
                if ($column->getName() == $foreign->getColumn()) {
659
                    $target->current->forgetForeign($foreign);
660
                }
661
            }
662
        }
663
664
        //We also have to adjusts indexes and foreign keys
665
        foreach ($this->getComparator()->alteredColumns() as $pair) {
666
            /**
667
             * @var AbstractColumn $initial
668
             * @var AbstractColumn $name
669
             */
670
            list($name, $initial) = $pair;
671
672
            foreach ($target->getIndexes() as $index) {
673
                if (in_array($initial->getName(), $index->getColumns())) {
674
                    $columns = $index->getColumns();
675
676
                    //Replacing column name
677
                    foreach ($columns as &$column) {
678
                        if ($column == $initial->getName()) {
679
                            $column = $name->getName();
680
                        }
681
682
                        unset($column);
683
                    }
684
685
                    $index->columns($columns);
686
                }
687
            }
688
689
            foreach ($target->getForeigns() as $foreign) {
690
                if ($initial->getName() == $foreign->getColumn()) {
691
                    $foreign->column($name->getName());
692
                }
693
            }
694
        }
695
696
        return $target;
697
    }
698
699
    /**
700
     * @return AbstractColumn|string
701
     */
702
    public function __toString(): string
703
    {
704
        return $this->getName();
705
    }
706
707
    /**
708
     * Cloning schemas as well.
709
     */
710
    public function __clone()
711
    {
712
        $this->initial = clone $this->initial;
713
        $this->current = clone $this->current;
714
    }
715
716
    /**
717
     * @return array
718
     */
719
    public function __debugInfo()
720
    {
721
        return [
722
            'name'        => $this->getName(),
723
            'primaryKeys' => $this->getPrimaryKeys(),
724
            'columns'     => array_values($this->getColumns()),
725
            'indexes'     => array_values($this->getIndexes()),
726
            'references'  => array_values($this->getForeigns()),
727
        ];
728
    }
729
730
    /**
731
     * Populate table schema with values from database.
732
     *
733
     * @param TableState $state
734
     */
735
    protected function initSchema(TableState $state)
736
    {
737
        foreach ($this->fetchColumns() as $column) {
738
            $state->registerColumn($column);
739
        }
740
741
        foreach ($this->fetchIndexes() as $index) {
742
            $state->registerIndex($index);
743
        }
744
745
        foreach ($this->fetchReferences() as $foreign) {
746
            $state->registerReference($foreign);
747
        }
748
749
        $state->setPrimaryKeys($this->fetchPrimaryKeys());
750
751
        //DBMS specific initialization can be placed here
752
    }
753
754
    /**
755
     * Fetch index declarations from database.
756
     *
757
     * @return AbstractColumn[]
758
     */
759
    abstract protected function fetchColumns(): array;
760
761
    /**
762
     * Fetch index declarations from database.
763
     *
764
     * @return AbstractIndex[]
765
     */
766
    abstract protected function fetchIndexes(): array;
767
768
    /**
769
     * Fetch references declaration from database.
770
     *
771
     * @return AbstractReference[]
772
     */
773
    abstract protected function fetchReferences(): array;
774
775
    /**
776
     * Fetch names of primary keys from table.
777
     *
778
     * @return array
779
     */
780
    abstract protected function fetchPrimaryKeys(): array;
781
782
    /**
783
     * Create column with a given name.
784
     *
785
     * @param string $name
786
     *
787
     * @return AbstractColumn
788
     */
789
    abstract protected function createColumn(string $name): AbstractColumn;
790
791
    /**
792
     * Create index for a given set of columns.
793
     *
794
     * @param string $name
795
     *
796
     * @return AbstractIndex
797
     */
798
    abstract protected function createIndex(string $name): AbstractIndex;
799
800
    /**
801
     * Create reference on a given column set.
802
     *
803
     * @param string $name
804
     *
805
     * @return AbstractReference
806
     */
807
    abstract protected function createForeign(string $name): AbstractReference;
808
809
    /**
810
     * Generate unique name for indexes and foreign keys.
811
     *
812
     * @param string $type
813
     * @param array  $columns
814
     *
815
     * @return string
816
     */
817
    protected function createIdentifier(string $type, array $columns): string
818
    {
819
        $name = $this->getName() . '_' . $type . '_' . join('_', $columns) . '_' . uniqid();
820
821
        if (strlen($name) > 64) {
822
            //Many DBMS has limitations on identifier length
823
            $name = md5($name);
824
        }
825
826
        return $name;
827
    }
828
}