Completed
Branch feature/pre-split (fc3374)
by Anton
02:58
created

AbstractHandler::syncTable()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 20
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 8
nc 2
nop 2
dl 0
loc 20
rs 9.4285
c 0
b 0
f 0
1
<?php
2
/**
3
 * Spiral Framework, Core Components
4
 *
5
 * @author    Wolfy-J
6
 */
7
namespace Spiral\Database\Entities;
8
9
use Psr\Log\LoggerInterface;
10
use Spiral\Core\Exceptions\InvalidArgumentException;
11
use Spiral\Database\Exceptions\DriverException;
12
use Spiral\Database\Exceptions\HandlerException;
13
use Spiral\Database\Exceptions\QueryException;
14
use Spiral\Database\Schemas\Prototypes\AbstractColumn;
15
use Spiral\Database\Schemas\Prototypes\AbstractElement;
16
use Spiral\Database\Schemas\Prototypes\AbstractIndex;
17
use Spiral\Database\Schemas\Prototypes\AbstractReference;
18
use Spiral\Database\Schemas\Prototypes\AbstractTable;
19
use Spiral\Database\Schemas\StateComparator;
20
21
/**
22
 * Handler class implements set of DBMS specific operations for schema manipulations. Can be used
23
 * on separate basis (for example in migrations).
24
 */
25
abstract class AbstractHandler
26
{
27
    /**
28
     * Behaviours.
29
     */
30
    const DROP_FOREIGNS   = 0b000000001;
31
    const CREATE_FOREIGNS = 0b000000010;
32
    const ALTER_FOREIGNS  = 0b000000100;
33
34
    //All foreign keys related operations
35
    const DO_FOREIGNS = self::DROP_FOREIGNS | self::ALTER_FOREIGNS | self::CREATE_FOREIGNS;
36
37
    const DROP_COLUMNS   = 0b000001000;
38
    const CREATE_COLUMNS = 0b000010000;
39
    const ALTER_COLUMNS  = 0b000100000;
40
41
    //All columns related operations
42
    const DO_COLUMNS = self::DROP_COLUMNS | self::ALTER_COLUMNS | self::CREATE_COLUMNS;
43
44
    const DROP_INDEXES   = 0b001000000;
45
    const CREATE_INDEXES = 0b010000000;
46
    const ALTER_INDEXES  = 0b100000000;
47
48
    //All index related operations
49
    const DO_INDEXES = self::DROP_INDEXES | self::ALTER_INDEXES | self::CREATE_INDEXES;
50
51
    //Schema operations
52
    const DO_RENAME = 0b10000000000;
53
    const DO_DROP   = 0b01000000000;
54
55
    //All operations
56
    const DO_ALL = self::DO_FOREIGNS | self::DO_INDEXES | self::DO_COLUMNS | self::DO_DROP | self::DO_RENAME;
57
58
    /**
59
     * @var LoggerInterface|null
60
     */
61
    private $logger = null;
62
63
    /**
64
     * @var Driver
65
     */
66
    protected $driver;
67
68
    /**
69
     * @param Driver               $driver
70
     * @param LoggerInterface|null $logger
71
     */
72
    public function __construct(Driver $driver, LoggerInterface $logger = null)
73
    {
74
        $this->driver = $driver;
75
        $this->logger = $logger;
76
    }
77
78
    /**
79
     * Associated driver.
80
     *
81
     * @return Driver
82
     */
83
    public function getDriver(): Driver
84
    {
85
        return $this->driver;
86
    }
87
88
    /**
89
     * Create table based on a given schema.
90
     *
91
     * @param AbstractTable $table
92
     *
93
     * @throws HandlerException
94
     */
95
    public function createTable(AbstractTable $table)
96
    {
97
        $this->log("Creating new table '{table}'.", ['table' => $table->getName()]);
98
99
        //Executing!
100
        $this->run($this->createStatement($table));
101
102
        //Not all databases support adding index while table creation, so we can do it after
103
        foreach ($table->getIndexes() as $index) {
104
            $this->createIndex($table, $index);
105
        }
106
    }
107
108
    /**
109
     * Drop table from database.
110
     *
111
     * @param AbstractTable $table
112
     *
113
     * @throws HandlerException
114
     */
115
    public function dropTable(AbstractTable $table)
116
    {
117
        $this->log("Dropping table '{table}'.", ['table' => $table->getName()]);
118
        $this->run("DROP TABLE {$this->identify($table->getInitialName())}");
119
    }
120
121
    /**
122
     * Sync given table schema.
123
     *
124
     * @param AbstractTable $table
125
     * @param int           $behaviour See behaviour constants.
126
     */
127
    public function syncTable(AbstractTable $table, int $behaviour = self::DO_ALL)
128
    {
129
        $comparator = $table->getComparator();
130
131
        if ($comparator->isRenamed() && $behaviour & self::DO_RENAME) {
132
            $this->log('Renaming table {table} to {name}.', [
133
                'table' => $this->identify($table->getInitialName()),
134
                'name'  => $this->identify($table->getName())
135
            ]);
136
137
            //Executing renaming
138
            $this->renameTable($table->getInitialName(), $table->getName());
139
        }
140
141
        /*
142
         * This is schema synchronization code, if you are reading it you are either experiencing
143
         * VERY weird bug, or you are very curious. Please contact me in a any scenario :)
144
         */
145
        $this->runChanges($table, $behaviour, $comparator);
146
    }
147
148
    /**
149
     * Rename table from one name to another.
150
     *
151
     * @param string $table
152
     * @param string $name
153
     *
154
     * @throws HandlerException
155
     */
156
    public function renameTable(string $table, string $name)
157
    {
158
        $this->run("ALTER TABLE {$this->identify($table)} RENAME TO {$this->identify($name)}");
159
    }
160
161
    /**
162
     * Driver specific column add command.
163
     *
164
     * @param AbstractTable  $table
165
     * @param AbstractColumn $column
166
     *
167
     * @throws HandlerException
168
     */
169
    public function createColumn(AbstractTable $table, AbstractColumn $column)
170
    {
171
        $this->run("ALTER TABLE {$this->identify($table)} ADD COLUMN {$column->sqlStatement($this->driver)}");
172
    }
173
174
    /**
175
     * Driver specific column remove (drop) command.
176
     *
177
     * @param AbstractTable  $table
178
     * @param AbstractColumn $column
179
     *
180
     * @return self
181
     */
182
    public function dropColumn(AbstractTable $table, AbstractColumn $column)
183
    {
184
        foreach ($column->getConstraints() as $constraint) {
185
            //We have to erase all associated constraints
186
            $this->dropConstrain($table, $constraint);
187
        }
188
189
        $this->run("ALTER TABLE {$this->identify($table)} DROP COLUMN {$this->identify($column)}");
190
    }
191
192
    /**
193
     * Driver specific column alter command.
194
     *
195
     * @param AbstractTable  $table
196
     * @param AbstractColumn $initial
197
     * @param AbstractColumn $column
198
     *
199
     * @throws HandlerException
200
     */
201
    abstract public function alterColumn(
202
        AbstractTable $table,
203
        AbstractColumn $initial,
204
        AbstractColumn $column
205
    );
206
207
    /**
208
     * Driver specific index adding command.
209
     *
210
     * @param AbstractTable $table
211
     * @param AbstractIndex $index
212
     *
213
     * @throws HandlerException
214
     */
215
    public function createIndex(AbstractTable $table, AbstractIndex $index)
1 ignored issue
show
Unused Code introduced by
The parameter $table 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...
216
    {
217
        $this->run("CREATE {$index->sqlStatement($this->driver)}");
218
    }
219
220
    /**
221
     * Driver specific index remove (drop) command.
222
     *
223
     * @param AbstractTable $table
224
     * @param AbstractIndex $index
225
     *
226
     * @throws HandlerException
227
     */
228
    public function dropIndex(AbstractTable $table, AbstractIndex $index)
229
    {
230
        $this->run("DROP INDEX {$this->identify($index)}");
231
    }
232
233
    /**
234
     * Driver specific index alter command, by default it will remove and add index.
235
     *
236
     * @param AbstractTable $table
237
     * @param AbstractIndex $initial
238
     * @param AbstractIndex $index
239
     *
240
     * @throws HandlerException
241
     */
242
    public function alterIndex(AbstractTable $table, AbstractIndex $initial, AbstractIndex $index)
243
    {
244
        $this->dropIndex($table, $initial);
245
        $this->createIndex($table, $index);
246
    }
247
248
    /**
249
     * Driver specific foreign key adding command.
250
     *
251
     * @param AbstractTable     $table
252
     * @param AbstractReference $foreign
253
     *
254
     * @throws HandlerException
255
     */
256
    public function createForeign(AbstractTable $table, AbstractReference $foreign)
257
    {
258
        $this->run("ALTER TABLE {$this->identify($table)} ADD {$foreign->sqlStatement($this->driver)}");
259
    }
260
261
    /**
262
     * Driver specific foreign key remove (drop) command.
263
     *
264
     * @param AbstractTable     $table
265
     * @param AbstractReference $foreign
266
     *
267
     * @throws HandlerException
268
     */
269
    public function dropForeign(AbstractTable $table, AbstractReference $foreign)
270
    {
271
        $this->dropConstrain($table, $foreign->getName());
272
    }
273
274
    /**
275
     * Driver specific foreign key alter command, by default it will remove and add foreign key.
276
     *
277
     * @param AbstractTable     $table
278
     * @param AbstractReference $initial
279
     * @param AbstractReference $foreign
280
     *
281
     * @throws HandlerException
282
     */
283
    public function alterForeign(
284
        AbstractTable $table,
285
        AbstractReference $initial,
286
        AbstractReference $foreign
287
    ) {
288
        $this->dropForeign($table, $initial);
289
        $this->createForeign($table, $foreign);
290
    }
291
292
    /**
293
     * Drop column constraint using it's name.
294
     *
295
     * @param AbstractTable $table
296
     * @param string        $constraint
297
     *
298
     * @throws HandlerException
299
     */
300
    public function dropConstrain(AbstractTable $table, $constraint)
301
    {
302
        $this->run("ALTER TABLE {$this->identify($table)} DROP CONSTRAINT {$this->identify($constraint)}");
303
    }
304
305
    /**
306
     * Get statement needed to create table. Indexes will be created separately.
307
     *
308
     * @param AbstractTable $table
309
     *
310
     * @return string
311
     */
312
    protected function createStatement(AbstractTable $table)
313
    {
314
        $statement = ["CREATE TABLE {$this->identify($table)} ("];
315
        $innerStatement = [];
316
317
        //Columns
318
        foreach ($table->getColumns() as $column) {
319
            $this->assertValid($column);
320
            $innerStatement[] = $column->sqlStatement($this->driver);
321
        }
322
323
        //Primary key
324
        if (!empty($table->getPrimaryKeys())) {
325
            $primaryKeys = array_map([$this, 'identify'], $table->getPrimaryKeys());
326
327
            $innerStatement[] = 'PRIMARY KEY (' . join(', ', $primaryKeys) . ')';
328
        }
329
330
        //Constraints and foreign keys
331
        foreach ($table->getForeigns() as $reference) {
332
            $innerStatement[] = $reference->sqlStatement($this->driver);
333
        }
334
335
        $statement[] = "    " . join(",\n    ", $innerStatement);
336
        $statement[] = ')';
337
338
        return join("\n", $statement);
339
    }
340
341
    /**
342
     * @param AbstractTable   $table
343
     * @param int             $behaviour
344
     * @param StateComparator $comparator
345
     */
346
    protected function runChanges(AbstractTable $table, int $behaviour, StateComparator $comparator)
347
    {
348
        if ($behaviour & self::DROP_FOREIGNS) {
349
            $this->dropForeigns($table, $comparator);
350
        }
351
352
        if ($behaviour & self::DROP_INDEXES) {
353
            $this->dropIndexes($table, $comparator);
354
        }
355
356
        if ($behaviour & self::DROP_COLUMNS) {
357
            $this->dropColumns($table, $comparator);
358
        }
359
360
        if ($behaviour & self::CREATE_COLUMNS) {
361
            $this->createColumns($table, $comparator);
362
        }
363
364
        if ($behaviour & self::ALTER_COLUMNS) {
365
            $this->alterColumns($table, $comparator);
366
        }
367
368
        if ($behaviour & self::CREATE_INDEXES) {
369
            $this->createIndexes($table, $comparator);
370
        }
371
372
        if ($behaviour & self::ALTER_INDEXES) {
373
            $this->alterIndexes($table, $comparator);
374
        }
375
376
        if ($behaviour & self::CREATE_FOREIGNS) {
377
            $this->createForeigns($table, $comparator);
378
        }
379
380
        if ($behaviour & self::ALTER_FOREIGNS) {
381
            $this->alterForeigns($table, $comparator);
382
        }
383
    }
384
385
    /**
386
     * Execute statement.
387
     *
388
     * @param string $statement
389
     * @param array  $parameters
390
     *
391
     * @return \PDOStatement
392
     *
393
     * @throws HandlerException
394
     */
395
    protected function run(string $statement, array $parameters = []): \PDOStatement
396
    {
397
        try {
398
            return $this->driver->statement($statement, $parameters);
399
        } catch (QueryException $e) {
400
            throw new HandlerException($e);
401
        }
402
    }
403
404
    /**
405
     * Helper function, saves log message into logger if any attached.
406
     *
407
     * @param string $message
408
     * @param array  $context
409
     */
410
    protected function log(string $message, array $context = [])
411
    {
412
        if (!empty($this->logger)) {
413
            $this->logger->debug($message, $context);
414
        }
415
    }
416
417
    /**
418
     * Create element identifier.
419
     *
420
     * @param AbstractElement|AbstractTable|string $element
421
     *
422
     * @return string
423
     */
424
    protected function identify($element)
425
    {
426
        if (is_string($element)) {
427
            return $this->driver->identifier($element);
428
        }
429
430
        if (!$element instanceof AbstractElement && !$element instanceof AbstractTable) {
431
            throw new InvalidArgumentException("Invalid argument type");
432
        }
433
434
        return $this->driver->identifier($element->getName());
435
    }
436
437
    /**
438
     * @param AbstractTable   $table
439
     * @param StateComparator $comparator
440
     */
441 View Code Duplication
    protected function alterForeigns(AbstractTable $table, StateComparator $comparator)
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...
442
    {
443
        foreach ($comparator->alteredForeigns() as $pair) {
444
            /**
445
             * @var AbstractReference $initial
446
             * @var AbstractReference $current
447
             */
448
            list($current, $initial) = $pair;
449
450
            $this->log('Altering foreign key [{statement}] to [{new}] in {table}.', [
451
                'statement' => $initial->sqlStatement($this->driver),
452
                'table'     => $this->identify($table),
453
            ]);
454
455
            $this->alterForeign($table, $initial, $current);
456
        }
457
    }
458
459
    /**
460
     * @param AbstractTable   $table
461
     * @param StateComparator $comparator
462
     */
463 View Code Duplication
    protected function createForeigns(AbstractTable $table, StateComparator $comparator)
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...
464
    {
465
        foreach ($comparator->addedForeigns() as $foreign) {
466
            $this->log('Adding foreign key [{statement}] into table {table}.', [
467
                'statement' => $foreign->sqlStatement($this->driver),
468
                'table'     => $this->identify($table),
469
            ]);
470
471
            $this->createForeign($table, $foreign);
472
        }
473
    }
474
475
    /**
476
     * @param AbstractTable   $table
477
     * @param StateComparator $comparator
478
     */
479 View Code Duplication
    protected function alterIndexes(AbstractTable $table, StateComparator $comparator)
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...
480
    {
481
        foreach ($comparator->alteredIndexes() as $pair) {
482
            /**
483
             * @var AbstractIndex $initial
484
             * @var AbstractIndex $current
485
             */
486
            list($current, $initial) = $pair;
487
488
            $this->log('Altering index [{statement}] to [{new}] in table {table}.', [
489
                'statement' => $initial->sqlStatement($this->driver),
490
                'new'       => $current->sqlStatement($this->driver),
491
                'table'     => $this->identify($table),
492
            ]);
493
494
            $this->alterIndex($table, $initial, $current);
495
        }
496
    }
497
498
    /**
499
     * @param AbstractTable   $table
500
     * @param StateComparator $comparator
501
     */
502 View Code Duplication
    protected function createIndexes(AbstractTable $table, StateComparator $comparator)
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...
503
    {
504
        foreach ($comparator->addedIndexes() as $index) {
505
            $this->log('Adding index [{statement}] into table {table}.', [
506
                'statement' => $index->sqlStatement($this->driver),
507
                'table'     => $this->identify($table),
508
            ]);
509
510
            $this->createIndex($table, $index);
511
        }
512
    }
513
514
    /**
515
     * @param AbstractTable   $table
516
     * @param StateComparator $comparator
517
     */
518 View Code Duplication
    protected function alterColumns(AbstractTable $table, StateComparator $comparator)
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...
519
    {
520
        foreach ($comparator->alteredColumns() as $pair) {
521
            /**
522
             * @var AbstractColumn $initial
523
             * @var AbstractColumn $current
524
             */
525
            list($current, $initial) = $pair;
526
527
            $this->log('Altering column [{statement}] to [{new}] in table {table}.', [
528
                'statement' => $initial->sqlStatement($this->driver),
529
                'new'       => $current->sqlStatement($this->driver),
530
                'table'     => $this->identify($table),
531
            ]);
532
533
            $this->assertValid($current);
534
            $this->alterColumn($table, $initial, $current);
535
        }
536
    }
537
538
    /**
539
     * @param AbstractTable   $table
540
     * @param StateComparator $comparator
541
     */
542 View Code Duplication
    protected function createColumns(AbstractTable $table, StateComparator $comparator)
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...
543
    {
544
        foreach ($comparator->addedColumns() as $column) {
545
            $this->log('Adding column [{statement}] into table {table}.', [
546
                'statement' => $column->sqlStatement($this->driver),
547
                'table'     => $this->identify($table),
548
            ]);
549
550
            $this->assertValid($column);
551
            $this->createColumn($table, $column);
552
        }
553
    }
554
555
    /**
556
     * @param AbstractTable   $table
557
     * @param StateComparator $comparator
558
     */
559 View Code Duplication
    protected function dropColumns(AbstractTable $table, StateComparator $comparator)
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...
560
    {
561
        foreach ($comparator->droppedColumns() as $column) {
562
            $this->log('Dropping column [{statement}] from table {table}.', [
563
                'statement' => $column->sqlStatement($this->driver),
564
                'table'     => $this->identify($table),
565
            ]);
566
567
            $this->dropColumn($table, $column);
568
        }
569
    }
570
571
    /**
572
     * @param AbstractTable   $table
573
     * @param StateComparator $comparator
574
     */
575 View Code Duplication
    protected function dropIndexes(AbstractTable $table, StateComparator $comparator)
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...
576
    {
577
        foreach ($comparator->droppedIndexes() as $index) {
578
            $this->log('Dropping index [{statement}] from table {table}.', [
579
                'statement' => $index->sqlStatement($this->driver),
580
                'table'     => $this->identify($table),
581
            ]);
582
583
            $this->dropIndex($table, $index);
584
        }
585
    }
586
587
    /**
588
     * @param AbstractTable   $table
589
     * @param StateComparator $comparator
590
     */
591 View Code Duplication
    protected function dropForeigns(AbstractTable $table, $comparator)
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...
592
    {
593
        foreach ($comparator->droppedForeigns() as $foreign) {
594
            $this->log('Dropping foreign key [{statement}] from table {table}.', [
595
                'statement' => $foreign->sqlStatement($this->driver),
596
                'table'     => $this->identify($table),
597
            ]);
598
599
            $this->dropForeign($table, $foreign);
600
        }
601
    }
602
603
    /**
604
     * Applied to every column in order to make sure that driver support it.
605
     *
606
     * @param AbstractColumn $column
607
     *
608
     * @throws DriverException
609
     */
610
    protected function assertValid(AbstractColumn $column)
611
    {
612
        //All valid by default
613
    }
614
}