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
|
|||
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 |
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.