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

Reflector::sortedTables()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2

Importance

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

190
                $driver->/** @scrutinizer ignore-call */ 
191
                         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...
191
            } else {
192
                $driver->beginTransaction(null);
193
            }
194
        }
195
    }
196
197
    /**
198
     * Commit mass transaction.
199
     */
200
    protected function commitTransaction(): void
201
    {
202
        foreach ($this->drivers as $driver) {
203 112
            $driver->commitTransaction();
204
        }
205 112
    }
206 112
207 112
    /**
208
     * Roll back mass transaction.
209
     */
210 112
    protected function rollbackTransaction(): void
211
    {
212
        foreach (array_reverse($this->drivers) as $driver) {
213
            $driver->rollbackTransaction();
214
        }
215 112
    }
216
217 112
    /**
218 72
     * Collecting all involved drivers.
219
     */
220
    private function collectDrivers(): void
221 112
    {
222 112
        foreach ($this->tables as $table) {
223 96
            if (!in_array($table->getDriver(), $this->drivers, true)) {
224 72
                $this->drivers[] = $table->getDriver();
225
            }
226
        }
227
    }
228 112
229 112
    /**
230 112
     * @psalm-param non-empty-string $key
231
     */
232
    private function sort(string $key, array $dependencies): void
233
    {
234
        if (isset($this->states[$key])) {
235
            return;
236
        }
237
238
        $this->states[$key] = self::STATE_NEW;
239
        foreach ($dependencies as $dependency) {
240
            if (isset($this->dependencies[$dependency])) {
241
                $this->sort($dependency, $this->dependencies[$dependency]);
242
            }
243
        }
244
245
        $this->stack[] = $this->tables[$key];
246
        $this->states[$key] = self::STATE_PASSED;
247
    }
248
}
249