Completed
Branch feature/pre-split (b5c37f)
by Anton
03:43
created

AbstractHandler::syncTable()   F

Complexity

Conditions 20
Paths 1536

Size

Total Lines 140
Code Lines 66

Duplication

Lines 110
Ratio 78.57 %

Importance

Changes 0
Metric Value
cc 20
eloc 66
nc 1536
nop 2
dl 110
loc 140
rs 2
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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