Converting Drupal 7 Media Tags During a Drupal 8 Migration

By John Ouellet,
Close-up image of code

Converting Drupal 7 Media Tags During a Drupal 8 Migration (Close-up image of code)

As developers of all skill levels begin to migrate from old versions of Drupal to Drupal 8, it is always good to have references for some of the more arduous tasks. Many of our clients for whom we've built Drupal 7 sites have used the media module and the awesomeness that it gives to their sites. Now, as I migrate some of these same sites to Drupal 8, I hit a bump in the road. The Drupal 7 Media tag can’t be rendered in the Drupal 8 site I am migrating to. This is because that functionality is not present in the Drupal 8 site. I spent some time researching and talking with people and the only way to win this was to write my own logic. After reading through this post, you will be able to transform data during a migration, and you can even use my code to do it.  

You will need to understand how the migrations from Drupal 7 to Drupal 8 work before trying this approach. I’ve written a couple articles in our Kalawiki on Migrating Sites to Drupal 8 and Altering data during a Drupal 8 Migration. These articles are pertinent to the task at hand.

I had two choices when manipulating the data. I could have done this by extending the node migration class from core, and then altering it via prepareRow. The issue with this approach is I have to also alter the revisions of this same node type. I would have been duplicating my work, and that just isn’t kosher with me. So I decided to use the process mechanism of the Drupal migration, as it is more granular in nature. Also, before I begin: I migrated all my files from the D7 site over first before importing my nodes. I ran my migration in three group: structure, files and finally nodes. What I am about to show you will not work unless you have the files over from the Drupal 7 site.

The first thing I needed to do was to add a process to the value of the body as it was migrating. The Migrate process overview gives you a good go-to on how to read some of the process portion of the yml file. Also, in the Altering Data wiki I linked above, I outlined a basic data transformation. I knew I had to change up the single plugin value to handle the plugin I was going to add. I changed the body value in my migrate_plus.migration.upgrade_d7_node_NODE-TYPE.yml file from:

  body:
    plugin: iterator
    source: body
    process:
      value: value

TO:

  body:
    plugin: iterator
    source: body
    process:
      value:
        plugin: fix_media_tags
        source: value

I did the same for the revision yml of the same node type since I am importing revisions as I mentioned previously.

I then created a file called FixMediaTags.php to the src/Plugin/migrate/process of my custom migration module. Here is the base code I need to get this started:

<?php

namespace Drupal\YOUR-CUSTOM-MODULE\Plugin\migrate\process;

use Drupal\migrate\MigrateException;
use Drupal\migrate\MigrateExecutableInterface;
use Drupal\migrate\ProcessPluginBase;
use Drupal\migrate\Row;

/**
 * Convert a Drupal 7 media tag to a rendered image field.
 *
 * @MigrateProcessPlugin(
 *   id = "fix_media_tags",
 * )
 */
class FixMediaTags extends ProcessPluginBase {

  /**
   * {@inheritdoc}
   */
  public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) {
    return $value;
  }
}

This now gives us the guts of what we need to do so we can then transform this data into something useful in Drupal 8. Side note: if you have already imported the migrate config, you will need to update or delete the config for your node migrations.

I then searched through the media module and looked for the “convert media tags to markup” string. I found the initial function that handled this: media_filter.  From there, I just went through the code and pulled out what was pertinent to the task at hand. There were about four functions in the media module I needed. However, for sake of brevity, here is the final class solution:

<?php

namespace Drupal\YOUR-MODULE\Plugin\migrate\process;

use Drupal\migrate\MigrateSkipRowException;
use Drupal\migrate\MigrateExecutableInterface;
use Drupal\migrate\ProcessPluginBase;
use Drupal\migrate\Row;
use Drupal\Component\Serialization\Json;

/**
 * Convert a Drupal 7 media tag to a rendered image field.
 *
 * @MigrateProcessPlugin(
 *   id = "fix_media_tags",
 * )
 */
class FixMediaTags extends ProcessPluginBase {

  /**
   * {@inheritdoc}
   */
  public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) {
    $value = ' ' . $value . ' ';
    $value = preg_replace_callback('/\[\[.*?\]\]/s', [&$this, 'replaceToken'], $value);
    return $value;
  }

  /**
   * Replace callback to convert a media file tag into HTML markup.
   *
   * Partially copied from 7.x media module media.filter.inc (media_filter).
   *
   * @param string $match
   *   Takes a match of tag code
   */
  private function replaceToken($match) {
    $settings = [];
    $match = str_replace("[[", "", $match);
    $match = str_replace("]]", "", $match);
    $tag = $match[0];

    try {
      if (!is_string($tag)) {
        throw new MigrateSkipRowException('No File Tag', TRUE);
      }

      // Make it into a fancy array.
      $tag_info = Json::decode($tag);
      if (!isset($tag_info['fid'])) {
        throw new MigrateSkipRowException('No FID', TRUE);
      }

      // Load the file.
      $file = file_load($tag_info['fid']);
      if (!$file) {
        throw new MigrateSkipRowException('Couldn\'t Load File', TRUE);
      }

      // Grab the uri.
      $uri = $file->getFileUri();

      // The class attributes is a string, but drupal requires it to be an array, so we fix it here.
      if (!empty($tag_info['attributes']['class'])) {
        $tag_info['attributes']['class'] = explode(" ", $tag_info['attributes']['class']);
      }

      $settings['attributes'] = is_array($tag_info['attributes']) ? $tag_info['attributes'] : [];

      // Many media formatters will want to apply width and height independently
      // of the style attribute or the corresponding HTML attributes, so pull
      // these two out into top-level settings. Different WYSIWYG editors have
      // different behavior with respect to whether they store user-specified
      // dimensions in the HTML attributes or the style attribute, so check both.
      // Per http://www.w3.org/TR/html5/the-map-element.html#attr-dim-width, the
      // HTML attributes are merely hints: CSS takes precedence.
      if (isset($settings['attributes']['style'])) {
        $css_properties = $this->MediaParseCssDeclarations($settings['attributes']['style']);
        foreach (['width', 'height'] as $dimension) {
          if (isset($css_properties[$dimension]) && substr($css_properties[$dimension], -2) == 'px') {
            $settings[$dimension] = substr($css_properties[$dimension], 0, -2);
          }
          elseif (isset($settings['attributes'][$dimension])) {
            $settings[$dimension] = $settings['attributes'][$dimension];
          }
        }
      }
    }
    catch (Exception $e) {
      $msg = t('Unable to render media from %tag. Error: %error', ['%tag' => $tag, '%error' => $e->getMessage()]);
      throw new MigrateSkipRowException($msg, TRUE);
    }

    // Render the image.
    $element = [
      '#theme' => 'image',
      '#uri' => $uri,
      '#attributes' => isset($settings['attributes']) ? $settings['attributes'] : '',
      '#width' => $settings['width'],
      '#height' => $settings['height'],
    ];

    $output = \Drupal::service('renderer')->renderRoot($element);

    return $output;
  }

  /**
   * Copied from 7.x media module media.filter.inc (media_parse_css_declarations).
   *
   * Parses the contents of a CSS declaration block and returns a keyed array of property names and values.
   *
   * @param $declarations
   *   One or more CSS declarations delimited by a semicolon. The same as a CSS
   *   declaration block (see http://www.w3.org/TR/CSS21/syndata.html#rule-sets),
   *   but without the opening and closing curly braces. Also the same as the
   *   value of an inline HTML style attribute.
   *
   * @return
   *   A keyed array. The keys are CSS property names, and the values are CSS
   *   property values.
   */
  private function MediaParseCssDeclarations($declarations) {
    $properties = array();
    foreach (array_map('trim', explode(";", $declarations)) as $declaration) {
      if ($declaration != '') {
        list($name, $value) = array_map('trim', explode(':', $declaration, 2));
        $properties[strtolower($name)] = $value;
      }
    }
    return $properties;
  }
}

This solution encompasses a typical image conversion, which in our case was all the file types in our WYSIWYG. You can of course, add your own logic to this to render different types of media, image styles, view modes, etc. Like I mentioned above, most of this code was copied from the Drupal 7 Media module. I tweaked it and removed lines to fit my needs. You could go back through and add back in what you would need to accomplish your goals.

This conversion was quite an adventure to figure out. The Drupal 8 Migration suite is done very nicely. It is an extremely time consuming task to get all of the correct.  However, the end result is well worth it.

John Ouellet

Director of Support

This support engineer was sent back in time to promote and serve Kalamuna. John wasn't manufactured with emotions - just a hardcoded desire for domination. With decades of actual programming experience, you can be assured that he actually knows what he's talking about, and that he won't waste your time with fluff and nonsense.