Extending Drupal 6-to-8 upgrade to create media entities

How can one extend the Drupal 6-to-8 core upgrade functionality? Recently, I've been working with Drupal upgrades using the UI and found the process to be fairly smooth, but missing some key functionality, like media; at the same time, I've been writing a series of blogposts on the Drupal 8 APIs: it turns out that, to extend the process of upgrades to Drupal 8, we can bring these two threads together.

Existing limitations of Drupal 8 core upgrades

Drupal 8's core upgrade from 6 (and, experimentally, 7) can migrate files, but has no concept of "media": managed files, with substantial extra metadata that could be differently structured for images, spreadsheets, other files etc. In Drupal 7 this was provided by the media project, but in Drupal 8 a more lightweight media_entity project can be extended in the handful of ways you might require: for example, by media_entity_image for capturing image metadata.

However, even if you were to run your Drupal 8 upgrade, with media_entity installed, that third-party, contributed module has limited capacity to extend the core upgrades with its own. At the time of writing, 8.2.x hardcodes the "module upgrade paths" in the upgrade UI's form (click on "view source" to see this), making it impossible for contributed modules to extend the upgrade with entirely independent migrations of their own. There is an open issue to autodiscover this list of module upgrade paths, which might unlock the upgrade to contributed modules: but it's not clear if this is the case; and beside, it's planned for the next minor feature release, 8.3.0.

Working around these limitations with the Events, Entity and Field APIs

The specific use case, of creating media entities during the upgrade process, is simple enough that we can accomplish it by a different route. The trick is that the new media entities will have a one-to-one correspondence with the file entities; we require that:

  1. For each file entity upgraded to Drupal 8
  2. Create one corresponding media entity
  3. (With bundle to be determined by the file's MIME type.)

This means that we can use Drupal 8's APIs, in a new custom module, to connect to the existing file entity migration process and create a media entity for each file entity. Respectively, the custom code will:

  1. Subscribe to the migration of each file entity using the events API.
  2. Create a new media entity using the entity API.
  3. Provide default bundles using exported configuration from the fields API.

A fully-functioning module, media_entity_upgrade is also available as this drupal.org sandbox project. We'll refer to some of the code in that repository below, for reasons of convenience.

Breakdown of the contents of the module

Module metadata: .info.yml

The file media_entity_upgrade.info.yml is as described in the tutorial on Extending and Altering Drupal, so we don't dwell on it here. Note the large number of dependencies: for simplicity, this could be trimmed down; but all of these other projects are useful when running migrations or upgrades.

Event subscriber class: src/FileMigrateEventSubscriber.php

This is the only PHP class file in the module, and we quote it below:

<?php
 
namespace Drupal\media_entity_upgrade;
 
use Drupal\migrate\Event\MigrateEvents;
use Drupal\migrate\Event\MigratePostRowSaveEvent;
use Drupal\Core\Entity\EntityManagerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
 
/**
 * Create a media entity whenever a file is migrated.
 */
class FileMigrateEventSubscriber implements EventSubscriberInterface {
 
  /**
   * @var X
   *   X provided by the factory injected below in the constructor.
   */
  protected $entityManager;
 
  /**
   * Implements __construct().
   *
   * Dependency injection defined in .services.yml.
   */
  public function __construct(EntityManagerInterface $entityManager) {
    $this->entityManager = $entityManager;
  }
 
  /**
   * {@inheritDoc}
   */
  public static function getSubscribedEvents() {
    return [MigrateEvents::POST_ROW_SAVE => [['onPostRowSave']]];
  }
 
  /**
   * Subscribed event callback: MigrateEvents::POST_ROW_SAVE.
   *
   * If the saved entity is a file, create a media entity to wrap around it.
   *
   * @param MigratePostRowSaveEvent $event
   *   The event triggered.
   */
  public function onPostRowSave(MigratePostRowSaveEvent $event) {
    if ($event->getMigration()->getPluginId() != 'd6_file') {
      return;
    }
 
    // Create 'document' media for all files of unknown type.
    $bundle = 'document';
    // Check if MIME type is known, and change media bundle accordingly.
    $destinationIds = $event->getDestinationIdValues();
    $file = $this->entityManager->getStorage('file')->load($destinationIds[0]);
    if (strpos($file->getMimeType(), 'image/') === 0) {
      $bundle = 'image';
    }
 
    // Create media entity and save.
    $media = $this->entityManager->getStorage('media')->create([
      'bundle' => $bundle,
      'uid' => $file->uid,
      'name' => $file->name,
      'field_media_file' => [
        'target_id' => $destinationIds[0]
      ],
    ]);
    $media->save();
  }
 
}

This class contains methods as follows:

Constructor and protected variables
This permits dependency injection of the entity manager, the primary interface to all later entity operations
Detailing event subscriptions: getSubscribedEvents()
This implements the events API, notifying Symfony's event dispatcher of subscription to MigrateEvents::POST_ROW_SAVE events.
Event handler: onPostRowSave
This responds to any events of the required type by: determining the correct migration (name) is being run (d6_file is the file/upload component of a Drupal upgrade); switching the media bundle based on the MIME type of the file entity that has been created; then creating the media entity with the minimum of information required.

Without the services file below, this class is dormant.

Registering the event subscriber: media_entity_upgrade.services.yml

services:
  media_entity_upgrade.event_subscriber:
    class: 'Drupal\media_entity_upgrade\FileMigrateEventSubscriber'
    arguments: ['@entity.manager']
    tags:
      - {name: event_subscriber}

Media bundles with configured fields: config/install/*.yml

There are a large number of configuration files for the entity bundles, as you can see. We therefore don't review them in any depth here. Apart from anything else, you might want to create your own bundles!

When the module is installed, the YAML files in the sandbox module create:

  • A document media bundle: with form and view displays; and fields including related file and storage for the automatically generated file size in bytes.
  • A image media bundle: with form and view displays; and fields including related file and storage for the automatically generated image width and height.

All media entities also contain base fields like the owner's user ID and a name (which we simply populate with the file entity's filename): these do not have to be configured with YAML files.

If you do decide to create your own media entity bundles, rather than the ones provided in the sandbox module, the UI takes you through the procedure in a fairly self-explanatory way. Just remember to update any bundle names or field names (especially field_media_file) in the event subscriber class.

How to take advantage of media "migration" during upgrade
Once you've enabled the new module, you should be able to run a Drupal upgrade e.g. via the URL /upgrade with no differences. You won't see a new upgrade path in the list or anything: strictly speaking, you've not so much created a new upgrade path as extended the existing d6_file upgrade path to create two entities per file, rather than one.

That's all there is to it: enable your module, and run upgrades as before. After the upgrade is finished, you should be able to navigate your new media entities, complete with thumbnails, MIME types etc.

Summary

The core Drupal 8 upgrade is not currently extensible to provide extra migrations within it. However, using the events API, custom code can subscribe to row-by-row events in the existing migrations provided by core, and extend them to create new entities or modify what's already being migrated. Because media entities have a one-to-one correspondence with file entities, such event subscription is sufficient for creating them during an upgrade.

Comments

It's important to note that the upgrade process actually is extensible - if you define a migration .yml with migration_tags of 'Drupal 6' and/or 'Drupal 7', the migration will be executed as part of the upgrade. The issue with the hardcoded list of migrations (and their related source and destination modules) only affects reporting - the list of available and "missing" migrations on the confirmation page in the upgrade UI. Your migration may not be listed there, but it will actually be executed.

Thanks, Mike. I did try this originally, in imitation of the core d6_file etc: but it didn't ostensibly work, which is why I went down this route.

It's possible as you say that I was just confounded by the UI not listing it, and so reasonably assumed that meant it wasn't going to run: if I've got time, I'll try again.