Using MigratePostRowSave Event to append data to a migrated item

Posted December 9th, 2019

The migrate system in Drupal 8 is very powerful. In very few lines of code, you can hook into various data sources, process and import data into Drupal.

One such important piece of data is carrying over URLs and redirects for legacy content. The good news is, redirects are entities in Drupal 8, so writing a migration specifically for those is pretty straightforward and can be ran after you have imported all other content. They look like any other migration definition, with your destination being entity:redirect. Here is a gist demonstrating this.

However, sometimes we are not able to acquire data from the source in the way that we need it. In this scenario, redirects are part of the item source record as an array. The migration yaml only processes 1 source to 1 destination for an entity. Even if you could use the sub_process plugin to stub redirect records and process them later, you would not be able to avoid them having the same source ID and likely not quite work as you'd expect. Plus, if you are doing on-going migrations instead of a one-time migration, you would create a coupling of 1+ migrations to each other every time you need to run them. So, how can we solve this?

Events to the Rescue

The Migrate system provides a number of events to listen for when running migrations. The key one to help us solve our problem here will be the MigratePostRowSaveEvent. This event fires after an item has been saved in a migration, which gives us an opportunity to also bring in our redirect data with the item.

First, we can make a modification to our migration(s) that have redirects provided, so we can check in our event whether or not to do any processing:

id: working_paper
label: Working Papers
migration_tags:
  - working papers
source:
  has_redirects: true
  plugin: url
  urls:
    - 'private://migration/working_paper.json'

...

With this setting in the source configuration, we can read it in our custom event.

Second, we provide the event subscriber, and some code to create Redirect entities: 

  
<?php

declare(strict_types = 1);

namespace Drupal\nber_migration\EventSubscriber;

use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\migrate\Event\EventBase;
use Drupal\migrate\Event\MigrateEvents;
use Drupal\migrate\Event\MigratePostRowSaveEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Drupal\redirect\Entity\Redirect;

/**
 * Class EntityRedirectMigrateSubscriber.
 *
 * @package Drupal\nber_migration\EventSubscriber
 */
class EntityRedirectMigrateSubscriber implements EventSubscriberInterface {

  /**
   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
   */
  protected $entityTypeManager;

  /**
   * EntityRedirectMigrateSubscriber constructor.
   *
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
   */
  public function __construct(EntityTypeManagerInterface $entity_type_manager) {
    $this->entityTypeManager = $entity_type_manager;
  }

  /**
   * Helper method to check if the current migration has redirects in its source.
   *
   * @param \Drupal\migrate\Event\EventBase $event
   *   The migrate event.
   *
   * @return bool
   *   True if the migration is configured with has_redirects.
   */
  protected function hasRedirects(EventBase $event) : bool {
    $migration = $event->getMigration();
    $source_configuration = $migration->getSourceConfiguration();
    return !empty($source_configuration['has_redirects']) && $source_configuration['has_redirects'] == TRUE;
  }

  /**
   * Maps the existing redirects to the new node id.
   *
   * @param \Drupal\migrate\Event\MigratePostRowSaveEvent $event
   *   The migrate post row save event.
   */
  public function onPostRowSave(MigratePostRowSaveEvent $event) : void {
    if ($this->hasRedirects($event)) {
      $row = $event->getRow();
      $source = $row->getSource();
      $id = $event->getDestinationIdValues();
      $id = reset($id);
      $redirects = $source["redirects"];

      if (count($redirects)) {
        foreach ($redirects as $redirect) {
          // check for duplicate first by path
          $redirect = ltrim($redirect, '/');
          $records = $this->entityTypeManager->getStorage('redirect')->loadByProperties(['redirect_source__path' => $redirect]);

          if (empty($records)) {
            Redirect::create([
              'redirect_source' => [
                'path' => $redirect,
                'query' => [],
              ],
              'redirect_redirect' => 'internal:/node/' . $id,
              'language' => 'en',
              'status_code' => '301',
            ])->save();
          }
        }
      }
    }
  }

  /**
   * {@inheritdoc}
   */
  public static function getSubscribedEvents() : array {
    $events = [];
    $events[MigrateEvents::POST_ROW_SAVE] = ['onPostRowSave'];
    return $events;
  }
}

Now when a row is migrated, any redirects on it are processed and created if they don't already exist.