Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
92.50% |
111 / 120 |
|
50.00% |
4 / 8 |
CRAP | |
0.00% |
0 / 1 |
schema_generator | |
92.50% |
111 / 120 |
|
50.00% |
4 / 8 |
32.43 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
1 | |||
get_schema | |
94.74% |
18 / 19 |
|
0.00% |
0 / 1 |
3.00 | |||
apply_migration_to_schema | |
95.83% |
46 / 48 |
|
0.00% |
0 / 1 |
9 | |||
for_each_table | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
3 | |||
set_all | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
4 | |||
unset_all | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
3 | |||
handle_add_column | |
88.24% |
15 / 17 |
|
0.00% |
0 / 1 |
4.03 | |||
get_value_transform | |
73.33% |
11 / 15 |
|
0.00% |
0 / 1 |
5.47 |
1 | <?php |
2 | /** |
3 | * |
4 | * This file is part of the phpBB Forum Software package. |
5 | * |
6 | * @copyright (c) phpBB Limited <https://www.phpbb.com> |
7 | * @license GNU General Public License, version 2 (GPL-2.0) |
8 | * |
9 | * For full copyright and license information, please see |
10 | * the docs/CREDITS.txt file. |
11 | * |
12 | */ |
13 | |
14 | namespace phpbb\db\migration; |
15 | |
16 | use Closure; |
17 | use LogicException; |
18 | use phpbb\config\config; |
19 | use phpbb\db\driver\driver_interface; |
20 | use phpbb\db\migrator; |
21 | use phpbb\db\tools\tools_interface; |
22 | use UnexpectedValueException; |
23 | use CHItA\TopologicalSort\TopologicalSort; |
24 | |
25 | /** |
26 | * The schema generator generates the schema based on the existing migrations |
27 | */ |
28 | class schema_generator |
29 | { |
30 | use TopologicalSort; |
31 | |
32 | /** @var config */ |
33 | protected $config; |
34 | |
35 | /** @var driver_interface */ |
36 | protected $db; |
37 | |
38 | /** @var tools_interface */ |
39 | protected $db_tools; |
40 | |
41 | /** @var array */ |
42 | protected $class_names; |
43 | |
44 | /** @var string */ |
45 | protected $table_prefix; |
46 | |
47 | /** @var string */ |
48 | protected $phpbb_root_path; |
49 | |
50 | /** @var string */ |
51 | protected $php_ext; |
52 | |
53 | /** @var array */ |
54 | protected $tables; |
55 | |
56 | /** @var array */ |
57 | protected $table_names; |
58 | |
59 | /** |
60 | * Constructor |
61 | * @param array $class_names |
62 | * @param config $config |
63 | * @param driver_interface $db |
64 | * @param tools_interface $db_tools |
65 | * @param string $phpbb_root_path |
66 | * @param string $php_ext |
67 | * @param string $table_prefix |
68 | * @param array $tables |
69 | */ |
70 | public function __construct( |
71 | array $class_names, |
72 | config $config, |
73 | driver_interface $db, |
74 | tools_interface $db_tools, |
75 | string $phpbb_root_path, |
76 | string $php_ext, |
77 | string $table_prefix, |
78 | array $tables) |
79 | { |
80 | $this->config = $config; |
81 | $this->db = $db; |
82 | $this->db_tools = $db_tools; |
83 | $this->class_names = $class_names; |
84 | $this->phpbb_root_path = $phpbb_root_path; |
85 | $this->php_ext = $php_ext; |
86 | $this->table_prefix = $table_prefix; |
87 | $this->table_names = $tables; |
88 | } |
89 | |
90 | /** |
91 | * Loads all migrations and their application state from the database. |
92 | * |
93 | * @return array An array describing the database schema. |
94 | * |
95 | * @throws UnexpectedValueException If a migration tries to use an undefined schema change. |
96 | * @throws UnexpectedValueException If a dependency can't be resolved or there are circular |
97 | * dependencies between migrations. |
98 | */ |
99 | public function get_schema() : array |
100 | { |
101 | if (!empty($this->tables)) |
102 | { |
103 | return $this->tables; |
104 | } |
105 | |
106 | $migrations = $this->class_names; |
107 | $filter = function($class_name) { |
108 | return !migrator::is_migration($class_name); |
109 | }; |
110 | |
111 | $edges = function($class_name) { |
112 | return $class_name::depends_on(); |
113 | }; |
114 | |
115 | $apply_for_each = function($class_name) { |
116 | $this->apply_migration_to_schema($class_name); |
117 | }; |
118 | |
119 | try |
120 | { |
121 | $this->topologicalSort($migrations, $edges, true, $apply_for_each, $filter); |
122 | } |
123 | catch (LogicException $e) |
124 | { |
125 | throw new UnexpectedValueException( |
126 | "Migrations either have circular dependencies or unsatisfiable dependencies." |
127 | ); |
128 | } |
129 | |
130 | ksort($this->tables); |
131 | return $this->tables; |
132 | } |
133 | |
134 | /** |
135 | * Apply the changes defined in the migration to the database schema. |
136 | * |
137 | * @param string $migration_class The name of the migration class. |
138 | * |
139 | * @throws UnexpectedValueException If a migration tries to use an undefined schema change. |
140 | */ |
141 | private function apply_migration_to_schema(string $migration_class) |
142 | { |
143 | $migration = new $migration_class( |
144 | $this->config, |
145 | $this->db, |
146 | $this->db_tools, |
147 | $this->phpbb_root_path, |
148 | $this->php_ext, |
149 | $this->table_prefix, |
150 | $this->table_names |
151 | ); |
152 | |
153 | $column_map = [ |
154 | 'add_tables' => null, |
155 | 'drop_tables' => null, |
156 | 'add_columns' => 'COLUMNS', |
157 | 'drop_columns' => 'COLUMNS', |
158 | 'change_columns' => 'COLUMNS', |
159 | 'add_index' => 'KEYS', |
160 | 'add_unique_index' => 'KEYS', |
161 | 'drop_keys' => 'KEYS', |
162 | ]; |
163 | |
164 | $schema_changes = $migration->update_schema(); |
165 | foreach ($schema_changes as $change_type => $changes) |
166 | { |
167 | if (!array_key_exists($change_type, $column_map)) |
168 | { |
169 | throw new UnexpectedValueException("$migration_class contains undefined schema changes: $change_type."); |
170 | } |
171 | |
172 | $split_position = strpos($change_type, '_'); |
173 | $schema_change_type = substr($change_type, 0, $split_position); |
174 | $schema_type = substr($change_type, $split_position + 1); |
175 | |
176 | $action = null; |
177 | switch ($schema_change_type) |
178 | { |
179 | case 'add': |
180 | case 'change': |
181 | $action = function(&$value, $changes, $value_transform = null) { |
182 | self::set_all($value, $changes, $value_transform); |
183 | }; |
184 | break; |
185 | |
186 | case 'drop': |
187 | $action = function(&$value, $changes, $value_transform = null) { |
188 | self::unset_all($value, $changes); |
189 | }; |
190 | break; |
191 | |
192 | default: |
193 | throw new UnexpectedValueException("$migration_class contains undefined schema changes: $change_type."); |
194 | } |
195 | |
196 | switch ($schema_type) |
197 | { |
198 | case 'tables': |
199 | $action($this->tables, $changes); |
200 | break; |
201 | |
202 | default: |
203 | $this->for_each_table( |
204 | $changes, |
205 | $action, |
206 | $column_map[$change_type], |
207 | self::get_value_transform($schema_change_type, $schema_type) |
208 | ); |
209 | } |
210 | } |
211 | } |
212 | |
213 | /** |
214 | * Apply `$callback` to each table specified in `$data`. |
215 | * |
216 | * @param array $data Array describing the schema changes. |
217 | * @param callable $callback Callback function to be applied. |
218 | * @param string|null $column Column of the `$this->tables` array for the table on which |
219 | * the change will be made or null. |
220 | * @param callable|null $value_transform Value transformation callback function or null. |
221 | */ |
222 | private function for_each_table(array $data, callable $callback, $column = null, $value_transform = null) |
223 | { |
224 | foreach ($data as $table => $values) |
225 | { |
226 | $target = &$this->tables[$table]; |
227 | if ($column !== null) |
228 | { |
229 | $target = &$target[$column]; |
230 | } |
231 | |
232 | $callback($target, $values, $value_transform); |
233 | } |
234 | } |
235 | |
236 | /** |
237 | * Set an array of key-value pairs in the schema. |
238 | * |
239 | * @param mixed $schema Reference to the schema entry. |
240 | * @param mixed $data Array of values to be set. |
241 | * @param callable|null $value_transform Callback to transform the value being set. |
242 | */ |
243 | private static function set_all(&$schema, $data, ?callable $value_transform = null) |
244 | { |
245 | $data = (!is_array($data)) ? [$data] : $data; |
246 | foreach ($data as $key => $change) |
247 | { |
248 | if (is_callable($value_transform)) |
249 | { |
250 | $value_transform($schema, $key, $change); |
251 | } |
252 | else |
253 | { |
254 | $schema[$key] = $change; |
255 | } |
256 | } |
257 | } |
258 | |
259 | /** |
260 | * Remove an array of values from the schema |
261 | * |
262 | * @param mixed $schema Reference to the schema entry. |
263 | * @param mixed $data Array of values to be removed. |
264 | */ |
265 | private static function unset_all(&$schema, $data) |
266 | { |
267 | $data = (!is_array($data)) ? [$data] : $data; |
268 | foreach ($data as $key) |
269 | { |
270 | unset($schema[$key]); |
271 | } |
272 | } |
273 | |
274 | /** |
275 | * Logic for adding a new column to a table. |
276 | * |
277 | * @param array $value The table column entry. |
278 | * @param string $key The column name to add. |
279 | * @param array $change The column data. |
280 | */ |
281 | private static function handle_add_column(array &$value, string $key, array $change) |
282 | { |
283 | if (!array_key_exists('after', $change)) |
284 | { |
285 | $value[$key] = $change; |
286 | return; |
287 | } |
288 | |
289 | $after = $change['after']; |
290 | unset($change['after']); |
291 | |
292 | if ($after === null) |
293 | { |
294 | $value[$key] = array_values($change); |
295 | return; |
296 | } |
297 | |
298 | $offset = array_search($after, array_keys($value)); |
299 | if ($offset === false) |
300 | { |
301 | $value[$key] = array_values($change); |
302 | return; |
303 | } |
304 | |
305 | $value = array_merge( |
306 | array_slice($value, 0, $offset + 1, true), |
307 | [$key => array_values($change)], |
308 | array_slice($value, $offset) |
309 | ); |
310 | } |
311 | |
312 | /** |
313 | * Returns the value transform for the change. |
314 | * |
315 | * @param string $change_type The type of the change. |
316 | * @param string $schema_type The schema type on which the change is to be performed. |
317 | * |
318 | * @return Closure|null The value transformation callback or null if it is not needed. |
319 | */ |
320 | private static function get_value_transform(string $change_type, string $schema_type) : ?Closure |
321 | { |
322 | if ($change_type !== 'add') |
323 | { |
324 | return null; |
325 | } |
326 | |
327 | switch ($schema_type) |
328 | { |
329 | case 'index': |
330 | return function(&$value, $key, $change) { |
331 | $value[$key] = ['INDEX', $change]; |
332 | }; |
333 | |
334 | case 'unique_index': |
335 | return function(&$value, $key, $change) { |
336 | $value[$key] = ['UNIQUE', $change]; |
337 | }; |
338 | |
339 | case 'columns': |
340 | return function(&$value, $key, $change) { |
341 | self::handle_add_column($value, $key, $change); |
342 | }; |
343 | } |
344 | |
345 | return null; |
346 | } |
347 | } |