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

One of those important data is URL transfer and redirects for legacy content. The good news is that redirects are entities in Drupal 8, so writing a migration specifically for them is pretty straightforward and can be run after you have imported the rest of the content. They look like any other migration definition, with their destination as entity:redirect.

However, sometimes we can't acquire data from the source the way we need it. In this scenario, the redirects are part of the element's source record as an array. The yaml migration only processes 1 source to 1 destination for an entity. Even if you could use the sub_process plugin to group the redirect records and process them later, you couldn't prevent them from having the same source ID and they probably wouldn't work as you expected. Also, if you are doing continuous migrations instead of a single 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 series of events to listen for when running migrations. The key to helping us solve our problem here will be MigratePostRowSaveEvent . This event fires after an item have been saved in a migration, allowing us to also incorporate our redirect data with the item.

First, we can modify our migrations that have redirects provided, so we can check in our case whether or not any processing is done:

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

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

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

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_source' => [
                'path' => $redirect,
                'query' => [],
              'redirect_redirect' => 'internal:/node/' . $id,
              'language' => 'en',
              'status_code' => '301',
   * {@inheritdoc}
  public static function getSubscribedEvents() : array {
    $events = [];
    $events[MigrateEvents::POST_ROW_SAVE] = ['onPostRowSave'];
    return $events;

Now, when a row is migrated, redirects are processed and created if they do not already exist.

Have Any Project in Mind?

If you want to do something in Drupal maybe you can hire me.

Either for consulting, development or maintenance of Drupal websites.