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;
20 class MigrateExecutable extends MigrateExecutableBase {
23 * Counters of map statuses.
26 * Set of counters, keyed by MigrateIdMapInterface::STATUS_* constant.
28 protected $saveCounters = array(
29 MigrateIdMapInterface::STATUS_FAILED => 0,
30 MigrateIdMapInterface::STATUS_IGNORED => 0,
31 MigrateIdMapInterface::STATUS_IMPORTED => 0,
32 MigrateIdMapInterface::STATUS_NEEDS_UPDATE => 0,
36 * Counter of map deletions.
40 protected $deleteCounter = 0;
43 * Maximum number of items to process in this migration. 0 indicates no limit
48 protected $itemLimit = 0;
51 * Frequency (in items) at which progress messages should be emitted.
55 protected $feedback = 0;
58 * List of specific source IDs to import.
62 protected $idlist = [];
65 * Count of number of items processed so far in this migration.
68 protected $counter = 0;
71 * Whether the destination item exists before saving.
75 protected $preExistingItem = FALSE;
78 * List of event listeners we have registered.
82 protected $listeners = [];
87 public function __construct(MigrationInterface $migration, MigrateMessageInterface $message, array $options = []) {
88 parent::__construct($migration, $message);
89 if (isset($options['limit'])) {
90 $this->itemLimit = $options['limit'];
92 if (isset($options['feedback'])) {
93 $this->feedback = $options['feedback'];
95 if (isset($options['idlist'])) {
96 $this->idlist = explode(',', $options['idlist']);
97 array_walk($this->idlist , function(&$value, $key) {
98 $value = explode(':', $value);
102 $this->listeners[MigrateEvents::MAP_SAVE] = [$this, 'onMapSave'];
103 $this->listeners[MigrateEvents::MAP_DELETE] = [$this, 'onMapDelete'];
104 $this->listeners[MigrateEvents::POST_IMPORT] = [$this, 'onPostImport'];
105 $this->listeners[MigrateEvents::POST_ROLLBACK] = [$this, 'onPostRollback'];
106 $this->listeners[MigrateEvents::PRE_ROW_SAVE] = [$this, 'onPreRowSave'];
107 $this->listeners[MigrateEvents::POST_ROW_DELETE] = [$this, 'onPostRowDelete'];
108 $this->listeners[MigratePlusEvents::PREPARE_ROW] = [$this, 'onPrepareRow'];
109 foreach ($this->listeners as $event => $listener) {
110 \Drupal::service('event_dispatcher')->addListener($event, $listener);
115 * Count up any map save events.
117 * @param \Drupal\migrate\Event\MigrateMapSaveEvent $event
120 public function onMapSave(MigrateMapSaveEvent $event) {
121 // Only count saves for this migration.
122 if ($event->getMap()->getQualifiedMapTableName() == $this->migration->getIdMap()->getQualifiedMapTableName()) {
123 $fields = $event->getFields();
124 // Distinguish between creation and update.
125 if ($fields['source_row_status'] == MigrateIdMapInterface::STATUS_IMPORTED &&
126 $this->preExistingItem
128 $this->saveCounters[MigrateIdMapInterface::STATUS_NEEDS_UPDATE]++;
131 $this->saveCounters[$fields['source_row_status']]++;
137 * Count up any rollback events.
139 * @param \Drupal\migrate\Event\MigrateMapDeleteEvent $event
142 public function onMapDelete(MigrateMapDeleteEvent $event) {
143 $this->deleteCounter++;
147 * Return the number of items created.
151 public function getCreatedCount() {
152 return $this->saveCounters[MigrateIdMapInterface::STATUS_IMPORTED];
156 * Return the number of items updated.
160 public function getUpdatedCount() {
161 return $this->saveCounters[MigrateIdMapInterface::STATUS_NEEDS_UPDATE];
165 * Return the number of items ignored.
169 public function getIgnoredCount() {
170 return $this->saveCounters[MigrateIdMapInterface::STATUS_IGNORED];
174 * Return the number of items that failed.
178 public function getFailedCount() {
179 return $this->saveCounters[MigrateIdMapInterface::STATUS_FAILED];
183 * Return the total number of items processed. Note that STATUS_NEEDS_UPDATE
184 * is not counted, since this is typically set on stubs created as side
185 * effects, not on the primary item being imported.
189 public function getProcessedCount() {
190 return $this->saveCounters[MigrateIdMapInterface::STATUS_IMPORTED] +
191 $this->saveCounters[MigrateIdMapInterface::STATUS_NEEDS_UPDATE] +
192 $this->saveCounters[MigrateIdMapInterface::STATUS_IGNORED] +
193 $this->saveCounters[MigrateIdMapInterface::STATUS_FAILED];
197 * Return the number of items rolled back.
201 public function getRollbackCount() {
202 return $this->deleteCounter;
206 * Reset all the per-status counters to 0.
208 protected function resetCounters() {
209 foreach ($this->saveCounters as $status => $count) {
210 $this->saveCounters[$status] = 0;
212 $this->deleteCounter = 0;
216 * React to migration completion.
218 * @param \Drupal\migrate\Event\MigrateImportEvent $event
221 public function onPostImport(MigrateImportEvent $event) {
222 $migrate_last_imported_store = \Drupal::keyValue('migrate_last_imported');
223 $migrate_last_imported_store->set($event->getMigration()->id(), round(microtime(TRUE) * 1000));
224 $this->progressMessage();
225 $this->removeListeners();
229 * Clean up all our event listeners.
231 protected function removeListeners() {
232 foreach ($this->listeners as $event => $listener) {
233 \Drupal::service('event_dispatcher')->removeListener($event, $listener);
238 * Emit information on what we've done since the last feedback (or the
239 * beginning of this migration).
243 protected function progressMessage($done = TRUE) {
244 $processed = $this->getProcessedCount();
246 $singular_message = "Processed 1 item (@created created, @updated updated, @failures failed, @ignored ignored) - done with '@name'";
247 $plural_message = "Processed @numitems items (@created created, @updated updated, @failures failed, @ignored ignored) - done with '@name'";
250 $singular_message = "Processed 1 item (@created created, @updated updated, @failures failed, @ignored ignored) - continuing with '@name'";
251 $plural_message = "Processed @numitems items (@created created, @updated updated, @failures failed, @ignored ignored) - continuing with '@name'";
253 $this->message->display(\Drupal::translation()->formatPlural($processed,
254 $singular_message, $plural_message,
255 array('@numitems' => $processed,
256 '@created' => $this->getCreatedCount(),
257 '@updated' => $this->getUpdatedCount(),
258 '@failures' => $this->getFailedCount(),
259 '@ignored' => $this->getIgnoredCount(),
260 '@name' => $this->migration->id())));
264 * React to rollback completion.
266 * @param \Drupal\migrate\Event\MigrateRollbackEvent $event
269 public function onPostRollback(MigrateRollbackEvent $event) {
270 $this->rollbackMessage();
271 $this->removeListeners();
275 * Emit information on what we've done since the last feedback (or the
276 * beginning of this migration).
280 protected function rollbackMessage($done = TRUE) {
281 $rolled_back = $this->getRollbackCount();
283 $singular_message = "Rolled back 1 item - done with '@name'";
284 $plural_message = "Rolled back @numitems items - done with '@name'";
287 $singular_message = "Rolled back 1 item - continuing with '@name'";
288 $plural_message = "Rolled back @numitems items - continuing with '@name'";
290 $this->message->display(\Drupal::translation()->formatPlural($rolled_back,
291 $singular_message, $plural_message,
292 array('@numitems' => $rolled_back,
293 '@name' => $this->migration->id())));
297 * React to an item about to be imported.
299 * @param \Drupal\migrate\Event\MigratePreRowSaveEvent $event
300 * The pre-save event.
302 public function onPreRowSave(MigratePreRowSaveEvent $event) {
303 $id_map = $event->getRow()->getIdMap();
304 if (!empty($id_map['destid1'])) {
305 $this->preExistingItem = TRUE;
308 $this->preExistingItem = FALSE;
313 * React to item rollback.
315 * @param \Drupal\migrate\Event\MigrateRowDeleteEvent $event
316 * The post-save event.
318 public function onPostRowDelete(MigrateRowDeleteEvent $event) {
319 if ($this->feedback && ($this->deleteCounter) && $this->deleteCounter % $this->feedback == 0) {
320 $this->rollbackMessage(FALSE);
321 $this->resetCounters();
326 * React to a new row.
328 * @param \Drupal\migrate_plus\Event\MigratePrepareRowEvent $event
329 * The prepare-row event.
331 * @throws \Drupal\migrate\MigrateSkipRowException
334 public function onPrepareRow(MigratePrepareRowEvent $event) {
335 if (!empty($this->idlist)) {
336 $row = $event->getRow();
338 * @TODO replace for $source_id = $row->getSourceIdValues(); when https://www.drupal.org/node/2698023 is fixed
340 $migration = $event->getMigration();
341 $source_id = array_merge(array_flip(array_keys($migration->getSourcePlugin()
342 ->getIds())), $row->getSourceIdValues());
344 foreach ($this->idlist as $item) {
345 if (array_values($source_id) === $item) {
351 throw new MigrateSkipRowException(NULL, FALSE);
354 if ($this->feedback && ($this->counter) && $this->counter % $this->feedback == 0) {
355 $this->progressMessage(FALSE);
356 $this->resetCounters();
359 if ($this->itemLimit && ($this->getProcessedCount() + 1) >= $this->itemLimit) {
360 $event->getMigration()->interruptMigration(MigrationInterface::RESULT_COMPLETED);