3 namespace Drupal\migrate_drupal_ui\Batch;
6 use Drupal\Core\StringTranslation\PluralTranslatableMarkup;
7 use Drupal\Core\StringTranslation\TranslatableMarkup;
9 use Drupal\migrate\Plugin\MigrationInterface;
10 use Drupal\migrate\Event\MigrateEvents;
11 use Drupal\migrate\Event\MigrateIdMapMessageEvent;
12 use Drupal\migrate\Event\MigrateMapDeleteEvent;
13 use Drupal\migrate\Event\MigrateMapSaveEvent;
14 use Drupal\migrate\Event\MigratePostRowSaveEvent;
15 use Drupal\migrate\Event\MigrateRowDeleteEvent;
16 use Drupal\migrate\MigrateExecutable;
19 * Runs a single migration batch.
21 class MigrateUpgradeImportBatch {
24 * Maximum number of previous messages to display.
26 const MESSAGE_LENGTH = 20;
29 * The processed items for one batch of a given migration.
33 protected static $numProcessed = 0;
36 * Ensure we only add the listeners once per request.
40 protected static $listenersAdded = FALSE;
43 * The maximum length in seconds to allow processing in a request.
49 protected static $maxExecTime;
52 * MigrateMessage instance to capture messages during the migration process.
54 * @var \Drupal\migrate_drupal_ui\Batch\MigrateMessageCapture
56 protected static $messages;
59 * Runs a single migrate batch import.
61 * @param int[] $initial_ids
62 * The full set of migration IDs to import.
63 * @param array $config
64 * An array of additional configuration from the form.
65 * @param array $context
68 public static function run($initial_ids, $config, &$context) {
69 if (!static::$listenersAdded) {
70 $event_dispatcher = \Drupal::service('event_dispatcher');
71 $event_dispatcher->addListener(MigrateEvents::POST_ROW_SAVE, [static::class, 'onPostRowSave']);
72 $event_dispatcher->addListener(MigrateEvents::MAP_SAVE, [static::class, 'onMapSave']);
73 $event_dispatcher->addListener(MigrateEvents::IDMAP_MESSAGE, [static::class, 'onIdMapMessage']);
75 static::$maxExecTime = ini_get('max_execution_time');
76 if (static::$maxExecTime <= 0) {
77 static::$maxExecTime = 60;
79 // Set an arbitrary threshold of 3 seconds (e.g., if max_execution_time is
80 // 45 seconds, we will quit at 42 seconds so a slow item or cleanup
81 // overhead don't put us over 45).
82 static::$maxExecTime -= 3;
83 static::$listenersAdded = TRUE;
85 if (!isset($context['sandbox']['migration_ids'])) {
86 $context['sandbox']['max'] = count($initial_ids);
87 $context['sandbox']['current'] = 1;
88 // Total number processed for this migration.
89 $context['sandbox']['num_processed'] = 0;
90 // migration_ids will be the list of IDs remaining to run.
91 $context['sandbox']['migration_ids'] = $initial_ids;
92 $context['sandbox']['messages'] = [];
93 $context['results']['failures'] = 0;
94 $context['results']['successes'] = 0;
97 // Number processed in this batch.
98 static::$numProcessed = 0;
100 $migration_id = reset($context['sandbox']['migration_ids']);
101 $definition = \Drupal::service('plugin.manager.migration')->getDefinition($migration_id);
104 // @todo Find a way to avoid this in https://www.drupal.org/node/2804611.
105 if ($definition['destination']['plugin'] === 'entity:file') {
106 // Make sure we have a single trailing slash.
107 if ($definition['source']['plugin'] === 'd7_file_private') {
108 $configuration['source']['constants']['source_base_path'] = rtrim($config['source_private_file_path'], '/') . '/';
110 $configuration['source']['constants']['source_base_path'] = rtrim($config['source_base_path'], '/') . '/';
113 /** @var \Drupal\migrate\Plugin\Migration $migration */
114 $migration = \Drupal::service('plugin.manager.migration')->createInstance($migration_id, $configuration);
117 static::$messages = new MigrateMessageCapture();
118 $executable = new MigrateExecutable($migration, static::$messages);
120 $migration_name = $migration->label() ? $migration->label() : $migration_id;
123 $migration_status = $executable->import();
125 catch (\Exception $e) {
126 \Drupal::logger('migrate_drupal_ui')->error($e->getMessage());
127 $migration_status = MigrationInterface::RESULT_FAILED;
130 switch ($migration_status) {
131 case MigrationInterface::RESULT_COMPLETED:
132 // Store the number processed in the sandbox.
133 $context['sandbox']['num_processed'] += static::$numProcessed;
134 $message = new PluralTranslatableMarkup(
135 $context['sandbox']['num_processed'], 'Upgraded @migration (processed 1 item total)', 'Upgraded @migration (processed @count items total)',
136 ['@migration' => $migration_name]);
137 $context['sandbox']['messages'][] = (string) $message;
138 \Drupal::logger('migrate_drupal_ui')->notice($message);
139 $context['sandbox']['num_processed'] = 0;
140 $context['results']['successes']++;
143 case MigrationInterface::RESULT_INCOMPLETE:
144 $context['sandbox']['messages'][] = (string) new PluralTranslatableMarkup(
145 static::$numProcessed, 'Continuing with @migration (processed 1 item)', 'Continuing with @migration (processed @count items)',
146 ['@migration' => $migration_name]);
147 $context['sandbox']['num_processed'] += static::$numProcessed;
150 case MigrationInterface::RESULT_STOPPED:
151 $context['sandbox']['messages'][] = (string) new TranslatableMarkup('Operation stopped by request');
154 case MigrationInterface::RESULT_FAILED:
155 $context['sandbox']['messages'][] = (string) new TranslatableMarkup('Operation on @migration failed', ['@migration' => $migration_name]);
156 $context['results']['failures']++;
157 \Drupal::logger('migrate_drupal_ui')->error('Operation on @migration failed', ['@migration' => $migration_name]);
160 case MigrationInterface::RESULT_SKIPPED:
161 $context['sandbox']['messages'][] = (string) new TranslatableMarkup('Operation on @migration skipped due to unfulfilled dependencies', ['@migration' => $migration_name]);
162 \Drupal::logger('migrate_drupal_ui')->error('Operation on @migration skipped due to unfulfilled dependencies', ['@migration' => $migration_name]);
165 case MigrationInterface::RESULT_DISABLED:
166 // Skip silently if disabled.
170 // Unless we're continuing on with this migration, take it off the list.
171 if ($migration_status != MigrationInterface::RESULT_INCOMPLETE) {
172 array_shift($context['sandbox']['migration_ids']);
173 $context['sandbox']['current']++;
176 // Add and log any captured messages.
177 foreach (static::$messages->getMessages() as $message) {
178 $context['sandbox']['messages'][] = (string) $message;
179 \Drupal::logger('migrate_drupal_ui')->error($message);
182 // Only display the last MESSAGE_LENGTH messages, in reverse order.
183 $message_count = count($context['sandbox']['messages']);
184 $context['message'] = '';
185 for ($index = max(0, $message_count - self::MESSAGE_LENGTH); $index < $message_count; $index++) {
186 $context['message'] = $context['sandbox']['messages'][$index] . "<br />\n" . $context['message'];
188 if ($message_count > self::MESSAGE_LENGTH) {
189 // Indicate there are earlier messages not displayed.
190 $context['message'] .= '…';
192 // At the top of the list, display the next one (which will be the one
193 // that is running while this message is visible).
194 if (!empty($context['sandbox']['migration_ids'])) {
195 $migration_id = reset($context['sandbox']['migration_ids']);
196 $migration = \Drupal::service('plugin.manager.migration')->createInstance($migration_id);
197 $migration_name = $migration->label() ? $migration->label() : $migration_id;
198 $context['message'] = (string) new TranslatableMarkup('Currently upgrading @migration (@current of @max total tasks)', [
199 '@migration' => $migration_name,
200 '@current' => $context['sandbox']['current'],
201 '@max' => $context['sandbox']['max'],
202 ]) . "<br />\n" . $context['message'];
206 array_shift($context['sandbox']['migration_ids']);
207 $context['sandbox']['current']++;
210 $context['finished'] = 1 - count($context['sandbox']['migration_ids']) / $context['sandbox']['max'];
214 * Callback executed when the Migrate Upgrade Import batch process completes.
216 * @param bool $success
217 * TRUE if batch successfully completed.
218 * @param array $results
220 * @param array $operations
221 * An array of methods run in the batch.
222 * @param string $elapsed
223 * The time to run the batch.
225 public static function finished($success, $results, $operations, $elapsed) {
226 $successes = $results['successes'];
227 $failures = $results['failures'];
229 // If we had any successes log that for the user.
230 if ($successes > 0) {
231 drupal_set_message(\Drupal::translation()
232 ->formatPlural($successes, 'Completed 1 upgrade task successfully', 'Completed @count upgrade tasks successfully'));
234 // If we had failures, log them and show the migration failed.
236 drupal_set_message(\Drupal::translation()
237 ->formatPlural($failures, '1 upgrade failed', '@count upgrades failed'));
238 drupal_set_message(t('Upgrade process not completed'), 'error');
241 // Everything went off without a hitch. We may not have had successes
242 // but we didn't have failures so this is fine.
243 drupal_set_message(t('Congratulations, you upgraded Drupal!'));
246 if (\Drupal::moduleHandler()->moduleExists('dblog')) {
247 $url = Url::fromRoute('migrate_drupal_ui.log');
248 drupal_set_message(Link::fromTextAndUrl(new TranslatableMarkup('Review the detailed upgrade log'), $url), $failures ? 'error' : 'status');
253 * Reacts to item import.
255 * @param \Drupal\migrate\Event\MigratePostRowSaveEvent $event
256 * The post-save event.
258 public static function onPostRowSave(MigratePostRowSaveEvent $event) {
259 // We want to interrupt this batch and start a fresh one.
260 if ((time() - REQUEST_TIME) > static::$maxExecTime) {
261 $event->getMigration()->interruptMigration(MigrationInterface::RESULT_INCOMPLETE);
266 * Reacts to item deletion.
268 * @param \Drupal\migrate\Event\MigrateRowDeleteEvent $event
269 * The post-save event.
271 public static function onPostRowDelete(MigrateRowDeleteEvent $event) {
272 // We want to interrupt this batch and start a fresh one.
273 if ((time() - REQUEST_TIME) > static::$maxExecTime) {
274 $event->getMigration()->interruptMigration(MigrationInterface::RESULT_INCOMPLETE);
279 * Counts up any map save events.
281 * @param \Drupal\migrate\Event\MigrateMapSaveEvent $event
284 public static function onMapSave(MigrateMapSaveEvent $event) {
285 static::$numProcessed++;
289 * Counts up any map delete events.
291 * @param \Drupal\migrate\Event\MigrateMapDeleteEvent $event
294 public static function onMapDelete(MigrateMapDeleteEvent $event) {
295 static::$numProcessed++;
299 * Displays any messages being logged to the ID map.
301 * @param \Drupal\migrate\Event\MigrateIdMapMessageEvent $event
304 public static function onIdMapMessage(MigrateIdMapMessageEvent $event) {
305 if ($event->getLevel() == MigrationInterface::MESSAGE_NOTICE || $event->getLevel() == MigrationInterface::MESSAGE_INFORMATIONAL) {
311 $source_id_string = implode(',', $event->getSourceIdValues());
312 $message = t('Source ID @source_id: @message', ['@source_id' => $source_id_string, '@message' => $event->getMessage()]);
313 static::$messages->display($message, $type);