Verzeichnisstruktur phpBB-3.1.0


Veröffentlicht
27.10.2014

So funktioniert es


Auf das letzte Element klicken. Dies geht jeweils ein Schritt zurück

Auf das Icon klicken, dies öffnet das Verzeichnis. Nochmal klicken schließt das Verzeichnis.
Auf den Verzeichnisnamen klicken, dies zeigt nur das Verzeichnis mit Inhalt an

(Beispiel Datei-Icons)

Auf das Icon klicken um den Quellcode anzuzeigen

migrator.php

Zuletzt modifiziert: 09.10.2024, 12:52 - Dateigröße: 19.91 KiB


001  <?php
002  /**
003  *
004  * This file is part of the phpBB Forum Software package.
005  *
006  * @copyright (c) phpBB Limited <https://www.phpbb.com>
007  * @license GNU General Public License, version 2 (GPL-2.0)
008  *
009  * For full copyright and license information, please see
010  * the docs/CREDITS.txt file.
011  *
012  */
013   
014  namespace phpbb\db;
015   
016  /**
017  * The migrator is responsible for applying new migrations in the correct order.
018  */
019  class migrator
020  {
021      /** @var \phpbb\config\config */
022      protected $config;
023   
024      /** @var \phpbb\db\driver\driver_interface */
025      protected $db;
026   
027      /** @var \phpbb\db\tools */
028      protected $db_tools;
029   
030      /** @var \phpbb\db\migration\helper */
031      protected $helper;
032   
033      /** @var string */
034      protected $table_prefix;
035   
036      /** @var string */
037      protected $phpbb_root_path;
038   
039      /** @var string */
040      protected $php_ext;
041   
042      /** @var string */
043      protected $migrations_table;
044   
045      /**
046      * State of all migrations
047      *
048      * (SELECT * FROM migrations table)
049      *
050      * @var array
051      */
052      protected $migration_state = array();
053   
054      /**
055      * Array of all migrations available to be run
056      *
057      * @var array
058      */
059      protected $migrations = array();
060   
061      /**
062      * 'name,' 'class,' and 'state' of the last migration run
063      *
064      * 'effectively_installed' set and set to true if the migration was effectively_installed
065      *
066      * @var array
067      */
068      public $last_run_migration = false;
069   
070      /**
071       * The output handler. A null handler is configured by default.
072       *
073       * @var migrator_output_handler
074       */
075      public $output_handler;
076   
077      /**
078      * Constructor of the database migrator
079      */
080      public function __construct(\phpbb\config\config $config, \phpbb\db\driver\driver_interface $db, \phpbb\db\tools $db_tools, $migrations_table, $phpbb_root_path, $php_ext, $table_prefix, $tools, \phpbb\db\migration\helper $helper)
081      {
082          $this->config = $config;
083          $this->db = $db;
084          $this->db_tools = $db_tools;
085          $this->helper = $helper;
086   
087          $this->migrations_table = $migrations_table;
088   
089          $this->phpbb_root_path = $phpbb_root_path;
090          $this->php_ext = $php_ext;
091   
092          $this->table_prefix = $table_prefix;
093   
094          $this->output_handler = new null_migrator_output_handler();
095   
096          foreach ($tools as $tool)
097          {
098              $this->tools[$tool->get_name()] = $tool;
099          }
100   
101          $this->tools['dbtools'] = $this->db_tools;
102   
103          $this->load_migration_state();
104      }
105   
106      /**
107       * Set the output handler.
108       *
109       * @param migrator_output_handler $handler The output handler
110       */
111      public function set_output_handler(migrator_output_handler_interface $handler)
112      {
113          $this->output_handler = $handler;
114      }
115   
116      /**
117      * Loads all migrations and their application state from the database.
118      *
119      * @return null
120      */
121      public function load_migration_state()
122      {
123          $this->migration_state = array();
124   
125          // prevent errors in case the table does not exist yet
126          $this->db->sql_return_on_error(true);
127   
128          $sql = "SELECT *
129              FROM " . $this->migrations_table;
130          $result = $this->db->sql_query($sql);
131   
132          if (!$this->db->get_sql_error_triggered())
133          {
134              while ($migration = $this->db->sql_fetchrow($result))
135              {
136                  $this->migration_state[$migration['migration_name']] = $migration;
137   
138                  $this->migration_state[$migration['migration_name']]['migration_depends_on'] = unserialize($migration['migration_depends_on']);
139              }
140          }
141   
142          $this->db->sql_freeresult($result);
143   
144          $this->db->sql_return_on_error(false);
145      }
146   
147      /**
148      * Sets the list of available migration class names to the given array.
149      *
150      * @param array $class_names An array of migration class names
151      * @return null
152      */
153      public function set_migrations($class_names)
154      {
155          $this->migrations = $class_names;
156      }
157   
158      /**
159      * Runs a single update step from the next migration to be applied.
160      *
161      * The update step can either be a schema or a (partial) data update. To
162      * check if update() needs to be called again use the finished() method.
163      *
164      * @return null
165      */
166      public function update()
167      {
168          foreach ($this->migrations as $name)
169          {
170              if (!isset($this->migration_state[$name]) ||
171                  !$this->migration_state[$name]['migration_schema_done'] ||
172                  !$this->migration_state[$name]['migration_data_done'])
173              {
174                  if (!$this->try_apply($name))
175                  {
176                      continue;
177                  }
178                  else
179                  {
180                      return;
181                  }
182              }
183              else
184              {
185                  $this->output_handler->write(array('MIGRATION_EFFECTIVELY_INSTALLED', $name), migrator_output_handler_interface::VERBOSITY_DEBUG);
186              }
187          }
188      }
189   
190      /**
191      * Attempts to apply a step of the given migration or one of its dependencies
192      *
193      * @param    string    $name The class name of the migration
194      * @return    bool    Whether any update step was successfully run
195      * @throws \phpbb\db\migration\exception
196      */
197      protected function try_apply($name)
198      {
199          if (!class_exists($name))
200          {
201              $this->output_handler->write(array('MIGRATION_NOT_VALID', $name), migrator_output_handler_interface::VERBOSITY_DEBUG);
202              return false;
203          }
204   
205          $migration = $this->get_migration($name);
206   
207          $state = (isset($this->migration_state[$name])) ?
208              $this->migration_state[$name] :
209              array(
210                  'migration_depends_on'    => $migration->depends_on(),
211                  'migration_schema_done' => false,
212                  'migration_data_done'    => false,
213                  'migration_data_state'    => '',
214                  'migration_start_time'    => 0,
215                  'migration_end_time'    => 0,
216              );
217   
218          if (!empty($state['migration_depends_on']))
219          {
220              $this->output_handler->write(array('MIGRATION_APPLY_DEPENDENCIES', $name), migrator_output_handler_interface::VERBOSITY_DEBUG);
221          }
222   
223          foreach ($state['migration_depends_on'] as $depend)
224          {
225              if ($this->unfulfillable($depend) !== false)
226              {
227                  throw new \phpbb\db\migration\exception('MIGRATION_NOT_FULFILLABLE', $name, $depend);
228              }
229   
230              if (!isset($this->migration_state[$depend]) ||
231                  !$this->migration_state[$depend]['migration_schema_done'] ||
232                  !$this->migration_state[$depend]['migration_data_done'])
233              {
234                  return $this->try_apply($depend);
235              }
236          }
237   
238          $this->last_run_migration = array(
239              'name'    => $name,
240              'class'    => $migration,
241              'state'    => $state,
242              'task'    => '',
243          );
244   
245          if (!isset($this->migration_state[$name]))
246          {
247              if ($state['migration_start_time'] == 0 && $migration->effectively_installed())
248              {
249                  $state = array(
250                      'migration_depends_on'    => $migration->depends_on(),
251                      'migration_schema_done' => true,
252                      'migration_data_done'    => true,
253                      'migration_data_state'    => '',
254                      'migration_start_time'    => 0,
255                      'migration_end_time'    => 0,
256                  );
257   
258                  $this->last_run_migration['effectively_installed'] = true;
259   
260                  $this->output_handler->write(array('MIGRATION_EFFECTIVELY_INSTALLED', $name), migrator_output_handler_interface::VERBOSITY_VERBOSE);
261              }
262              else
263              {
264                  $state['migration_start_time'] = time();
265              }
266          }
267   
268          $this->set_migration_state($name, $state);
269   
270          if (!$state['migration_schema_done'])
271          {
272              $this->output_handler->write(array('MIGRATION_SCHEMA_RUNNING', $name), migrator_output_handler_interface::VERBOSITY_VERBOSE);
273   
274              $this->last_run_migration['task'] = 'process_schema_step';
275              $elapsed_time = microtime(true);
276              $steps = $this->helper->get_schema_steps($migration->update_schema());
277              $result = $this->process_data_step($steps, $state['migration_data_state']);
278              $elapsed_time = microtime(true) - $elapsed_time;
279   
280              $state['migration_data_state'] = ($result === true) ? '' : $result;
281              $state['migration_schema_done'] = ($result === true);
282   
283              $this->output_handler->write(array('MIGRATION_SCHEMA_DONE', $name, $elapsed_time), migrator_output_handler_interface::VERBOSITY_NORMAL);
284          }
285          else if (!$state['migration_data_done'])
286          {
287              try
288              {
289                  $this->output_handler->write(array('MIGRATION_DATA_RUNNING', $name), migrator_output_handler_interface::VERBOSITY_VERBOSE);
290   
291                  $this->last_run_migration['task'] = 'process_data_step';
292   
293                  $elapsed_time = microtime(true);
294                  $result = $this->process_data_step($migration->update_data(), $state['migration_data_state']);
295                  $elapsed_time = microtime(true) - $elapsed_time;
296   
297                  $state['migration_data_state'] = ($result === true) ? '' : $result;
298                  $state['migration_data_done'] = ($result === true);
299                  $state['migration_end_time'] = ($result === true) ? time() : 0;
300   
301                  if ($state['migration_schema_done'])
302                  {
303                      $this->output_handler->write(array('MIGRATION_DATA_DONE', $name, $elapsed_time), migrator_output_handler_interface::VERBOSITY_NORMAL);
304                  }
305                  else
306                  {
307                      $this->output_handler->write(array('MIGRATION_DATA_IN_PROGRESS', $name, $elapsed_time), migrator_output_handler_interface::VERBOSITY_VERY_VERBOSE);
308                  }
309              }
310              catch (\phpbb\db\migration\exception $e)
311              {
312                  // Revert the schema changes
313                  $this->revert($name);
314   
315                  // Rethrow exception
316                  throw $e;
317              }
318          }
319   
320          $this->set_migration_state($name, $state);
321   
322          return true;
323      }
324   
325      /**
326      * Runs a single revert step from the last migration installed
327      *
328      * YOU MUST ADD/SET ALL MIGRATIONS THAT COULD BE DEPENDENT ON THE MIGRATION TO REVERT TO BEFORE CALLING THIS METHOD!
329      * The revert step can either be a schema or a (partial) data revert. To
330      * check if revert() needs to be called again use the migration_state() method.
331      *
332      * @param string $migration String migration name to revert (including any that depend on this migration)
333      * @return null
334      */
335      public function revert($migration)
336      {
337          if (!isset($this->migration_state[$migration]))
338          {
339              // Not installed
340              return;
341          }
342   
343          foreach ($this->migration_state as $name => $state)
344          {
345              if (!empty($state['migration_depends_on']) && in_array($migration, $state['migration_depends_on']))
346              {
347                  $this->revert($name);
348              }
349          }
350   
351          $this->try_revert($migration);
352      }
353   
354      /**
355      * Attempts to revert a step of the given migration or one of its dependencies
356      *
357      * @param    string    $name The class name of the migration
358      * @return    bool    Whether any update step was successfully run
359      */
360      protected function try_revert($name)
361      {
362          if (!class_exists($name))
363          {
364              return false;
365          }
366   
367          $migration = $this->get_migration($name);
368   
369          $state = $this->migration_state[$name];
370   
371          $this->last_run_migration = array(
372              'name'    => $name,
373              'class'    => $migration,
374              'task'    => '',
375          );
376   
377          if ($state['migration_data_done'])
378          {
379              if ($state['migration_data_state'] !== 'revert_data')
380              {
381                  $result = $this->process_data_step($migration->update_data(), $state['migration_data_state'], true);
382   
383                  $state['migration_data_state'] = ($result === true) ? 'revert_data' : $result;
384              }
385              else
386              {
387                  $result = $this->process_data_step($migration->revert_data(), '', false);
388   
389                  $state['migration_data_state'] = ($result === true) ? '' : $result;
390                  $state['migration_data_done'] = ($result === true) ? false : true;
391              }
392   
393              $this->set_migration_state($name, $state);
394          }
395          else if ($state['migration_schema_done'])
396          {
397              $steps = $this->helper->get_schema_steps($migration->revert_schema());
398              $result = $this->process_data_step($steps, $state['migration_data_state']);
399   
400              $state['migration_data_state'] = ($result === true) ? '' : $result;
401              $state['migration_schema_done'] = ($result === true) ? false : true;
402   
403              if (!$state['migration_schema_done'])
404              {
405                  $sql = 'DELETE FROM ' . $this->migrations_table . "
406                      WHERE migration_name = '" . $this->db->sql_escape($name) . "'";
407                  $this->db->sql_query($sql);
408   
409                  unset($this->migration_state[$name]);
410              }
411          }
412   
413          return true;
414      }
415   
416      /**
417      * Process the data step of the migration
418      *
419      * @param array $steps The steps to run
420      * @param bool|string $state Current state of the migration
421      * @param bool $revert true to revert a data step
422      * @return bool|string migration state. True if completed, serialized array if not finished
423      * @throws \phpbb\db\migration\exception
424      */
425      protected function process_data_step($steps, $state, $revert = false)
426      {
427          $state = ($state) ? unserialize($state) : false;
428   
429          // reverse order of steps if reverting
430          if ($revert === true)
431          {
432              $steps = array_reverse($steps);
433          }
434   
435          foreach ($steps as $step_identifier => $step)
436          {
437              $last_result = 0;
438              if ($state)
439              {
440                  // Continue until we reach the step that matches the last step called
441                  if ($state['step'] != $step_identifier)
442                  {
443                      continue;
444                  }
445   
446                  // We send the result from last time to the callable function
447                  $last_result = $state['result'];
448   
449                  // Set state to false since we reached the point we were at
450                  $state = false;
451              }
452   
453              try
454              {
455                  // Result will be null or true if everything completed correctly
456                  $result = $this->run_step($step, $last_result, $revert);
457                  if ($result !== null && $result !== true)
458                  {
459                      return serialize(array(
460                          'result'    => $result,
461                          'step'        => $step_identifier,
462                      ));
463                  }
464              }
465              catch (\phpbb\db\migration\exception $e)
466              {
467                  // We should try rolling back here
468                  foreach ($steps as $reverse_step_identifier => $reverse_step)
469                  {
470                      // If we've reached the current step we can break because we reversed everything that was run
471                      if ($reverse_step_identifier == $step_identifier)
472                      {
473                          break;
474                      }
475   
476                      // Reverse the step that was run
477                      $result = $this->run_step($reverse_step, false, !$revert);
478                  }
479   
480                  // rethrow the exception
481                  throw $e;
482              }
483          }
484   
485          return true;
486      }
487   
488      /**
489      * Run a single step
490      *
491      * An exception should be thrown if an error occurs
492      *
493      * @param mixed $step Data step from migration
494      * @param mixed $last_result Result to pass to the callable (only for 'custom' method)
495      * @param bool $reverse False to install, True to attempt uninstallation by reversing the call
496      * @return null
497      */
498      protected function run_step($step, $last_result = 0, $reverse = false)
499      {
500          $callable_and_parameters = $this->get_callable_from_step($step, $last_result, $reverse);
501   
502          if ($callable_and_parameters === false)
503          {
504              return;
505          }
506   
507          $callable = $callable_and_parameters[0];
508          $parameters = $callable_and_parameters[1];
509   
510          return call_user_func_array($callable, $parameters);
511      }
512   
513      /**
514      * Get a callable statement from a data step
515      *
516      * @param array $step Data step from migration
517      * @param mixed $last_result Result to pass to the callable (only for 'custom' method)
518      * @param bool $reverse False to install, True to attempt uninstallation by reversing the call
519      * @return array Array with parameters for call_user_func_array(), 0 is the callable, 1 is parameters
520      * @throws \phpbb\db\migration\exception
521      */
522      protected function get_callable_from_step(array $step, $last_result = 0, $reverse = false)
523      {
524          $type = $step[0];
525          $parameters = $step[1];
526   
527          $parts = explode('.', $type);
528   
529          $class = $parts[0];
530          $method = false;
531   
532          if (isset($parts[1]))
533          {
534              $method = $parts[1];
535          }
536   
537          switch ($class)
538          {
539              case 'if':
540                  if (!isset($parameters[0]))
541                  {
542                      throw new \phpbb\db\migration\exception('MIGRATION_INVALID_DATA_MISSING_CONDITION', $step);
543                  }
544   
545                  if (!isset($parameters[1]))
546                  {
547                      throw new \phpbb\db\migration\exception('MIGRATION_INVALID_DATA_MISSING_STEP', $step);
548                  }
549   
550                  $condition = $parameters[0];
551   
552                  if (!$condition)
553                  {
554                      return false;
555                  }
556   
557                  $step = $parameters[1];
558   
559                  return $this->get_callable_from_step($step);
560              break;
561   
562              case 'custom':
563                  if (!is_callable($parameters[0]))
564                  {
565                      throw new \phpbb\db\migration\exception('MIGRATION_INVALID_DATA_CUSTOM_NOT_CALLABLE', $step);
566                  }
567   
568                  if ($reverse)
569                  {
570                      return false;
571                  }
572                  else
573                  {
574                      return array(
575                          $parameters[0],
576                          array($last_result),
577                      );
578                  }
579              break;
580   
581              default:
582                  if (!$method)
583                  {
584                      throw new \phpbb\db\migration\exception('MIGRATION_INVALID_DATA_UNKNOWN_TYPE', $step);
585                  }
586   
587                  if (!isset($this->tools[$class]))
588                  {
589                      throw new \phpbb\db\migration\exception('MIGRATION_INVALID_DATA_UNDEFINED_TOOL', $step);
590                  }
591   
592                  if (!method_exists(get_class($this->tools[$class]), $method))
593                  {
594                      throw new \phpbb\db\migration\exception('MIGRATION_INVALID_DATA_UNDEFINED_METHOD', $step);
595                  }
596   
597                  // Attempt to reverse operations
598                  if ($reverse)
599                  {
600                      array_unshift($parameters, $method);
601   
602                      return array(
603                          array($this->tools[$class], 'reverse'),
604                          $parameters,
605                      );
606                  }
607   
608                  return array(
609                      array($this->tools[$class], $method),
610                      $parameters,
611                  );
612              break;
613          }
614      }
615   
616      /**
617      * Insert/Update migration row into the database
618      *
619      * @param string $name Name of the migration
620      * @param array $state
621      * @return null
622      */
623      protected function set_migration_state($name, $state)
624      {
625          $migration_row = $state;
626          $migration_row['migration_depends_on'] = serialize($state['migration_depends_on']);
627   
628          if (isset($this->migration_state[$name]))
629          {
630              $sql = 'UPDATE ' . $this->migrations_table . '
631                  SET ' . $this->db->sql_build_array('UPDATE', $migration_row) . "
632                  WHERE migration_name = '" . $this->db->sql_escape($name) . "'";
633              $this->db->sql_query($sql);
634          }
635          else
636          {
637              $migration_row['migration_name'] = $name;
638              $sql = 'INSERT INTO ' . $this->migrations_table . '
639                  ' . $this->db->sql_build_array('INSERT', $migration_row);
640              $this->db->sql_query($sql);
641          }
642   
643          $this->migration_state[$name] = $state;
644   
645          $this->last_run_migration['state'] = $state;
646      }
647   
648      /**
649      * Checks if a migration's dependencies can even theoretically be satisfied.
650      *
651      * @param string    $name The class name of the migration
652      * @return bool|string False if fulfillable, string of missing migration name if unfulfillable
653      */
654      public function unfulfillable($name)
655      {
656          if (isset($this->migration_state[$name]))
657          {
658              return false;
659          }
660   
661          if (!class_exists($name))
662          {
663              return $name;
664          }
665   
666          $migration = $this->get_migration($name);
667          $depends = $migration->depends_on();
668   
669          foreach ($depends as $depend)
670          {
671              $unfulfillable = $this->unfulfillable($depend);
672              if ($unfulfillable !== false)
673              {
674                  return $unfulfillable;
675              }
676          }
677   
678          return false;
679      }
680   
681      /**
682      * Checks whether all available, fulfillable migrations have been applied.
683      *
684      * @return bool Whether the migrations have been applied
685      */
686      public function finished()
687      {
688          foreach ($this->migrations as $name)
689          {
690              if (!isset($this->migration_state[$name]))
691              {
692                  // skip unfulfillable migrations, but fulfillables mean we
693                  // are not finished yet
694                  if ($this->unfulfillable($name) !== false)
695                  {
696                      continue;
697                  }
698   
699                  return false;
700              }
701   
702              $migration = $this->migration_state[$name];
703   
704              if (!$migration['migration_schema_done'] || !$migration['migration_data_done'])
705              {
706                  return false;
707              }
708          }
709   
710          return true;
711      }
712   
713      /**
714      * Gets a migration state (whether it is installed and to what extent)
715      *
716      * @param string $migration String migration name to check if it is installed
717      * @return bool|array False if the migration has not at all been installed, array
718      */
719      public function migration_state($migration)
720      {
721          if (!isset($this->migration_state[$migration]))
722          {
723              return false;
724          }
725   
726          return $this->migration_state[$migration];
727      }
728   
729      /**
730      * Helper to get a migration
731      *
732      * @param string $name Name of the migration
733      * @return \phpbb\db\migration\migration
734      */
735      protected function get_migration($name)
736      {
737          return new $name($this->config, $this->db, $this->db_tools, $this->phpbb_root_path, $this->php_ext, $this->table_prefix);
738      }
739   
740      /**
741      * This function adds all migrations sent to it to the migrations table
742      *
743      * THIS SHOULD NOT GENERALLY BE USED! THIS IS FOR THE PHPBB INSTALLER.
744      * THIS WILL THROW ERRORS IF MIGRATIONS ALREADY EXIST IN THE TABLE, DO NOT CALL MORE THAN ONCE!
745      *
746      * @param array $migrations Array of migrations (names) to add to the migrations table
747      * @return null
748      */
749      public function populate_migrations($migrations)
750      {
751          foreach ($migrations as $name)
752          {
753              if ($this->migration_state($name) === false)
754              {
755                  $state = array(
756                      'migration_depends_on'    => $name::depends_on(),
757                      'migration_schema_done' => true,
758                      'migration_data_done'    => true,
759                      'migration_data_state'    => '',
760                      'migration_start_time'    => time(),
761                      'migration_end_time'    => time(),
762                  );
763                  $this->set_migration_state($name, $state);
764              }
765          }
766      }
767   
768      /**
769      * Creates the migrations table if it does not exist.
770      * @return null
771      */
772      public function create_migrations_table()
773      {
774          // Make sure migrations have been installed.
775          if (!$this->db_tools->sql_table_exists($this->table_prefix . 'migrations'))
776          {
777              $this->db_tools->sql_create_table($this->table_prefix . 'migrations', array(
778                  'COLUMNS'        => array(
779                      'migration_name'            => array('VCHAR', ''),
780                      'migration_depends_on'        => array('TEXT', ''),
781                      'migration_schema_done'        => array('BOOL', 0),
782                      'migration_data_done'        => array('BOOL', 0),
783                      'migration_data_state'        => array('TEXT', ''),
784                      'migration_start_time'        => array('TIMESTAMP', 0),
785                      'migration_end_time'        => array('TIMESTAMP', 0),
786                  ),
787                  'PRIMARY_KEY'    => 'migration_name',
788              ));
789          }
790      }
791  }
792