migration.inc 56 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661
  1. <?php
  2. /**
  3. * @file
  4. * Defines the base class for import/rollback processes.
  5. */
  6. /**
  7. * The base class for all import objects. This is where most of the smarts
  8. * of the migrate module resides. Migrations are created by deriving from this
  9. * class, and in the constructor (after calling parent::__construct())
  10. * initializing at a minimum the name, description, source, and destination
  11. * properties. The constructor will also usually make several calls to
  12. * addFieldMapping().
  13. */
  14. abstract class Migration extends MigrationBase {
  15. /**
  16. * Source object for the migration, derived from MigrateSource.
  17. *
  18. * @var MigrateSource
  19. */
  20. protected $source;
  21. public function getSource() {
  22. return $this->source;
  23. }
  24. public function setSource(MigrateSource $source) {
  25. $this->source = $source;
  26. }
  27. /**
  28. * Destination object for the migration, derived from MigrateDestination.
  29. *
  30. * @var MigrateDestination
  31. */
  32. protected $destination;
  33. public function getDestination() {
  34. return $this->destination;
  35. }
  36. public function setDestination(MigrateDestination $destination) {
  37. $this->destination = $destination;
  38. }
  39. /**
  40. * Map object tracking relationships between source and destination data
  41. *
  42. * @var MigrateMap
  43. */
  44. protected $map;
  45. public function getMap() {
  46. return $this->map;
  47. }
  48. public function setMap(MigrateMap $map) {
  49. $this->map = $map;
  50. }
  51. /**
  52. * Indicate whether the primary system of record for this migration is the
  53. * source, or the destination (Drupal). In the source case, migration of
  54. * an existing object will completely replace the Drupal object with data from
  55. * the source side. In the destination case, the existing Drupal object will
  56. * be loaded, then changes from the source applied; also, rollback will not be
  57. * supported.
  58. *
  59. * @var int
  60. */
  61. const SOURCE = 1;
  62. const DESTINATION = 2;
  63. protected $systemOfRecord = Migration::SOURCE;
  64. public function getSystemOfRecord() {
  65. return $this->systemOfRecord;
  66. }
  67. public function setSystemOfRecord($system_of_record) {
  68. $this->systemOfRecord = $system_of_record;
  69. }
  70. /**
  71. * Specify value of needs_update for current map row. Usually set by
  72. * MigrateFieldHandler implementations.
  73. *
  74. * @var int
  75. */
  76. public $needsUpdate = MigrateMap::STATUS_IMPORTED;
  77. /**
  78. * The default rollback action for this migration. Can be overridden on
  79. * a per-row basis by setting $row->rollbackAction in prepareRow().
  80. *
  81. * @var int
  82. */
  83. protected $defaultRollbackAction = MigrateMap::ROLLBACK_DELETE;
  84. public function getDefaultRollbackAction() {
  85. return $this->defaultRollbackAction;
  86. }
  87. public function setDefaultRollbackAction($rollback_action) {
  88. $this->defaultRollbackAction = $rollback_action;
  89. }
  90. /**
  91. * The rollback action to be saved for the current row.
  92. *
  93. * @var int
  94. */
  95. public $rollbackAction;
  96. /**
  97. * Field mappings defined in code.
  98. *
  99. * @var array
  100. */
  101. protected $storedFieldMappings = array();
  102. protected $storedFieldMappingsRetrieved = FALSE;
  103. public function getStoredFieldMappings() {
  104. if (!$this->storedFieldMappingsRetrieved) {
  105. $this->loadFieldMappings();
  106. $this->storedFieldMappingsRetrieved = TRUE;
  107. }
  108. return $this->storedFieldMappings;
  109. }
  110. /**
  111. * Field mappings retrieved from storage.
  112. *
  113. * @var array
  114. */
  115. protected $codedFieldMappings = array();
  116. public function getCodedFieldMappings() {
  117. return $this->codedFieldMappings;
  118. }
  119. /**
  120. * All field mappings, with those retrieved from the database overriding those
  121. * defined in code.
  122. *
  123. * @var array
  124. */
  125. protected $allFieldMappings = array();
  126. public function getFieldMappings() {
  127. if (empty($allFieldMappings)) {
  128. $this->allFieldMappings = array_merge($this->getCodedFieldMappings(),
  129. $this->getStoredFieldMappings());
  130. // If there are multiple mappings of a given source field to no
  131. // destination field, keep only the last (so the UI can override a source
  132. // field DNM that was defined in code).
  133. $no_destination = array();
  134. // But also remove a mapping of a source field to nothing, if there is
  135. // a mapping to something.
  136. $mapped_source_fields = array();
  137. /** @var MigrateFieldMapping $mapping */
  138. foreach ($this->allFieldMappings as $destination_field => $mapping) {
  139. $source_field = $mapping->getSourceField();
  140. // If the source field is not mapped to a destination field, the
  141. // array index is integer.
  142. if (is_int($destination_field)) {
  143. if (isset($no_destination[$source_field])) {
  144. unset($this->allFieldMappings[$no_destination[$source_field]]);
  145. unset($no_destination[$source_field]);
  146. }
  147. $no_destination[$source_field] = $destination_field;
  148. if (isset($mapped_source_fields[$source_field])) {
  149. unset($this->allFieldMappings[$destination_field]);
  150. }
  151. }
  152. else {
  153. $mapped_source_fields[$source_field] = $source_field;
  154. }
  155. }
  156. // Make sure primary fields come before their subfields
  157. ksort($this->allFieldMappings);
  158. }
  159. return $this->allFieldMappings;
  160. }
  161. /**
  162. * An array of counts. Initially used for cache hit/miss tracking.
  163. *
  164. * @var array
  165. */
  166. protected $counts = array();
  167. /**
  168. * When performing a bulkRollback(), the maximum number of items to pass in
  169. * a single call. Can be overridden in derived class constructor.
  170. *
  171. * @var int
  172. */
  173. protected $rollbackBatchSize = 50;
  174. /**
  175. * If present, an array with keys name and alias (optional). Name refers to
  176. * the source columns used for tracking highwater marks. alias is an
  177. * optional table alias.
  178. *
  179. * @var array
  180. */
  181. protected $highwaterField = array();
  182. public function getHighwaterField() {
  183. return $this->highwaterField;
  184. }
  185. public function setHighwaterField(array $highwater_field) {
  186. $this->highwaterField = $highwater_field;
  187. }
  188. /**
  189. * The object currently being constructed
  190. *
  191. * @var stdClass
  192. */
  193. protected $destinationValues;
  194. /**
  195. * The current data row retrieved from the source.
  196. *
  197. * @var stdClass
  198. */
  199. protected $sourceValues;
  200. /**
  201. * Queue up messages that can't be safely saved (in particular, if they're
  202. * generated in prepareRow().
  203. *
  204. * @var array
  205. */
  206. protected $queuedMessages = array();
  207. /**
  208. * General initialization of a Migration object.
  209. */
  210. public function __construct($arguments = array()) {
  211. parent::__construct($arguments);
  212. }
  213. /**
  214. * Register a new migration process in the migrate_status table. This will
  215. * generally be used in two contexts - by the class detection code for
  216. * static (one instance per class) migrations, and by the module implementing
  217. * dynamic (parameterized class) migrations.
  218. *
  219. * @param string $class_name
  220. * @param string $machine_name
  221. * @param array $arguments
  222. */
  223. static public function registerMigration($class_name, $machine_name = NULL,
  224. array $arguments = array()) {
  225. // Record any field mappings provided via arguments.
  226. if (isset($arguments['field_mappings'])) {
  227. self::saveFieldMappings($machine_name, $arguments['field_mappings']);
  228. unset($arguments['field_mappings']);
  229. }
  230. parent::registerMigration($class_name, $machine_name, $arguments);
  231. }
  232. /**
  233. * Deregister a migration - remove all traces of it from the database (without
  234. * touching any content which was created by this migration).
  235. *
  236. * We'd like to do this at uninstall time, but the implementing module is
  237. * already disabled, so we can't instantiate it to get at the map. This can
  238. * be done in hook_disable(), however.
  239. *
  240. * @param string $machine_name
  241. */
  242. static public function deregisterMigration($machine_name) {
  243. try {
  244. // Remove map and message tables
  245. $migration = self::getInstance($machine_name);
  246. if ($migration && method_exists($migration, 'getMap')) {
  247. $migration->getMap()->destroy();
  248. }
  249. // @todo: Clear log entries? Or keep for historical purposes?
  250. // Remove stored field mappings for this migration
  251. $rows_deleted = db_delete('migrate_field_mapping')
  252. ->condition('machine_name', $machine_name)
  253. ->execute();
  254. // Call the parent deregistration (which clears migrate_status) last, the
  255. // above will reference it.
  256. parent::deregisterMigration($machine_name);
  257. } catch (Exception $e) {
  258. // Fail silently if it's already gone
  259. }
  260. }
  261. /**
  262. * Record an array of field mappings to the database.
  263. *
  264. * @param $machine_name
  265. * @param array $field_mappings
  266. */
  267. static public function saveFieldMappings($machine_name, array $field_mappings) {
  268. // Clear existing field mappings
  269. db_delete('migrate_field_mapping')
  270. ->condition('machine_name', $machine_name)
  271. ->execute();
  272. foreach ($field_mappings as $field_mapping) {
  273. $destination_field = $field_mapping->getDestinationField();
  274. $source_field = $field_mapping->getSourceField();
  275. db_insert('migrate_field_mapping')
  276. ->fields(array(
  277. 'machine_name' => $machine_name,
  278. 'destination_field' => is_null($destination_field) ? '' : $destination_field,
  279. 'source_field' => is_null($source_field) ? '' : $source_field,
  280. 'options' => serialize($field_mapping),
  281. ))
  282. ->execute();
  283. }
  284. }
  285. /**
  286. * Load any stored field mappings from the database.
  287. */
  288. public function loadFieldMappings() {
  289. $result = db_select('migrate_field_mapping', 'mfm')
  290. ->fields('mfm', array('destination_field', 'source_field', 'options'))
  291. ->condition('machine_name', $this->machineName)
  292. ->execute();
  293. foreach ($result as $row) {
  294. $field_mapping = unserialize($row->options);
  295. $field_mapping->setMappingSource(MigrateFieldMapping::MAPPING_SOURCE_DB);
  296. if (empty($row->destination_field)) {
  297. $this->storedFieldMappings[] = $field_mapping;
  298. }
  299. else {
  300. $this->storedFieldMappings[$row->destination_field] = $field_mapping;
  301. }
  302. }
  303. }
  304. ////////////////////////////////////////////////////////////////////
  305. // Processing
  306. /**
  307. * Add a mapping for a destination field, specifying a source field and/or
  308. * a default value.
  309. *
  310. * @param string $destinationField
  311. * Name of the destination field.
  312. * @param string $sourceField
  313. * Name of the source field (optional).
  314. * @param boolean $warn_on_override
  315. * Set to FALSE to prevent warnings when there's an existing mapping
  316. * for this destination field.
  317. */
  318. public function addFieldMapping($destination_field, $source_field = NULL,
  319. $warn_on_override = TRUE) {
  320. // Warn of duplicate mappings
  321. if ($warn_on_override && !is_null($destination_field) &&
  322. isset($this->codedFieldMappings[$destination_field])) {
  323. self::displayMessage(
  324. t('!name addFieldMapping: !dest was previously mapped from !source, overridden',
  325. array(
  326. '!name' => $this->machineName,
  327. '!dest' => $destination_field,
  328. '!source' => $this->codedFieldMappings[$destination_field]->getSourceField(),
  329. )),
  330. 'warning');
  331. }
  332. $mapping = new MigrateFieldMapping($destination_field, $source_field);
  333. if (is_null($destination_field)) {
  334. $this->codedFieldMappings[] = $mapping;
  335. }
  336. else {
  337. $this->codedFieldMappings[$destination_field] = $mapping;
  338. }
  339. return $mapping;
  340. }
  341. /**
  342. * Remove any existing coded mappings for a given destination or source field.
  343. *
  344. * @param string $destination_field
  345. * Name of the destination field.
  346. * @param string $source_field
  347. * Name of the source field.
  348. */
  349. public function removeFieldMapping($destination_field, $source_field = NULL) {
  350. if (isset($destination_field)) {
  351. unset($this->codedFieldMappings[$destination_field]);
  352. }
  353. if (isset($source_field)) {
  354. foreach ($this->codedFieldMappings as $key => $mapping) {
  355. if ($mapping->getSourceField() == $source_field) {
  356. unset($this->codedFieldMappings[$key]);
  357. }
  358. }
  359. }
  360. }
  361. /**
  362. * Shortcut for adding several fields which have the same name on both source
  363. * and destination sides.
  364. *
  365. * @param array $fields
  366. * List of field names to map.
  367. */
  368. public function addSimpleMappings(array $fields) {
  369. foreach ($fields as $field) {
  370. $this->addFieldMapping($field, $field);
  371. }
  372. }
  373. /**
  374. * Shortcut for adding several destination fields which are to be explicitly
  375. * not migrated.
  376. *
  377. * @param array $fields
  378. * List of fields to mark as not for migration.
  379. *
  380. * @param string $issue_group
  381. * Issue group name to apply to the generated mappings (defaults to 'DNM').
  382. */
  383. public function addUnmigratedDestinations(array $fields, $issue_group = NULL, $warn_on_override = TRUE) {
  384. if (!$issue_group) {
  385. $issue_group = t('DNM');
  386. }
  387. foreach ($fields as $field) {
  388. $this->addFieldMapping($field, NULL, $warn_on_override)
  389. ->issueGroup($issue_group);
  390. }
  391. }
  392. /**
  393. * Shortcut for adding several source fields which are to be explicitly
  394. * not migrated.
  395. *
  396. * @param array $fields
  397. * List of fields to mark as not for migration.
  398. *
  399. * @param string $issue_group
  400. * Issue group name to apply to the generated mappings (defaults to 'DNM').
  401. */
  402. public function addUnmigratedSources(array $fields, $issue_group = NULL, $warn_on_override = TRUE) {
  403. if (!$issue_group) {
  404. $issue_group = t('DNM');
  405. }
  406. foreach ($fields as $field) {
  407. $this->addFieldMapping(NULL, $field, $warn_on_override)
  408. ->issueGroup($issue_group);
  409. }
  410. }
  411. /**
  412. * Reports whether this migration process is complete (i.e., all available
  413. * source rows have been processed).
  414. */
  415. public function isComplete() {
  416. $total = $this->sourceCount(TRUE);
  417. // If the source is uncountable, we have no way of knowing if it's
  418. // complete, so stipulate that it is.
  419. if ($total < 0) {
  420. return TRUE;
  421. }
  422. $processed = $this->processedCount();
  423. return $total <= $processed;
  424. }
  425. /**
  426. * Override MigrationBase::beginProcess, to make sure the map/message tables
  427. * are present.
  428. *
  429. * @param int $newStatus
  430. * Migration::STATUS_IMPORTING or Migration::STATUS_ROLLING_BACK
  431. */
  432. protected function beginProcess($newStatus) {
  433. parent::beginProcess($newStatus);
  434. // Do some standard setup
  435. if (isset($this->options['feedback']) && isset($this->options['feedback']['value']) &&
  436. isset($this->options['feedback']['unit'])) {
  437. $this->feedback = $this->options['feedback']['value'];
  438. $this->feedback_unit = $this->options['feedback']['unit'];
  439. if ($this->feedback_unit == 'item') {
  440. $this->feedback_unit = 'items';
  441. }
  442. elseif ($this->feedback_unit == 'second') {
  443. $this->feedback_unit = 'seconds';
  444. }
  445. }
  446. $this->lastfeedback = $this->starttime;
  447. $this->total_processed = $this->total_successes =
  448. $this->processed_since_feedback = $this->successes_since_feedback = 0;
  449. // Call pre-process methods
  450. if ($this->status == Migration::STATUS_IMPORTING) {
  451. $this->preImport();
  452. }
  453. elseif ($this->status == Migration::STATUS_ROLLING_BACK) {
  454. $this->preRollback();
  455. }
  456. }
  457. /**
  458. * Override MigrationBase::endProcess, to call post hooks. Note that it must
  459. * be public to be callable as the shutdown function.
  460. */
  461. public function endProcess() {
  462. // Call post-process methods
  463. if ($this->status == Migration::STATUS_IMPORTING) {
  464. $this->postImport();
  465. }
  466. elseif ($this->status == Migration::STATUS_ROLLING_BACK) {
  467. $this->postRollback();
  468. }
  469. parent::endProcess();
  470. }
  471. /**
  472. * Default implementations of pre/post import/rollback methods. These call
  473. * the destination methods (if they exist) - when overriding, always
  474. * call parent::preImport() etc.
  475. */
  476. protected function preImport() {
  477. if (method_exists($this->destination, 'preImport')) {
  478. $this->destination->preImport();
  479. }
  480. }
  481. protected function preRollback() {
  482. if (method_exists($this->destination, 'preRollback')) {
  483. $this->destination->preRollback();
  484. }
  485. }
  486. protected function postImport() {
  487. if (method_exists($this->destination, 'postImport')) {
  488. $this->destination->postImport();
  489. }
  490. }
  491. protected function postRollback() {
  492. if (method_exists($this->destination, 'postRollback')) {
  493. $this->destination->postRollback();
  494. }
  495. }
  496. /**
  497. * Perform a rollback operation - remove migrated items from the destination.
  498. */
  499. protected function rollback() {
  500. $return = MigrationBase::RESULT_COMPLETED;
  501. $itemlimit = $this->getItemLimit();
  502. $idlist = $this->getOption('idlist');
  503. if ($idlist) {
  504. // Make the IDs keys, to more easily identify them
  505. $idlist = array_flip(explode(',', $idlist));
  506. }
  507. if (method_exists($this->destination, 'bulkRollback')) {
  508. // Too many at once can lead to memory issues, so batch 'em up
  509. $destids = array();
  510. $sourceids = array();
  511. $batch_count = 0;
  512. foreach ($this->map as $destination_key) {
  513. if ($this->timeOptionExceeded()) {
  514. break;
  515. }
  516. if (($return = $this->checkStatus()) != MigrationBase::RESULT_COMPLETED) {
  517. break;
  518. }
  519. if ($itemlimit && ($this->total_processed + $batch_count >= $itemlimit)) {
  520. break;
  521. }
  522. $current_source_key = $this->map->getCurrentKey();
  523. // If there's an idlist, skip anything not in the list
  524. if ($idlist && !isset($idlist[$current_source_key['sourceid1']])) {
  525. continue;
  526. }
  527. // Note that bulk rollback is only supported for single-column keys
  528. $sourceids[] = $current_source_key;
  529. if (!empty($destination_key->destid1)) {
  530. $map_row = $this->map->getRowByDestination((array) $destination_key);
  531. if ($map_row['rollback_action'] == MigrateMap::ROLLBACK_DELETE) {
  532. $destids[] = $destination_key->destid1;
  533. }
  534. }
  535. $batch_count++;
  536. if ($batch_count >= $this->rollbackBatchSize) {
  537. try {
  538. if ($this->systemOfRecord == Migration::SOURCE) {
  539. if (!empty($destids)) {
  540. migrate_instrument_start('destination bulkRollback');
  541. $this->destination->bulkRollback($destids);
  542. migrate_instrument_stop('destination bulkRollback');
  543. }
  544. }
  545. // Keep track in case of interruption
  546. migrate_instrument_start('rollback map/message update');
  547. $this->map->deleteBulk($sourceids);
  548. migrate_instrument_stop('rollback map/message update');
  549. $this->total_successes += $batch_count;
  550. $this->successes_since_feedback += $batch_count;
  551. } catch (Exception $e) {
  552. $this->handleException($e, FALSE);
  553. migrate_instrument_stop('bulkRollback');
  554. migrate_instrument_stop('rollback map/message update');
  555. }
  556. $destids = array();
  557. $sourceids = array();
  558. // Will increment even if there was an exception... But we don't
  559. // really have a way to know how many really were successfully rolled back
  560. $this->total_processed += $batch_count;
  561. $this->processed_since_feedback += $batch_count;
  562. $batch_count = 0;
  563. }
  564. }
  565. if ($batch_count > 0) {
  566. if ($this->systemOfRecord == Migration::SOURCE) {
  567. if (!empty($destids)) {
  568. migrate_instrument_start('destination bulkRollback');
  569. $this->destination->bulkRollback($destids);
  570. migrate_instrument_stop('destination bulkRollback');
  571. }
  572. $this->total_processed += $batch_count;
  573. $this->total_successes += $batch_count;
  574. $this->processed_since_feedback += $batch_count;
  575. $this->successes_since_feedback += $batch_count;
  576. }
  577. migrate_instrument_start('rollback map/message update');
  578. $this->map->deleteBulk($sourceids);
  579. migrate_instrument_stop('rollback map/message update');
  580. }
  581. }
  582. else {
  583. foreach ($this->map as $destination_key) {
  584. if ($this->timeOptionExceeded()) {
  585. break;
  586. }
  587. if (($return = $this->checkStatus()) != MigrationBase::RESULT_COMPLETED) {
  588. break;
  589. }
  590. if ($this->itemOptionExceeded()) {
  591. break;
  592. }
  593. $current_source_key = $this->map->getCurrentKey();
  594. // If there's an idlist, skip anything not in the list
  595. if ($idlist && !isset($idlist[$current_source_key['sourceid1']])) {
  596. continue;
  597. }
  598. // Rollback one record
  599. try {
  600. if ($this->systemOfRecord == Migration::SOURCE) {
  601. // Skip when the destination key is null
  602. $skip = FALSE;
  603. foreach ($destination_key as $key_value) {
  604. if (is_null($key_value)) {
  605. $skip = TRUE;
  606. break;
  607. }
  608. }
  609. if (!$skip) {
  610. $map_row = $this->map->getRowByDestination((array) $destination_key);
  611. if ($map_row['rollback_action'] == MigrateMap::ROLLBACK_DELETE) {
  612. migrate_instrument_start('destination rollback');
  613. $this->destination->rollback((array) $destination_key);
  614. migrate_instrument_stop('destination rollback');
  615. }
  616. }
  617. }
  618. migrate_instrument_start('rollback map/message update');
  619. $this->map->delete($current_source_key);
  620. migrate_instrument_stop('rollback map/message update');
  621. $this->total_successes++;
  622. $this->successes_since_feedback++;
  623. } catch (Exception $e) {
  624. // TODO: At least count failures
  625. continue;
  626. }
  627. $this->total_processed++;
  628. $this->processed_since_feedback++;
  629. }
  630. }
  631. $this->map->clearMessages();
  632. $this->progressMessage($return);
  633. // If we're using highwater marks, reset at completion of a full rollback
  634. // TODO: What about partial rollbacks? Probably little we can do to make
  635. // that work cleanly...
  636. if ($this->highwaterField) {
  637. $this->saveHighwater('', TRUE);
  638. }
  639. return $return;
  640. }
  641. /**
  642. * Perform an import operation - migrate items from source to destination.
  643. */
  644. protected function import() {
  645. $return = MigrationBase::RESULT_COMPLETED;
  646. try {
  647. $this->source->rewind();
  648. } catch (Exception $e) {
  649. self::displayMessage(
  650. t('Migration for %class failed with source plugin exception: %e, in %file:%line',
  651. array(
  652. '%class' => get_class($this),
  653. '%e' => $e->getMessage(),
  654. '%file' => $e->getFile(),
  655. '%line' => $e->getLine(),
  656. )));
  657. return MigrationBase::RESULT_FAILED;
  658. }
  659. while ($this->source->valid()) {
  660. $data_row = $this->source->current();
  661. // Wipe old messages, and save any new messages.
  662. $this->map->delete($this->currentSourceKey(), TRUE);
  663. $this->saveQueuedMessages();
  664. $this->sourceValues = $data_row;
  665. $this->applyMappings();
  666. try {
  667. migrate_instrument_start('destination import', TRUE);
  668. $ids = $this->destination->import($this->destinationValues, $this->sourceValues);
  669. migrate_instrument_stop('destination import');
  670. if ($ids) {
  671. $this->map->saveIDMapping($this->sourceValues, $ids,
  672. $this->needsUpdate, $this->rollbackAction,
  673. $data_row->migrate_map_hash);
  674. $this->successes_since_feedback++;
  675. $this->total_successes++;
  676. }
  677. else {
  678. $this->map->saveIDMapping($this->sourceValues, array(),
  679. MigrateMap::STATUS_FAILED, $this->rollbackAction,
  680. NULL);
  681. if ($this->map->messageCount() == 0) {
  682. $message = t('New object was not saved, no error provided');
  683. $this->saveMessage($message);
  684. self::displayMessage($message);
  685. }
  686. }
  687. } catch (MigrateException $e) {
  688. $this->map->saveIDMapping($this->sourceValues, array(),
  689. $e->getStatus(), $this->rollbackAction, $data_row->migrate_map_hash);
  690. $this->saveMessage($e->getMessage(), $e->getLevel());
  691. self::displayMessage($e->getMessage());
  692. } catch (Exception $e) {
  693. $this->map->saveIDMapping($this->sourceValues, array(),
  694. MigrateMap::STATUS_FAILED, $this->rollbackAction,
  695. NULL);
  696. $this->handleException($e);
  697. }
  698. $this->total_processed++;
  699. $this->processed_since_feedback++;
  700. if ($this->highwaterField) {
  701. $this->saveHighwater($this->sourceValues->{$this->highwaterField['name']});
  702. }
  703. // Reset row properties.
  704. unset($this->sourceValues, $this->destinationValues);
  705. $this->needsUpdate = MigrateMap::STATUS_IMPORTED;
  706. // TODO: Temporary. Remove when http://drupal.org/node/375494 is committed.
  707. // TODO: Should be done in MigrateDestinationEntity
  708. if (!empty($this->destination->entityType)) {
  709. entity_get_controller($this->destination->entityType)->resetCache();
  710. }
  711. if ($this->timeOptionExceeded()) {
  712. break;
  713. }
  714. if (($return = $this->checkStatus()) != MigrationBase::RESULT_COMPLETED) {
  715. break;
  716. }
  717. if ($this->itemOptionExceeded()) {
  718. break;
  719. }
  720. try {
  721. $this->source->next();
  722. } catch (Exception $e) {
  723. self::displayMessage(
  724. t('Migration for %class failed with source plugin exception: %e, in %file:%line',
  725. array(
  726. '%class' => get_class($this),
  727. '%e' => $e->getMessage(),
  728. '%file' => $e->getFile(),
  729. '%line' => $e->getLine(),
  730. )));
  731. return MigrationBase::RESULT_FAILED;
  732. }
  733. }
  734. $this->progressMessage($return);
  735. return $return;
  736. }
  737. /**
  738. * Perform an analysis operation - report on field values in the source.
  739. *
  740. * @return array
  741. * Array of analysis details - each element is keyed by field name and
  742. * contains an array describing the field values.
  743. */
  744. public function analyze() {
  745. // The source needs this to find the map table.
  746. self::$currentMigration = $this;
  747. try {
  748. $this->source->rewind();
  749. } catch (Exception $e) {
  750. self::displayMessage(
  751. t('Migration analysis failed with source plugin exception: !e',
  752. array('!e' => $e->getMessage())));
  753. self::$currentMigration = NULL;
  754. return array();
  755. }
  756. // Get the documented fields first
  757. $source_fields = $this->source->fields();
  758. $analysis = array();
  759. $field_init = array(
  760. 'is_numeric' => TRUE,
  761. 'min_numeric' => NULL,
  762. 'max_numeric' => NULL,
  763. 'min_strlen' => 0,
  764. 'max_strlen' => 0,
  765. 'distinct_values' => array(),
  766. );
  767. foreach ($source_fields as $field_name => $description) {
  768. // Ignore fields from the map table
  769. if (substr($field_name, 0, strlen('migrate_map_')) == 'migrate_map_') {
  770. continue;
  771. }
  772. $analysis[$field_name] = $field_init +
  773. array('description' => $description);
  774. }
  775. // For each data row...
  776. while ($this->source->valid()) {
  777. $row = $this->source->current();
  778. // Cheat for XML migrations, which don't pick up the source values
  779. // until applyMappings() applies the xpath()
  780. if (is_a($this, 'XMLMigration') && isset($row->xml)) {
  781. $this->sourceValues = $row;
  782. $this->applyMappings();
  783. $row = $this->sourceValues;
  784. unset($row->xml);
  785. }
  786. // For each field in this row...
  787. foreach ($row as $field_name => $raw_value) {
  788. // Ignore fields from the map table
  789. if (substr($field_name, 0, strlen('migrate_map_')) == 'migrate_map_') {
  790. continue;
  791. }
  792. // It might be an array of values, check each value
  793. if (!is_array($raw_value)) {
  794. $raw_value = array($raw_value);
  795. }
  796. foreach ($raw_value as $field_value) {
  797. // If this is an undocumented field, initialize it
  798. if (!isset($analysis[$field_name])) {
  799. $analysis[$field_name] = $field_init +
  800. array('description' => '');
  801. }
  802. // Ignore leading/trailing spaces in determing numerics
  803. $trimmed_value = trim($field_value);
  804. if (is_numeric($trimmed_value)) {
  805. $trimmed_value = floatval($trimmed_value);
  806. // First numeric value, initialize the min/max
  807. if (is_null($analysis[$field_name]['min_numeric'])) {
  808. $analysis[$field_name]['min_numeric'] = $trimmed_value;
  809. $analysis[$field_name]['max_numeric'] = $trimmed_value;
  810. }
  811. else {
  812. $analysis[$field_name]['min_numeric'] = min($trimmed_value,
  813. $analysis[$field_name]['min_numeric']);
  814. $analysis[$field_name]['max_numeric'] = max($trimmed_value,
  815. $analysis[$field_name]['max_numeric']);
  816. }
  817. }
  818. elseif ($trimmed_value !== '') {
  819. // Empty strings are !is_numeric(), but treat as empty rather than
  820. // assuming we don't have a numeric field
  821. $analysis[$field_name]['is_numeric'] = FALSE;
  822. }
  823. $strlen = strlen($field_value);
  824. // First string value, initialize both min and max
  825. if ($analysis[$field_name]['max_strlen'] == 0) {
  826. $analysis[$field_name]['min_strlen'] = $strlen;
  827. $analysis[$field_name]['max_strlen'] = $strlen;
  828. }
  829. else {
  830. $analysis[$field_name]['min_strlen'] = min($strlen,
  831. $analysis[$field_name]['min_strlen']);
  832. $analysis[$field_name]['max_strlen'] = max($strlen,
  833. $analysis[$field_name]['max_strlen']);
  834. }
  835. // Track up to 10 distinct values
  836. if (count($analysis[$field_name]['distinct_values']) <= 10) {
  837. $analysis[$field_name]['distinct_values'][$trimmed_value]++;
  838. }
  839. }
  840. }
  841. try {
  842. $this->source->next();
  843. } catch (Exception $e) {
  844. self::displayMessage(
  845. t('Migration analysis failed with source plugin exception: !e. Partial results follow:',
  846. array('!e' => $e->getMessage())));
  847. self::$currentMigration = NULL;
  848. return $analysis;
  849. }
  850. }
  851. self::$currentMigration = NULL;
  852. return $analysis;
  853. }
  854. /**
  855. * Fetch the key array for the current source record.
  856. *
  857. * @return array
  858. */
  859. protected function currentSourceKey() {
  860. return $this->source->getCurrentKey();
  861. }
  862. /**
  863. * Default implementation of prepareKey. This method is called from the source
  864. * plugin immediately after retrieving the raw data from the source - by
  865. * default, it simply assigns the key values based on the field names passed
  866. * to MigrateSQLMap(). Override this if you need to generate your own key
  867. * (e.g., the source doesn't have a natural unique key). Be sure to also
  868. * set any values you generate in $row.
  869. *
  870. * @param array $source_key
  871. * @param object $row
  872. *
  873. * @return array
  874. */
  875. public function prepareKey($source_key, $row) {
  876. $key = array();
  877. foreach ($source_key as $field_name => $field_schema) {
  878. $key[$field_name] = $row->{$field_name};
  879. }
  880. return $key;
  881. }
  882. /**
  883. * Default implementation of prepareRow(). This method is called from the
  884. * source plugin upon first pulling the raw data from the source.
  885. *
  886. * @param $row
  887. * Object containing raw source data.
  888. *
  889. * @return bool
  890. * TRUE to process this row, FALSE to have the source skip it.
  891. */
  892. public function prepareRow($row) {
  893. $this->rollbackAction = $this->defaultRollbackAction;
  894. return TRUE;
  895. }
  896. ////////////////////////////////////////////////////////////////////
  897. // Utility methods
  898. /**
  899. * Convenience function to return count of total source records
  900. *
  901. * @param boolean $refresh
  902. * Pass TRUE to refresh the cached count.
  903. */
  904. public function sourceCount($refresh = FALSE) {
  905. try {
  906. $count = $this->source->count($refresh);
  907. } catch (Exception $e) {
  908. $count = t('N/A');
  909. self::displayMessage($e->getMessage());
  910. }
  911. return $count;
  912. }
  913. /**
  914. * Get the number of source records processed.
  915. *
  916. * @return int
  917. * Number of processed records.
  918. */
  919. public function processedCount() {
  920. try {
  921. $count = $this->map->processedCount();
  922. } catch (Exception $e) {
  923. $count = t('N/A');
  924. self::displayMessage($e->getMessage());
  925. }
  926. return $count;
  927. }
  928. /**
  929. * Get the number of records successfully imported.
  930. *
  931. * @return int
  932. * Number of imported records.
  933. */
  934. public function importedCount() {
  935. try {
  936. $count = $this->map->importedCount();
  937. } catch (Exception $e) {
  938. $count = t('N/A');
  939. self::displayMessage($e->getMessage());
  940. }
  941. return $count;
  942. }
  943. /**
  944. * Get the number of records marked as needing update.
  945. *
  946. * @return int
  947. */
  948. public function updateCount() {
  949. try {
  950. $count = $this->map->updateCount();
  951. } catch (Exception $e) {
  952. $count = t('N/A');
  953. self::displayMessage($e->getMessage());
  954. }
  955. return $count;
  956. }
  957. /**
  958. * Test whether we've exceeded the designated item limit.
  959. *
  960. * @return boolean
  961. * TRUE if the threshold is exceeded, FALSE if not.
  962. */
  963. protected function itemOptionExceeded() {
  964. $itemlimit = $this->getItemLimit();
  965. if ($itemlimit && $this->total_processed >= $itemlimit) {
  966. return TRUE;
  967. }
  968. return FALSE;
  969. }
  970. /**
  971. * Get the number of source records which failed to import.
  972. * TODO: Doesn't yet account for informationals, or multiple errors for
  973. * a source record.
  974. *
  975. * @return int
  976. * Number of records errored out.
  977. */
  978. public function errorCount() {
  979. return $this->map->errorCount();
  980. }
  981. /**
  982. * Get the number of messages associated with this migration
  983. *
  984. * @return int
  985. * Number of messages.
  986. */
  987. public function messageCount() {
  988. return $this->map->messageCount();
  989. }
  990. /**
  991. * Prepares this migration to run as an update - that is, in addition to
  992. * unmigrated content (source records not in the map table) being imported,
  993. * previously-migrated content will also be updated in place.
  994. */
  995. public function prepareUpdate() {
  996. $this->map->prepareUpdate();
  997. }
  998. /**
  999. * Outputs a progress message, reflecting the current status of a migration
  1000. * process.
  1001. *
  1002. * @param int $result
  1003. * Status of the process, represented by one of the Migration::RESULT_*
  1004. * constants.
  1005. */
  1006. protected function progressMessage($result) {
  1007. $time = microtime(TRUE) - $this->lastfeedback;
  1008. if ($time > 0) {
  1009. $perminute = round(60 * $this->processed_since_feedback / $time);
  1010. $time = round($time, 1);
  1011. }
  1012. else {
  1013. $perminute = '?';
  1014. }
  1015. if ($this->status == Migration::STATUS_IMPORTING) {
  1016. switch ($result) {
  1017. case Migration::RESULT_COMPLETED:
  1018. $basetext = "Processed !numitems (!created created, !updated updated, !failed failed, !ignored ignored) in !time sec (!perminute/min) - done with '!name'";
  1019. $type = 'completed';
  1020. break;
  1021. case Migration::RESULT_FAILED:
  1022. $basetext = "Processed !numitems (!created created, !updated updated, !failed failed, !ignored ignored) in !time sec (!perminute/min) - failure with '!name'";
  1023. $type = 'failed';
  1024. break;
  1025. case Migration::RESULT_INCOMPLETE:
  1026. $basetext = "Processed !numitems (!created created, !updated updated, !failed failed, !ignored ignored) in !time sec (!perminute/min) - continuing with '!name'";
  1027. $type = 'status';
  1028. break;
  1029. case Migration::RESULT_STOPPED:
  1030. $basetext = "Processed !numitems (!created created, !updated updated, !failed failed, !ignored ignored) in !time sec (!perminute/min) - stopped '!name'";
  1031. $type = 'warning';
  1032. break;
  1033. }
  1034. }
  1035. else {
  1036. switch ($result) {
  1037. case Migration::RESULT_COMPLETED:
  1038. $basetext = "Rolled back !numitems in !time sec (!perminute/min) - done with '!name'";
  1039. $type = 'completed';
  1040. break;
  1041. case Migration::RESULT_FAILED:
  1042. $basetext = "Rolled back !numitems in !time sec (!perminute/min) - failure with '!name'";
  1043. $type = 'failed';
  1044. break;
  1045. case Migration::RESULT_INCOMPLETE:
  1046. $basetext = "Rolled back !numitems in !time sec (!perminute/min) - continuing with '!name'";
  1047. $type = 'status';
  1048. break;
  1049. case Migration::RESULT_STOPPED:
  1050. $basetext = "Rolled back !numitems in !time sec (!perminute/min) - stopped '!name'";
  1051. $type = 'warning';
  1052. break;
  1053. }
  1054. }
  1055. $numitems = $this->processed_since_feedback + $this->source->getIgnored();
  1056. $message = t($basetext,
  1057. array(
  1058. '!numitems' => $numitems,
  1059. '!successes' => $this->successes_since_feedback,
  1060. '!failed' => $this->processed_since_feedback - $this->successes_since_feedback,
  1061. '!created' => $this->destination->getCreated(),
  1062. '!updated' => $this->destination->getUpdated(),
  1063. '!ignored' => $this->source->getIgnored(),
  1064. '!time' => $time,
  1065. '!perminute' => $perminute,
  1066. '!name' => $this->machineName,
  1067. ));
  1068. self::displayMessage($message, $type);
  1069. // Report on lookup_cache hit rate. Only visible at 'debug' level.
  1070. if ($result != Migration::RESULT_INCOMPLETE && !empty($this->counts['lookup_cache'])) {
  1071. foreach ($this->counts['lookup_cache'] as $name => $tallies) {
  1072. $tallies += array(
  1073. 'hit' => 0,
  1074. 'miss_hit' => 0,
  1075. 'miss_miss' => 0,
  1076. ); // Set defaults to avoid NOTICE.
  1077. $sum = $tallies['hit'] + $tallies['miss_hit'] + $tallies['miss_miss'];
  1078. self::displayMessage(
  1079. t('Lookup cache: !mn SM=!name !hit hit, !miss_hit miss_hit, !miss_miss miss_miss (!total total).', array(
  1080. '!mn' => $this->machineName,
  1081. '!name' => $name,
  1082. '!hit' => round((100 * $tallies['hit']) / $sum) . '%',
  1083. '!miss_hit' => round((100 * $tallies['miss_hit']) / $sum) . '%',
  1084. '!miss_miss' => round((100 * $tallies['miss_miss']) / $sum) . '%',
  1085. '!total' => $sum,
  1086. )), 'debug');
  1087. }
  1088. $this->counts['lookup_cache'] = array();
  1089. }
  1090. if ($result == Migration::RESULT_INCOMPLETE) {
  1091. $this->lastfeedback = time();
  1092. $this->processed_since_feedback = $this->successes_since_feedback = 0;
  1093. $this->source->resetStats();
  1094. $this->destination->resetStats();
  1095. }
  1096. }
  1097. /**
  1098. * Standard top-of-loop stuff, common between rollback and import - check
  1099. * for exceptional conditions, and display feedback.
  1100. */
  1101. protected function checkStatus() {
  1102. if ($this->memoryExceeded()) {
  1103. return MigrationBase::RESULT_INCOMPLETE;
  1104. }
  1105. if ($this->timeExceeded()) {
  1106. return MigrationBase::RESULT_INCOMPLETE;
  1107. }
  1108. if ($this->getStatus() == Migration::STATUS_STOPPING) {
  1109. return MigrationBase::RESULT_STOPPED;
  1110. }
  1111. // If feedback is requested, produce a progress message at the proper time
  1112. if (isset($this->feedback)) {
  1113. if (($this->feedback_unit == 'seconds' && time() - $this->lastfeedback >= $this->feedback) ||
  1114. ($this->feedback_unit == 'items' && $this->processed_since_feedback >= $this->feedback)) {
  1115. $this->progressMessage(MigrationBase::RESULT_INCOMPLETE);
  1116. }
  1117. }
  1118. return MigrationBase::RESULT_COMPLETED;
  1119. }
  1120. /**
  1121. * Apply field mappings to a data row received from the source, returning
  1122. * a populated destination object.
  1123. */
  1124. protected function applyMappings() {
  1125. $this->destinationValues = new stdClass;
  1126. foreach ($this->getFieldMappings() as $mapping) {
  1127. $destination = $mapping->getDestinationField();
  1128. // Skip mappings with no destination (source fields marked DNM)
  1129. if ($destination) {
  1130. $source = $mapping->getSourceField();
  1131. $default = $mapping->getDefaultValue();
  1132. // When updating existing items, make sure we don't create a destination
  1133. // field that is not mapped to anything (a source field or a default value)
  1134. if (!$source && !isset($default)) {
  1135. continue;
  1136. }
  1137. $destination_values = NULL;
  1138. // If there's a source mapping, and a source value in the data row, copy
  1139. // to the destination
  1140. if ($source && isset($this->sourceValues->{$source})) {
  1141. $destination_values = $this->sourceValues->{$source};
  1142. }
  1143. // Otherwise, apply the default value (if any)
  1144. elseif (!is_null($default)) {
  1145. $destination_values = $default;
  1146. }
  1147. // If there's a separator specified for this destination, then it
  1148. // will be populated as an array exploded from the source value
  1149. $separator = $mapping->getSeparator();
  1150. if ($separator && isset($destination_values)) {
  1151. if (is_array($separator)) {
  1152. if (isset($separator['group separator'])) {
  1153. $destination_values = explode($separator['group separator'], $destination_values);
  1154. }
  1155. else {
  1156. $destination_values = array($destination_values);
  1157. }
  1158. foreach ($destination_values as $group => $value) {
  1159. if (isset($separator['key separator'])) {
  1160. $destination_values[$group] = explode($separator['key separator'], $value);
  1161. }
  1162. else {
  1163. $destination_values[$group] = array($value);
  1164. }
  1165. }
  1166. }
  1167. else {
  1168. $destination_values = explode($separator, $destination_values);
  1169. }
  1170. }
  1171. // If a source migration is supplied, use the current value for this field
  1172. // to look up a destination ID from the provided migration
  1173. $source_migration = $mapping->getSourceMigration();
  1174. if ($source_migration && isset($destination_values)) {
  1175. $destination_values = $this->handleSourceMigration($source_migration, $destination_values, $default, $this);
  1176. }
  1177. // Call any designated callbacks
  1178. $callbacks = $mapping->getCallbacks();
  1179. foreach ($callbacks as $callback) {
  1180. if (isset($destination_values)) {
  1181. $destination_values = call_user_func_array($callback['callback'], array_merge(array($destination_values), $callback['params']));
  1182. }
  1183. }
  1184. // If specified, assure a unique value for this property.
  1185. $dedupe = $mapping->getDedupe();
  1186. if ($dedupe && isset($destination_values)) {
  1187. $destination_values = $this->handleDedupe($dedupe, $destination_values);
  1188. }
  1189. // Assign any arguments
  1190. if (isset($destination_values)) {
  1191. $arguments = $mapping->getArguments();
  1192. if ($arguments) {
  1193. if (!is_array($destination_values)) {
  1194. $destination_values = array($destination_values);
  1195. }
  1196. // TODO: Stuffing arguments into the destination field is gross - can
  1197. // we come up with a better way to communicate them to the field
  1198. // handlers?
  1199. $destination_values['arguments'] = array();
  1200. foreach ($arguments as $argname => $destarg) {
  1201. if (is_array($destarg) && isset($destarg['source_field']) && property_exists($this->sourceValues, $destarg['source_field'])) {
  1202. $destination_values['arguments'][$argname] = $this->sourceValues->{$destarg['source_field']};
  1203. }
  1204. elseif (is_array($destarg) && isset($destarg['default_value'])) {
  1205. $destination_values['arguments'][$argname] = $destarg['default_value'];
  1206. }
  1207. else {
  1208. $destination_values['arguments'][$argname] = $destarg;
  1209. }
  1210. }
  1211. }
  1212. }
  1213. // Are we dealing with the primary value of the destination field, or a
  1214. // subfield?
  1215. $destination = explode(':', $destination);
  1216. // Count how many levels of fields are in the mapping. We'll use the
  1217. // last one.
  1218. $destination_count = count($destination);
  1219. $destination_field = $destination[0];
  1220. if ($destination_count == 2) {
  1221. $subfield = $destination[1];
  1222. // We're processing the subfield before the primary value, initialize it
  1223. if (!property_exists($this->destinationValues, $destination_field)) {
  1224. $this->destinationValues->{$destination_field} = array();
  1225. }
  1226. // We have a value, and need to convert to an array so we can add
  1227. // arguments.
  1228. elseif (!is_array($this->destinationValues->{$destination_field})) {
  1229. $this->destinationValues->{$destination_field} = array($this->destinationValues->{$destination_field});
  1230. }
  1231. // Add the subfield value to the arguments array.
  1232. $this->destinationValues->{$destination_field}['arguments'][$subfield] = $destination_values;
  1233. }
  1234. elseif ($destination_count == 3) {
  1235. $subfield2 = $destination[2];
  1236. // We're processing the subfield before the primary value, initialize it
  1237. if (!property_exists($this->destinationValues, $destination_field)) {
  1238. $this->destinationValues->{$destination_field} = array();
  1239. }
  1240. // We have a value, and need to convert to an array so we can add
  1241. // arguments.
  1242. elseif (!is_array($this->destinationValues->{$destination_field})) {
  1243. $this->destinationValues->{$destination_field} = array($this->destinationValues->{$destination_field});
  1244. }
  1245. if (!is_array($this->destinationValues->{$destination_field}['arguments'][$destination[1]])) {
  1246. // Convert first subfield level to an array so we can add to it.
  1247. $this->destinationValues->{$destination_field}['arguments'][$destination[1]] = array($this->destinationValues->{$destination_field}['arguments'][$destination[1]]);
  1248. }
  1249. // Add the subfield value to the arguments array.
  1250. $this->destinationValues->{$destination_field}['arguments'][$destination[1]]['arguments'][$subfield2] = $destination_values;
  1251. }
  1252. // Just the primary value, the first time through for this field, simply
  1253. // set it.
  1254. elseif (!property_exists($this->destinationValues, $destination_field)) {
  1255. $this->destinationValues->{$destination_field} = $destination_values;
  1256. }
  1257. // We've seen a subfield, so add as an array value.
  1258. else {
  1259. $this->destinationValues->{$destination_field} = array_merge(
  1260. (array) $destination_values, $this->destinationValues->{$destination_field});
  1261. }
  1262. }
  1263. }
  1264. }
  1265. /**
  1266. * Look up a value migrated in another migration.
  1267. *
  1268. * @param mixed $source_migrations
  1269. * An array of source migrations, or string for a single migration.
  1270. * @param mixed $source_keys
  1271. * Key(s) to be looked up against the source migration(s). This may be a
  1272. * simple value (one single-field key), an array of values (multiple
  1273. * single-field keys to each be looked up), or an array of arrays (multiple
  1274. * multi-field keys to each be looked up).
  1275. * @param mixed $default
  1276. * The default value, if no ID was found.
  1277. * @param $migration
  1278. * The implementing migration.
  1279. *
  1280. * @return
  1281. * Destination value(s) from the source migration(s), as a single value if
  1282. * a single key was passed in, or an array of values if there were multiple
  1283. * keys to look up.
  1284. */
  1285. protected function handleSourceMigration($source_migrations, $source_keys, $default = NULL, $migration = NULL) {
  1286. // Handle the source migration(s) as an array.
  1287. $source_migrations = (array) $source_migrations;
  1288. // We want to treat source keys consistently as an array of arrays (each
  1289. // representing one key).
  1290. if (is_array($source_keys)) {
  1291. if (empty($source_keys)) {
  1292. // Empty value should return empty results.
  1293. return NULL;
  1294. }
  1295. elseif (is_array(reset($source_keys))) {
  1296. // Already an array of key arrays, fall through
  1297. }
  1298. else {
  1299. // An array of single-key values - make each one an array
  1300. $new_source_keys = array();
  1301. foreach ($source_keys as $source_key) {
  1302. $new_source_keys[] = array($source_key);
  1303. }
  1304. $source_keys = $new_source_keys;
  1305. }
  1306. }
  1307. else {
  1308. // A simple value - make it an array within an array
  1309. $source_keys = array(array($source_keys));
  1310. }
  1311. // Instantiate each migration, and store back in the array.
  1312. foreach ($source_migrations as $key => $source_migration) {
  1313. $source_migrations[$key] = Migration::getInstance($source_migration);
  1314. if (!isset($source_migrations[$key])) {
  1315. MigrationBase::displayMessage(t('The @source cannot be resolved to a migration instance.',
  1316. array('@source' => $source_migration)));
  1317. unset($source_migrations[$key]);
  1318. }
  1319. }
  1320. $results = array();
  1321. // Each $source_key will be an array of key values
  1322. foreach ($source_keys as $source_key) {
  1323. // If any source keys are NULL, skip this set
  1324. $continue = FALSE;
  1325. foreach ($source_key as $value) {
  1326. if (!isset($value)) {
  1327. $continue = TRUE;
  1328. break;
  1329. }
  1330. }
  1331. // Occasionally $source_key comes through with an empty string.
  1332. $sanity_check = array_filter($source_key);
  1333. if ($continue || empty($source_key) || empty($sanity_check)) {
  1334. continue;
  1335. }
  1336. // Loop through each source migration, checking for an existing dest ID.
  1337. foreach ($source_migrations as $source_migration) {
  1338. // Break out of the loop as soon as a destination ID is found.
  1339. if ($destids = $source_migration->getMap()
  1340. ->lookupDestinationID($source_key)) {
  1341. if (!empty($destids['destid1'])) {
  1342. break;
  1343. }
  1344. }
  1345. }
  1346. // If no destination ID was found, give each source migration a chance to
  1347. // create a stub.
  1348. if (!$destids) {
  1349. foreach ($source_migrations as $source_migration) {
  1350. // Is this a self reference?
  1351. if ($source_migration->machineName == $this->machineName) {
  1352. if (!array_diff($source_key, $this->currentSourceKey())) {
  1353. $destids = array();
  1354. $this->needsUpdate = MigrateMap::STATUS_NEEDS_UPDATE;
  1355. break;
  1356. }
  1357. }
  1358. // Break out of the loop if a stub was successfully created.
  1359. if ($destids = $source_migration->createStubWrapper($source_key, $migration)) {
  1360. break;
  1361. }
  1362. }
  1363. }
  1364. if ($destids) {
  1365. // Assume that if the destination key is a single value, it
  1366. // should be passed as such
  1367. if (count($destids) == 1) {
  1368. $results[] = reset($destids);
  1369. }
  1370. else {
  1371. $results[] = $destids;
  1372. }
  1373. }
  1374. // If no match found, apply the default value (if any)
  1375. elseif (!is_null($default)) {
  1376. $results[] = $default;
  1377. }
  1378. }
  1379. // Return a single result if we had a single key
  1380. if (count($source_keys) > 1) {
  1381. return $results;
  1382. }
  1383. else {
  1384. $value = reset($results);
  1385. return empty($value) && $value !== 0 && $value !== '0' ? NULL : $value;
  1386. }
  1387. }
  1388. /**
  1389. * For fields which require uniqueness, assign a new unique value if
  1390. * necessary.
  1391. *
  1392. * @param array $dedupe
  1393. * An array with two keys, 'table' the name of the Drupal table and 'column'
  1394. * the column within that table where uniqueness must be maintained.
  1395. * @param $original
  1396. * The value coming in, which must be checked for uniqueness.
  1397. *
  1398. * @return string
  1399. * The value to use - either the original, or a variation created by
  1400. * appending a sequence number.
  1401. */
  1402. protected function handleDedupe($dedupe, $original) {
  1403. // If we're remigrating a previously-existing value, simply running through
  1404. // our normal process will re-dedupe it - we must be sure to preserve the
  1405. // previously-written value. Note that this means that you cannot migrate
  1406. // changes to this field - the originally-migrated value will always
  1407. // remain, because we can't tell what the original was.
  1408. if (isset($this->sourceValues->migrate_map_destid1)) {
  1409. $key_field = key($this->destination->getKeySchema());
  1410. $existing_value = db_select($dedupe['table'], 't')
  1411. ->fields('t', array($dedupe['column']))
  1412. ->range(0, 1)
  1413. ->condition($key_field, $this->sourceValues->migrate_map_destid1)
  1414. ->execute()
  1415. ->fetchField();
  1416. // Note that if, for some reason, we don't find a value, fall through
  1417. // to the normal deduping process
  1418. if ($existing_value) {
  1419. return $existing_value;
  1420. }
  1421. }
  1422. $i = 1;
  1423. $candidate = $original;
  1424. while (db_select($dedupe['table'], 't')
  1425. ->fields('t', array($dedupe['column']))
  1426. ->range(0, 1)
  1427. ->condition('t.' . $dedupe['column'], $candidate)
  1428. ->execute()
  1429. ->rowCount() > 0) {
  1430. // We already have the candidate value. Find a non-existing value.
  1431. $i++;
  1432. // @TODO: support custom replacement pattern instead of just append.
  1433. $candidate = $original . '_' . $i;
  1434. }
  1435. if ($i > 1) {
  1436. $message = t('Replacing !column !original with !candidate',
  1437. array(
  1438. '!column' => $dedupe['column'],
  1439. '!original' => $original,
  1440. '!candidate' => $candidate,
  1441. ));
  1442. $migration = Migration::currentMigration();
  1443. $migration->saveMessage($message, Migration::MESSAGE_INFORMATIONAL);
  1444. }
  1445. return $candidate;
  1446. }
  1447. /**
  1448. * If stub creation is enabled, try to create a stub and save the mapping.
  1449. */
  1450. protected function createStubWrapper(array $source_key, $migration = NULL) {
  1451. if (method_exists($this, 'createStub')) {
  1452. $destids = $this->createStub($migration, $source_key);
  1453. if ($destids) {
  1454. // Fake a data row with the source key in it
  1455. $map_source_key = $this->map->getSourceKey();
  1456. $data_row = new stdClass;
  1457. $i = 0;
  1458. foreach ($map_source_key as $key => $definition) {
  1459. $data_row->{$key} = $source_key[$i++];
  1460. }
  1461. $this->map->saveIDMapping($data_row, $destids,
  1462. MigrateMap::STATUS_NEEDS_UPDATE, $this->defaultRollbackAction);
  1463. }
  1464. }
  1465. else {
  1466. $destids = NULL;
  1467. }
  1468. return $destids;
  1469. }
  1470. /**
  1471. * Pass messages through to the map class.
  1472. *
  1473. * @param string $message
  1474. * The message to record.
  1475. * @param int $level
  1476. * Optional message severity (defaults to MESSAGE_ERROR).
  1477. */
  1478. public function saveMessage($message, $level = MigrationBase::MESSAGE_ERROR) {
  1479. $this->map->saveMessage($this->currentSourceKey(), $message, $level);
  1480. }
  1481. /**
  1482. * Queue messages to be later saved through the map class.
  1483. *
  1484. * @param string $message
  1485. * The message to record.
  1486. * @param int $level
  1487. * Optional message severity (defaults to MESSAGE_ERROR).
  1488. */
  1489. public function queueMessage($message, $level = MigrationBase::MESSAGE_ERROR) {
  1490. $this->queuedMessages[] = array('message' => $message, 'level' => $level);
  1491. }
  1492. /**
  1493. * Save any messages we've queued up to the message table.
  1494. */
  1495. public function saveQueuedMessages() {
  1496. foreach ($this->queuedMessages as $queued_message) {
  1497. $this->saveMessage($queued_message['message'], $queued_message['level']);
  1498. }
  1499. $this->queuedMessages = array();
  1500. }
  1501. /**
  1502. * Set the specified row to be updated, if it exists.
  1503. */
  1504. public function setUpdate(array $source_key = NULL) {
  1505. if (!$source_key) {
  1506. $source_key = $this->currentSourceKey();
  1507. }
  1508. $this->map->setUpdate($source_key);
  1509. }
  1510. }
  1511. /**
  1512. * @deprecated - This class is no longer necessary, inherit directly from
  1513. * Migration instead.
  1514. */
  1515. abstract class DynamicMigration extends Migration {
  1516. static $deprecationWarning = FALSE;
  1517. public function __construct($arguments) {
  1518. parent::__construct($arguments);
  1519. if (variable_get('migrate_deprecation_warnings', 1) &&
  1520. !self::$deprecationWarning) {
  1521. self::displayMessage(t('The DynamicMigration class is no longer necessary and is now deprecated - please derive your migration classes directly from Migration.'));
  1522. self::$deprecationWarning = TRUE;
  1523. }
  1524. }
  1525. /**
  1526. * Overrides default of FALSE
  1527. */
  1528. static public function isDynamic() {
  1529. return TRUE;
  1530. }
  1531. }