Passed
Pull Request — 2.x (#105)
by Maxim
17:54
created

Handler::alterForeignKeys()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
eloc 3
nc 2
nop 2
dl 0
loc 10
ccs 4
cts 4
cp 1
crap 2
rs 10
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * This file is part of Cycle ORM package.
5
 *
6
 * For the full copyright and license information, please view the LICENSE
7
 * file that was distributed with this source code.
8
 */
9
10
declare(strict_types=1);
11
12
namespace Cycle\Database\Driver;
13
14
use Cycle\Database\Exception\DBALException;
15
use Cycle\Database\Exception\DriverException;
16
use Cycle\Database\Exception\HandlerException;
17
use Cycle\Database\Exception\StatementException;
18
use Cycle\Database\Schema\AbstractColumn;
0 ignored issues
show
Bug introduced by
The type Cycle\Database\Schema\AbstractColumn was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
19
use Cycle\Database\Schema\AbstractForeignKey;
20
use Cycle\Database\Schema\AbstractIndex;
21
use Cycle\Database\Schema\AbstractTable;
22
use Cycle\Database\Schema\ComparatorInterface;
23
use Cycle\Database\Schema\ElementInterface;
24
25
abstract class Handler implements HandlerInterface
26
{
27
    protected ?DriverInterface $driver = null;
28
29 76
    public function withDriver(DriverInterface $driver): HandlerInterface
30
    {
31 76
        $handler = clone $this;
32 76
        $handler->driver = $driver;
33
34 76
        return $handler;
35
    }
36
37
    /**
38
     * Associated driver.
39
     */
40
    public function getDriver(): DriverInterface
41
    {
42
        return $this->driver;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->driver could return the type null which is incompatible with the type-hinted return Cycle\Database\Driver\DriverInterface. Consider adding an additional type-check to rule them out.
Loading history...
43
    }
44
45 1948
    public function createTable(AbstractTable $table): void
46
    {
47 1948
        $this->run($this->createStatement($table));
48
49
        //Not all databases support adding index while table creation, so we can do it after
50 1942
        foreach ($table->getIndexes() as $index) {
51 282
            $this->createIndex($table, $index);
52
        }
53 1942
    }
54
55 1930
    public function dropTable(AbstractTable $table): void
56
    {
57 1930
        $this->run(
58 1930
            "DROP TABLE {$this->identify($table->getInitialName())}"
59
        );
60 1930
    }
61
62 538
    public function syncTable(AbstractTable $table, int $operation = self::DO_ALL): void
63
    {
64 538
        $comparator = $table->getComparator();
65
66 538
        $comparator->isPrimaryChanged() and throw new DBALException('Unable to change primary keys for existed table');
67
68 538
        if ($operation & self::DO_RENAME && $comparator->isRenamed()) {
69 8
            $this->renameTable($table->getInitialName(), $table->getFullName());
70
        }
71
72
        /*
73
         * This is schema synchronization code, if you are reading it you are either experiencing
74
         * VERY weird bug, or you are very curious. Please contact me in a any scenario :)
75
         */
76 538
        $this->executeChanges($table, $operation, $comparator);
77 538
    }
78
79
    /**
80
     * @psalm-param non-empty-string $table
81
     * @psalm-param non-empty-string $name
82
     */
83 122
    public function renameTable(string $table, string $name): void
84
    {
85 122
        $this->run(
86 122
            "ALTER TABLE {$this->identify($table)} RENAME TO {$this->identify($name)}"
87
        );
88 122
    }
89
90 80
    public function createColumn(AbstractTable $table, AbstractColumn $column): void
91
    {
92 80
        $this->run(
93 80
            "ALTER TABLE {$this->identify($table)} ADD COLUMN {$column->sqlStatement($this->driver)}"
94
        );
95 80
    }
96
97 24
    public function dropColumn(AbstractTable $table, AbstractColumn $column): void
98
    {
99 24
        foreach ($column->getConstraints() as $constraint) {
100
            //We have to erase all associated constraints
101
            $this->dropConstrain($table, $constraint);
102
        }
103
104 24
        $this->run(
105 24
            "ALTER TABLE {$this->identify($table)} DROP COLUMN {$this->identify($column)}"
106
        );
107 24
    }
108
109 458
    public function createIndex(AbstractTable $table, AbstractIndex $index): void
110
    {
111 458
        $this->run("CREATE {$index->sqlStatement($this->driver)}");
0 ignored issues
show
Bug introduced by
It seems like $this->driver can also be of type null; however, parameter $driver of Cycle\Database\Schema\Ab...ctIndex::sqlStatement() does only seem to accept Cycle\Database\Driver\DriverInterface, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

111
        $this->run("CREATE {$index->sqlStatement(/** @scrutinizer ignore-type */ $this->driver)}");
Loading history...
112 458
    }
113
114 24
    public function dropIndex(AbstractTable $table, AbstractIndex $index): void
115
    {
116 24
        $this->run("DROP INDEX {$this->identify($index)}");
117 24
    }
118
119 6
    public function alterIndex(
120
        AbstractTable $table,
121
        AbstractIndex $initial,
122
        AbstractIndex $index
123
    ): void {
124 6
        $this->dropIndex($table, $initial);
125 6
        $this->createIndex($table, $index);
126 6
    }
127
128 132
    public function createForeignKey(AbstractTable $table, AbstractForeignKey $foreignKey): void
129
    {
130 132
        $this->run(
131 132
            "ALTER TABLE {$this->identify($table)} ADD {$foreignKey->sqlStatement($this->driver)}"
0 ignored issues
show
Bug introduced by
It seems like $this->driver can also be of type null; however, parameter $driver of Cycle\Database\Schema\Ab...eignKey::sqlStatement() does only seem to accept Cycle\Database\Driver\DriverInterface, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

131
            "ALTER TABLE {$this->identify($table)} ADD {$foreignKey->sqlStatement(/** @scrutinizer ignore-type */ $this->driver)}"
Loading history...
132
        );
133 132
    }
134
135 112
    public function dropForeignKey(AbstractTable $table, AbstractForeignKey $foreignKey): void
136
    {
137 112
        $this->dropConstrain($table, $foreignKey->getName());
138 112
    }
139
140 42
    public function alterForeignKey(
141
        AbstractTable $table,
142
        AbstractForeignKey $initial,
143
        AbstractForeignKey $foreignKey
144
    ): void {
145 42
        $this->dropForeignKey($table, $initial);
146 42
        $this->createForeignKey($table, $foreignKey);
147 42
    }
148
149 130
    public function dropConstrain(AbstractTable $table, string $constraint): void
150
    {
151 130
        $this->run(
152 130
            "ALTER TABLE {$this->identify($table)} DROP CONSTRAINT {$this->identify($constraint)}"
153
        );
154 130
    }
155
156
    /**
157
     * This method will be called by Reflector in the run method before the changes are synchronized.
158
     *
159 1948
     * @param AbstractTable[] $tables
160
     */
161 1948
    public function beforeSync(array $tables): void
0 ignored issues
show
Unused Code introduced by
The parameter $tables is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

161
    public function beforeSync(/** @scrutinizer ignore-unused */ array $tables): void

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
162 1948
    {
163
    }
164
165 1948
    /**
166 1948
     * This method will be called by Reflector in the run method after the changes are synchronized.
167 1944
     *
168
     * @param AbstractTable[] $tables
169
     */
170
    public function afterSync(array $tables): void
0 ignored issues
show
Unused Code introduced by
The parameter $tables is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

170
    public function afterSync(/** @scrutinizer ignore-unused */ array $tables): void

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
171 1944
    {
172 1380
    }
173
174 1380
    /**
175
     * @psalm-return non-empty-string
176
     */
177
    protected function createStatement(AbstractTable $table): string
178 1944
    {
179 134
        $statement = ["CREATE TABLE {$this->identify($table)} ("];
180
        $innerStatement = [];
181
182 1944
        //Columns
183 1944
        foreach ($table->getColumns() as $column) {
184
            $this->assertValid($column);
185 1944
            $innerStatement[] = $column->sqlStatement($this->driver);
186
        }
187
188 538
        //Primary key
189
        if ($table->getPrimaryKeys() !== []) {
190
            $primaryKeys = array_map([$this, 'identify'], $table->getPrimaryKeys());
191
192
            $innerStatement[] = 'PRIMARY KEY (' . implode(', ', $primaryKeys) . ')';
193
        }
194 538
195
        //Constraints and foreign keys
196 538
        foreach ($table->getForeignKeys() as $reference) {
197
            $innerStatement[] = $reference->sqlStatement($this->driver);
198 470
        }
199
200
        $statement[] = '    ' . implode(",\n    ", $innerStatement);
201 538
        $statement[] = ')';
202
203 470
        return implode("\n", $statement);
204
    }
205
206
    protected function executeChanges(
207 538
        AbstractTable $table,
208 538
        int $operation,
209
        ComparatorInterface $comparator
210
    ): void {
211
        //Remove all non needed table constraints
212
        $this->dropConstrains($table, $operation, $comparator);
213
214
        if ($operation & self::CREATE_COLUMNS) {
215
            //After drops and before creations we can add new columns
216
            $this->createColumns($table, $comparator);
217 1946
        }
218
219
        if ($operation & self::ALTER_COLUMNS) {
220 1946
            //We can alter columns now
221 10
            $this->alterColumns($table, $comparator);
222 2
        }
223
224
        //Add new constrains and modify existed one
225
        $this->setConstrains($table, $operation, $comparator);
226
    }
227
228
    /**
229
     * Execute statement.
230
     *
231 1950
     * @psalm-param non-empty-string $statement
232
     *
233 1950
     * @throws HandlerException
234 1942
     */
235
    protected function run(string $statement, array $parameters = []): int
236
    {
237 1950
        try {
238 1950
            return $this->driver->execute($statement, $parameters);
0 ignored issues
show
Bug introduced by
The method execute() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

238
            return $this->driver->/** @scrutinizer ignore-call */ execute($statement, $parameters);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
239
        } catch (StatementException $e) {
240
            throw new HandlerException($e);
241 178
        }
242 178
    }
243
244
    /**
245
     * Create element identifier.
246
     *
247
     * @psalm-return non-empty-string
248 470
     */
249
    protected function identify(AbstractTable|ElementInterface|string $element): string
250 470
    {
251
        if (\is_string($element)) {
252
            return $this->driver->identifier($element);
0 ignored issues
show
Bug introduced by
The method identifier() does not exist on Cycle\Database\Driver\DriverInterface. It seems like you code against a sub-type of Cycle\Database\Driver\DriverInterface such as Cycle\Database\Driver\Driver. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

252
            return $this->driver->/** @scrutinizer ignore-call */ identifier($element);
Loading history...
253
        }
254
255 42
        if ($element instanceof AbstractTable) {
256
            return $this->driver->identifier($element->getFullName());
257 42
        }
258
259 470
        if ($element instanceof ElementInterface) {
0 ignored issues
show
introduced by
$element is always a sub-type of Cycle\Database\Schema\ElementInterface.
Loading history...
260
            return $this->driver->identifier($element->getName());
261 496
        }
262
263 496
        throw new \InvalidArgumentException('Invalid argument type');
264 90
    }
265
266 496
    protected function alterForeignKeys(AbstractTable $table, ComparatorInterface $comparator): void
267
    {
268 470
        foreach ($comparator->alteredForeignKeys() as $pair) {
269
            /**
270 470
             * @var AbstractForeignKey $initial
271
             * @var AbstractForeignKey $current
272
             */
273
            [$current, $initial] = $pair;
274
275 8
            $this->alterForeignKey($table, $initial, $current);
276
        }
277 8
    }
278
279 470
    protected function createForeignKeys(AbstractTable $table, ComparatorInterface $comparator): void
280
    {
281 470
        foreach ($comparator->addedForeignKeys() as $foreign) {
282
            $this->createForeignKey($table, $foreign);
283 470
        }
284 180
    }
285
286 470
    protected function alterIndexes(AbstractTable $table, ComparatorInterface $comparator): void
287
    {
288 470
        foreach ($comparator->alteredIndexes() as $pair) {
289
            /**
290 470
             * @var AbstractIndex $initial
291
             * @var AbstractIndex $current
292
             */
293
            [$current, $initial] = $pair;
294
295 116
            $this->alterIndex($table, $initial, $current);
296
        }
297 116
    }
298 116
299
    protected function createIndexes(AbstractTable $table, ComparatorInterface $comparator): void
300 470
    {
301
        foreach ($comparator->addedIndexes() as $index) {
302 470
            $this->createIndex($table, $index);
303
        }
304 470
    }
305 120
306 120
    protected function alterColumns(AbstractTable $table, ComparatorInterface $comparator): void
307
    {
308 470
        foreach ($comparator->alteredColumns() as $pair) {
309
            /**
310 470
             * @var AbstractColumn $initial
311
             * @var AbstractColumn $current
312 470
             */
313 24
            [$current, $initial] = $pair;
314
315 470
            $this->assertValid($current);
316
            $this->alterColumn($table, $initial, $current);
317 484
        }
318
    }
319 484
320 40
    protected function createColumns(AbstractTable $table, ComparatorInterface $comparator): void
321
    {
322 484
        foreach ($comparator->addedColumns() as $column) {
323
            $this->assertValid($column);
324 538
            $this->createColumn($table, $column);
325
        }
326 538
    }
327 168
328
    protected function dropColumns(AbstractTable $table, ComparatorInterface $comparator): void
329 538
    {
330
        foreach ($comparator->droppedColumns() as $column) {
331
            $this->dropColumn($table, $column);
332
        }
333
    }
334
335
    protected function dropIndexes(AbstractTable $table, ComparatorInterface $comparator): void
336 1472
    {
337
        foreach ($comparator->droppedIndexes() as $index) {
338
            $this->dropIndex($table, $index);
339 1472
        }
340
    }
341 538
342
    protected function dropForeignKeys(AbstractTable $table, ComparatorInterface $comparator): void
343
    {
344
        foreach ($comparator->droppedForeignKeys() as $foreign) {
345
            $this->dropForeignKey($table, $foreign);
346 538
        }
347 538
    }
348
349
    /**
350 538
     * Applied to every column in order to make sure that driver support it.
351 484
     *
352
     * @throws DriverException
353
     */
354 538
    protected function assertValid(AbstractColumn $column): void
355 470
    {
356
        //All valid by default
357 538
    }
358
359 538
    protected function dropConstrains(
360
        AbstractTable $table,
361
        int $operation,
362
        ComparatorInterface $comparator
363
    ): void {
364 538
        if ($operation & self::DROP_FOREIGN_KEYS) {
365 470
            $this->dropForeignKeys($table, $comparator);
366
        }
367
368 538
        if ($operation & self::DROP_INDEXES) {
369 470
            $this->dropIndexes($table, $comparator);
370
        }
371
372 538
        if ($operation & self::DROP_COLUMNS) {
373 496
            $this->dropColumns($table, $comparator);
374
        }
375
    }
376 538
377 470
    protected function setConstrains(
378
        AbstractTable $table,
379 538
        int $operation,
380
        ComparatorInterface $comparator
381
    ): void {
382
        if ($operation & self::CREATE_INDEXES) {
383
            $this->createIndexes($table, $comparator);
384
        }
385
386
        if ($operation & self::ALTER_INDEXES) {
387
            $this->alterIndexes($table, $comparator);
388
        }
389
390
        if ($operation & self::CREATE_FOREIGN_KEYS) {
391
            $this->createForeignKeys($table, $comparator);
392
        }
393
394
        if ($operation & self::ALTER_FOREIGN_KEYS) {
395
            $this->alterForeignKeys($table, $comparator);
396
        }
397
    }
398
}
399