Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
85.60% |
309 / 361 |
|
48.00% |
12 / 25 |
CRAP | |
0.00% |
0 / 1 |
migrator | |
85.60% |
309 / 361 |
|
48.00% |
12 / 25 |
207.84 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
16 / 16 |
|
100.00% |
1 / 1 |
2 | |||
set_output_handler | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
load_migration_state | |
100.00% |
12 / 12 |
|
100.00% |
1 / 1 |
4 | |||
get_last_run_migration | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
set_migrations | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
3 | |||
get_migrations | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
get_installable_migrations | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
30 | |||
update | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
get_valid_name | |
75.00% |
6 / 8 |
|
0.00% |
0 / 1 |
6.56 | |||
update_do | |
88.89% |
8 / 9 |
|
0.00% |
0 / 1 |
6.05 | |||
try_apply | |
96.67% |
87 / 90 |
|
0.00% |
0 / 1 |
28 | |||
revert | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
revert_do | |
87.50% |
7 / 8 |
|
0.00% |
0 / 1 |
7.10 | |||
try_revert | |
96.23% |
51 / 53 |
|
0.00% |
0 / 1 |
18 | |||
process_data_step | |
95.45% |
21 / 22 |
|
0.00% |
0 / 1 |
13 | |||
run_step | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
2 | |||
get_callable_from_step | |
81.82% |
36 / 44 |
|
0.00% |
0 / 1 |
19.95 | |||
set_migration_state | |
100.00% |
14 / 14 |
|
100.00% |
1 / 1 |
3 | |||
unfulfillable | |
100.00% |
14 / 14 |
|
100.00% |
1 / 1 |
6 | |||
finished | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
6 | |||
migration_state | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
get_migration | |
75.00% |
3 / 4 |
|
0.00% |
0 / 1 |
2.06 | |||
populate_migrations | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
12 | |||
create_migrations_table | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
6 | |||
is_migration | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
4 |
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; |
15 | |
16 | use phpbb\config\config; |
17 | use phpbb\db\driver\driver_interface; |
18 | use phpbb\db\migration\exception; |
19 | use phpbb\db\migration\helper; |
20 | use phpbb\db\output_handler\migrator_output_handler_interface; |
21 | use phpbb\db\output_handler\null_migrator_output_handler; |
22 | use phpbb\db\tools\tools_interface; |
23 | use Symfony\Component\DependencyInjection\ContainerAwareInterface; |
24 | use Symfony\Component\DependencyInjection\ContainerInterface; |
25 | |
26 | /** |
27 | * The migrator is responsible for applying new migrations in the correct order. |
28 | */ |
29 | class migrator |
30 | { |
31 | /** |
32 | * @var ContainerInterface |
33 | */ |
34 | protected $container; |
35 | |
36 | /** @var config */ |
37 | protected $config; |
38 | |
39 | /** @var driver_interface */ |
40 | protected $db; |
41 | |
42 | /** @var tools_interface */ |
43 | protected $db_tools; |
44 | |
45 | /** @var helper */ |
46 | protected $helper; |
47 | |
48 | /** @var string */ |
49 | protected $table_prefix; |
50 | |
51 | /** @var array */ |
52 | protected $tables; |
53 | |
54 | /** @var array */ |
55 | protected $tools; |
56 | |
57 | /** @var string */ |
58 | protected $phpbb_root_path; |
59 | |
60 | /** @var string */ |
61 | protected $php_ext; |
62 | |
63 | /** @var string */ |
64 | protected $migrations_table; |
65 | |
66 | /** |
67 | * State of all migrations |
68 | * |
69 | * (SELECT * FROM migrations table) |
70 | * |
71 | * @var array |
72 | */ |
73 | protected $migration_state = array(); |
74 | |
75 | /** |
76 | * Array of all migrations available to be run |
77 | * |
78 | * @var array |
79 | */ |
80 | protected $migrations = array(); |
81 | |
82 | /** |
83 | * Array of migrations that have been determined to be fulfillable |
84 | * |
85 | * @var array |
86 | */ |
87 | protected $fulfillable_migrations = array(); |
88 | |
89 | /** |
90 | * 'name,' 'class,' and 'state' of the last migration run |
91 | * |
92 | * 'effectively_installed' set and set to true if the migration was effectively_installed |
93 | * |
94 | * @var array |
95 | */ |
96 | protected $last_run_migration = []; |
97 | |
98 | /** |
99 | * The output handler. A null handler is configured by default. |
100 | * |
101 | * @var migrator_output_handler_interface |
102 | */ |
103 | protected $output_handler; |
104 | |
105 | /** |
106 | * Constructor of the database migrator |
107 | * @param ContainerInterface $container |
108 | * @param config $config |
109 | * @param driver\driver_interface $db |
110 | * @param tools\tools_interface $db_tools |
111 | * @param $migrations_table |
112 | * @param $phpbb_root_path |
113 | * @param $php_ext |
114 | * @param $table_prefix |
115 | * @param $tables |
116 | * @param $tools |
117 | * @param migration\helper $helper |
118 | */ |
119 | public function __construct(ContainerInterface $container, config $config, driver_interface $db, tools_interface $db_tools, $migrations_table, $phpbb_root_path, $php_ext, $table_prefix, $tables, $tools, helper $helper) |
120 | { |
121 | $this->container = $container; |
122 | $this->config = $config; |
123 | $this->db = $db; |
124 | $this->db_tools = $db_tools; |
125 | $this->helper = $helper; |
126 | |
127 | $this->migrations_table = $migrations_table; |
128 | |
129 | $this->phpbb_root_path = $phpbb_root_path; |
130 | $this->php_ext = $php_ext; |
131 | |
132 | $this->table_prefix = $table_prefix; |
133 | $this->tables = $tables; |
134 | |
135 | $this->output_handler = new null_migrator_output_handler(); |
136 | |
137 | $this->tools = []; |
138 | foreach ($tools as $tool) |
139 | { |
140 | $this->tools[$tool->get_name()] = $tool; |
141 | } |
142 | |
143 | $this->tools['dbtools'] = $this->db_tools; |
144 | |
145 | $this->load_migration_state(); |
146 | } |
147 | |
148 | /** |
149 | * Set the output handler. |
150 | * |
151 | * @param migrator_output_handler_interface $handler The output handler |
152 | */ |
153 | public function set_output_handler(migrator_output_handler_interface $handler) |
154 | { |
155 | $this->output_handler = $handler; |
156 | } |
157 | |
158 | /** |
159 | * Loads all migrations and their application state from the database. |
160 | * |
161 | * @return void |
162 | */ |
163 | public function load_migration_state() |
164 | { |
165 | $this->migration_state = array(); |
166 | |
167 | // prevent errors in case the table does not exist yet |
168 | $this->db->sql_return_on_error(true); |
169 | |
170 | $sql = "SELECT * |
171 | FROM " . $this->migrations_table; |
172 | $result = $this->db->sql_query($sql); |
173 | |
174 | if (!$this->db->get_sql_error_triggered()) |
175 | { |
176 | while ($migration = $this->db->sql_fetchrow($result)) |
177 | { |
178 | $this->migration_state[$migration['migration_name']] = $migration; |
179 | |
180 | $this->migration_state[$migration['migration_name']]['migration_depends_on'] = unserialize($migration['migration_depends_on']); |
181 | $this->migration_state[$migration['migration_name']]['migration_data_state'] = !empty($migration['migration_data_state']) ? unserialize($migration['migration_data_state']) : ''; |
182 | } |
183 | } |
184 | |
185 | $this->db->sql_freeresult($result); |
186 | |
187 | $this->db->sql_return_on_error(false); |
188 | } |
189 | |
190 | /** |
191 | * Get an array with information about the last migration run. |
192 | * |
193 | * The array contains 'name', 'class' and 'state'. 'effectively_installed' is set |
194 | * and set to true if the last migration was effectively_installed. |
195 | * |
196 | * @return array Last run migration information or false if no migration has been run yet |
197 | */ |
198 | public function get_last_run_migration(): array |
199 | { |
200 | return $this->last_run_migration; |
201 | } |
202 | |
203 | /** |
204 | * Sets the list of available migration class names to the given array. |
205 | * |
206 | * @param array $class_names An array of migration class names |
207 | * @return void |
208 | */ |
209 | public function set_migrations($class_names) |
210 | { |
211 | foreach ($class_names as $key => $class) |
212 | { |
213 | if (!self::is_migration($class)) |
214 | { |
215 | unset($class_names[$key]); |
216 | } |
217 | } |
218 | |
219 | $this->migrations = $class_names; |
220 | } |
221 | |
222 | /** |
223 | * Get the list of available migration class names |
224 | * |
225 | * @return array Array of all migrations available to be run |
226 | */ |
227 | public function get_migrations() |
228 | { |
229 | return $this->migrations; |
230 | } |
231 | |
232 | /** |
233 | * Get the list of available and not installed migration class names |
234 | * |
235 | * @return array |
236 | */ |
237 | public function get_installable_migrations() |
238 | { |
239 | $unfinished_migrations = array(); |
240 | |
241 | foreach ($this->migrations as $name) |
242 | { |
243 | if (!isset($this->migration_state[$name]) || |
244 | !$this->migration_state[$name]['migration_schema_done'] || |
245 | !$this->migration_state[$name]['migration_data_done']) |
246 | { |
247 | $unfinished_migrations[] = $name; |
248 | } |
249 | } |
250 | |
251 | return $unfinished_migrations; |
252 | } |
253 | |
254 | /** |
255 | * Runs a single update step from the next migration to be applied. |
256 | * |
257 | * The update step can either be a schema or a (partial) data update. To |
258 | * check if update() needs to be called again use the finished() method. |
259 | * |
260 | * @return void |
261 | */ |
262 | public function update() |
263 | { |
264 | $this->container->get('event_dispatcher')->disable(); |
265 | $this->update_do(); |
266 | $this->container->get('event_dispatcher')->enable(); |
267 | } |
268 | |
269 | /** |
270 | * Get a valid migration name from the migration state array in case the |
271 | * supplied name is not in the migration state list. |
272 | * |
273 | * @param string $name Migration name |
274 | * @return string Migration name |
275 | */ |
276 | protected function get_valid_name($name) |
277 | { |
278 | // Try falling back to a valid migration name with or without leading backslash |
279 | if (!isset($this->migration_state[$name])) |
280 | { |
281 | $prepended_name = ($name[0] == '\\' ? '' : '\\') . $name; |
282 | $prefixless_name = $name[0] == '\\' ? substr($name, 1) : $name; |
283 | |
284 | if (isset($this->migration_state[$prepended_name])) |
285 | { |
286 | $name = $prepended_name; |
287 | } |
288 | else if (isset($this->migration_state[$prefixless_name])) |
289 | { |
290 | $name = $prefixless_name; |
291 | } |
292 | } |
293 | |
294 | return $name; |
295 | } |
296 | |
297 | /** |
298 | * Effectively runs a single update step from the next migration to be applied. |
299 | * |
300 | * @return void |
301 | */ |
302 | protected function update_do() |
303 | { |
304 | foreach ($this->migrations as $name) |
305 | { |
306 | $name = $this->get_valid_name($name); |
307 | |
308 | if (!isset($this->migration_state[$name]) || |
309 | !$this->migration_state[$name]['migration_schema_done'] || |
310 | !$this->migration_state[$name]['migration_data_done']) |
311 | { |
312 | if (!$this->try_apply($name)) |
313 | { |
314 | continue; |
315 | } |
316 | else |
317 | { |
318 | return; |
319 | } |
320 | } |
321 | else |
322 | { |
323 | $this->output_handler->write(array('MIGRATION_EFFECTIVELY_INSTALLED', $name), migrator_output_handler_interface::VERBOSITY_DEBUG); |
324 | } |
325 | } |
326 | } |
327 | |
328 | /** |
329 | * Attempts to apply a step of the given migration or one of its dependencies |
330 | * |
331 | * @param string $name The class name of the migration |
332 | * @return bool Whether any update step was successfully run |
333 | * @throws exception |
334 | */ |
335 | protected function try_apply($name) |
336 | { |
337 | if (!class_exists($name)) |
338 | { |
339 | $this->output_handler->write(array('MIGRATION_NOT_VALID', $name), migrator_output_handler_interface::VERBOSITY_DEBUG); |
340 | return false; |
341 | } |
342 | |
343 | $migration = $this->get_migration($name); |
344 | |
345 | $state = (isset($this->migration_state[$name])) ? |
346 | $this->migration_state[$name] : |
347 | array( |
348 | 'migration_depends_on' => $migration->depends_on(), |
349 | 'migration_schema_done' => false, |
350 | 'migration_data_done' => false, |
351 | 'migration_data_state' => '', |
352 | 'migration_start_time' => 0, |
353 | 'migration_end_time' => 0, |
354 | ); |
355 | |
356 | if (!empty($state['migration_depends_on'])) |
357 | { |
358 | $this->output_handler->write(array('MIGRATION_APPLY_DEPENDENCIES', $name), migrator_output_handler_interface::VERBOSITY_DEBUG); |
359 | } |
360 | |
361 | foreach ($state['migration_depends_on'] as $depend) |
362 | { |
363 | $depend = $this->get_valid_name($depend); |
364 | |
365 | // Test all possible namings before throwing exception |
366 | $missing = $this->unfulfillable($depend); |
367 | if ($missing !== false) |
368 | { |
369 | throw new exception('MIGRATION_NOT_FULFILLABLE', $name, $missing); |
370 | } |
371 | |
372 | if (!isset($this->migration_state[$depend]) || |
373 | !$this->migration_state[$depend]['migration_schema_done'] || |
374 | !$this->migration_state[$depend]['migration_data_done']) |
375 | { |
376 | return $this->try_apply($depend); |
377 | } |
378 | } |
379 | |
380 | $this->last_run_migration = array( |
381 | 'name' => $name, |
382 | 'class' => $migration, |
383 | 'state' => $state, |
384 | 'task' => '', |
385 | ); |
386 | |
387 | if (!isset($this->migration_state[$name])) |
388 | { |
389 | if ($state['migration_start_time'] == 0 && $migration->effectively_installed()) |
390 | { |
391 | $state = array( |
392 | 'migration_depends_on' => $migration->depends_on(), |
393 | 'migration_schema_done' => true, |
394 | 'migration_data_done' => true, |
395 | 'migration_data_state' => '', |
396 | 'migration_start_time' => 0, |
397 | 'migration_end_time' => 0, |
398 | ); |
399 | |
400 | $this->last_run_migration['effectively_installed'] = true; |
401 | |
402 | $this->output_handler->write(array('MIGRATION_EFFECTIVELY_INSTALLED', $name), migrator_output_handler_interface::VERBOSITY_VERBOSE); |
403 | } |
404 | else |
405 | { |
406 | $state['migration_start_time'] = time(); |
407 | } |
408 | } |
409 | |
410 | $this->set_migration_state($name, $state); |
411 | |
412 | if (!$state['migration_schema_done']) |
413 | { |
414 | $verbosity = empty($state['migration_data_state']) ? |
415 | migrator_output_handler_interface::VERBOSITY_VERBOSE : migrator_output_handler_interface::VERBOSITY_DEBUG; |
416 | $this->output_handler->write(array('MIGRATION_SCHEMA_RUNNING', $name), $verbosity); |
417 | |
418 | $this->last_run_migration['task'] = 'process_schema_step'; |
419 | |
420 | $total_time = (is_array($state['migration_data_state']) && isset($state['migration_data_state']['_total_time'])) ? |
421 | $state['migration_data_state']['_total_time'] : 0.0; |
422 | $elapsed_time = microtime(true); |
423 | |
424 | $steps = $this->helper->get_schema_steps($migration->update_schema()); |
425 | $result = $this->process_data_step($steps, $state['migration_data_state']); |
426 | |
427 | $elapsed_time = microtime(true) - $elapsed_time; |
428 | $total_time += $elapsed_time; |
429 | |
430 | if (is_array($result)) |
431 | { |
432 | $result['_total_time'] = $total_time; |
433 | } |
434 | |
435 | $state['migration_data_state'] = ($result === true) ? '' : $result; |
436 | $state['migration_schema_done'] = ($result === true); |
437 | |
438 | if ($state['migration_schema_done']) |
439 | { |
440 | $this->output_handler->write(array('MIGRATION_SCHEMA_DONE', $name, $total_time), migrator_output_handler_interface::VERBOSITY_NORMAL); |
441 | } |
442 | else |
443 | { |
444 | $this->output_handler->write(array('MIGRATION_SCHEMA_IN_PROGRESS', $name, $elapsed_time), migrator_output_handler_interface::VERBOSITY_VERY_VERBOSE); |
445 | } |
446 | } |
447 | else if (!$state['migration_data_done']) |
448 | { |
449 | try |
450 | { |
451 | $verbosity = empty($state['migration_data_state']) ? |
452 | migrator_output_handler_interface::VERBOSITY_VERBOSE : migrator_output_handler_interface::VERBOSITY_DEBUG; |
453 | $this->output_handler->write(array('MIGRATION_DATA_RUNNING', $name), $verbosity); |
454 | |
455 | $this->last_run_migration['task'] = 'process_data_step'; |
456 | |
457 | $total_time = (is_array($state['migration_data_state']) && isset($state['migration_data_state']['_total_time'])) ? |
458 | $state['migration_data_state']['_total_time'] : 0.0; |
459 | $elapsed_time = microtime(true); |
460 | |
461 | $result = $this->process_data_step($migration->update_data(), $state['migration_data_state']); |
462 | |
463 | $elapsed_time = microtime(true) - $elapsed_time; |
464 | $total_time += $elapsed_time; |
465 | |
466 | if (is_array($result)) |
467 | { |
468 | $result['_total_time'] = $total_time; |
469 | } |
470 | |
471 | $state['migration_data_state'] = ($result === true) ? '' : $result; |
472 | $state['migration_data_done'] = ($result === true); |
473 | $state['migration_end_time'] = ($result === true) ? time() : 0; |
474 | |
475 | if ($state['migration_data_done']) |
476 | { |
477 | $this->output_handler->write(array('MIGRATION_DATA_DONE', $name, $total_time), migrator_output_handler_interface::VERBOSITY_NORMAL); |
478 | } |
479 | else |
480 | { |
481 | $this->output_handler->write(array('MIGRATION_DATA_IN_PROGRESS', $name, $elapsed_time), migrator_output_handler_interface::VERBOSITY_VERY_VERBOSE); |
482 | } |
483 | } |
484 | catch (exception $e) |
485 | { |
486 | // Reset data state and revert the schema changes |
487 | $state['migration_data_state'] = ''; |
488 | $this->set_migration_state($name, $state); |
489 | |
490 | $this->revert_do($name); |
491 | |
492 | throw $e; |
493 | } |
494 | } |
495 | |
496 | $this->set_migration_state($name, $state); |
497 | |
498 | return true; |
499 | } |
500 | |
501 | /** |
502 | * Runs a single revert step from the last migration installed |
503 | * |
504 | * YOU MUST ADD/SET ALL MIGRATIONS THAT COULD BE DEPENDENT ON THE MIGRATION TO REVERT TO BEFORE CALLING THIS METHOD! |
505 | * The revert step can either be a schema or a (partial) data revert. To |
506 | * check if revert() needs to be called again use the migration_state() method. |
507 | * |
508 | * @param string $migration String migration name to revert (including any that depend on this migration) |
509 | */ |
510 | public function revert($migration) |
511 | { |
512 | $this->container->get('event_dispatcher')->disable(); |
513 | $this->revert_do($migration); |
514 | $this->container->get('event_dispatcher')->enable(); |
515 | } |
516 | |
517 | /** |
518 | * Effectively runs a single revert step from the last migration installed |
519 | * |
520 | * @param string $migration String migration name to revert (including any that depend on this migration) |
521 | * @return void |
522 | */ |
523 | protected function revert_do($migration) |
524 | { |
525 | if (!isset($this->migration_state[$migration])) |
526 | { |
527 | // Not installed |
528 | return; |
529 | } |
530 | |
531 | foreach ($this->migrations as $name) |
532 | { |
533 | $state = $this->migration_state($name); |
534 | |
535 | if ($state && in_array($migration, $state['migration_depends_on']) && ($state['migration_schema_done'] || $state['migration_data_done'])) |
536 | { |
537 | $this->revert_do($name); |
538 | return; |
539 | } |
540 | } |
541 | |
542 | $this->try_revert($migration); |
543 | } |
544 | |
545 | /** |
546 | * Attempts to revert a step of the given migration or one of its dependencies |
547 | * |
548 | * @param string $name The class name of the migration |
549 | * @return bool Whether any update step was successfully run |
550 | */ |
551 | protected function try_revert($name) |
552 | { |
553 | if (!class_exists($name)) |
554 | { |
555 | return false; |
556 | } |
557 | |
558 | $migration = $this->get_migration($name); |
559 | |
560 | $state = $this->migration_state[$name]; |
561 | |
562 | $this->last_run_migration = array( |
563 | 'name' => $name, |
564 | 'class' => $migration, |
565 | 'task' => '', |
566 | ); |
567 | |
568 | if ($state['migration_data_done']) |
569 | { |
570 | $verbosity = empty($state['migration_data_state']) ? |
571 | migrator_output_handler_interface::VERBOSITY_VERBOSE : migrator_output_handler_interface::VERBOSITY_DEBUG; |
572 | $this->output_handler->write(array('MIGRATION_REVERT_DATA_RUNNING', $name), $verbosity); |
573 | |
574 | $total_time = (is_array($state['migration_data_state']) && isset($state['migration_data_state']['_total_time'])) ? |
575 | $state['migration_data_state']['_total_time'] : 0.0; |
576 | $elapsed_time = microtime(true); |
577 | |
578 | $steps = array_merge($this->helper->reverse_update_data($migration->update_data()), $migration->revert_data()); |
579 | $result = $this->process_data_step($steps, $state['migration_data_state']); |
580 | |
581 | $elapsed_time = microtime(true) - $elapsed_time; |
582 | $total_time += $elapsed_time; |
583 | |
584 | if (is_array($result)) |
585 | { |
586 | $result['_total_time'] = $total_time; |
587 | } |
588 | |
589 | $state['migration_data_state'] = ($result === true) ? '' : $result; |
590 | $state['migration_data_done'] = ($result === true) ? false : true; |
591 | |
592 | $this->set_migration_state($name, $state); |
593 | |
594 | if (!$state['migration_data_done']) |
595 | { |
596 | $this->output_handler->write(array('MIGRATION_REVERT_DATA_DONE', $name, $total_time), migrator_output_handler_interface::VERBOSITY_NORMAL); |
597 | } |
598 | else |
599 | { |
600 | $this->output_handler->write(array('MIGRATION_REVERT_DATA_IN_PROGRESS', $name, $elapsed_time), migrator_output_handler_interface::VERBOSITY_VERY_VERBOSE); |
601 | } |
602 | } |
603 | else if ($state['migration_schema_done']) |
604 | { |
605 | $verbosity = empty($state['migration_data_state']) ? |
606 | migrator_output_handler_interface::VERBOSITY_VERBOSE : migrator_output_handler_interface::VERBOSITY_DEBUG; |
607 | $this->output_handler->write(array('MIGRATION_REVERT_SCHEMA_RUNNING', $name), $verbosity); |
608 | |
609 | $total_time = (is_array($state['migration_data_state']) && isset($state['migration_data_state']['_total_time'])) ? |
610 | $state['migration_data_state']['_total_time'] : 0.0; |
611 | $elapsed_time = microtime(true); |
612 | |
613 | $steps = $this->helper->get_schema_steps($migration->revert_schema()); |
614 | $result = $this->process_data_step($steps, $state['migration_data_state']); |
615 | |
616 | $elapsed_time = microtime(true) - $elapsed_time; |
617 | $total_time += $elapsed_time; |
618 | |
619 | if (is_array($result)) |
620 | { |
621 | $result['_total_time'] = $total_time; |
622 | } |
623 | |
624 | $state['migration_data_state'] = ($result === true) ? '' : $result; |
625 | $state['migration_schema_done'] = ($result === true) ? false : true; |
626 | |
627 | if (!$state['migration_schema_done']) |
628 | { |
629 | $sql = 'DELETE FROM ' . $this->migrations_table . " |
630 | WHERE migration_name = '" . $this->db->sql_escape($name) . "'"; |
631 | $this->db->sql_query($sql); |
632 | |
633 | $this->last_run_migration = []; |
634 | unset($this->migration_state[$name]); |
635 | |
636 | $this->output_handler->write(array('MIGRATION_REVERT_SCHEMA_DONE', $name, $total_time), migrator_output_handler_interface::VERBOSITY_NORMAL); |
637 | } |
638 | else |
639 | { |
640 | $this->set_migration_state($name, $state); |
641 | |
642 | $this->output_handler->write(array('MIGRATION_REVERT_SCHEMA_IN_PROGRESS', $name, $elapsed_time), migrator_output_handler_interface::VERBOSITY_VERY_VERBOSE); |
643 | } |
644 | } |
645 | |
646 | return true; |
647 | } |
648 | |
649 | /** |
650 | * Process the data step of the migration |
651 | * |
652 | * @param array $steps The steps to run |
653 | * @param bool|string $state Current state of the migration |
654 | * @param bool $revert true to revert a data step |
655 | * @return bool|array migration state. True if completed, serialized array if not finished |
656 | * @psalm-return bool|array{result: mixed, step: int} |
657 | * @throws exception |
658 | */ |
659 | protected function process_data_step($steps, $state, $revert = false) |
660 | { |
661 | if (count($steps) === 0) |
662 | { |
663 | return true; |
664 | } |
665 | |
666 | $state = is_array($state) ? $state : false; |
667 | |
668 | // reverse order of steps if reverting |
669 | if ($revert === true) |
670 | { |
671 | $steps = array_reverse($steps); |
672 | } |
673 | |
674 | $step = $last_result = 0; |
675 | if ($state) |
676 | { |
677 | $step = $state['step']; |
678 | |
679 | // We send the result from last time to the callable function |
680 | $last_result = $state['result']; |
681 | } |
682 | |
683 | try |
684 | { |
685 | // Result will be null or true if everything completed correctly |
686 | // Stop after each update step, to let the updater control the script runtime |
687 | $result = $this->run_step($steps[$step], $last_result, $revert); |
688 | if (($result !== null && $result !== true) || $step + 1 < count($steps)) |
689 | { |
690 | return array( |
691 | 'result' => $result, |
692 | // Move on if the last call finished |
693 | 'step' => ($result !== null && $result !== true) ? $step : $step + 1, |
694 | ); |
695 | } |
696 | } |
697 | catch (exception $e) |
698 | { |
699 | // We should try rolling back here |
700 | foreach ($steps as $reverse_step_identifier => $reverse_step) |
701 | { |
702 | // If we've reached the current step we can break because we reversed everything that was run |
703 | if ($reverse_step_identifier == $step) |
704 | { |
705 | break; |
706 | } |
707 | |
708 | // Reverse the step that was run |
709 | $this->run_step($reverse_step, false, !$revert); |
710 | } |
711 | |
712 | throw $e; |
713 | } |
714 | |
715 | return true; |
716 | } |
717 | |
718 | /** |
719 | * Run a single step |
720 | * |
721 | * An exception should be thrown if an error occurs |
722 | * |
723 | * @param mixed $step Data step from migration |
724 | * @param mixed $last_result Result to pass to the callable (only for 'custom' method) |
725 | * @param bool $reverse False to install, True to attempt uninstallation by reversing the call |
726 | * @return mixed |
727 | * @throws exception |
728 | */ |
729 | protected function run_step($step, $last_result = 0, $reverse = false) |
730 | { |
731 | $callable_and_parameters = $this->get_callable_from_step($step, $last_result, $reverse); |
732 | |
733 | if ($callable_and_parameters === false) |
734 | { |
735 | return null; |
736 | } |
737 | |
738 | $callable = $callable_and_parameters[0]; |
739 | $parameters = $callable_and_parameters[1]; |
740 | |
741 | return call_user_func_array($callable, $parameters); |
742 | } |
743 | |
744 | /** |
745 | * Get a callable statement from a data step |
746 | * |
747 | * @param array $step Data step from migration |
748 | * @param mixed $last_result Result to pass to the callable (only for 'custom' method) |
749 | * @param bool $reverse False to install, True to attempt uninstallation by reversing the call |
750 | * @return array|false Array with parameters for call_user_func_array(), 0 is the callable, 1 is parameters; |
751 | * false if no callable can be created from data setp |
752 | * @throws exception |
753 | */ |
754 | protected function get_callable_from_step(array $step, $last_result = 0, $reverse = false) |
755 | { |
756 | $type = $step[0]; |
757 | $parameters = $step[1]; |
758 | |
759 | $parts = explode('.', $type); |
760 | |
761 | $class = $parts[0]; |
762 | $method = false; |
763 | |
764 | if (isset($parts[1])) |
765 | { |
766 | $method = $parts[1]; |
767 | } |
768 | |
769 | switch ($class) |
770 | { |
771 | case 'if': |
772 | if (!isset($parameters[0])) |
773 | { |
774 | throw new exception('MIGRATION_INVALID_DATA_MISSING_CONDITION', $step); |
775 | } |
776 | |
777 | if (!isset($parameters[1])) |
778 | { |
779 | throw new exception('MIGRATION_INVALID_DATA_MISSING_STEP', $step); |
780 | } |
781 | |
782 | if ($reverse) |
783 | { |
784 | // We might get unexpected results when trying |
785 | // to revert this, so just avoid it |
786 | return false; |
787 | } |
788 | |
789 | $condition = $parameters[0]; |
790 | |
791 | if (!$condition || (is_array($condition) && !$this->run_step($condition, $last_result, $reverse))) |
792 | { |
793 | return false; |
794 | } |
795 | |
796 | $step = $parameters[1]; |
797 | |
798 | return $this->get_callable_from_step($step); |
799 | break; |
800 | |
801 | case 'custom': |
802 | if (!is_callable($parameters[0])) |
803 | { |
804 | throw new exception('MIGRATION_INVALID_DATA_CUSTOM_NOT_CALLABLE', $step); |
805 | } |
806 | |
807 | if ($reverse) |
808 | { |
809 | return false; |
810 | } |
811 | else |
812 | { |
813 | return array( |
814 | $parameters[0], |
815 | isset($parameters[1]) ? array_merge($parameters[1], array($last_result)) : array($last_result), |
816 | ); |
817 | } |
818 | break; |
819 | |
820 | default: |
821 | if (!$method) |
822 | { |
823 | throw new exception('MIGRATION_INVALID_DATA_UNKNOWN_TYPE', $step); |
824 | } |
825 | |
826 | if (!isset($this->tools[$class])) |
827 | { |
828 | throw new exception('MIGRATION_INVALID_DATA_UNDEFINED_TOOL', $step); |
829 | } |
830 | |
831 | if (!method_exists(get_class($this->tools[$class]), $method)) |
832 | { |
833 | throw new exception('MIGRATION_INVALID_DATA_UNDEFINED_METHOD', $step); |
834 | } |
835 | |
836 | // Attempt to reverse operations |
837 | if ($reverse) |
838 | { |
839 | array_unshift($parameters, $method); |
840 | |
841 | return array( |
842 | array($this->tools[$class], 'reverse'), |
843 | $parameters, |
844 | ); |
845 | } |
846 | |
847 | return array( |
848 | array($this->tools[$class], $method), |
849 | $parameters, |
850 | ); |
851 | break; |
852 | } |
853 | } |
854 | |
855 | /** |
856 | * Insert/Update migration row into the database |
857 | * |
858 | * @param string $name Name of the migration |
859 | * @param array $state |
860 | * @return void |
861 | */ |
862 | protected function set_migration_state($name, $state) |
863 | { |
864 | $migration_row = $state; |
865 | $migration_row['migration_depends_on'] = serialize($state['migration_depends_on']); |
866 | $migration_row['migration_data_state'] = !empty($state['migration_data_state']) ? serialize($state['migration_data_state']) : ''; |
867 | |
868 | if (isset($this->migration_state[$name])) |
869 | { |
870 | $sql = 'UPDATE ' . $this->migrations_table . ' |
871 | SET ' . $this->db->sql_build_array('UPDATE', $migration_row) . " |
872 | WHERE migration_name = '" . $this->db->sql_escape($name) . "'"; |
873 | $this->db->sql_query($sql); |
874 | } |
875 | else |
876 | { |
877 | $migration_row['migration_name'] = $name; |
878 | $sql = 'INSERT INTO ' . $this->migrations_table . ' |
879 | ' . $this->db->sql_build_array('INSERT', $migration_row); |
880 | $this->db->sql_query($sql); |
881 | } |
882 | |
883 | $this->migration_state[$name] = $state; |
884 | |
885 | $this->last_run_migration['state'] = $state; |
886 | } |
887 | |
888 | /** |
889 | * Checks if a migration's dependencies can even theoretically be satisfied. |
890 | * |
891 | * @param string $name The class name of the migration |
892 | * @return bool|string False if fulfillable, string of missing migration name if unfulfillable |
893 | */ |
894 | public function unfulfillable($name) |
895 | { |
896 | $name = $this->get_valid_name($name); |
897 | |
898 | if (isset($this->migration_state[$name]) || isset($this->fulfillable_migrations[$name])) |
899 | { |
900 | return false; |
901 | } |
902 | |
903 | if (!class_exists($name)) |
904 | { |
905 | return $name; |
906 | } |
907 | |
908 | $migration = $this->get_migration($name); |
909 | $depends = $migration->depends_on(); |
910 | |
911 | foreach ($depends as $depend) |
912 | { |
913 | $depend = $this->get_valid_name($depend); |
914 | $unfulfillable = $this->unfulfillable($depend); |
915 | if ($unfulfillable !== false) |
916 | { |
917 | return $unfulfillable; |
918 | } |
919 | } |
920 | $this->fulfillable_migrations[$name] = true; |
921 | |
922 | return false; |
923 | } |
924 | |
925 | /** |
926 | * Checks whether all available, fulfillable migrations have been applied. |
927 | * |
928 | * @return bool Whether the migrations have been applied |
929 | */ |
930 | public function finished() |
931 | { |
932 | foreach ($this->migrations as $name) |
933 | { |
934 | if (!isset($this->migration_state[$name])) |
935 | { |
936 | // skip unfulfillable migrations, but fulfillables mean we |
937 | // are not finished yet |
938 | if ($this->unfulfillable($name) !== false) |
939 | { |
940 | continue; |
941 | } |
942 | |
943 | return false; |
944 | } |
945 | |
946 | $migration = $this->migration_state[$name]; |
947 | |
948 | if (!$migration['migration_schema_done'] || !$migration['migration_data_done']) |
949 | { |
950 | return false; |
951 | } |
952 | } |
953 | |
954 | return true; |
955 | } |
956 | |
957 | /** |
958 | * Gets a migration state (whether it is installed and to what extent) |
959 | * |
960 | * @param string $migration String migration name to check if it is installed |
961 | * @return bool|array False if the migration has not at all been installed, array |
962 | */ |
963 | public function migration_state($migration) |
964 | { |
965 | if (!isset($this->migration_state[$migration])) |
966 | { |
967 | return false; |
968 | } |
969 | |
970 | return $this->migration_state[$migration]; |
971 | } |
972 | |
973 | /** |
974 | * Helper to get a migration |
975 | * |
976 | * @param string $name Name of the migration |
977 | * @return \phpbb\db\migration\migration |
978 | */ |
979 | public function get_migration($name) |
980 | { |
981 | $migration = new $name($this->config, $this->db, $this->db_tools, $this->phpbb_root_path, $this->php_ext, $this->table_prefix, $this->tables); |
982 | |
983 | if ($migration instanceof ContainerAwareInterface) |
984 | { |
985 | $migration->setContainer($this->container); |
986 | } |
987 | |
988 | return $migration; |
989 | } |
990 | |
991 | /** |
992 | * This function adds all migrations sent to it to the migrations table |
993 | * |
994 | * THIS SHOULD NOT GENERALLY BE USED! THIS IS FOR THE PHPBB INSTALLER. |
995 | * THIS WILL THROW ERRORS IF MIGRATIONS ALREADY EXIST IN THE TABLE, DO NOT CALL MORE THAN ONCE! |
996 | * |
997 | * @param array $migrations Array of migrations (names) to add to the migrations table |
998 | * @return void |
999 | */ |
1000 | public function populate_migrations($migrations) |
1001 | { |
1002 | foreach ($migrations as $name) |
1003 | { |
1004 | if ($this->migration_state($name) === false) |
1005 | { |
1006 | $state = array( |
1007 | 'migration_depends_on' => $name::depends_on(), |
1008 | 'migration_schema_done' => true, |
1009 | 'migration_data_done' => true, |
1010 | 'migration_data_state' => '', |
1011 | 'migration_start_time' => time(), |
1012 | 'migration_end_time' => time(), |
1013 | ); |
1014 | $this->set_migration_state($name, $state); |
1015 | } |
1016 | } |
1017 | } |
1018 | |
1019 | /** |
1020 | * Creates the migrations table if it does not exist. |
1021 | * @return void |
1022 | */ |
1023 | public function create_migrations_table() |
1024 | { |
1025 | // Make sure migrations have been installed. |
1026 | if (!$this->db_tools->sql_table_exists($this->table_prefix . 'migrations')) |
1027 | { |
1028 | $this->db_tools->sql_create_table($this->table_prefix . 'migrations', array( |
1029 | 'COLUMNS' => array( |
1030 | 'migration_name' => array('VCHAR', ''), |
1031 | 'migration_depends_on' => array('TEXT', ''), |
1032 | 'migration_schema_done' => array('BOOL', 0), |
1033 | 'migration_data_done' => array('BOOL', 0), |
1034 | 'migration_data_state' => array('TEXT', ''), |
1035 | 'migration_start_time' => array('TIMESTAMP', 0), |
1036 | 'migration_end_time' => array('TIMESTAMP', 0), |
1037 | ), |
1038 | 'PRIMARY_KEY' => 'migration_name', |
1039 | )); |
1040 | } |
1041 | } |
1042 | |
1043 | /** |
1044 | * Check if a class is a migration. |
1045 | * |
1046 | * @param string $migration A migration class name |
1047 | * @return bool Return true if class is a migration, false otherwise |
1048 | */ |
1049 | public static function is_migration($migration) |
1050 | { |
1051 | if (class_exists($migration)) |
1052 | { |
1053 | // Migration classes should extend the abstract class |
1054 | // phpbb\db\migration\migration (which implements the |
1055 | // migration_interface) and be instantiable. |
1056 | $reflector = new \ReflectionClass($migration); |
1057 | if ($reflector->implementsInterface('\phpbb\db\migration\migration_interface') && $reflector->isInstantiable()) |
1058 | { |
1059 | return true; |
1060 | } |
1061 | } |
1062 | |
1063 | return false; |
1064 | } |
1065 | } |