migrate_ui.wizard.inc 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663
  1. <?php
  2. /**
  3. * @file
  4. * Migration wizard framework.
  5. */
  6. /**
  7. * The primary formbuilder function for the wizard form.
  8. *
  9. * This form has two defined submit handlers to process the different steps:
  10. * - Previous: handles the way to get back one step in the wizard.
  11. * - Next: handles each step form submission,
  12. *
  13. * The third handler, the finish button handler, is the default form _submit
  14. * handler used to process the information.
  15. *
  16. * @param string $class_name
  17. * Name of the MigrateUIWizard clsas for this wizard.
  18. */
  19. function migrate_ui_wizard($form, &$form_state, $class_name = '') {
  20. // Rather than track state in $form_state, we simply keep our wizard
  21. // instance there, and it encapsulates all the state. We just need
  22. // to create the instance the first time in, and it will be serialized
  23. // between steps.
  24. /** @var MigrateUIWizard $wizard */
  25. if (empty($form_state['wizard'])) {
  26. $wizard = new $class_name();
  27. // Add any extenders.
  28. $module_apis = migrate_get_module_apis();
  29. // Need a second pass at this to add wizard extenders.
  30. foreach ($module_apis as $module => $info) {
  31. // Add any extenders.
  32. // @todo: consider allowing extender classes to declare dependencies on
  33. // other extender classes, to ensure they work in the correct order?
  34. if (isset($info['wizard extenders'])) {
  35. foreach ($info['wizard extenders'] as $wizard_class => $extender_classes) {
  36. // Note that $class_name is in lower case, so we can't just use isset()
  37. // to find our wizard.
  38. if (strtolower($wizard_class) == $class_name) {
  39. foreach ($extender_classes as $extender_class) {
  40. $wizard->addExtender($extender_class);
  41. }
  42. }
  43. }
  44. }
  45. }
  46. $form_state['wizard'] = $wizard;
  47. }
  48. else {
  49. $wizard = $form_state['wizard'];
  50. }
  51. // Fetch the form for the wizard's current step.
  52. $form = $wizard->form($form_state);
  53. return $form;
  54. }
  55. /**
  56. * Submit handler for the "previous" button. Moves the wizard back to the
  57. * previous step, and retrieves the values that were submitted on that step.
  58. *
  59. * @todo: Can we remove steps that were dynamically added?
  60. */
  61. function migrate_ui_wizard_previous_submit($form, &$form_state) {
  62. /** @var MigrateUIWizard $wizard */
  63. $wizard = $form_state['wizard'];
  64. $wizard->gotoPreviousStep($form_state);
  65. }
  66. /**
  67. * Validate handler for the 'next' button. Dispatches to the wizard's current
  68. * step for validation.
  69. */
  70. function migrate_ui_wizard_next_validate($form, &$form_state) {
  71. /** @var MigrateUIWizard $wizard */
  72. $wizard = $form_state['wizard'];
  73. $wizard->formValidate($form_state);
  74. }
  75. /**
  76. * Submit handler for the 'next' button. Saves the form values for the step
  77. * we're leaving, so Previous can pick them up, and moves the wizard to the
  78. * next step.
  79. */
  80. function migrate_ui_wizard_next_submit($form, &$form_state) {
  81. /** @var MigrateUIWizard $wizard */
  82. $wizard = $form_state['wizard'];
  83. $wizard->gotoNextStep($form_state);
  84. }
  85. /**
  86. * Submit handler for the Save settings button. Register the migrations that were
  87. * (implicitly) defined along the way and redirect to the Migrate dashboard.
  88. */
  89. function migrate_ui_wizard_submit($form, &$form_state) {
  90. /** @var MigrateUIWizard $wizard */
  91. $wizard = $form_state['wizard'];
  92. $wizard->formSaveSettings();
  93. $form_state['redirect'] = 'admin/content/migrate/groups/' .
  94. $wizard->getGroupName();
  95. }
  96. /**
  97. * Submit handler for the "Save settings and import" button. Register the
  98. * migrations that were (implicitly) defined along the way, run the import, and
  99. * redirect to the Migrate dashboard.
  100. */
  101. function migrate_ui_wizard_migrate_submit($form, &$form_state) {
  102. /** @var MigrateUIWizard $wizard */
  103. $wizard = $form_state['wizard'];
  104. $wizard->formSaveSettings();
  105. $wizard->formPerformImport();
  106. $form_state['redirect'] = 'admin/content/migrate/groups/' .
  107. $wizard->getGroupName();
  108. }
  109. /**
  110. * The base class for migration wizards. Extend this class to implement a
  111. * wizard UI for importing into Drupal from a given source format (Drupal,
  112. * WordPress, etc.).
  113. */
  114. abstract class MigrateUIWizard {
  115. /**
  116. * We maintain a doubly-linked list of wizard steps, both to support
  117. * previous/next, and to easily insert steps dynamically.
  118. *
  119. * The first step of the wizard, which has no predecessor. Will generally be
  120. * an overview/introductory page.
  121. *
  122. * @var MigrateUIStep
  123. */
  124. protected $firstStep;
  125. /**
  126. * The last step of the wizard, which has no successor. Will generally be a
  127. * review page.
  128. *
  129. * @var MigrateUIStep
  130. */
  131. protected $lastStep;
  132. /**
  133. * Get the list of steps currently defined.
  134. *
  135. * @return
  136. * An array of MigrateUIStep objects, in the order defined, keyed by the step
  137. * name.
  138. */
  139. protected function getSteps() {
  140. $steps = array();
  141. $steps[$this->firstStep->getName()] = $this->firstStep;
  142. $next_step = $this->firstStep->nextStep;
  143. while (!is_null($next_step)) {
  144. $steps[$next_step->getName()] = $next_step;
  145. $next_step = $next_step->nextStep;
  146. }
  147. return $steps;
  148. }
  149. /**
  150. * The current step of the wizard (the one being shown in the UI, and the one
  151. * whose button is being clicked on).
  152. *
  153. * @var MigrateUIStep
  154. */
  155. protected $currentStep;
  156. /**
  157. * The step number, used in the page title.
  158. *
  159. * @var int
  160. */
  161. protected $stepNumber = 1;
  162. /**
  163. * The group name to assign to any Migration instances created.
  164. *
  165. * @var string
  166. */
  167. protected $groupName = 'default';
  168. public function getGroupName() {
  169. return $this->groupName;
  170. }
  171. /**
  172. * The user-visible title of the group.
  173. *
  174. * @var string
  175. */
  176. protected $groupTitle = 'default';
  177. /**
  178. * Any arguments that apply to all migrations in the group.
  179. *
  180. * @var array
  181. */
  182. protected $groupArguments = array();
  183. /**
  184. * Array of Migration argument arrays, keyed by machine name. On Finish, used
  185. * to register Migrations.
  186. *
  187. * @var array
  188. */
  189. protected $migrations = array();
  190. /**
  191. * Array of MigrateUIWizardExtender objects that extend this wizard.
  192. *
  193. * @var array
  194. */
  195. protected $extenders = array();
  196. public function getExtender($extender_class) {
  197. if (isset($this->extenders[$extender_class])) {
  198. return $this->extenders[$extender_class];
  199. }
  200. else {
  201. return NULL;
  202. }
  203. }
  204. /**
  205. * Returns the translatable name representing the source of the data (e.g.,
  206. * "Drupal", "WordPress", etc.).
  207. *
  208. * @return string
  209. */
  210. abstract public function getSourceName();
  211. public function __construct() {}
  212. /**
  213. * Add a wizard extender.
  214. *
  215. * This initializes the new extender and adds it to our internal list.
  216. *
  217. * @param $extender_class
  218. * The name of an extender class.
  219. */
  220. public function addExtender($extender_class) {
  221. $steps = $this->getSteps();
  222. $extender = new $extender_class($this, $steps);
  223. $this->extenders[$extender_class] = $extender;
  224. }
  225. /**
  226. * Add a step to the wizard, using a step name and method.
  227. *
  228. * @param string $name
  229. * Translatable name for the step, to be used in the page title.
  230. * @param callable $form_method
  231. * Callable returning the form array for the step. This can be either the
  232. * name of a MigrateUIWizard method, or a callable array specifying a method
  233. * on a wizard extender. The validation method is formed from the method's
  234. * name with the suffix 'Validate' added.
  235. * @param MigrateUIStep $after
  236. * Optional step after which to insert the new step. If omitted, add it at
  237. * the end.
  238. * @param mixed $context
  239. * Optional data to be used by this step's form.
  240. *
  241. * @return MigrateUIStep
  242. */
  243. public function addStep($name, $form_method, MigrateUIStep $after = NULL, $context = NULL) {
  244. if (!is_array($form_method)) {
  245. $form_method = array($this, $form_method);
  246. }
  247. $new_step = new MigrateUIStep($name, $form_method, $context);
  248. // There were no steps, so this is the only one.
  249. if (is_null($this->firstStep)) {
  250. $this->firstStep = $this->lastStep = $this->currentStep = $new_step;
  251. }
  252. else {
  253. // If no insertion point is specified, append to the end.
  254. if (is_null($after)) {
  255. $after = $this->lastStep;
  256. }
  257. // Do the insert, rewriting the links appropriately.
  258. $new_step->nextStep = $after->nextStep;
  259. if (is_null($new_step->nextStep)) {
  260. $this->lastStep = $new_step;
  261. }
  262. else {
  263. $new_step->nextStep->previousStep = $new_step;
  264. }
  265. $new_step->previousStep = $after;
  266. $after->nextStep = $new_step;
  267. }
  268. return $new_step;
  269. }
  270. /**
  271. * Remove the named step from the wizard.
  272. *
  273. * @param $name
  274. */
  275. protected function removeStep($name) {
  276. for ($current_step = $this->firstStep; !is_null($current_step); $current_step = $current_step->nextStep) {
  277. if ($current_step->getName() == $name) {
  278. if (is_null($current_step->previousStep)) {
  279. $this->firstStep = $current_step->nextStep;
  280. }
  281. else {
  282. $current_step->previousStep->nextStep = $current_step->nextStep;
  283. }
  284. if (is_null($current_step->nextStep)) {
  285. $this->lastStep = $current_step->previousStep;
  286. }
  287. else {
  288. $current_step->nextStep->previousStep = $current_step->previousStep;
  289. }
  290. break;
  291. }
  292. }
  293. }
  294. /**
  295. * Move the wizard to the next step in line (if any), first squirreling away
  296. * the current step's form values.
  297. */
  298. public function gotoNextStep(&$form_state) {
  299. if ($this->currentStep && $this->currentStep->nextStep) {
  300. $this->currentStep->setFormValues($form_state['values']);
  301. $form_state['rebuild'] = TRUE;
  302. $this->currentStep = $this->currentStep->nextStep;
  303. $this->stepNumber++;
  304. // Ensure a page reload remains on the current step.
  305. $current_step_form_values = $this->currentStep->getFormValues();
  306. if (!empty($current_step_form_values)) {
  307. $form_state['values'] = $current_step_form_values;
  308. }
  309. else {
  310. $form_state['values'] = array();
  311. }
  312. }
  313. }
  314. /**
  315. * Move the wizard to the previous step in line (if any), restoring its
  316. * form values.
  317. */
  318. public function gotoPreviousStep(&$form_state) {
  319. if ($this->currentStep && $this->currentStep->previousStep) {
  320. $this->currentStep = $this->currentStep->previousStep;
  321. $this->stepNumber--;
  322. $form_state['values'] = $this->currentStep->getFormValues();
  323. $form_state['rebuild'] = TRUE;
  324. }
  325. }
  326. /**
  327. * Build the form for the current step.
  328. *
  329. * @return array
  330. */
  331. public function form(&$form_state) {
  332. drupal_set_title(t('Import from @source_title',
  333. array('@source_title' => $this->getSourceName())));
  334. $form_method = $this->currentStep->getFormMethod();
  335. $form['title'] = array(
  336. '#prefix' => '<h2>',
  337. '#markup' => t('Step @step: @step_name',
  338. array(
  339. '@step' => $this->stepNumber,
  340. '@step_name' => $this->currentStep->getName())),
  341. '#suffix' => '</h2>',
  342. );
  343. $form += call_user_func($form_method, $form_state);
  344. $form['actions'] = array('#type' => 'actions');
  345. // Show the 'previous' button if appropriate. Note that #submit is set to
  346. // a special submit handler, and that we use #limit_validation_errors to
  347. // skip all complaints about validation when using the back button. The
  348. // values entered will be discarded, but they will not be validated, which
  349. // would be annoying in a "back" button.
  350. if ($this->currentStep != $this->firstStep) {
  351. $form['actions']['prev'] = array(
  352. '#type' => 'submit',
  353. '#value' => t('Previous'),
  354. '#name' => 'prev',
  355. '#submit' => array('migrate_ui_wizard_previous_submit'),
  356. '#limit_validation_errors' => array(),
  357. );
  358. }
  359. // Show the Next button only if there are more steps defined.
  360. if ($this->currentStep == $this->lastStep) {
  361. $form['actions']['finish'] = array(
  362. '#type' => 'submit',
  363. '#value' => t('Save import settings'),
  364. );
  365. $form['actions']['migrate'] = array(
  366. '#type' => 'submit',
  367. '#value' => t('Save import settings and run import'),
  368. '#submit' => array('migrate_ui_wizard_migrate_submit'),
  369. );
  370. }
  371. else {
  372. $form['actions']['next'] = array(
  373. '#type' => 'submit',
  374. '#value' => t('Next'),
  375. '#name' => 'next',
  376. '#submit' => array('migrate_ui_wizard_next_submit'),
  377. '#validate' => array('migrate_ui_wizard_next_validate'),
  378. );
  379. }
  380. return $form;
  381. }
  382. /**
  383. * Call the validation function for the current form (which has the same
  384. * name of the form function with 'Validate' appended).
  385. *
  386. * @param array $form_state
  387. */
  388. public function formValidate(&$form_state) {
  389. $validate_method = $this->currentStep->getFormMethod();
  390. // This is an array for a method, or a function name.
  391. if (is_array($validate_method)) {
  392. $validate_method[1] .= 'Validate';
  393. }
  394. else {
  395. $validate_method .= 'Validate';
  396. }
  397. if (is_callable($validate_method)) {
  398. call_user_func($validate_method, $form_state);
  399. }
  400. }
  401. /**
  402. * Take the information we've accumulated throughout the wizard, and create
  403. * the Migrations to perform the import.
  404. */
  405. public function formSaveSettings() {
  406. MigrateGroup::register($this->groupName, $this->groupTitle, $this->groupArguments);
  407. $info['arguments']['group_name'] = $this->groupName;
  408. foreach ($this->migrations as $machine_name => $info) {
  409. // Call the right registerMigration implementation. Note that this means
  410. // that classes that override registerMigration() must handle registration
  411. // themselves, they cannot leave it to us and expect their extension to be
  412. // called.
  413. if (is_subclass_of($info['class_name'], 'Migration')) {
  414. Migration::registerMigration($info['class_name'], $machine_name,
  415. $info['arguments']);
  416. }
  417. else {
  418. MigrationBase::registerMigration($info['class_name'], $machine_name,
  419. $info['arguments']);
  420. }
  421. };
  422. menu_rebuild();
  423. }
  424. /**
  425. * Run the import process for the migration group we've defined.
  426. */
  427. public function formPerformImport() {
  428. $migrations = migrate_migrations();
  429. $operations = array();
  430. /** @var Migration $migration */
  431. foreach ($migrations as $migration) {
  432. $group_name = $migration->getGroup()->getName();
  433. if ($group_name == $this->groupName) {
  434. $operations[] = array('migrate_ui_batch', array('import', $migration->getMachineName(), NULL, 0));
  435. }
  436. }
  437. if (count($operations) > 0) {
  438. $batch = array(
  439. 'operations' => $operations,
  440. 'title' => t('Import processing'),
  441. 'file' => drupal_get_path('module', 'migrate_ui') . '/migrate_ui.pages.inc',
  442. 'init_message' => t('Starting import process'),
  443. 'progress_message' => t(''),
  444. 'error_message' => t('An error occurred. Some or all of the import processing has failed.'),
  445. 'finished' => 'migrate_ui_batch_finish',
  446. );
  447. batch_set($batch);
  448. }
  449. }
  450. /**
  451. * Record all the information necessary to register a migration when this is
  452. * all over.
  453. *
  454. * @param string $machine_name
  455. * Machine name for the migration class.
  456. * @param string $class_name
  457. * Name of the Migration class to instantiate.
  458. * @param array $arguments
  459. * Further information configuring the migration.
  460. */
  461. public function addMigration($machine_name, $class_name, $arguments) {
  462. // Give extenders an opportunity to modify or reject this migration.
  463. foreach ($this->extenders as $extender) {
  464. if (!$extender->addMigrationAlter($machine_name, $class_name, $arguments, $this)) {
  465. return FALSE;
  466. }
  467. }
  468. $machine_name = $this->groupName . $machine_name;
  469. if (isset($arguments['dependencies'])) {
  470. foreach ($arguments['dependencies'] as $index => $dependency) {
  471. $arguments['dependencies'][$index] = $this->groupName . $dependency;
  472. }
  473. }
  474. if (isset($arguments['soft_dependencies'])) {
  475. foreach ($arguments['soft_dependencies'] as $index => $dependency) {
  476. $arguments['soft_dependencies'][$index] = $this->groupName . $dependency;
  477. }
  478. }
  479. $arguments += array(
  480. 'group_name' => $this->groupName,
  481. 'machine_name' => $machine_name,
  482. );
  483. $this->migrations[$machine_name] = array(
  484. 'class_name' => $class_name,
  485. 'arguments' => $arguments,
  486. );
  487. return TRUE;
  488. }
  489. }
  490. /**
  491. * Class representing one step of a wizard.
  492. */
  493. class MigrateUIStep {
  494. /**
  495. * A translatable string briefly describing this step, to be used in the page
  496. * title for the step form.
  497. *
  498. * @var string
  499. */
  500. protected $name;
  501. public function getName() {
  502. return $this->name;
  503. }
  504. /**
  505. * Callable that returns the form array for this step.
  506. *
  507. * @var string
  508. */
  509. protected $formMethod;
  510. public function getFormMethod() {
  511. return $this->formMethod;
  512. }
  513. /**
  514. * The form values ($form_state['values']) submitted for this step, saved in
  515. * case we need to restore them on a Previous action.
  516. *
  517. * @var array
  518. */
  519. protected $formValues;
  520. public function getFormValues() {
  521. return $this->formValues;
  522. }
  523. public function setFormValues($form_values) {
  524. $this->formValues = $form_values;
  525. }
  526. /**
  527. * Any contextual data needed by the form for this step. For example, a
  528. * field mapping form would need to know the source and destination content
  529. * types so it can determine what fields to expose.
  530. *
  531. * @var mixed
  532. */
  533. protected $context;
  534. public function getContext() {
  535. return $this->context;
  536. }
  537. /**
  538. * The step object is a node in a doubly-linked list - it links to its
  539. * predecessor and successor steps.
  540. *
  541. * @var MigrateUIStep
  542. */
  543. public $nextStep;
  544. /**
  545. * @var MigrateUIStep
  546. */
  547. public $previousStep;
  548. /**
  549. * Class constructor.
  550. *
  551. * @param $name
  552. * The machine name of the wizard step.
  553. * @param $form_method
  554. * A callable for the form array for this step. The validation method is
  555. * formed from the method name with the suffix 'Validate' added, regardless
  556. * of which object it is on.
  557. * @param $context = NULL
  558. * Contextual data needed by the form for this step.
  559. */
  560. public function __construct($name, $form_method, $context = NULL) {
  561. $this->name = $name;
  562. $this->formMethod = $form_method;
  563. $this->context = $context;
  564. }
  565. }
  566. /**
  567. *
  568. */
  569. abstract class MigrateUIWizardExtender {
  570. /**
  571. * Reference to the wizard object that this extender applies to.
  572. */
  573. protected $wizard;
  574. /**
  575. * Class constructor.
  576. *
  577. * Wizard extenders should override this to add their steps to the wizard.
  578. */
  579. public function __construct(MigrateUIWizard $wizard, array $wizard_steps) {
  580. $this->wizard = $wizard;
  581. }
  582. /**
  583. * Alter the arguments to a migration before it is registered, or potentially
  584. * reject it.
  585. *
  586. * @param string $machine_name
  587. * Machine name for the migration class.
  588. * @param string $class_name
  589. * Name of the Migration class to instantiate.
  590. * @param array $arguments
  591. * Further information configuring the migration.
  592. * @param MigrateUIWizard $wizard
  593. * The wizard class performing the registration.
  594. *
  595. * @return bool
  596. * Return FALSE to prevent registration of this migration.
  597. */
  598. public function addMigrationAlter($machine_name, $class_name, &$arguments, $wizard) {
  599. return TRUE;
  600. }
  601. }