Completed
Branch feature/pre-split (42159e)
by Anton
05:36
created

AbstractHandler::assertValid()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 1
dl 0
loc 4
rs 10
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
        if ($behaviour & self::DROP_FOREIGNS) {
146
            $this->dropForeigns($table, $comparator);
147
        }
148
149
        if ($behaviour & self::DROP_INDEXES) {
150
            $this->dropIndexes($table, $comparator);
151
        }
152
153
        if ($behaviour & self::DROP_COLUMNS) {
154
            $this->dropColumns($table, $comparator);
155
        }
156
157
        if ($behaviour & self::CREATE_COLUMNS) {
158
            $this->createColumns($table, $comparator);
159
        }
160
161
        if ($behaviour & self::ALTER_COLUMNS) {
162
            $this->alterColumns($table, $comparator);
163
        }
164
165
        if ($behaviour & self::CREATE_INDEXES) {
166
            $this->createIndexes($table, $comparator);
167
        }
168
169
        if ($behaviour & self::ALTER_INDEXES) {
170
            $this->alterIndexes($table, $comparator);
171
        }
172
173
        if ($behaviour & self::CREATE_FOREIGNS) {
174
            $this->createForeigns($table, $comparator);
175
        }
176
177
        if ($behaviour & self::ALTER_FOREIGNS) {
178
            $this->alterForeigns($table, $comparator);
179
        }
180
    }
181
182
    /**
183
     * Rename table from one name to another.
184
     *
185
     * @param string $table
186
     * @param string $name
187
     *
188
     * @throws HandlerException
189
     */
190
    public function renameTable(string $table, string $name)
191
    {
192
        $this->run("ALTER TABLE {$this->identify($table)} RENAME TO {$this->identify($name)}");
193
    }
194
195
    /**
196
     * Driver specific column add command.
197
     *
198
     * @param AbstractTable  $table
199
     * @param AbstractColumn $column
200
     *
201
     * @throws HandlerException
202
     */
203
    public function createColumn(AbstractTable $table, AbstractColumn $column)
204
    {
205
        $this->run("ALTER TABLE {$this->identify($table)} ADD COLUMN {$column->sqlStatement($this->driver)}");
206
    }
207
208
    /**
209
     * Driver specific column remove (drop) command.
210
     *
211
     * @param AbstractTable  $table
212
     * @param AbstractColumn $column
213
     *
214
     * @return self
215
     */
216
    public function dropColumn(AbstractTable $table, AbstractColumn $column)
217
    {
218
        foreach ($column->getConstraints() as $constraint) {
219
            //We have to erase all associated constraints
220
            $this->dropConstrain($table, $constraint);
221
        }
222
223
        $this->run("ALTER TABLE {$this->identify($table)} DROP COLUMN {$this->identify($column)}");
224
    }
225
226
    /**
227
     * Driver specific column alter command.
228
     *
229
     * @param AbstractTable  $table
230
     * @param AbstractColumn $initial
231
     * @param AbstractColumn $column
232
     *
233
     * @throws HandlerException
234
     */
235
    abstract public function alterColumn(
236
        AbstractTable $table,
237
        AbstractColumn $initial,
238
        AbstractColumn $column
239
    );
240
241
    /**
242
     * Driver specific index adding command.
243
     *
244
     * @param AbstractTable $table
245
     * @param AbstractIndex $index
246
     *
247
     * @throws HandlerException
248
     */
249
    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...
250
    {
251
        $this->run("CREATE {$index->sqlStatement($this->driver)}");
252
    }
253
254
    /**
255
     * Driver specific index remove (drop) command.
256
     *
257
     * @param AbstractTable $table
258
     * @param AbstractIndex $index
259
     *
260
     * @throws HandlerException
261
     */
262
    public function dropIndex(AbstractTable $table, AbstractIndex $index)
263
    {
264
        $this->run("DROP INDEX {$this->identify($index)}");
265
    }
266
267
    /**
268
     * Driver specific index alter command, by default it will remove and add index.
269
     *
270
     * @param AbstractTable $table
271
     * @param AbstractIndex $initial
272
     * @param AbstractIndex $index
273
     *
274
     * @throws HandlerException
275
     */
276
    public function alterIndex(AbstractTable $table, AbstractIndex $initial, AbstractIndex $index)
277
    {
278
        $this->dropIndex($table, $initial);
279
        $this->createIndex($table, $index);
280
    }
281
282
    /**
283
     * Driver specific foreign key adding command.
284
     *
285
     * @param AbstractTable     $table
286
     * @param AbstractReference $foreign
287
     *
288
     * @throws HandlerException
289
     */
290
    public function createForeign(AbstractTable $table, AbstractReference $foreign)
291
    {
292
        $this->run("ALTER TABLE {$this->identify($table)} ADD {$foreign->sqlStatement($this->driver)}");
293
    }
294
295
    /**
296
     * Driver specific foreign key remove (drop) command.
297
     *
298
     * @param AbstractTable     $table
299
     * @param AbstractReference $foreign
300
     *
301
     * @throws HandlerException
302
     */
303
    public function dropForeign(AbstractTable $table, AbstractReference $foreign)
304
    {
305
        $this->dropConstrain($table, $foreign->getName());
306
    }
307
308
    /**
309
     * Driver specific foreign key alter command, by default it will remove and add foreign key.
310
     *
311
     * @param AbstractTable     $table
312
     * @param AbstractReference $initial
313
     * @param AbstractReference $foreign
314
     *
315
     * @throws HandlerException
316
     */
317
    public function alterForeign(
318
        AbstractTable $table,
319
        AbstractReference $initial,
320
        AbstractReference $foreign
321
    ) {
322
        $this->dropForeign($table, $initial);
323
        $this->createForeign($table, $foreign);
324
    }
325
326
    /**
327
     * Drop column constraint using it's name.
328
     *
329
     * @param AbstractTable $table
330
     * @param string        $constraint
331
     *
332
     * @throws HandlerException
333
     */
334
    public function dropConstrain(AbstractTable $table, $constraint)
335
    {
336
        $this->run("ALTER TABLE {$this->identify($table)} DROP CONSTRAINT {$this->identify($constraint)}");
337
    }
338
339
    /**
340
     * Get statement needed to create table. Indexes will be created separately.
341
     *
342
     * @param AbstractTable $table
343
     *
344
     * @return string
345
     */
346
    protected function createStatement(AbstractTable $table)
347
    {
348
        $statement = ["CREATE TABLE {$this->identify($table)} ("];
349
        $innerStatement = [];
350
351
        //Columns
352
        foreach ($table->getColumns() as $column) {
353
            $this->assertValid($column);
354
            $innerStatement[] = $column->sqlStatement($this->driver);
355
        }
356
357
        //Primary key
358
        if (!empty($table->getPrimaryKeys())) {
359
            $primaryKeys = array_map([$this, 'identify'], $table->getPrimaryKeys());
360
361
            $innerStatement[] = 'PRIMARY KEY (' . join(', ', $primaryKeys) . ')';
362
        }
363
364
        //Constraints and foreign keys
365
        foreach ($table->getForeigns() as $reference) {
366
            $innerStatement[] = $reference->sqlStatement($this->driver);
367
        }
368
369
        $statement[] = "    " . join(",\n    ", $innerStatement);
370
        $statement[] = ')';
371
372
        return join("\n", $statement);
373
    }
374
375
    /**
376
     * Execute statement.
377
     *
378
     * @param string $statement
379
     * @param array  $parameters
380
     *
381
     * @return \PDOStatement
382
     *
383
     * @throws HandlerException
384
     */
385
    protected function run(string $statement, array $parameters = []): \PDOStatement
386
    {
387
        try {
388
            return $this->driver->statement($statement, $parameters);
389
        } catch (QueryException $e) {
390
            throw new HandlerException($e);
391
        }
392
    }
393
394
    /**
395
     * Helper function, saves log message into logger if any attached.
396
     *
397
     * @param string $message
398
     * @param array  $context
399
     */
400
    protected function log(string $message, array $context = [])
401
    {
402
        if (!empty($this->logger)) {
403
            $this->logger->debug($message, $context);
404
        }
405
    }
406
407
    /**
408
     * Create element identifier.
409
     *
410
     * @param AbstractElement|AbstractTable|string $element
411
     *
412
     * @return string
413
     */
414
    protected function identify($element)
415
    {
416
        if (is_string($element)) {
417
            return $this->driver->identifier($element);
418
        }
419
420
        if (!$element instanceof AbstractElement && !$element instanceof AbstractTable) {
421
            throw new InvalidArgumentException("Invalid argument type");
422
        }
423
424
        return $this->driver->identifier($element->getName());
425
    }
426
427
    /**
428
     * @param AbstractTable   $table
429
     * @param StateComparator $comparator
430
     */
431 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...
432
    {
433
        foreach ($comparator->alteredForeigns() as $pair) {
434
            /**
435
             * @var AbstractReference $initial
436
             * @var AbstractReference $current
437
             */
438
            list($current, $initial) = $pair;
439
440
            $this->log('Altering foreign key [{statement}] to [{new}] in {table}.', [
441
                'statement' => $initial->sqlStatement($this->driver),
442
                'table'     => $this->identify($table),
443
            ]);
444
445
            $this->alterForeign($table, $initial, $current);
446
        }
447
    }
448
449
    /**
450
     * @param AbstractTable   $table
451
     * @param StateComparator $comparator
452
     */
453 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...
454
    {
455
        foreach ($comparator->addedForeigns() as $foreign) {
456
            $this->log('Adding foreign key [{statement}] into table {table}.', [
457
                'statement' => $foreign->sqlStatement($this->driver),
458
                'table'     => $this->identify($table),
459
            ]);
460
461
            $this->createForeign($table, $foreign);
462
        }
463
    }
464
465
    /**
466
     * @param AbstractTable   $table
467
     * @param StateComparator $comparator
468
     */
469 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...
470
    {
471
        foreach ($comparator->alteredIndexes() as $pair) {
472
            /**
473
             * @var AbstractIndex $initial
474
             * @var AbstractIndex $current
475
             */
476
            list($current, $initial) = $pair;
477
478
            $this->log('Altering index [{statement}] to [{new}] in table {table}.', [
479
                'statement' => $initial->sqlStatement($this->driver),
480
                'new'       => $current->sqlStatement($this->driver),
481
                'table'     => $this->identify($table),
482
            ]);
483
484
            $this->alterIndex($table, $initial, $current);
485
        }
486
    }
487
488
    /**
489
     * @param AbstractTable   $table
490
     * @param StateComparator $comparator
491
     */
492 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...
493
    {
494
        foreach ($comparator->addedIndexes() as $index) {
495
            $this->log('Adding index [{statement}] into table {table}.', [
496
                'statement' => $index->sqlStatement($this->driver),
497
                'table'     => $this->identify($table),
498
            ]);
499
500
            $this->createIndex($table, $index);
501
        }
502
    }
503
504
    /**
505
     * @param AbstractTable   $table
506
     * @param StateComparator $comparator
507
     */
508 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...
509
    {
510
        foreach ($comparator->alteredColumns() as $pair) {
511
            /**
512
             * @var AbstractColumn $initial
513
             * @var AbstractColumn $current
514
             */
515
            list($current, $initial) = $pair;
516
517
            $this->log('Altering column [{statement}] to [{new}] in table {table}.', [
518
                'statement' => $initial->sqlStatement($this->driver),
519
                'new'       => $current->sqlStatement($this->driver),
520
                'table'     => $this->identify($table),
521
            ]);
522
523
            $this->assertValid($current);
524
            $this->alterColumn($table, $initial, $current);
525
        }
526
    }
527
528
    /**
529
     * @param AbstractTable   $table
530
     * @param StateComparator $comparator
531
     */
532 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...
533
    {
534
        foreach ($comparator->addedColumns() as $column) {
535
            $this->log('Adding column [{statement}] into table {table}.', [
536
                'statement' => $column->sqlStatement($this->driver),
537
                'table'     => $this->identify($table),
538
            ]);
539
540
            $this->assertValid($column);
541
            $this->createColumn($table, $column);
542
        }
543
    }
544
545
    /**
546
     * @param AbstractTable   $table
547
     * @param StateComparator $comparator
548
     */
549 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...
550
    {
551
        foreach ($comparator->droppedColumns() as $column) {
552
            $this->log('Dropping column [{statement}] from table {table}.', [
553
                'statement' => $column->sqlStatement($this->driver),
554
                'table'     => $this->identify($table),
555
            ]);
556
557
            $this->dropColumn($table, $column);
558
        }
559
    }
560
561
    /**
562
     * @param AbstractTable   $table
563
     * @param StateComparator $comparator
564
     */
565 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...
566
    {
567
        foreach ($comparator->droppedIndexes() as $index) {
568
            $this->log('Dropping index [{statement}] from table {table}.', [
569
                'statement' => $index->sqlStatement($this->driver),
570
                'table'     => $this->identify($table),
571
            ]);
572
573
            $this->dropIndex($table, $index);
574
        }
575
    }
576
577
    /**
578
     * @param AbstractTable   $table
579
     * @param StateComparator $comparator
580
     */
581 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...
582
    {
583
        foreach ($comparator->droppedForeigns() as $foreign) {
584
            $this->log('Dropping foreign key [{statement}] from table {table}.', [
585
                'statement' => $foreign->sqlStatement($this->driver),
586
                'table'     => $this->identify($table),
587
            ]);
588
589
            $this->dropForeign($table, $foreign);
590
        }
591
    }
592
593
    /**
594
     * Applied to every column in order to make sure that driver support it.
595
     *
596
     * @param AbstractColumn $column
597
     *
598
     * @throws DriverException
599
     */
600
    protected function assertValid(AbstractColumn $column)
601
    {
602
        //All valid by default
603
    }
604
}