Completed
Branch feature/pre-split (c6befc)
by Anton
03:31
created

AbstractHandler::syncTable()   B

Complexity

Conditions 4
Paths 3

Size

Total Lines 24
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

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