Reflector::run()   B
last analyzed

Complexity

Conditions 7
Paths 27

Size

Total Lines 37
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 7.2694

Importance

Changes 0
Metric Value
cc 7
eloc 19
nc 27
nop 0
dl 0
loc 37
ccs 14
cts 17
cp 0.8235
crap 7.2694
rs 8.8333
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\Schema;
13
14
use Cycle\Database\Driver\Driver;
15
use Cycle\Database\Driver\DriverInterface;
16
use Cycle\Database\Driver\HandlerInterface;
17
18
/**
19
 * Saves multiple linked tables at once but treating their cross dependency.
20
 * Attention, not every DBMS support transactional schema manipulations!
21
 */
22
final class Reflector
23
{
24
    public const STATE_NEW = 1;
25
    public const STATE_PASSED = 2;
26
27
    /** @var AbstractTable[] */
28
    private array $tables = [];
29
30
    private array $dependencies = [];
31
32
    /** @var DriverInterface[] */
33
    private array $drivers = [];
34
35
    private array $states = [];
36
    private array $stack = [];
37
38
    /**
39
     * Add table to the collection.
40
     */
41
    public function addTable(AbstractTable $table): void
42 112
    {
43
        $this->tables[$table->getFullName()] = $table;
44 112
        $this->dependencies[$table->getFullName()] = $table->getDependencies();
45 112
46
        $this->collectDrivers();
47 112
    }
48 112
49
    /**
50
     * @return AbstractTable[]
51
     */
52
    public function getTables(): array
53 112
    {
54
        return \array_values($this->tables);
55 112
    }
56
57
    /**
58
     * Return sorted stack.
59
     */
60
    public function sortedTables(): array
61 112
    {
62
        $items = \array_keys($this->tables);
63 112
        $this->states = $this->stack = [];
64 112
65
        foreach ($items as $item) {
66 112
            $this->sort($item, $this->dependencies[$item]);
67 112
        }
68
69
        return $this->stack;
70 112
    }
71
72
    /**
73
     * Synchronize tables.
74
     *
75
     * @throws \Throwable
76
     */
77
    public function run(): void
78 112
    {
79
        $hasChanges = false;
80 112
        foreach ($this->tables as $table) {
81 112
            if (
82
                $table->getStatus() === AbstractTable::STATUS_DECLARED_DROPPED
83 112
                || $table->getComparator()->hasChanges()
84 112
            ) {
85
                $hasChanges = true;
86 112
                break;
87 112
            }
88
        }
89
90
        if (!$hasChanges) {
91 112
            //Nothing to do
92
            return;
93 8
        }
94
95
        $this->beginTransaction();
96 112
97
        try {
98
            //Drop not-needed foreign keys and alter everything else
99
            $this->dropForeignKeys();
100 112
101
            //Drop not-needed indexes
102
            $this->dropIndexes();
103 112
104
            //Other changes [NEW TABLES WILL BE CREATED HERE!]
105
            foreach ($this->commitChanges() as $table) {
106 112
                $table->save(HandlerInterface::CREATE_FOREIGN_KEYS, true);
107 112
            }
108
        } catch (\Throwable $e) {
109
            $this->rollbackTransaction();
110
            throw $e;
111
        }
112
113
        $this->commitTransaction();
114 112
    }
115 112
116
    /**
117
     * Drop all removed table references.
118
     */
119
    protected function dropForeignKeys(): void
120 112
    {
121
        foreach ($this->sortedTables() as $table) {
122 112
            if ($table->exists()) {
123 112
                $table->save(HandlerInterface::DROP_FOREIGN_KEYS, false);
124 72
            }
125
        }
126
    }
127 112
128
    /**
129
     * Drop all removed table indexes.
130
     */
131
    protected function dropIndexes(): void
132 112
    {
133
        foreach ($this->sortedTables() as $table) {
134 112
            if ($table->exists()) {
135 112
                $table->save(HandlerInterface::DROP_INDEXES, false);
136 72
            }
137
        }
138
    }
139 112
140
    /*
141
     * @return AbstractTable[] Created or updated tables.
142
     */
143
    protected function commitChanges(): array
144 112
    {
145
        $updated = [];
146 112
        foreach ($this->sortedTables() as $table) {
147 112
            if ($table->getStatus() === AbstractTable::STATUS_DECLARED_DROPPED) {
148 112
                $table->save(HandlerInterface::DO_DROP);
149 16
                continue;
150 16
            }
151
152
            $updated[] = $table;
153 112
            $table->save(
154 112
                HandlerInterface::DO_ALL
155
                ^ HandlerInterface::DROP_FOREIGN_KEYS
156
                ^ HandlerInterface::DROP_INDEXES
157
                ^ HandlerInterface::CREATE_FOREIGN_KEYS,
158 112
            );
159
        }
160
161
        return $updated;
162 112
    }
163
164
    /**
165
     * Begin mass transaction.
166
     */
167
    protected function beginTransaction(): void
168 112
    {
169
        foreach ($this->drivers as $driver) {
170 112
            if ($driver instanceof Driver) {
171 112
                // do not cache statements for this transaction
172
                $driver->beginTransaction(null, false);
0 ignored issues
show
Unused Code introduced by
The call to Cycle\Database\Driver\Driver::beginTransaction() has too many arguments starting with false. ( Ignorable by Annotation )

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

172
                $driver->/** @scrutinizer ignore-call */ 
173
                         beginTransaction(null, false);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
173 112
            } else {
174
                $driver->beginTransaction(null);
175
            }
176
        }
177
    }
178 112
179
    /**
180
     * Commit mass transaction.
181
     */
182
    protected function commitTransaction(): void
183 112
    {
184
        foreach ($this->drivers as $driver) {
185 112
            $driver->commitTransaction();
186 112
        }
187
    }
188 112
189
    /**
190
     * Roll back mass transaction.
191
     */
192
    protected function rollbackTransaction(): void
193
    {
194
        foreach (\array_reverse($this->drivers) as $driver) {
195
            $driver->rollbackTransaction();
196
        }
197
    }
198
199
    /**
200
     * Collecting all involved drivers.
201
     */
202
    private function collectDrivers(): void
203 112
    {
204
        foreach ($this->tables as $table) {
205 112
            if (!\in_array($table->getDriver(), $this->drivers, true)) {
206 112
                $this->drivers[] = $table->getDriver();
207 112
            }
208
        }
209
    }
210 112
211
    /**
212
     * @psalm-param non-empty-string $key
213
     */
214
    private function sort(string $key, array $dependencies): void
215 112
    {
216
        if (isset($this->states[$key])) {
217 112
            return;
218 72
        }
219
220
        $this->states[$key] = self::STATE_NEW;
221 112
        foreach ($dependencies as $dependency) {
222 112
            if (isset($this->dependencies[$dependency])) {
223 96
                $this->sort($dependency, $this->dependencies[$dependency]);
224 72
            }
225
        }
226
227
        $this->stack[] = $this->tables[$key];
228 112
        $this->states[$key] = self::STATE_PASSED;
229 112
    }
230
}
231