Completed
Branch feature/pre-split (370939)
by Anton
03:16
created

AbstractHandler::executeChanges()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 21
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 10
nc 4
nop 3
dl 0
loc 21
rs 9.3142
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->executeChanges($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 executeChanges(
347
        AbstractTable $table,
348
        int $behaviour,
349
        StateComparator $comparator
350
    ) {
351
        //Remove all non needed table constraints
352
        $this->dropConstrains($table, $behaviour, $comparator);
353
354
        if ($behaviour & self::CREATE_COLUMNS) {
355
            //After drops and before creations we can add new columns
356
            $this->createColumns($table, $comparator);
357
        }
358
359
        if ($behaviour & self::ALTER_COLUMNS) {
360
            //We can alter columns now
361
            $this->alterColumns($table, $comparator);
362
        }
363
364
        //Add new constrains and modify existed one
365
        $this->setConstrains($table, $behaviour, $comparator);
366
    }
367
368
    /**
369
     * Execute statement.
370
     *
371
     * @param string $statement
372
     * @param array  $parameters
373
     *
374
     * @return \PDOStatement
375
     *
376
     * @throws HandlerException
377
     */
378
    protected function run(string $statement, array $parameters = []): \PDOStatement
379
    {
380
        try {
381
            return $this->driver->statement($statement, $parameters);
382
        } catch (QueryException $e) {
383
            throw new HandlerException($e);
384
        }
385
    }
386
387
    /**
388
     * Helper function, saves log message into logger if any attached.
389
     *
390
     * @param string $message
391
     * @param array  $context
392
     */
393
    protected function log(string $message, array $context = [])
394
    {
395
        if (!empty($this->logger)) {
396
            $this->logger->debug($message, $context);
397
        }
398
    }
399
400
    /**
401
     * Create element identifier.
402
     *
403
     * @param AbstractElement|AbstractTable|string $element
404
     *
405
     * @return string
406
     */
407
    protected function identify($element)
408
    {
409
        if (is_string($element)) {
410
            return $this->driver->identifier($element);
411
        }
412
413
        if (!$element instanceof AbstractElement && !$element instanceof AbstractTable) {
414
            throw new InvalidArgumentException("Invalid argument type");
415
        }
416
417
        return $this->driver->identifier($element->getName());
418
    }
419
420
    /**
421
     * @param AbstractTable   $table
422
     * @param StateComparator $comparator
423
     */
424 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...
425
    {
426
        foreach ($comparator->alteredForeigns() as $pair) {
427
            /**
428
             * @var AbstractReference $initial
429
             * @var AbstractReference $current
430
             */
431
            list($current, $initial) = $pair;
432
433
            $this->log('Altering foreign key [{statement}] to [{new}] in {table}.', [
434
                'statement' => $initial->sqlStatement($this->driver),
435
                'table'     => $this->identify($table),
436
            ]);
437
438
            $this->alterForeign($table, $initial, $current);
439
        }
440
    }
441
442
    /**
443
     * @param AbstractTable   $table
444
     * @param StateComparator $comparator
445
     */
446 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...
447
    {
448
        foreach ($comparator->addedForeigns() as $foreign) {
449
            $this->log('Adding foreign key [{statement}] into table {table}.', [
450
                'statement' => $foreign->sqlStatement($this->driver),
451
                'table'     => $this->identify($table),
452
            ]);
453
454
            $this->createForeign($table, $foreign);
455
        }
456
    }
457
458
    /**
459
     * @param AbstractTable   $table
460
     * @param StateComparator $comparator
461
     */
462 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...
463
    {
464
        foreach ($comparator->alteredIndexes() as $pair) {
465
            /**
466
             * @var AbstractIndex $initial
467
             * @var AbstractIndex $current
468
             */
469
            list($current, $initial) = $pair;
470
471
            $this->log('Altering index [{statement}] to [{new}] in table {table}.', [
472
                'statement' => $initial->sqlStatement($this->driver),
473
                'new'       => $current->sqlStatement($this->driver),
474
                'table'     => $this->identify($table),
475
            ]);
476
477
            $this->alterIndex($table, $initial, $current);
478
        }
479
    }
480
481
    /**
482
     * @param AbstractTable   $table
483
     * @param StateComparator $comparator
484
     */
485 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...
486
    {
487
        foreach ($comparator->addedIndexes() as $index) {
488
            $this->log('Adding index [{statement}] into table {table}.', [
489
                'statement' => $index->sqlStatement($this->driver),
490
                'table'     => $this->identify($table),
491
            ]);
492
493
            $this->createIndex($table, $index);
494
        }
495
    }
496
497
    /**
498
     * @param AbstractTable   $table
499
     * @param StateComparator $comparator
500
     */
501 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...
502
    {
503
        foreach ($comparator->alteredColumns() as $pair) {
504
            /**
505
             * @var AbstractColumn $initial
506
             * @var AbstractColumn $current
507
             */
508
            list($current, $initial) = $pair;
509
510
            $this->log('Altering column [{statement}] to [{new}] in table {table}.', [
511
                'statement' => $initial->sqlStatement($this->driver),
512
                'new'       => $current->sqlStatement($this->driver),
513
                'table'     => $this->identify($table),
514
            ]);
515
516
            $this->assertValid($current);
517
            $this->alterColumn($table, $initial, $current);
518
        }
519
    }
520
521
    /**
522
     * @param AbstractTable   $table
523
     * @param StateComparator $comparator
524
     */
525 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...
526
    {
527
        foreach ($comparator->addedColumns() as $column) {
528
            $this->log('Adding column [{statement}] into table {table}.', [
529
                'statement' => $column->sqlStatement($this->driver),
530
                'table'     => $this->identify($table),
531
            ]);
532
533
            $this->assertValid($column);
534
            $this->createColumn($table, $column);
535
        }
536
    }
537
538
    /**
539
     * @param AbstractTable   $table
540
     * @param StateComparator $comparator
541
     */
542 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...
543
    {
544
        foreach ($comparator->droppedColumns() as $column) {
545
            $this->log('Dropping column [{statement}] from table {table}.', [
546
                'statement' => $column->sqlStatement($this->driver),
547
                'table'     => $this->identify($table),
548
            ]);
549
550
            $this->dropColumn($table, $column);
551
        }
552
    }
553
554
    /**
555
     * @param AbstractTable   $table
556
     * @param StateComparator $comparator
557
     */
558 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...
559
    {
560
        foreach ($comparator->droppedIndexes() as $index) {
561
            $this->log('Dropping index [{statement}] from table {table}.', [
562
                'statement' => $index->sqlStatement($this->driver),
563
                'table'     => $this->identify($table),
564
            ]);
565
566
            $this->dropIndex($table, $index);
567
        }
568
    }
569
570
    /**
571
     * @param AbstractTable   $table
572
     * @param StateComparator $comparator
573
     */
574 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...
575
    {
576
        foreach ($comparator->droppedForeigns() as $foreign) {
577
            $this->log('Dropping foreign key [{statement}] from table {table}.', [
578
                'statement' => $foreign->sqlStatement($this->driver),
579
                'table'     => $this->identify($table),
580
            ]);
581
582
            $this->dropForeign($table, $foreign);
583
        }
584
    }
585
586
    /**
587
     * Applied to every column in order to make sure that driver support it.
588
     *
589
     * @param AbstractColumn $column
590
     *
591
     * @throws DriverException
592
     */
593
    protected function assertValid(AbstractColumn $column)
594
    {
595
        //All valid by default
596
    }
597
598
    /**
599
     * @param AbstractTable   $table
600
     * @param int             $behaviour
601
     * @param StateComparator $comparator
602
     */
603
    protected function dropConstrains(
604
        AbstractTable $table,
605
        int $behaviour,
606
        StateComparator $comparator
607
    ) {
608
        if ($behaviour & self::DROP_FOREIGNS) {
609
            $this->dropForeigns($table, $comparator);
610
        }
611
612
        if ($behaviour & self::DROP_INDEXES) {
613
            $this->dropIndexes($table, $comparator);
614
        }
615
616
        if ($behaviour & self::DROP_COLUMNS) {
617
            $this->dropColumns($table, $comparator);
618
        }
619
    }
620
621
    /**
622
     * @param AbstractTable   $table
623
     * @param int             $behaviour
624
     * @param StateComparator $comparator
625
     */
626
    protected function setConstrains(
627
        AbstractTable $table,
628
        int $behaviour,
629
        StateComparator $comparator
630
    ) {
631
        if ($behaviour & self::CREATE_INDEXES) {
632
            $this->createIndexes($table, $comparator);
633
        }
634
635
        if ($behaviour & self::ALTER_INDEXES) {
636
            $this->alterIndexes($table, $comparator);
637
        }
638
639
        if ($behaviour & self::CREATE_FOREIGNS) {
640
            $this->createForeigns($table, $comparator);
641
        }
642
643
        if ($behaviour & self::ALTER_FOREIGNS) {
644
            $this->alterForeigns($table, $comparator);
645
        }
646
    }
647
}