3 namespace Drupal\migrate_tools;
5 use Drupal\migrate\Event\MigratePreRowSaveEvent;
6 use Drupal\migrate\Event\MigrateRollbackEvent;
7 use Drupal\migrate\Event\MigrateRowDeleteEvent;
8 use Drupal\migrate\MigrateExecutable as MigrateExecutableBase;
9 use Drupal\migrate\MigrateMessageInterface;
10 use Drupal\migrate\Plugin\MigrationInterface;
11 use Drupal\migrate\MigrateSkipRowException;
12 use Drupal\migrate\Plugin\MigrateIdMapInterface;
13 use Drupal\migrate\Event\MigrateEvents;
14 use Drupal\migrate_plus\Event\MigrateEvents as MigratePlusEvents;
15 use Drupal\migrate\Event\MigrateMapSaveEvent;
16 use Drupal\migrate\Event\MigrateMapDeleteEvent;
17 use Drupal\migrate\Event\MigrateImportEvent;
18 use Drupal\migrate_plus\Event\MigratePrepareRowEvent;
21 * Defines a migrate executable class for drush.
23 class MigrateExecutable extends MigrateExecutableBase {
26 * Counters of map statuses.
29 * Set of counters, keyed by MigrateIdMapInterface::STATUS_* constant.
31 protected $saveCounters = [
32 MigrateIdMapInterface::STATUS_FAILED => 0,
33 MigrateIdMapInterface::STATUS_IGNORED => 0,
34 MigrateIdMapInterface::STATUS_IMPORTED => 0,
35 MigrateIdMapInterface::STATUS_NEEDS_UPDATE => 0,
39 * Counter of map saves, used to detect the item limit threshold.
43 protected $itemLimitCounter = 0;
46 * Counter of map deletions.
50 protected $deleteCounter = 0;
53 * Maximum number of items to process in this migration.
55 * 0 indicates no limit is to be applied.
59 protected $itemLimit = 0;
62 * Frequency (in items) at which progress messages should be emitted.
66 protected $feedback = 0;
69 * List of specific source IDs to import.
73 protected $idlist = [];
76 * Count of number of items processed so far in this migration.
80 protected $counter = 0;
83 * Whether the destination item exists before saving.
87 protected $preExistingItem = FALSE;
90 * List of event listeners we have registered.
94 protected $listeners = [];
99 public function __construct(MigrationInterface $migration, MigrateMessageInterface $message, array $options = []) {
100 parent::__construct($migration, $message);
101 if (isset($options['limit'])) {
102 $this->itemLimit = $options['limit'];
104 if (isset($options['feedback'])) {
105 $this->feedback = $options['feedback'];
107 if (isset($options['idlist'])) {
108 if (is_string($options['idlist'])) {
109 $this->idlist = explode(',', $options['idlist']);
110 array_walk($this->idlist, function (&$value, $key) {
111 $value = explode(':', $value);
116 $this->listeners[MigrateEvents::MAP_SAVE] = [$this, 'onMapSave'];
117 $this->listeners[MigrateEvents::MAP_DELETE] = [$this, 'onMapDelete'];
118 $this->listeners[MigrateEvents::POST_IMPORT] = [$this, 'onPostImport'];
119 $this->listeners[MigrateEvents::POST_ROLLBACK] = [$this, 'onPostRollback'];
120 $this->listeners[MigrateEvents::PRE_ROW_SAVE] = [$this, 'onPreRowSave'];
121 $this->listeners[MigrateEvents::POST_ROW_DELETE] = [$this, 'onPostRowDelete'];
122 $this->listeners[MigratePlusEvents::PREPARE_ROW] = [$this, 'onPrepareRow'];
123 foreach ($this->listeners as $event => $listener) {
124 \Drupal::service('event_dispatcher')->addListener($event, $listener);
129 * Count up any map save events.
131 * @param \Drupal\migrate\Event\MigrateMapSaveEvent $event
134 public function onMapSave(MigrateMapSaveEvent $event) {
135 // Only count saves for this migration.
136 if ($event->getMap()->getQualifiedMapTableName() == $this->migration->getIdMap()->getQualifiedMapTableName()) {
137 $fields = $event->getFields();
138 $this->itemLimitCounter++;
139 // Distinguish between creation and update.
140 if ($fields['source_row_status'] == MigrateIdMapInterface::STATUS_IMPORTED &&
141 $this->preExistingItem
143 $this->saveCounters[MigrateIdMapInterface::STATUS_NEEDS_UPDATE]++;
146 $this->saveCounters[$fields['source_row_status']]++;
152 * Count up any rollback events.
154 * @param \Drupal\migrate\Event\MigrateMapDeleteEvent $event
157 public function onMapDelete(MigrateMapDeleteEvent $event) {
158 $this->deleteCounter++;
162 * Return the number of items created.
165 * The number of items created.
167 public function getCreatedCount() {
168 return $this->saveCounters[MigrateIdMapInterface::STATUS_IMPORTED];
172 * Return the number of items updated.
177 public function getUpdatedCount() {
178 return $this->saveCounters[MigrateIdMapInterface::STATUS_NEEDS_UPDATE];
182 * Return the number of items ignored.
187 public function getIgnoredCount() {
188 return $this->saveCounters[MigrateIdMapInterface::STATUS_IGNORED];
192 * Return the number of items that failed.
197 public function getFailedCount() {
198 return $this->saveCounters[MigrateIdMapInterface::STATUS_FAILED];
202 * Return the total number of items processed.
204 * Note that STATUS_NEEDS_UPDATE is not counted, since this is typically set
205 * on stubs created as side effects, not on the primary item being imported.
208 * The processed count.
210 public function getProcessedCount() {
211 return $this->saveCounters[MigrateIdMapInterface::STATUS_IMPORTED] +
212 $this->saveCounters[MigrateIdMapInterface::STATUS_NEEDS_UPDATE] +
213 $this->saveCounters[MigrateIdMapInterface::STATUS_IGNORED] +
214 $this->saveCounters[MigrateIdMapInterface::STATUS_FAILED];
218 * Return the number of items rolled back.
221 * The rollback count.
223 public function getRollbackCount() {
224 return $this->deleteCounter;
228 * Reset all the per-status counters to 0.
230 protected function resetCounters() {
231 foreach ($this->saveCounters as $status => $count) {
232 $this->saveCounters[$status] = 0;
234 $this->deleteCounter = 0;
238 * React to migration completion.
240 * @param \Drupal\migrate\Event\MigrateImportEvent $event
243 public function onPostImport(MigrateImportEvent $event) {
244 $migrate_last_imported_store = \Drupal::keyValue('migrate_last_imported');
245 $migrate_last_imported_store->set($event->getMigration()->id(), round(microtime(TRUE) * 1000));
246 $this->progressMessage();
247 $this->removeListeners();
251 * Clean up all our event listeners.
253 protected function removeListeners() {
254 foreach ($this->listeners as $event => $listener) {
255 \Drupal::service('event_dispatcher')->removeListener($event, $listener);
260 * Emit information on what we've done.
262 * Either since the last feedback or the beginning of this migration.
265 * TRUE if this is the last items to process. Otherwise FALSE.
267 protected function progressMessage($done = TRUE) {
268 $processed = $this->getProcessedCount();
270 $singular_message = "Processed 1 item (@created created, @updated updated, @failures failed, @ignored ignored) - done with '@name'";
271 $plural_message = "Processed @numitems items (@created created, @updated updated, @failures failed, @ignored ignored) - done with '@name'";
274 $singular_message = "Processed 1 item (@created created, @updated updated, @failures failed, @ignored ignored) - continuing with '@name'";
275 $plural_message = "Processed @numitems items (@created created, @updated updated, @failures failed, @ignored ignored) - continuing with '@name'";
277 $this->message->display(\Drupal::translation()->formatPlural($processed,
278 $singular_message, $plural_message,
280 '@numitems' => $processed,
281 '@created' => $this->getCreatedCount(),
282 '@updated' => $this->getUpdatedCount(),
283 '@failures' => $this->getFailedCount(),
284 '@ignored' => $this->getIgnoredCount(),
285 '@name' => $this->migration->id(),
291 * React to rollback completion.
293 * @param \Drupal\migrate\Event\MigrateRollbackEvent $event
296 public function onPostRollback(MigrateRollbackEvent $event) {
297 $this->rollbackMessage();
298 $this->removeListeners();
302 * Emit information on what we've done.
304 * Either since the last feedback or the beginning of this migration.
307 * TRUE if this is the last items to rollback. Otherwise FALSE.
309 protected function rollbackMessage($done = TRUE) {
310 $rolled_back = $this->getRollbackCount();
312 $singular_message = "Rolled back 1 item - done with '@name'";
313 $plural_message = "Rolled back @numitems items - done with '@name'";
316 $singular_message = "Rolled back 1 item - continuing with '@name'";
317 $plural_message = "Rolled back @numitems items - continuing with '@name'";
319 $this->message->display(\Drupal::translation()->formatPlural($rolled_back,
320 $singular_message, $plural_message,
322 '@numitems' => $rolled_back,
323 '@name' => $this->migration->id(),
329 * React to an item about to be imported.
331 * @param \Drupal\migrate\Event\MigratePreRowSaveEvent $event
332 * The pre-save event.
334 public function onPreRowSave(MigratePreRowSaveEvent $event) {
335 $id_map = $event->getRow()->getIdMap();
336 if (!empty($id_map['destid1'])) {
337 $this->preExistingItem = TRUE;
340 $this->preExistingItem = FALSE;
345 * React to item rollback.
347 * @param \Drupal\migrate\Event\MigrateRowDeleteEvent $event
348 * The post-save event.
350 public function onPostRowDelete(MigrateRowDeleteEvent $event) {
351 if ($this->feedback && ($this->deleteCounter) && $this->deleteCounter % $this->feedback == 0) {
352 $this->rollbackMessage(FALSE);
353 $this->resetCounters();
358 * React to a new row.
360 * @param \Drupal\migrate_plus\Event\MigratePrepareRowEvent $event
361 * The prepare-row event.
363 * @throws \Drupal\migrate\MigrateSkipRowException
365 public function onPrepareRow(MigratePrepareRowEvent $event) {
366 if (!empty($this->idlist)) {
367 $row = $event->getRow();
368 // TODO: replace for $source_id = $row->getSourceIdValues();
369 // when https://www.drupal.org/node/2698023 is fixed.
370 $migration = $event->getMigration();
371 $source_id = array_merge(array_flip(array_keys($migration->getSourcePlugin()
372 ->getIds())), $row->getSourceIdValues());
374 foreach ($this->idlist as $item) {
375 if (array_values($source_id) == $item) {
381 throw new MigrateSkipRowException(NULL, FALSE);
384 if ($this->feedback && ($this->counter) && $this->counter % $this->feedback == 0) {
385 $this->progressMessage(FALSE);
386 $this->resetCounters();
389 if ($this->itemLimit && ($this->itemLimitCounter + 1) >= $this->itemLimit) {
390 $event->getMigration()->interruptMigration(MigrationInterface::RESULT_COMPLETED);