data migration image

Drupal Migrate Tricks: Disabling rules before running a migration.

Learn how to disable rules in the most efficient way before running a data migration.

Photo of Salvador Molina Moreno
Mon, 2016-05-09 12:37By salva

Most Drupal sites make use of the Rules module in some way, and plenty of Drupal sites use the Migrate module to perform either one-off or regular migrations of external data into the site. Sometimes, the two modules can collide, in scenarios where the behavior provided by certain rules is not desired when the migrations are running. There are some ways to work around this situation. In this post I cover the one I think is the most efficient and reliable.

A real use case

Let's add some background information of what made me use this technique in a recent project. On one side, we had a rule to create several nodes every time a new user activated his account. On the other side, there were several user migrations to perform, with users coming from different sources of data, and processed as different migrations (some users would come from a database, whereas others would be provided via CSV files). However, none of the migrated users should trigger the creation of those nodes, so we had to find a way to disable those rules before executing the migrations.

It's worth mentioning that some of the migrations had to be executed just before releasing the site, but some wouldn't be processed until weeks later. This is an important detail, but bear with me, you'll see why later. With this in mind, let's jump into the implementation details:

First off, your migration class should extend the "Migration" class, provided by Migrate module itself. This is fairly standard, and all migrations normally extend it, so that shouldn't be a problem. All we have to do, is implement two simple methods: preImport() and postImport(). In the first, we'll get the list of rules to disable, and switch them off. In the second, we'll reuse that list to re-enable them. Let's see the code (explained below).

  /**
   * Runs before an import starts.
   *
   * Used to disable any rules which could cause problems during
   * the import.
   */
  public function preImport() {
    // Alter the ignored words provided by pathauto by default so that
    // clean urls can be generated for legacy contents.
    global $conf;
    $conf['pathauto_ignore_words'] = '';
 
    parent::preImport();
 
    if (!empty($this->arguments['disable_rules']) && module_exists('rules')) {
      if (!is_array($this->arguments['disable_rules'])) {
        $this->arguments['disable_rules'] = array($this->arguments['disable_rules']);
      }
 
      // Fetch only the rules that are actually active.
      $rules = db_select('rules_config', 'rc')
        ->fields('rc', array('name'))
        ->condition('name', $this->arguments['disable_rules'], 'IN')
        ->condition('active', 1)
        ->execute()
        ->fetchAll();
      $this->arguments['disable_rules'] = array();
      foreach ($rules as $rule) {
        $this->arguments['disable_rules'][] = $rule->name;
      }
 
      // Disable the relevant rules.
      if (!empty($this->arguments['disable_rules'])) {
        db_update('rules_config')
          ->fields(array('active' => 0))
          ->condition('name', $this->arguments['disable_rules'], 'IN')
          ->execute();
        rules_clear_cache(TRUE);
        self::displayMessage(t('Disabled selected rules for current migration: ' . implode(', ', $this->arguments['disable_rules'])));
      }
    }
  }
 
  /**
   * Ran after completion of a migration.
   *
   * Used to turn any rules that were disabled back on.
   */
  public function postImport() {
    parent::postImport();
 
    if (!empty($this->arguments['disable_rules']) && module_exists('rules')) {
      db_update('rules_config')
        ->fields(array('active' => 1))
        ->condition('name', $this->arguments['disable_rules'], 'IN')
        ->execute();
      rules_clear_cache(TRUE);
      self::displayMessage(t('Enabled rules that had been disabled before starting the migration: ' . implode(', ', $this->arguments['disable_rules'])));
    }
  }

 

Note how on the preImport() method, the list of rules to disable is not blindly trusted, and a query is used to check which of them are actually active. This ensures that if a rule which is disabled is passed by accident, it doesn't get enabled again when the migration job finishes. The postImport() method is even simpler. It just picks that curated list of the rules that were disabled, and enables them again.

You might have noticed the list of rules comes from $this->arguments['disable_rules']. So we need to pass the list of rules to the class constructor, as we would pass any other arguments to migrate classes. This is done when declaring your migrations hook_migrate_api(). Let's look at the migration array as it stands for the users example.

      'project_external_users' => array_merge($migration_arguments, array(
        'class_name' => 'ExternalUserMigration',
        'description' => t('Migration of User accounts from the External system.'),
        'source_file' => drupal_get_path('module', 'my_module') . '/csv/external_users.csv',
        // Do not create default lists for migrated users.
        'disable_rules'     => array(
          'rules_create_default_lists',
        ),
        'group_name' => 'project_group',
      )),

 

And that's all we need. This makes the process fully transparent to the person executing the migration. Before anything is done with the source data, rules are disabled, and after the migration has finished, they are enabled again. This ensures the site is left in the exact same state it was before migrating the data, and no one has to worry about it.

As an extra note, keep in mind code reuse is important. I didn't enter into the details of where you should write the new methods, but my advice is that you always have a small base class that all your migrations extend. It's very likely that at some point, all of them need some generic logic that applies to them all, so adding this base class at the beginning is better, even if it's empty, since it has zero overhead and will serve to avoid future refactoring. It will also provide a clean place to keep all the generic utilities and methods for your migrations, like this one.

Why this way?

Of course, you could avoid all this, and simply disable things manually before running a migration, and enable them afterwards. There are a few reasons why I'd recommend sticking to the code approach, though, some of which are context-based:

  • Zero-touch deployment of migrations: You just run the command and forget. This allows for easier integration with CI tools (e.g: Jenkins).
  • Avoid human errors: You might know some rules need disabling. Which doesn't mean you can't forget about it, unless you keep a migration checklist.
  • Removes the need for documentation: You might be sick, on holiday, or simply gone, and your colleagues will have to do the migration for you. Did you document the process for them? Sure you did... didn't you?
  • One of the migrations went well, but the next one will be in a few months (like our case). Even if the process is documented, it's better to save yourself some pain.

I'm sure that's enough to highlight the benefits. While I haven't tried this for Drupal 8 yet, there's likely to exist a similar approach to do the same thing. 

The code in this post has been contributed by a user of the Drupal community, and the aim of this post is purely to redistribute it with a real case as example, after having met some people that were not aware of this technique in situations where it would have helped them.