Completed
Branch feature/pre-split (39c4b2)
by Anton
03:05
created

AbstractTable::declareDropped()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 4
nc 2
nop 0
dl 0
loc 9
rs 9.6666
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\LoggerAwareInterface;
11
use Spiral\Core\Component;
12
use Spiral\Database\Entities\Driver;
13
use Spiral\Database\Exceptions\SchemaException;
14
use Spiral\Database\Schemas\ColumnInterface;
15
use Spiral\Database\Schemas\IndexInterface;
16
use Spiral\Database\Schemas\ReferenceInterface;
17
use Spiral\Database\Schemas\StateComparator;
18
use Spiral\Database\Schemas\TableInterface;
19
use Spiral\Database\Schemas\TableState;
20
use Spiral\Debug\Traits\LoggerTrait;
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 extends Component implements TableInterface, LoggerAwareInterface
54
{
55
    use LoggerTrait;
56
57
    /**
58
     * Table states.
59
     */
60
    const STATUS_NEW     = 0;
61
    const STATUS_EXISTS  = 1;
62
    const STATUS_DROPPED = 2;
63
64
    /**
65
     * Indication that table is exists and current schema is fetched from database.
66
     *
67
     * @var int
68
     */
69
    private $status = self::STATUS_NEW;
70
71
    /**
72
     * Database specific tablePrefix. Required for table renames.
73
     *
74
     * @var string
75
     */
76
    private $prefix = '';
77
78
    /**
79
     * @invisible
80
     *
81
     * @var Driver
82
     */
83
    protected $driver = null;
84
85
    /**
86
     * Initial table state.
87
     *
88
     * @invisible
89
     * @var TableState
90
     */
91
    protected $initialState = null;
92
93
    /**
94
     * Currently defined table state.
95
     *
96
     * @invisible
97
     * @var TableState
98
     */
99
    protected $currentState = null;
100
101
    /**
102
     * @param Driver $driver Parent driver.
103
     * @param string $name   Table name, must include table prefix.
104
     * @param string $prefix Database specific table prefix.
105
     */
106
    public function __construct(Driver $driver, string $name, string $prefix)
107
    {
108
        $this->driver = $driver;
109
        $this->prefix = $prefix;
110
111
        //Initializing states
112
        $this->initialState = new TableState($this->prefix . $name);
113
        $this->currentState = new TableState($this->prefix . $name);
114
115
        if ($this->driver->hasTable($this->getName())) {
116
            $this->status = self::STATUS_EXISTS;
117
        }
118
119
        if ($this->exists()) {
120
            //Initiating table schema
121
            $this->initSchema($this->initialState);
122
        }
123
124
        $this->setStatus($this->initialState);
125
    }
126
127
    /**
128
     * Get instance of associated driver.
129
     *
130
     * @return Driver
131
     */
132
    public function getDriver(): Driver
133
    {
134
        return $this->driver;
135
    }
136
137
    /**
138
     * @return StateComparator
139
     */
140
    public function getComparator(): StateComparator
141
    {
142
        return new StateComparator($this->initialState, $this->currentState);
143
    }
144
145
    /**
146
     * Check if table schema has been modified since synchronization.
147
     *
148
     * @return bool
149
     */
150
    protected function hasChanges(): bool
151
    {
152
        return $this->getComparator()->hasChanges();
153
    }
154
155
    /**
156
     * {@inheritdoc}
157
     */
158
    public function exists(): bool
159
    {
160
        return $this->status == self::STATUS_EXISTS || $this->status == self::STATUS_DROPPED;
161
    }
162
163
    /**
164
     * Table status (see codes above).
165
     *
166
     * @return int
167
     */
168
    public function getStatus(): int
169
    {
170
        return $this->status;
171
    }
172
173
    /**
174
     * Return database specific table prefix.
175
     *
176
     * @return string
177
     */
178
    public function getPrefix(): string
179
    {
180
        return $this->prefix;
181
    }
182
183
    /**
184
     * Sets table name. Use this function in combination with save to rename table.
185
     *
186
     * @param string $name
187
     *
188
     * @return string Prefixed table name.
189
     */
190
    public function setName(string $name): string
191
    {
192
        $this->currentState->setName($this->prefix . $name);
193
194
        return $this->getName();
195
    }
196
197
    /**
198
     * {@inheritdoc}
199
     */
200
    public function getName(): string
201
    {
202
        return $this->currentState->getName();
203
    }
204
205
    /**
206
     * Declare table as dropped, you have to sync table using "save" method in order to apply this
207
     * change.
208
     */
209
    public function declareDropped()
210
    {
211
        if ($this->status == self::STATUS_NEW) {
212
            throw new SchemaException("Unable to drop non existed table");
213
        }
214
215
        //Declaring as dropper
216
        $this->status = self::STATUS_DROPPED;
217
    }
218
219
    /**
220
     * Set table primary keys. Operation can only be applied for newly created tables. Now every
221
     * database might support compound indexes.
222
     *
223
     * @param array $columns
224
     *
225
     * @return self
226
     */
227
    public function setPrimaryKeys(array $columns): AbstractTable
228
    {
229
        //Originally we were forcing an exception when primary key were changed, now we should
230
        //force it when table will be synced
231
232
        //Updating primary keys in current state
233
        $this->currentState->setPrimaryKeys($columns);
234
235
        return $this;
236
    }
237
238
    /**
239
     * {@inheritdoc}
240
     */
241
    public function getPrimaryKeys(): array
242
    {
243
        return $this->currentState->getPrimaryKeys();
244
    }
245
246
    /**
247
     * {@inheritdoc}
248
     */
249
    public function hasColumn(string $name): bool
250
    {
251
        return $this->currentState->hasColumn($name);
252
    }
253
254
    /**
255
     * {@inheritdoc}
256
     *
257
     * @return ColumnInterface[]|AbstractColumn[]
258
     */
259
    public function getColumns(): array
260
    {
261
        return $this->currentState->getColumns();
262
    }
263
264
    /**
265
     * {@inheritdoc}
266
     */
267
    public function hasIndex(array $columns = []): bool
268
    {
269
        return $this->currentState->hasIndex($columns);
270
    }
271
272
    /**
273
     * {@inheritdoc}
274
     *
275
     * @return IndexInterface[]|AbstractIndex[]
276
     */
277
    public function getIndexes(): array
278
    {
279
        return $this->currentState->getIndexes();
280
    }
281
282
    /**
283
     * {@inheritdoc}
284
     */
285
    public function hasForeign(string $column): bool
286
    {
287
        return $this->currentState->hasForeign($column);
288
    }
289
290
    /**
291
     * {@inheritdoc}
292
     *
293
     * @return ReferenceInterface[]|AbstractReference[]
294
     */
295
    public function getForeigns(): array
296
    {
297
        return $this->currentState->getForeigns();
298
    }
299
300
    /**
301
     * {@inheritdoc}
302
     */
303
    public function getDependencies(): array
304
    {
305
        $tables = [];
306
        foreach ($this->currentState->getForeigns() as $foreign) {
307
            $tables[] = $foreign->getForeignTable();
308
        }
309
310
        return $tables;
311
    }
312
313
    /**
314
     * Get/create instance of AbstractColumn associated with current table.
315
     *
316
     * Examples:
317
     * $table->column('name')->string();
318
     *
319
     * @param string $name
320
     *
321
     * @return AbstractColumn
322
     */
323
    public function column(string $name): AbstractColumn
324
    {
325
        if ($this->currentState->hasColumn($name)) {
326
            //Column already exists
327
            return $this->currentState->findColumn($name);
328
        }
329
330
        $column = $this->createColumn($name);
331
        $this->currentState->registerColumn($column);
332
333
        return $column;
334
    }
335
336
    /**
337
     * Shortcut for column() method.
338
     *
339
     * @param string $column
340
     *
341
     * @return AbstractColumn
342
     */
343
    public function __get(string $column)
344
    {
345
        return $this->column($column);
346
    }
347
348
    /**
349
     * Column creation/altering shortcut, call chain is identical to:
350
     * AbstractTable->column($name)->$type($arguments).
351
     *
352
     * Example:
353
     * $table->string("name");
354
     * $table->text("some_column");
355
     *
356
     * @param string $type
357
     * @param array  $arguments Type specific parameters.
358
     *
359
     * @return AbstractColumn
360
     */
361
    public function __call(string $type, array $arguments)
362
    {
363
        return call_user_func_array(
364
            [$this->column($arguments[0]), $type],
365
            array_slice($arguments, 1)
366
        );
367
    }
368
369
    /**
370
     * Get/create instance of AbstractIndex associated with current table based on list of forming
371
     * column names.
372
     *
373
     * Example:
374
     * $table->index('key');
375
     * $table->index('key', 'key2');
376
     * $table->index(['key', 'key2']);
377
     *
378
     * @param mixed $columns Column name, or array of columns.
379
     *
380
     * @return AbstractIndex
381
     *
382
     * @throws SchemaException
383
     */
384
    public function index($columns): AbstractIndex
385
    {
386
        $columns = is_array($columns) ? $columns : func_get_args();
387
388
        foreach ($columns as $column) {
389
            if (!$this->hasColumn($column)) {
390
                throw new SchemaException("Undefined column '{$column}' in '{$this->getName()}'");
391
            }
392
        }
393
394
        if ($this->hasIndex($columns)) {
395
            return $this->currentState->findIndex($columns);
396
        }
397
398
        $index = $this->createIndex($this->createIdentifier('index', $columns));
399
        $index->columns($columns);
400
        $this->currentState->registerIndex($index);
401
402
        return $index;
403
    }
404
405
    /**
406
     * Get/create instance of AbstractReference associated with current table based on local column
407
     * name.
408
     *
409
     * @param string $column
410
     *
411
     * @return AbstractReference
412
     *
413
     * @throws SchemaException
414
     */
415
    public function foreign(string $column): AbstractReference
416
    {
417
        if (!$this->hasColumn($column)) {
418
            throw new SchemaException("Undefined column '{$column}' in '{$this->getName()}'");
419
        }
420
421
        if ($this->hasForeign($column)) {
422
            return $this->currentState->findForeign($column);
423
        }
424
425
        $foreign = $this->createReference($this->createIdentifier('foreign', [$column]));
426
        $foreign->column($column);
427
        $this->currentState->registerReference($foreign);
428
429
        //Let's ensure index existence
430
        $this->index($column);
431
432
        return $foreign;
433
    }
434
435
    /**
436
     * Rename column (only if column exists).
437
     *
438
     * @param string $column
439
     * @param string $name New column name.
440
     *
441
     * @return self
442
     *
443
     * @throws SchemaException
444
     */
445
    public function renameColumn(string $column, string $name): AbstractTable
446
    {
447
        if (!$this->hasColumn($column)) {
448
            throw new SchemaException("Undefined column '{$column}' in '{$this->getName()}'");
449
        }
450
451
        //Rename operation is simple about declaring new name
452
        $this->column($column)->setName($name);
453
454
        return $this;
455
    }
456
457
    /**
458
     * Rename index (only if index exists).
459
     *
460
     * @param array  $columns Index forming columns.
461
     * @param string $name    New index name.
462
     *
463
     * @return self
464
     *
465
     * @throws SchemaException
466
     */
467
    public function renameIndex(array $columns, string $name): AbstractTable
468
    {
469
        if (!$this->hasIndex($columns)) {
470
            throw new SchemaException(
471
                "Undefined index ['" . join("', '", $columns) . "'] in '{$this->getName()}'"
472
            );
473
        }
474
475
        //Declaring new index name
476
        $this->index($columns)->setName($name);
477
478
        return $this;
479
    }
480
481
    /**
482
     * Drop column by it's name.
483
     *
484
     * @param string $column
485
     *
486
     * @return self
487
     *
488
     * @throws SchemaException
489
     */
490 View Code Duplication
    public function dropColumn(string $column): 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...
491
    {
492
        if (!empty($schema = $this->currentState->findColumn($column))) {
493
            throw new SchemaException("Undefined column '{$column}' in '{$this->getName()}'");
494
        }
495
496
        //Dropping column from current schema
497
        $this->currentState->forgetColumn($schema);
0 ignored issues
show
Documentation introduced by
$schema is of type null, but the function expects a object<Spiral\Database\S...totypes\AbstractColumn>.

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...
498
499
        return $this;
500
    }
501
502
    /**
503
     * Drop index by it's forming columns.
504
     *
505
     * @param array $columns
506
     *
507
     * @return self
508
     *
509
     * @throws SchemaException
510
     */
511
    public function dropIndex(array $columns): AbstractTable
512
    {
513
        if (!empty($schema = $this->currentState->findIndex($columns))) {
514
            throw new SchemaException(
515
                "Undefined index ['" . join("', '", $columns) . "'] in '{$this->getName()}'"
516
            );
517
        }
518
519
        //Dropping index from current schema
520
        $this->currentState->forgetIndex($schema);
0 ignored issues
show
Documentation introduced by
$schema is of type null, but the function expects a object<Spiral\Database\S...ototypes\AbstractIndex>.

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...
521
522
        return $this;
523
    }
524
525
    /**
526
     * Drop foreign key by it's name.
527
     *
528
     * @param string $column
529
     *
530
     * @return self
531
     *
532
     * @throws SchemaException
533
     */
534 View Code Duplication
    public function dropForeign($column): 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...
535
    {
536
        if (!empty($schema = $this->currentState->findForeign($column))) {
537
            throw new SchemaException(
538
                "Undefined FK on '{$column}' in '{$this->getName()}'"
539
            );
540
        }
541
542
        //Dropping foreign from current schema
543
        $this->currentState->forgetForeign($schema);
0 ignored issues
show
Documentation introduced by
$schema is of type null, but the function expects a object<Spiral\Database\S...ypes\AbstractReference>.

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...
544
545
        return $this;
546
    }
547
548
    /**
549
     * Reset table state to new form.
550
     *
551
     * @param TableState $status Use null to flush table schema.
552
     *
553
     * @return self|$this
554
     */
555
    public function setStatus(TableState $status = null): AbstractTable
556
    {
557
        $this->currentState = new TableState($this->initialState->getName());
558
559
        if (!empty($status)) {
560
            $this->currentState->setName($status->getName());
561
            $this->currentState->syncState($status);
0 ignored issues
show
Documentation introduced by
$status is of type object<Spiral\Database\Schemas\TableState>, but the function expects a object<self>.

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...
562
        }
563
564
        return $this;
565
    }
566
567
    /**
568
     * Reset table state to it initial form.
569
     *
570
     * @return self|$this
571
     */
572
    public function resetState(): AbstractTable
573
    {
574
        $this->setStatus($this->initialState);
575
576
        return $this;
577
    }
578
579
    /**
580
     * Save table schema including every column, index, foreign key creation/altering. If table
581
     * does not exist it must be created. If table declared as dropped it will be removed from
582
     * the database.
583
     */
584
    public function save(int $behaviour)
0 ignored issues
show
Unused Code introduced by
The parameter $behaviour is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
585
    {
586
        //working in here
587
    }
588
589
    /**
590
     * @return AbstractColumn|string
591
     */
592
    public function __toString(): string
593
    {
594
        return $this->getName();
595
    }
596
597
    /**
598
     * @return array
599
     */
600
    public function __debugInfo()
601
    {
602
        return [
603
            'name'        => $this->getName(),
604
            'primaryKeys' => $this->getPrimaryKeys(),
605
            'columns'     => array_values($this->getColumns()),
606
            'indexes'     => array_values($this->getIndexes()),
607
            'references'  => array_values($this->getForeigns()),
608
        ];
609
    }
610
611
    /**
612
     * Populate table schema with values from database.
613
     *
614
     * @param TableState $state
615
     */
616
    protected function initSchema(TableState $state)
617
    {
618
        foreach ($this->fetchColumns() as $column) {
619
            $state->registerColumn($column);
0 ignored issues
show
Compatibility introduced by
$column of type object<Spiral\Database\Schemas\ColumnInterface> is not a sub-type of object<Spiral\Database\S...totypes\AbstractColumn>. It seems like you assume a concrete implementation of the interface Spiral\Database\Schemas\ColumnInterface to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
620
        }
621
622
        foreach ($this->fetchIndexes() as $index) {
623
            $state->registerIndex($index);
0 ignored issues
show
Compatibility introduced by
$index of type object<Spiral\Database\Schemas\IndexInterface> is not a sub-type of object<Spiral\Database\S...ototypes\AbstractIndex>. It seems like you assume a concrete implementation of the interface Spiral\Database\Schemas\IndexInterface to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
624
        }
625
626
        foreach ($this->fetchReferences() as $foreign) {
627
            $state->registerReference($foreign);
0 ignored issues
show
Compatibility introduced by
$foreign of type object<Spiral\Database\S...mas\ReferenceInterface> is not a sub-type of object<Spiral\Database\S...ypes\AbstractReference>. It seems like you assume a concrete implementation of the interface Spiral\Database\Schemas\ReferenceInterface to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
628
        }
629
630
        $state->setPrimaryKeys($this->fetchPrimaryKeys());
631
632
        //DBMS specific initialization can be placed here
633
    }
634
635
    /**
636
     * Fetch index declarations from database.
637
     *
638
     * @return ColumnInterface[]
639
     */
640
    abstract protected function fetchColumns(): array;
641
642
    /**
643
     * Fetch index declarations from database.
644
     *
645
     * @return IndexInterface[]
646
     */
647
    abstract protected function fetchIndexes(): array;
648
649
    /**
650
     * Fetch references declaration from database.
651
     *
652
     * @return ReferenceInterface[]
653
     */
654
    abstract protected function fetchReferences(): array;
655
656
    /**
657
     * Fetch names of primary keys from table.
658
     *
659
     * @return array
660
     */
661
    abstract protected function fetchPrimaryKeys(): array;
662
663
    /**
664
     * Create column with a given name.
665
     *
666
     * @param string $name
667
     *
668
     * @return AbstractColumn
669
     */
670
    abstract protected function createColumn(string $name): AbstractColumn;
671
672
    /**
673
     * Create index for a given set of columns.
674
     *
675
     * @param string $name
676
     *
677
     * @return AbstractIndex
678
     */
679
    abstract protected function createIndex(string $name): AbstractIndex;
680
681
    /**
682
     * Create reference on a given column set.
683
     *
684
     * @param string $name
685
     *
686
     * @return AbstractReference
687
     */
688
    abstract protected function createReference(string $name): AbstractReference;
689
690
    /**
691
     * Generate unique name for indexes and foreign keys.
692
     *
693
     * @param string $type
694
     * @param array  $columns
695
     *
696
     * @return string
697
     */
698
    protected function createIdentifier(string $type, array $columns): string
699
    {
700
        $name = $this->getName() . '_' . $type . '_' . join('_', $columns) . '_' . uniqid();
701
702
        if (strlen($name) > 64) {
703
            //Many DBMS has limitations on identifier length
704
            $name = md5($name);
705
        }
706
707
        return $name;
708
    }
709
710
    /**
711
     * @return ContainerInterface
712
     */
713
    protected function iocContainer()
714
    {
715
        //Falling back to driver specific container
716
        return $this->driver->iocContainer();
0 ignored issues
show
Bug introduced by
The method iocContainer() cannot be called from this context as it is declared protected in class Spiral\Core\Component.

This check looks for access to methods that are not accessible from the current context.

If you need to make a method accessible to another context you can raise its visibility level in the defining class.

Loading history...
717
    }
718
}