migrate.module 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558
  1. <?php
  2. /**
  3. * @file
  4. * API and drush commands to support migration of data from external sources
  5. * into a Drupal installation.
  6. */
  7. // TODO:
  8. // Continue hook_schema_alter() for map & message tables?
  9. // Views hooks for map/message tables
  10. // xlat support?
  11. // Documentation
  12. // Tests
  13. define('MIGRATE_API_VERSION', 2);
  14. /**
  15. * Retrieve a list of all active migrations, ordered by dependencies. To be
  16. * recognized, a class must be non-abstract, and derived from MigrationBase.
  17. *
  18. * @return
  19. * Array of migration objects, keyed by the machine name.
  20. */
  21. function migrate_migrations() {
  22. static $migrations = array();
  23. if (!empty($migrations)) {
  24. return $migrations;
  25. }
  26. $dependent_migrations = array();
  27. $required_migrations = array();
  28. $result = db_select('migrate_status', 'ms')
  29. ->fields('ms', array('machine_name', 'class_name', 'arguments'))
  30. ->execute();
  31. foreach ($result as $row) {
  32. if (class_exists($row->class_name)) {
  33. $reflect = new ReflectionClass($row->class_name);
  34. if (!$reflect->isAbstract() && $reflect->isSubclassOf('MigrationBase')) {
  35. $arguments = unserialize($row->arguments);
  36. if (!$arguments || !is_array($arguments)) {
  37. $arguments = array();
  38. }
  39. $migration = MigrationBase::getInstance($row->machine_name,
  40. $row->class_name, $arguments);
  41. $dependencies = $migration->getDependencies();
  42. if (count($dependencies) > 0) {
  43. // Set classes with dependencies aside for reordering
  44. $dependent_migrations[$row->machine_name] = $migration;
  45. $required_migrations += $dependencies;
  46. }
  47. else {
  48. // No dependencies, just add
  49. $migrations[$row->machine_name] = $migration;
  50. }
  51. }
  52. else {
  53. MigrationBase::displayMessage(t('Class !class is no longer a valid concrete migration class',
  54. array('!class' => $row->class_name)));
  55. }
  56. }
  57. else {
  58. MigrationBase::displayMessage(t('Class !class no longer exists',
  59. array('!class' => $row->class_name)));
  60. }
  61. }
  62. // Scan modules with dependencies - we'll take 20 passes at it before
  63. // giving up
  64. // TODO: Can we share code with _migrate_class_list()?
  65. $iterations = 0;
  66. while (count($dependent_migrations) > 0) {
  67. if ($iterations++ > 20) {
  68. $migration_names = implode(',', array_keys($dependent_migrations));
  69. throw new MigrateException(t('Failure to sort migration list - most likely due ' .
  70. 'to circular dependencies involving !migration_names',
  71. array('!migration_names' => $migration_names)));
  72. }
  73. foreach ($dependent_migrations as $name => $migration) {
  74. $ready = TRUE;
  75. // Scan all the dependencies for this class and make sure they're all
  76. // in the final list
  77. foreach ($migration->getDependencies() as $dependency) {
  78. if (!isset($migrations[$dependency])) {
  79. $ready = FALSE;
  80. break;
  81. }
  82. }
  83. if ($ready) {
  84. // Yes they are! Move this class to the final list
  85. $migrations[$name] = $migration;
  86. unset($dependent_migrations[$name]);
  87. }
  88. }
  89. }
  90. // The migrations are now ordered according to their own dependencies - now order
  91. // them by group
  92. $groups = MigrateGroup::groups();
  93. // Seed the final list by properly-ordered groups.
  94. $final_migrations = array();
  95. foreach ($groups as $name => $group) {
  96. $final_migrations[$name] = array();
  97. }
  98. // Fill in the grouped list
  99. foreach ($migrations as $machine_name => $migration) {
  100. $final_migrations[$migration->getGroup()->getName()][$machine_name] = $migration;
  101. }
  102. // Then flatten the list
  103. $migrations = array();
  104. foreach ($final_migrations as $group_name => $group_migrations) {
  105. foreach ($group_migrations as $machine_name => $migration) {
  106. $migrations[$machine_name] = $migration;
  107. }
  108. }
  109. return $migrations;
  110. }
  111. /**
  112. * On cache clear, scan the Drupal code registry for any new migration classes
  113. * for us to register in migrate_status.
  114. */
  115. function migrate_flush_caches() {
  116. // Get list of modules implementing Migrate API
  117. $modules = array_keys(migrate_get_module_apis(TRUE));
  118. // Get list of classes we already know about
  119. $existing_classes = db_select('migrate_status', 'ms')
  120. ->fields('ms', array('class_name'))
  121. ->execute()
  122. ->fetchCol();
  123. // Discover class names registered with Drupal by modules implementing our API
  124. $result = db_select('registry', 'r')
  125. ->fields('r', array('name'))
  126. ->condition('type', 'class')
  127. ->condition('module', $modules, 'IN')
  128. ->condition('filename', '%.test', 'NOT LIKE')
  129. ->execute();
  130. foreach ($result as $record) {
  131. $class_name = $record->name;
  132. // If we already know about this class, skip it
  133. if (isset($existing_classes[$class_name])) {
  134. continue;
  135. }
  136. // Validate it's an implemented subclass of the parent class
  137. // Ignore errors
  138. try {
  139. $class = new ReflectionClass($class_name);
  140. }
  141. catch (Exception $e) {
  142. continue;
  143. }
  144. if (!$class->isAbstract() && $class->isSubclassOf('MigrationBase')) {
  145. // Verify that it's not a dynamic class (the implementor will be responsible
  146. // for registering those).
  147. $dynamic = call_user_func(array($class_name, 'isDynamic'));
  148. if (!$dynamic) {
  149. // OK, this is a new non-dynamic migration class, register it
  150. MigrationBase::registerMigration($class_name);
  151. }
  152. }
  153. }
  154. }
  155. /**
  156. * Invoke any available handlers attached to a given destination type.
  157. * If any handlers have dependencies defined, they will be invoked after
  158. * the specified handlers.
  159. *
  160. * @param $destination
  161. * Destination type ('Node', 'User', etc.) - generally the same string as
  162. * the destination class name without the MigrateDestination prefix.
  163. * @param $method
  164. * Method name such as 'prepare' (called at the beginning of an import operation)
  165. * or 'complete' (called at the end of an import operation).
  166. * @param ...
  167. * Parameters to be passed to the handler.
  168. */
  169. function migrate_handler_invoke_all($destination, $method) {
  170. $args = func_get_args();
  171. array_shift($args);
  172. array_shift($args);
  173. $return = array();
  174. $class_list = _migrate_class_list('MigrateDestinationHandler');
  175. $disabled = unserialize(variable_get('migrate_disabled_handlers', serialize(array())));
  176. foreach ($class_list as $class_name => $handler) {
  177. if (!in_array($class_name, $disabled) && $handler->handlesType($destination)
  178. && method_exists($handler, $method)) {
  179. migrate_instrument_start($class_name . '->' . $method);
  180. $result = call_user_func_array(array($handler, $method), $args);
  181. migrate_instrument_stop($class_name . '->' . $method);
  182. if (isset($result) && is_array($result)) {
  183. $return = array_merge($return, $result);
  184. }
  185. elseif (isset($result)) {
  186. $return[] = $result;
  187. }
  188. }
  189. }
  190. return $return;
  191. }
  192. /**
  193. * Invoke any available handlers attached to a given field type.
  194. * If any handlers have dependencies defined, they will be invoked after
  195. * the specified handlers.
  196. *
  197. * @param $entity
  198. * The object we are building up before calling example_save().
  199. * @param $field_info
  200. * Array of info on the field, from field_info_field().
  201. * @param $instance
  202. * Array of info in the field instance, from field_info_instances().
  203. * @param $values
  204. * Array of incoming values, to be transformed into the appropriate structure
  205. * for the field type.
  206. * @param $method
  207. * Handler method to call (defaults to prepare()).
  208. */
  209. function migrate_field_handler_invoke_all($entity, array $field_info, array $instance,
  210. array $values, $method = 'prepare') {
  211. $return = array();
  212. $type = $field_info['type'];
  213. $class_list = _migrate_class_list('MigrateFieldHandler');
  214. $disabled = unserialize(variable_get('migrate_disabled_handlers', serialize(array())));
  215. foreach ($class_list as $class_name => $handler) {
  216. if (!in_array($class_name, $disabled) && $handler->handlesType($type)
  217. && method_exists($handler, $method)) {
  218. migrate_instrument_start($class_name . '->' . $method);
  219. $result = call_user_func_array(array($handler, $method),
  220. array($entity, $field_info, $instance, $values));
  221. migrate_instrument_stop($class_name . '->' . $method);
  222. if (isset($result) && is_array($result)) {
  223. $return = array_merge_recursive($return, $result);
  224. }
  225. elseif (isset($result)) {
  226. $return[] = $result;
  227. }
  228. }
  229. }
  230. return $return;
  231. }
  232. /**
  233. * For a given parent class, identify and instantiate objects for any non-abstract
  234. * classes derived from the parent, returning an array of the objects indexed by
  235. * class name. The array will be ordered such that any classes with dependencies
  236. * are listed after the classes they are dependent on.
  237. *
  238. * @param $parent_class
  239. * Name of a class from which results will be derived.
  240. * @param $existing
  241. * Instances already known, which don't need to be instantiated.
  242. * @return
  243. * Array of objects, keyed by the class name.
  244. */
  245. function _migrate_class_list($parent_class, array $existing = array()) {
  246. // Get list of modules implementing Migrate API
  247. static $modules;
  248. if (!isset($modules)) {
  249. $modules = array_keys(migrate_get_module_apis());
  250. }
  251. static $class_lists = array();
  252. if (!isset($class_lists[$parent_class])) {
  253. $class_lists[$parent_class] = array();
  254. $dependent_classes = array();
  255. $required_classes = array();
  256. // Discover class names registered with Drupal by modules implementing our API
  257. $result = db_select('registry', 'r')
  258. ->fields('r', array('name'))
  259. ->condition('type', 'class')
  260. ->condition('module', $modules, 'IN')
  261. ->condition('filename', '%.test', 'NOT LIKE')
  262. ->execute();
  263. foreach ($result as $record) {
  264. // Validate it's an implemented subclass of the parent class
  265. // We can get funky errors here, ignore them (and the class that caused them)
  266. try {
  267. $class = new ReflectionClass($record->name);
  268. }
  269. catch (Exception $e) {
  270. continue;
  271. }
  272. if (!$class->isAbstract() && $class->isSubclassOf($parent_class)) {
  273. // If the constructor has required parameters, this may fail. We will
  274. // silently ignore - it is up to the implementor of such a class to
  275. // instantiate it in hook_migrations_alter().
  276. try {
  277. $object = new $record->name;
  278. }
  279. catch (Exception $e) {
  280. unset($object);
  281. }
  282. if (isset($object)) {
  283. $dependencies = $object->getDependencies();
  284. if (count($dependencies) > 0) {
  285. // Set classes with dependencies aside for reordering
  286. $dependent_classes[$record->name] = $object;
  287. $required_classes += $dependencies;
  288. }
  289. else {
  290. // No dependencies, just add
  291. $class_lists[$parent_class][$record->name] = $object;
  292. }
  293. }
  294. }
  295. }
  296. // Validate that each depended-on class at least exists
  297. foreach ($required_classes as $class_name) {
  298. if ((!isset($dependent_classes[$class_name])) && !isset($class_lists[$parent_class][$class_name])) {
  299. throw new MigrateException(t('Dependency on non-existent class !class - make sure ' .
  300. 'you have added the file defining !class to the .info file.',
  301. array('!class' => $class_name)));
  302. }
  303. }
  304. // Scan modules with dependencies - we'll take 20 passes at it before
  305. // giving up
  306. $iterations = 0;
  307. while (count($dependent_classes) > 0) {
  308. if ($iterations++ > 20) {
  309. $class_names = implode(',', array_keys($dependent_classes));
  310. throw new MigrateException(t('Failure to sort class list - most likely due ' .
  311. 'to circular dependencies involving !class_names.',
  312. array('!class_names' => $class_names)));
  313. }
  314. foreach ($dependent_classes as $name => $object) {
  315. $ready = TRUE;
  316. // Scan all the dependencies for this class and make sure they're all
  317. // in the final list
  318. foreach ($object->getDependencies() as $dependency) {
  319. if (!isset($class_lists[$parent_class][$dependency])) {
  320. $ready = FALSE;
  321. break;
  322. }
  323. }
  324. if ($ready) {
  325. // Yes they are! Move this class to the final list
  326. $class_lists[$parent_class][$name] = $object;
  327. unset($dependent_classes[$name]);
  328. }
  329. }
  330. }
  331. }
  332. return $class_lists[$parent_class];
  333. }
  334. /**
  335. * Implements hook_hook_info().
  336. */
  337. function migrate_hook_info() {
  338. $hooks['migrate_api'] = array(
  339. 'group' => 'migrate',
  340. );
  341. return $hooks;
  342. }
  343. /*
  344. * Implements hook_migrate_api().
  345. */
  346. function migrate_migrate_api() {
  347. $api = array(
  348. 'api' => MIGRATE_API_VERSION,
  349. );
  350. return $api;
  351. }
  352. /**
  353. * Get a list of modules that support the current migrate API.
  354. */
  355. function migrate_get_module_apis($reset = FALSE) {
  356. static $cache = NULL;
  357. if ($reset) {
  358. $cache = NULL;
  359. }
  360. if (!isset($cache)) {
  361. $cache = array();
  362. foreach (module_implements('migrate_api') as $module) {
  363. $function = $module . '_migrate_api';
  364. $info = $function();
  365. if (isset($info['api']) && $info['api'] == MIGRATE_API_VERSION) {
  366. $cache[$module] = $info;
  367. }
  368. else {
  369. drupal_set_message(t('%function supports Migrate API version %modversion,
  370. Migrate module API version is %version - migration support not loaded.',
  371. array('%function' => $function, '%modversion' => $info['api'],
  372. '%version' => MIGRATE_API_VERSION)));
  373. }
  374. }
  375. }
  376. return $cache;
  377. }
  378. /**
  379. * Implements hook_watchdog().
  380. * Find the migration that is currently running and notify it.
  381. *
  382. * @param array $log_entry
  383. */
  384. function migrate_watchdog($log_entry) {
  385. // Ensure that the Migration class exists, as different bootstrap phases may
  386. // not have included migration.inc yet.
  387. if (class_exists('Migration') && $migration = Migration::currentMigration()) {
  388. switch ($log_entry['severity']) {
  389. case WATCHDOG_EMERGENCY:
  390. case WATCHDOG_ALERT:
  391. case WATCHDOG_CRITICAL:
  392. case WATCHDOG_ERROR:
  393. $severity = MigrationBase::MESSAGE_ERROR;
  394. break;
  395. case WATCHDOG_WARNING:
  396. $severity = MigrationBase::MESSAGE_WARNING;
  397. break;
  398. case WATCHDOG_NOTICE:
  399. $severity = MigrationBase::MESSAGE_NOTICE;
  400. break;
  401. case WATCHDOG_DEBUG:
  402. case WATCHDOG_INFO:
  403. default:
  404. $severity = MigrationBase::MESSAGE_INFORMATIONAL;
  405. break;
  406. }
  407. $variables = is_array($log_entry['variables']) ? $log_entry['variables'] : array();
  408. $migration->saveMessage(t($log_entry['message'], $variables), $severity);
  409. }
  410. }
  411. /**
  412. * Resource functions modeled on Drupal's timer functions
  413. */
  414. /**
  415. * Save memory usage with the specified name. If you start and stop the same
  416. * memory name multiple times, the measured differences will be accumulated.
  417. *
  418. * @param name
  419. * The name of the memory measurement.
  420. */
  421. function migrate_memory_start($name) {
  422. global $_migrate_memory;
  423. $_migrate_memory[$name]['start'] = memory_get_usage();
  424. $_migrate_memory[$name]['count'] =
  425. isset($_migrate_memory[$name]['count']) ? ++$_migrate_memory[$name]['count'] : 1;
  426. }
  427. /**
  428. * Read the current memory value without recording the change.
  429. *
  430. * @param name
  431. * The name of the memory measurement.
  432. * @return
  433. * The change in bytes since the last start.
  434. */
  435. function migrate_memory_read($name) {
  436. global $_migrate_memory;
  437. if (isset($_migrate_memory[$name]['start'])) {
  438. $stop = memory_get_usage();
  439. $diff = $stop - $_migrate_memory[$name]['start'];
  440. if (isset($_migrate_memory[$name]['bytes'])) {
  441. $diff += $_migrate_memory[$name]['bytes'];
  442. }
  443. return $diff;
  444. }
  445. return $_migrate_memory[$name]['bytes'];
  446. }
  447. /**
  448. * Stop the memory counter with the specified name.
  449. *
  450. * @param name
  451. * The name of the memory measurement.
  452. * @return
  453. * A memory array. The array contains the number of times the memory has been
  454. * started and stopped (count) and the accumulated memory difference value in bytes.
  455. */
  456. function migrate_memory_stop($name) {
  457. global $_migrate_memory;
  458. if (isset($_migrate_memory[$name])) {
  459. if (isset($_migrate_memory[$name]['start'])) {
  460. $stop = memory_get_usage();
  461. $diff = $stop - $_migrate_memory[$name]['start'];
  462. if (isset($_migrate_memory[$name]['bytes'])) {
  463. $_migrate_memory[$name]['bytes'] += $diff;
  464. }
  465. else {
  466. $_migrate_memory[$name]['bytes'] = $diff;
  467. }
  468. unset($_migrate_memory[$name]['start']);
  469. }
  470. return $_migrate_memory[$name];
  471. }
  472. }
  473. /**
  474. * Start measuring time and (optionally) memory consumption over a section of code.
  475. * Note that the memory consumption measurement is generally not useful in
  476. * lower areas of the code, where data is being generated that will be freed
  477. * by the next call to the same area. For example, measuring the memory
  478. * consumption of db_query is not going to be helpful.
  479. *
  480. * @param $name
  481. * The name of the measurement.
  482. * @param $include_memory
  483. * Measure both memory and timers. Defaults to FALSE (timers only).
  484. */
  485. function migrate_instrument_start($name, $include_memory = FALSE) {
  486. global $_migrate_track_memory, $_migrate_track_timer;
  487. if ($_migrate_track_memory && $include_memory) {
  488. migrate_memory_start($name);
  489. }
  490. if ($_migrate_track_timer) {
  491. timer_start($name);
  492. }
  493. }
  494. /**
  495. * Stop measuring both memory and time consumption over a section of code.
  496. *
  497. * @param $name
  498. * The name of the measurement.
  499. */
  500. function migrate_instrument_stop($name) {
  501. global $_migrate_track_memory, $_migrate_track_timer;
  502. if ($_migrate_track_timer) {
  503. timer_stop($name);
  504. }
  505. if ($_migrate_track_memory) {
  506. migrate_memory_stop($name);
  507. }
  508. }
  509. /**
  510. * Call hook_migrate_overview for overall documentation on implemented migrations.
  511. */
  512. function migrate_overview() {
  513. $overview = '';
  514. $results = module_invoke_all('migrate_overview');
  515. foreach ($results as $result) {
  516. $overview .= $result . ' ';
  517. }
  518. return $overview;
  519. }