MigrateUpgradeImportBatch.php 14.1 KB
Newer Older
1 2
<?php

3
namespace Drupal\migrate_drupal_ui\Batch;
4 5

use Drupal\Core\Link;
6 7
use Drupal\Core\StringTranslation\PluralTranslatableMarkup;
use Drupal\Core\StringTranslation\TranslatableMarkup;
8
use Drupal\Core\Url;
9
use Drupal\migrate\Plugin\MigrationInterface;
10 11
use Drupal\migrate\Event\MigrateEvents;
use Drupal\migrate\Event\MigrateIdMapMessageEvent;
12
use Drupal\migrate\Event\MigrateImportEvent;
13 14 15 16 17
use Drupal\migrate\Event\MigrateMapDeleteEvent;
use Drupal\migrate\Event\MigrateMapSaveEvent;
use Drupal\migrate\Event\MigratePostRowSaveEvent;
use Drupal\migrate\Event\MigrateRowDeleteEvent;
use Drupal\migrate\MigrateExecutable;
18
use Drupal\migrate_drupal\Plugin\MigrationWithFollowUpInterface;
19 20 21 22

/**
 * Runs a single migration batch.
 */
23
class MigrateUpgradeImportBatch {
24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55

  /**
   * Maximum number of previous messages to display.
   */
  const MESSAGE_LENGTH = 20;

  /**
   * The processed items for one batch of a given migration.
   *
   * @var int
   */
  protected static $numProcessed = 0;

  /**
   * Ensure we only add the listeners once per request.
   *
   * @var bool
   */
  protected static $listenersAdded = FALSE;

  /**
   * The maximum length in seconds to allow processing in a request.
   *
   * @see self::run()
   *
   * @var int
   */
  protected static $maxExecTime;

  /**
   * MigrateMessage instance to capture messages during the migration process.
   *
56
   * @var \Drupal\migrate_drupal_ui\Batch\MigrateMessageCapture
57 58 59
   */
  protected static $messages;

60 61 62
  /**
   * The follow-up migrations.
   *
63
   * @var \Drupal\migrate\Plugin\MigrationInterface[]
64 65 66
   */
  protected static $followUpMigrations;

67
  /**
68
   * Runs a single migrate batch import.
69 70 71
   *
   * @param int[] $initial_ids
   *   The full set of migration IDs to import.
72 73
   * @param array $config
   *   An array of additional configuration from the form.
74 75 76
   * @param array $context
   *   The batch context.
   */
77
  public static function run($initial_ids, $config, &$context) {
78 79
    if (!static::$listenersAdded) {
      $event_dispatcher = \Drupal::service('event_dispatcher');
80
      $event_dispatcher->addListener(MigrateEvents::POST_ROW_SAVE, [static::class, 'onPostRowSave']);
81
      $event_dispatcher->addListener(MigrateEvents::POST_IMPORT, [static::class, 'onPostImport']);
82 83 84
      $event_dispatcher->addListener(MigrateEvents::MAP_SAVE, [static::class, 'onMapSave']);
      $event_dispatcher->addListener(MigrateEvents::IDMAP_MESSAGE, [static::class, 'onIdMapMessage']);

85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110
      static::$maxExecTime = ini_get('max_execution_time');
      if (static::$maxExecTime <= 0) {
        static::$maxExecTime = 60;
      }
      // Set an arbitrary threshold of 3 seconds (e.g., if max_execution_time is
      // 45 seconds, we will quit at 42 seconds so a slow item or cleanup
      // overhead don't put us over 45).
      static::$maxExecTime -= 3;
      static::$listenersAdded = TRUE;
    }
    if (!isset($context['sandbox']['migration_ids'])) {
      $context['sandbox']['max'] = count($initial_ids);
      $context['sandbox']['current'] = 1;
      // Total number processed for this migration.
      $context['sandbox']['num_processed'] = 0;
      // migration_ids will be the list of IDs remaining to run.
      $context['sandbox']['migration_ids'] = $initial_ids;
      $context['sandbox']['messages'] = [];
      $context['results']['failures'] = 0;
      $context['results']['successes'] = 0;
    }

    // Number processed in this batch.
    static::$numProcessed = 0;

    $migration_id = reset($context['sandbox']['migration_ids']);
111 112
    $definition = \Drupal::service('plugin.manager.migration')->getDefinition($migration_id);
    $configuration = [];
113

114 115
    // @todo Find a way to avoid this in https://www.drupal.org/node/2804611.
    if ($definition['destination']['plugin'] === 'entity:file') {
116
      // Make sure we have a single trailing slash.
117 118 119
      if ($definition['source']['plugin'] === 'd7_file_private') {
        $configuration['source']['constants']['source_base_path'] = rtrim($config['source_private_file_path'], '/') . '/';
      }
120
      $configuration['source']['constants']['source_base_path'] = rtrim($config['source_base_path'], '/') . '/';
121 122
    }

123 124 125
    /** @var \Drupal\migrate\Plugin\Migration $migration */
    $migration = \Drupal::service('plugin.manager.migration')->createInstance($migration_id, $configuration);

126 127 128 129 130 131 132
    if ($migration) {
      static::$messages = new MigrateMessageCapture();
      $executable = new MigrateExecutable($migration, static::$messages);

      $migration_name = $migration->label() ? $migration->label() : $migration_id;

      try {
133
        $migration_status = $executable->import();
134 135
      }
      catch (\Exception $e) {
136
        \Drupal::logger('migrate_drupal_ui')->error($e->getMessage());
137 138 139 140 141 142 143
        $migration_status = MigrationInterface::RESULT_FAILED;
      }

      switch ($migration_status) {
        case MigrationInterface::RESULT_COMPLETED:
          // Store the number processed in the sandbox.
          $context['sandbox']['num_processed'] += static::$numProcessed;
144 145 146
          $message = new PluralTranslatableMarkup(
            $context['sandbox']['num_processed'], 'Upgraded @migration (processed 1 item total)', 'Upgraded @migration (processed @count items total)',
            ['@migration' => $migration_name]);
147
          $context['sandbox']['messages'][] = (string) $message;
148
          \Drupal::logger('migrate_drupal_ui')->notice($message);
149 150
          $context['sandbox']['num_processed'] = 0;
          $context['results']['successes']++;
151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169

          // If the completed migration has any follow-up migrations, add them
          // to the batch migrations.
          // @see onPostImport()
          if (!empty(static::$followUpMigrations)) {
            foreach (static::$followUpMigrations as $migration_id => $migration) {
              if (!in_array($migration_id, $context['sandbox']['migration_ids'], TRUE)) {
                // Add the follow-up migration ID to the batch migration IDs for
                // later execution.
                $context['sandbox']['migration_ids'][] = $migration_id;
                // Increase the number of migrations in the batch to update the
                // progress bar and keep it accurate.
                $context['sandbox']['max']++;
                // Unset the follow-up migration to make sure it won't get added
                // to the batch twice.
                unset(static::$followUpMigrations[$migration_id]);
              }
            }
          }
170 171 172
          break;

        case MigrationInterface::RESULT_INCOMPLETE:
173 174 175
          $context['sandbox']['messages'][] = (string) new PluralTranslatableMarkup(
            static::$numProcessed, 'Continuing with @migration (processed 1 item)', 'Continuing with @migration (processed @count items)',
            ['@migration' => $migration_name]);
176 177 178 179
          $context['sandbox']['num_processed'] += static::$numProcessed;
          break;

        case MigrationInterface::RESULT_STOPPED:
180
          $context['sandbox']['messages'][] = (string) new TranslatableMarkup('Operation stopped by request');
181 182 183
          break;

        case MigrationInterface::RESULT_FAILED:
184
          $context['sandbox']['messages'][] = (string) new TranslatableMarkup('Operation on @migration failed', ['@migration' => $migration_name]);
185
          $context['results']['failures']++;
186
          \Drupal::logger('migrate_drupal_ui')->error('Operation on @migration failed', ['@migration' => $migration_name]);
187 188 189
          break;

        case MigrationInterface::RESULT_SKIPPED:
190
          $context['sandbox']['messages'][] = (string) new TranslatableMarkup('Operation on @migration skipped due to unfulfilled dependencies', ['@migration' => $migration_name]);
191
          \Drupal::logger('migrate_drupal_ui')->error('Operation on @migration skipped due to unfulfilled dependencies', ['@migration' => $migration_name]);
192 193 194 195 196 197 198 199 200 201 202 203 204 205 206
          break;

        case MigrationInterface::RESULT_DISABLED:
          // Skip silently if disabled.
          break;
      }

      // Unless we're continuing on with this migration, take it off the list.
      if ($migration_status != MigrationInterface::RESULT_INCOMPLETE) {
        array_shift($context['sandbox']['migration_ids']);
        $context['sandbox']['current']++;
      }

      // Add and log any captured messages.
      foreach (static::$messages->getMessages() as $message) {
207
        $context['sandbox']['messages'][] = (string) $message;
208
        \Drupal::logger('migrate_drupal_ui')->error($message);
209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224
      }

      // Only display the last MESSAGE_LENGTH messages, in reverse order.
      $message_count = count($context['sandbox']['messages']);
      $context['message'] = '';
      for ($index = max(0, $message_count - self::MESSAGE_LENGTH); $index < $message_count; $index++) {
        $context['message'] = $context['sandbox']['messages'][$index] . "<br />\n" . $context['message'];
      }
      if ($message_count > self::MESSAGE_LENGTH) {
        // Indicate there are earlier messages not displayed.
        $context['message'] .= '&hellip;';
      }
      // At the top of the list, display the next one (which will be the one
      // that is running while this message is visible).
      if (!empty($context['sandbox']['migration_ids'])) {
        $migration_id = reset($context['sandbox']['migration_ids']);
225
        $migration = \Drupal::service('plugin.manager.migration')->createInstance($migration_id);
226
        $migration_name = $migration->label() ? $migration->label() : $migration_id;
227
        $context['message'] = (string) new TranslatableMarkup('Currently upgrading @migration (@current of @max total tasks)', [
228 229 230 231 232 233 234 235 236 237 238 239 240 241 242
            '@migration' => $migration_name,
            '@current' => $context['sandbox']['current'],
            '@max' => $context['sandbox']['max'],
          ]) . "<br />\n" . $context['message'];
      }
    }
    else {
      array_shift($context['sandbox']['migration_ids']);
      $context['sandbox']['current']++;
    }

    $context['finished'] = 1 - count($context['sandbox']['migration_ids']) / $context['sandbox']['max'];
  }

  /**
243
   * Callback executed when the Migrate Upgrade Import batch process completes.
244
   *
245 246
   * @param bool $success
   *   TRUE if batch successfully completed.
247
   * @param array $results
248 249 250 251 252
   *   Batch results.
   * @param array $operations
   *   An array of methods run in the batch.
   * @param string $elapsed
   *   The time to run the batch.
253
   */
254
  public static function finished($success, $results, $operations, $elapsed) {
255 256 257 258 259
    $successes = $results['successes'];
    $failures = $results['failures'];

    // If we had any successes log that for the user.
    if ($successes > 0) {
260
      \Drupal::messenger()->addStatus(\Drupal::translation()
261
        ->formatPlural($successes, 'Completed 1 upgrade task successfully', 'Completed @count upgrade tasks successfully'));
262 263 264
    }
    // If we had failures, log them and show the migration failed.
    if ($failures > 0) {
265
      \Drupal::messenger()->addStatus(\Drupal::translation()
266
        ->formatPlural($failures, '1 upgrade failed', '@count upgrades failed'));
267
      \Drupal::messenger()->addError(t('Upgrade process not completed'));
268 269
    }
    else {
270 271
      // Everything went off without a hitch. We may not have had successes
      // but we didn't have failures so this is fine.
272
      \Drupal::messenger()->addStatus(t('Congratulations, you upgraded Drupal!'));
273 274 275 276
    }

    if (\Drupal::moduleHandler()->moduleExists('dblog')) {
      $url = Url::fromRoute('migrate_drupal_ui.log');
277
      \Drupal::messenger()->addMessage(Link::fromTextAndUrl(new TranslatableMarkup('Review the detailed upgrade log'), $url), $failures ? 'error' : 'status');
278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293
    }
  }

  /**
   * Reacts to item import.
   *
   * @param \Drupal\migrate\Event\MigratePostRowSaveEvent $event
   *   The post-save event.
   */
  public static function onPostRowSave(MigratePostRowSaveEvent $event) {
    // We want to interrupt this batch and start a fresh one.
    if ((time() - REQUEST_TIME) > static::$maxExecTime) {
      $event->getMigration()->interruptMigration(MigrationInterface::RESULT_INCOMPLETE);
    }
  }

294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311
  /**
   * Adds follow-up migrations.
   *
   * @param \Drupal\migrate\Event\MigrateImportEvent $event
   *   The import event.
   */
  public static function onPostImport(MigrateImportEvent $event) {
    $migration = $event->getMigration();
    if ($migration instanceof MigrationWithFollowUpInterface) {
      // After the migration on which they depend has been successfully
      // executed, the follow-up migrations are immediately added to the batch
      // and removed from the $followUpMigrations property. This means that the
      // $followUpMigrations property is always empty at this point and it's OK
      // to override it with the next follow-up migrations.
      static::$followUpMigrations = $migration->generateFollowUpMigrations();
    }
  }

312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358
  /**
   * Reacts to item deletion.
   *
   * @param \Drupal\migrate\Event\MigrateRowDeleteEvent $event
   *   The post-save event.
   */
  public static function onPostRowDelete(MigrateRowDeleteEvent $event) {
    // We want to interrupt this batch and start a fresh one.
    if ((time() - REQUEST_TIME) > static::$maxExecTime) {
      $event->getMigration()->interruptMigration(MigrationInterface::RESULT_INCOMPLETE);
    }
  }

  /**
   * Counts up any map save events.
   *
   * @param \Drupal\migrate\Event\MigrateMapSaveEvent $event
   *   The map event.
   */
  public static function onMapSave(MigrateMapSaveEvent $event) {
    static::$numProcessed++;
  }

  /**
   * Counts up any map delete events.
   *
   * @param \Drupal\migrate\Event\MigrateMapDeleteEvent $event
   *   The map event.
   */
  public static function onMapDelete(MigrateMapDeleteEvent $event) {
    static::$numProcessed++;
  }

  /**
   * Displays any messages being logged to the ID map.
   *
   * @param \Drupal\migrate\Event\MigrateIdMapMessageEvent $event
   *   The message event.
   */
  public static function onIdMapMessage(MigrateIdMapMessageEvent $event) {
    if ($event->getLevel() == MigrationInterface::MESSAGE_NOTICE || $event->getLevel() == MigrationInterface::MESSAGE_INFORMATIONAL) {
      $type = 'status';
    }
    else {
      $type = 'error';
    }
    $source_id_string = implode(',', $event->getSourceIdValues());
359
    $message = t('Source ID @source_id: @message', ['@source_id' => $source_id_string, '@message' => $event->getMessage()]);
360 361 362 363
    static::$messages->display($message, $type);
  }

}