Completed
Branch feature/pre-split (d89158)
by Anton
04:43
created

AbstractHandler   C

Complexity

Total Complexity 57

Size/Duplication

Total Lines 562
Duplicated Lines 21.17 %

Coupling/Cohesion

Components 1
Dependencies 9

Importance

Changes 0
Metric Value
dl 119
loc 562
rs 6.433
c 0
b 0
f 0
wmc 57
lcom 1
cbo 9

29 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 5 1
A getDriver() 0 4 1
A createTable() 0 12 2
A dropTable() 0 6 1
D syncTable() 0 54 11
A renameTable() 0 4 1
A createColumn() 0 4 1
A dropColumn() 0 9 2
alterColumn() 0 5 ?
A createIndex() 0 4 1
A dropIndex() 0 4 1
A alterIndex() 0 5 1
A createForeign() 0 4 1
A dropForeign() 0 4 1
A alterForeign() 0 8 1
A dropConstrain() 0 4 1
B createStatement() 0 27 4
A run() 0 8 2
A log() 0 6 2
A identify() 0 12 4
A alterForeigns() 17 17 2
A createForeigns() 11 11 2
A alterIndexes() 18 18 2
A createIndexes() 11 11 2
A alterColumns() 18 18 2
A createColumns() 11 11 2
A dropColumns() 11 11 2
A dropIndexes() 11 11 2
A dropForeigns() 11 11 2

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like AbstractHandler often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use AbstractHandler, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * 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\HandlerException;
12
use Spiral\Database\Exceptions\QueryException;
13
use Spiral\Database\Schemas\Prototypes\AbstractColumn;
14
use Spiral\Database\Schemas\Prototypes\AbstractElement;
15
use Spiral\Database\Schemas\Prototypes\AbstractIndex;
16
use Spiral\Database\Schemas\Prototypes\AbstractReference;
17
use Spiral\Database\Schemas\Prototypes\AbstractTable;
18
use Spiral\Database\Schemas\StateComparator;
19
20
/**
21
 * Handler class implements set of DBMS specific operations for schema manipulations. Can be used
22
 * on separate basis (for example in migrations).
23
 */
24
abstract class AbstractHandler
25
{
26
    /**
27
     * Behaviours.
28
     */
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
    const DROP_COLUMNS   = 0b000001000;
37
    const CREATE_COLUMNS = 0b000010000;
38
    const ALTER_COLUMNS  = 0b000100000;
39
40
    //All columns related operations
41
    const DO_COLUMNS = self::DROP_COLUMNS | self::ALTER_COLUMNS | self::CREATE_COLUMNS;
42
43
    const DROP_INDEXES   = 0b001000000;
44
    const CREATE_INDEXES = 0b010000000;
45
    const ALTER_INDEXES  = 0b100000000;
46
47
    //All index related operations
48
    const DO_INDEXES = self::DROP_INDEXES | self::ALTER_INDEXES | self::CREATE_INDEXES;
49
50
    //All operations
51
    const DO_ALL = self::DO_FOREIGNS | self::DO_INDEXES | self::DO_COLUMNS;
52
53
    /**
54
     * @var LoggerInterface|null
55
     */
56
    private $logger = null;
57
58
    /**
59
     * @var Driver
60
     */
61
    protected $driver;
62
63
    /**
64
     * @param Driver               $driver
65
     * @param LoggerInterface|null $logger
66
     */
67
    public function __construct(Driver $driver, LoggerInterface $logger = null)
68
    {
69
        $this->driver = $driver;
70
        $this->logger = $logger;
71
    }
72
73
    /**
74
     * Associated driver.
75
     *
76
     * @return Driver
77
     */
78
    public function getDriver(): Driver
79
    {
80
        return $this->driver;
81
    }
82
83
    /**
84
     * Create table based on a given schema.
85
     *
86
     * @param AbstractTable $table
87
     *
88
     * @throws HandlerException
89
     */
90
    public function createTable(AbstractTable $table)
91
    {
92
        $this->log("Creating new table '{table}'.", ['table' => $table->getName()]);
93
94
        //Executing!
95
        $this->run($this->createStatement($table));
96
97
        //Not all databases support adding index while table creation, so we can do it after
98
        foreach ($table->getIndexes() as $index) {
99
            $this->createIndex($table, $index);
0 ignored issues
show
Compatibility introduced by
$index of type object<Spiral\Database\Schemas\IndexInterface> is not a sub-type of object<Spiral\Database\S...ototypes\AbstractIndex>. It seems like you assume a concrete implementation of the interface Spiral\Database\Schemas\IndexInterface to be always present.

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

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

Loading history...
100
        }
101
    }
102
103
    /**
104
     * Drop table from database.
105
     *
106
     * @param AbstractTable $table
107
     *
108
     * @throws HandlerException
109
     */
110
    public function dropTable(AbstractTable $table)
111
    {
112
        $this->log("Dropping table '{table}'.", ['table' => $table->getName()]);
113
114
        $this->run("DROP TABLE {$this->identify($table->getInitialName())}");
115
    }
116
117
    /**
118
     * Sync given table schema.
119
     *
120
     * @param AbstractTable $table
121
     * @param int           $behaviour See behaviour constants.
122
     */
123
    public function syncTable(AbstractTable $table, int $behaviour = self::DO_ALL)
124
    {
125
        $comparator = $table->getComparator();
126
127
        if ($comparator->isRenamed()) {
128
            $this->log('Renaming table {table} to {name}.', [
129
                'table' => $this->identify($table->getInitialName()),
130
                'name'  => $this->identify($table->getName())
131
            ]);
132
133
            //Executing renaming
134
            $this->renameTable($table->getInitialName(), $table->getName());
135
        }
136
137
        /*
138
         * This is schema synchronization code, if you are reading it you are either experiencing
139
         * VERY weird bug, or you are very curious. Please contact me in a any scenario :)
140
         */
141
        if ($behaviour & self::DROP_FOREIGNS) {
142
            $this->dropForeigns($table, $comparator);
143
        }
144
145
        if ($behaviour & self::DROP_INDEXES) {
146
            $this->dropIndexes($table, $comparator);
147
        }
148
149
        if ($behaviour & self::DROP_COLUMNS) {
150
            $this->dropColumns($table, $comparator);
151
        }
152
153
        if ($behaviour & self::CREATE_COLUMNS) {
154
            $this->createColumns($table, $comparator);
155
        }
156
157
        if ($behaviour & self::ALTER_COLUMNS) {
158
            $this->alterColumns($table, $comparator);
159
        }
160
161
        if ($behaviour & self::CREATE_INDEXES) {
162
            $this->createIndexes($table, $comparator);
163
        }
164
165
        if ($behaviour & self::ALTER_INDEXES) {
166
            $this->alterIndexes($table, $comparator);
167
        }
168
169
        if ($behaviour & self::CREATE_FOREIGNS) {
170
            $this->createForeigns($table, $comparator);
171
        }
172
173
        if ($behaviour & self::ALTER_FOREIGNS) {
174
            $this->alterForeigns($table, $comparator);
175
        }
176
    }
177
178
    /**
179
     * Rename table from one name to another.
180
     *
181
     * @param string $table
182
     * @param string $name
183
     *
184
     * @throws HandlerException
185
     */
186
    public function renameTable(string $table, string $name)
187
    {
188
        $this->run("ALTER TABLE {$this->identify($table)} RENAME TO {$this->identify($name)}");
189
    }
190
191
    /**
192
     * Driver specific column add command.
193
     *
194
     * @param AbstractTable  $table
195
     * @param AbstractColumn $column
196
     *
197
     * @throws HandlerException
198
     */
199
    public function createColumn(AbstractTable $table, AbstractColumn $column)
200
    {
201
        $this->run("ALTER TABLE {$this->identify($table)} ADD COLUMN {$column->sqlStatement($this->driver)}");
202
    }
203
204
    /**
205
     * Driver specific column remove (drop) command.
206
     *
207
     * @param AbstractTable  $table
208
     * @param AbstractColumn $column
209
     *
210
     * @return self
211
     */
212
    public function dropColumn(AbstractTable $table, AbstractColumn $column)
213
    {
214
        foreach ($column->getConstraints() as $constraint) {
215
            //We have to erase all associated constraints
216
            $this->dropConstrain($table, $constraint);
217
        }
218
219
        $this->run("ALTER TABLE {$this->identify($table)} DROP COLUMN {$this->identify($column)}");
220
    }
221
222
    /**
223
     * Driver specific column alter command.
224
     *
225
     * @param AbstractTable  $table
226
     * @param AbstractColumn $initial
227
     * @param AbstractColumn $column
228
     *
229
     * @throws HandlerException
230
     */
231
    abstract public function alterColumn(
232
        AbstractTable $table,
233
        AbstractColumn $initial,
234
        AbstractColumn $column
235
    );
236
237
    /**
238
     * Driver specific index adding command.
239
     *
240
     * @param AbstractTable $table
241
     * @param AbstractIndex $index
242
     *
243
     * @throws HandlerException
244
     */
245
    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...
246
    {
247
        $this->run("CREATE {$index->sqlStatement($this->driver)}");
248
    }
249
250
    /**
251
     * Driver specific index remove (drop) command.
252
     *
253
     * @param AbstractTable $table
254
     * @param AbstractIndex $index
255
     *
256
     * @throws HandlerException
257
     */
258
    public function dropIndex(AbstractTable $table, AbstractIndex $index)
259
    {
260
        $this->run("DROP INDEX {$this->identify($index)}");
261
    }
262
263
    /**
264
     * Driver specific index alter command, by default it will remove and add index.
265
     *
266
     * @param AbstractTable $table
267
     * @param AbstractIndex $initial
268
     * @param AbstractIndex $index
269
     *
270
     * @throws HandlerException
271
     */
272
    public function alterIndex(AbstractTable $table, AbstractIndex $initial, AbstractIndex $index)
273
    {
274
        $this->dropIndex($table, $initial);
275
        $this->createIndex($table, $index);
276
    }
277
278
    /**
279
     * Driver specific foreign key adding command.
280
     *
281
     * @param AbstractTable     $table
282
     * @param AbstractReference $foreign
283
     *
284
     * @throws HandlerException
285
     */
286
    public function createForeign(AbstractTable $table, AbstractReference $foreign)
287
    {
288
        $this->run("ALTER TABLE {$this->identify($table)} ADD {$foreign->sqlStatement($this->driver)}");
289
    }
290
291
    /**
292
     * Driver specific foreign key remove (drop) command.
293
     *
294
     * @param AbstractTable     $table
295
     * @param AbstractReference $foreign
296
     *
297
     * @throws HandlerException
298
     */
299
    public function dropForeign(AbstractTable $table, AbstractReference $foreign)
300
    {
301
        $this->dropConstrain($table, $foreign->getName());
302
    }
303
304
    /**
305
     * Driver specific foreign key alter command, by default it will remove and add foreign key.
306
     *
307
     * @param AbstractTable     $table
308
     * @param AbstractReference $initial
309
     * @param AbstractReference $foreign
310
     *
311
     * @throws HandlerException
312
     */
313
    public function alterForeign(
314
        AbstractTable $table,
315
        AbstractReference $initial,
316
        AbstractReference $foreign
317
    ) {
318
        $this->dropForeign($table, $initial);
319
        $this->createForeign($table, $foreign);
320
    }
321
322
    /**
323
     * Drop column constraint using it's name.
324
     *
325
     * @param AbstractTable $table
326
     * @param string        $constraint
327
     *
328
     * @throws HandlerException
329
     */
330
    public function dropConstrain(AbstractTable $table, $constraint)
331
    {
332
        $this->run("ALTER TABLE {$this->identify($table)} DROP CONSTRAINT {$this->identify($constraint)}");
333
    }
334
335
    /**
336
     * Get statement needed to create table. Indexes will be created separately.
337
     *
338
     * @param AbstractTable $table
339
     *
340
     * @return string
341
     */
342
    protected function createStatement(AbstractTable $table)
343
    {
344
        $statement = ["CREATE TABLE {$this->identify($table)} ("];
345
        $innerStatement = [];
346
347
        //Columns
348
        foreach ($table->getColumns() as $column) {
349
            $innerStatement[] = $column->sqlStatement($this->driver);
350
        }
351
352
        //Primary key
353
        if (!empty($table->getPrimaryKeys())) {
354
            $primaryKeys = array_map([$this, 'identify'], $table->getPrimaryKeys());
355
356
            $innerStatement[] = 'PRIMARY KEY (' . join(', ', $primaryKeys) . ')';
357
        }
358
359
        //Constraints and foreign keys
360
        foreach ($table->getForeigns() as $reference) {
361
            $innerStatement[] = $reference->sqlStatement($this->driver);
362
        }
363
364
        $statement[] = "    " . join(",\n    ", $innerStatement);
365
        $statement[] = ')';
366
367
        return join("\n", $statement);
368
    }
369
370
    /**
371
     * Execute statement.
372
     *
373
     * @param string $statement
374
     * @param array  $parameters
375
     *
376
     * @return \PDOStatement
377
     *
378
     * @throws HandlerException
379
     */
380
    protected function run(string $statement, array $parameters = []): \PDOStatement
381
    {
382
        try {
383
            return $this->driver->statement($statement, $parameters);
384
        } catch (QueryException $e) {
385
            throw new HandlerException($e);
386
        }
387
    }
388
389
    /**
390
     * Helper function, saves log message into logger if any attached.
391
     *
392
     * @param string $message
393
     * @param array  $context
394
     */
395
    protected function log(string $message, array $context = [])
396
    {
397
        if (!empty($this->logger)) {
398
            $this->logger->debug($message, $context);
399
        }
400
    }
401
402
    /**
403
     * Create element identifier.
404
     *
405
     * @param AbstractElement|AbstractTable|string $element
406
     *
407
     * @return string
408
     */
409
    protected function identify($element)
410
    {
411
        if (is_string($element)) {
412
            return $this->driver->identifier($element);
413
        }
414
415
        if (!$element instanceof AbstractElement && !$element instanceof AbstractTable) {
416
            throw new InvalidArgumentException("Invalid argument type");
417
        }
418
419
        return $this->driver->identifier($element->getName());
420
    }
421
422
    /**
423
     * @param AbstractTable   $table
424
     * @param StateComparator $comparator
425
     */
426 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...
427
    {
428
        foreach ($comparator->alteredForeigns() as $pair) {
429
            /**
430
             * @var AbstractReference $initial
431
             * @var AbstractReference $current
432
             */
433
            list($current, $initial) = $pair;
434
435
            $this->log('Altering foreign key [{statement}] to [{new}] in {table}.', [
436
                'statement' => $initial->sqlStatement($this->driver),
437
                'table'     => $this->identify($table),
438
            ]);
439
440
            $this->alterForeign($table, $initial, $current);
441
        }
442
    }
443
444
    /**
445
     * @param AbstractTable   $table
446
     * @param StateComparator $comparator
447
     */
448 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...
449
    {
450
        foreach ($comparator->addedForeigns() as $foreign) {
451
            $this->log('Adding foreign key [{statement}] into table {table}.', [
452
                'statement' => $foreign->sqlStatement($this->driver),
453
                'table'     => $this->identify($table),
454
            ]);
455
456
            $this->createForeign($table, $foreign);
457
        }
458
    }
459
460
    /**
461
     * @param AbstractTable   $table
462
     * @param StateComparator $comparator
463
     */
464 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...
465
    {
466
        foreach ($comparator->alteredIndexes() as $pair) {
467
            /**
468
             * @var AbstractIndex $initial
469
             * @var AbstractIndex $current
470
             */
471
            list($current, $initial) = $pair;
472
473
            $this->log('Altering index [{statement}] to [{new}] in table {table}.', [
474
                'statement' => $initial->sqlStatement($this->driver),
475
                'new'       => $current->sqlStatement($this->driver),
476
                'table'     => $this->identify($table),
477
            ]);
478
479
            $this->alterIndex($table, $initial, $current);
480
        }
481
    }
482
483
    /**
484
     * @param AbstractTable   $table
485
     * @param StateComparator $comparator
486
     */
487 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...
488
    {
489
        foreach ($comparator->addedIndexes() as $index) {
490
            $this->log('Adding index [{statement}] into table {table}.', [
491
                'statement' => $index->sqlStatement($this->driver),
492
                'table'     => $this->identify($table),
493
            ]);
494
495
            $this->createIndex($table, $index);
496
        }
497
    }
498
499
    /**
500
     * @param AbstractTable   $table
501
     * @param StateComparator $comparator
502
     */
503 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...
504
    {
505
        foreach ($comparator->alteredColumns() as $pair) {
506
            /**
507
             * @var AbstractColumn $initial
508
             * @var AbstractColumn $current
509
             */
510
            list($current, $initial) = $pair;
511
512
            $this->log('Altering column [{statement}] to [{new}] in table {table}.', [
513
                'statement' => $initial->sqlStatement($this->driver),
514
                'new'       => $current->sqlStatement($this->driver),
515
                'table'     => $this->identify($table),
516
            ]);
517
518
            $this->alterColumn($table, $initial, $current);
519
        }
520
    }
521
522
    /**
523
     * @param AbstractTable   $table
524
     * @param StateComparator $comparator
525
     */
526 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...
527
    {
528
        foreach ($comparator->addedColumns() as $column) {
529
            $this->log('Adding column [{statement}] into table {table}.', [
530
                'statement' => $column->sqlStatement($this->driver),
531
                'table'     => $this->identify($table),
532
            ]);
533
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
}