Drupal 8 Migrations, part 3: Migrating Taxonomies from Drupal 7

October 11, 2017: Now updated for Drupal 8.4!

Drupal 8 provides a flexible, plugin-based architecture for migrating data into a site. In Part 2 of this series, we explored how to migrate users from a Drupal 7 site. We will now expand on this by migrating Taxonomy vocabularies and terms from a Drupal 7 site into Drupal 8.

This article continues our work from Part 2. The code examples pick up where that post left off. If you are trying this code out yourself, it is recommended to start building your custom migration module according to the examples in that post.

Migrating Taxonomy Vocabularies

We'll start by writing a new migration definition, then write a source plugin to match. This will expand upon the "Migrate Custom" module we created in Part 2.

The migration definition:

Create a file named modules/migrate_custom/config/install/migrate_plus.migration.custom_taxonomy_vocabulary.yml with the following contents:

id: custom_taxonomy_vocabulary
label: Drupal 7 taxonomy vocabularies
migration_group: custom
dependencies:
  enforced:
    module:
      - migrate_custom
source:
  plugin: custom_taxonomy_vocabulary
process:
  vid:
    -
      plugin: machine_name
      source: machine_name
    -
      plugin: dedupe_entity
      entity_type: taxonomy_vocabulary
      field: vid
      length: 32
  label: name
  name: name
  description: description
  hierarchy: hierarchy
  module: module
  weight: weight
destination:
  plugin: entity:taxonomy_vocabulary

Here we have examples of a few plugins not seen in the previous post:

  • machine_name converts the string into a valid machine name.
  • dedupe_entity prevents machine name conflicts, which would cause imported data to overwrite existing data. For example, a machine name "foo" would be renamed to "foo_2" if name "foo" already existed.

The source plugin

To define the source of our vocabulary data, we create a new file modules/migrate_custom/src/Plugin/migrate/source/Vocabulary.php with the following contents:

<?php
 
/**
 * @file
 * Contains \Drupal\migrate_custom\Plugin\migrate\source\Vocabulary.
 */
 
namespace Drupal\migrate_custom\Plugin\migrate\source;
 
use Drupal\migrate\Row;
use Drupal\migrate\Plugin\migrate\source\SqlBase;
 
/**
 * Drupal 7 vocabularies source from database.
 *
 * @MigrateSource(
 *   id = "custom_taxonomy_vocabulary",
 *   source_provider = "taxonomy"
 * )
 */
class Vocabulary extends SqlBase {
 
  /**
   * {@inheritdoc}
   */
  public function query() {
    $query = $this->select('taxonomy_vocabulary', 'v')
      ->fields('v', array(
        'vid',
        'name',
        'description',
        'hierarchy',
        'module',
        'weight',
        'machine_name'
      ));
    return $query;
  }
 
  /**
   * {@inheritdoc}
   */
  public function fields() {
    return array(
      'vid' => $this->t('The vocabulary ID.'),
      'name' => $this->t('The name of the vocabulary.'),
      'description' => $this->t('The description of the vocabulary.'),
      'help' => $this->t('Help text to display for the vocabulary.'),
      'relations' => $this->t('Whether or not related terms are enabled within the vocabulary. (0 = disabled, 1 = enabled)'),
      'hierarchy' => $this->t('The type of hierarchy allowed within the vocabulary. (0 = disabled, 1 = single, 2 = multiple)'),
      'weight' => $this->t('The weight of the vocabulary in relation to other vocabularies.'),
      'parents' => $this->t("The Drupal term IDs of the term's parents."),
      'node_types' => $this->t('The names of the node types the vocabulary may be used with.'),
    );
  }
 
  /**
   * {@inheritdoc}
   */
  public function getIds() {
    $ids['vid']['type'] = 'integer';
    return $ids;
  }
 
}

Note: this file was adapted from the Drupal 6 version in Drupal 8 core. For the original file, see core/modules/migrate_drupal/src/Plugin/migrate/source/d6/Vocabulary.php

The structure of this file is similar to the User source plugin in the previous article. However, because all the data we need is stored in the `taxonomy_vocabulary` table in the source database, we do not need to define the prepareRow() method.

Migrating Taxonomy Terms

We can use a second migration definition to migrate our taxonomy terms. Create the following file:

modules/migrate_custom/config/install/migrate_plus.migration.custom_taxonomy_term.yml

id: custom_taxonomy_term
label: Drupal 7 taxonomy terms
migration_group: custom
dependencies:
  enforced:
    module:
      - migrate_custom
source:
  plugin: custom_taxonomy_term
process:
  tid: tid
  vid:
    plugin: migration_lookup
    migration: custom_taxonomy_vocabulary
    source: vid
  name: name
  description: description
  weight: weight
  parent:
    -
      plugin: skip_on_empty
      source: parent
    -
      plugin: migration_lookup
      migration: custom_taxonomy_term
  changed: timestamp
destination:
  plugin: entity:taxonomy_term
migration_dependencies:
  required:
    - custom_taxonomy_vocabulary

In this migration, we make use of the migration process plugin for two of our properties, the vocabulary ID and the parent term ID. This preserves these references in case the referenced entity's ID or machine name changed during the import.

Some machine names and/or IDs will likely change when running your import. This is to be expected, especially because Drupal 8 stores taxonomy vocabularies in the 'config' table, where they are accessed by their machine names instead of by the numeric IDs used in Drupal 7. Fortunately for us, the Migrate module records a map of the old and new IDs in the database. We can then use the migration source plugin to easily look up the old ID or machine name.

The source plugin

To define the source of our term data, we create a new file modules/migrate_custom/src/Plugin/migrate/source/Term.php with the following contents:

<?php
 
/**
 * @file
 * Contains \Drupal\migrate_custom\Plugin\migrate\source\Term.
 */
 
namespace Drupal\migrate_custom\Plugin\migrate\source;
 
use Drupal\migrate\Row;
use Drupal\migrate\Plugin\migrate\source\SqlBase;
 
/**
 * Drupal 7 taxonomy terms source from database.
 *
 * @todo Support term_relation, term_synonym table if possible.
 *
 * @MigrateSource(
 *   id = "custom_taxonomy_term",
 *   source_provider = "taxonomy"
 * )
 */
class Term extends SqlBase {
 
  /**
   * {@inheritdoc}
   */
  public function query() {
    $query = $this->select('taxonomy_term_data', 'td')
      ->fields('td', array('tid', 'vid', 'name', 'description', 'weight', 'format'))
      ->distinct();
    return $query;
  }
 
  /**
   * {@inheritdoc}
   */
  public function fields() {
    return array(
      'tid' => $this->t('The term ID.'),
      'vid' => $this->t('Existing term VID'),
      'name' => $this->t('The name of the term.'),
      'description' => $this->t('The term description.'),
      'weight' => $this->t('Weight'),
      'parent' => $this->t("The Drupal term IDs of the term's parents."),
    );
  }
 
  /**
   * {@inheritdoc}
   */
  public function prepareRow(Row $row) {
    // Find parents for this row.
    $parents = $this->select('taxonomy_term_hierarchy', 'th')
      ->fields('th', array('parent', 'tid'))
      ->condition('tid', $row->getSourceProperty('tid'))
      ->execute()
      ->fetchCol();
    $row->setSourceProperty('parent', $parents);
    return parent::prepareRow($row);
  }
 
  /**
   * {@inheritdoc}
   */
  public function getIds() {
    $ids['tid']['type'] = 'integer';
    return $ids;
  }
 
}

Reloading the configuration

Remember that migration definitions are configuration entities. To reload the configuration, we need to uninstall and reinstall our module. Here's a handy Drush command to do this:

drush pm-uninstall migrate_custom -y && drush en migrate_custom

Running the migration

As we did with our user migration, we now run the migration using Drush.

drush migrate-import custom_taxonomy_vocabulary
drush migrate-import custom_taxonomy_term

Next post: Migrating Nodes from Drupal 7.

Comments

Hi,

I found what I'm not sure if it's a but. Reinstalling the module throws the message:

exception 'Drupal\Core\Config\PreExistingConfigException' with message 'Configuration objects[error]
(migrate.migration.d7_ladder) provided by migrate_drupal7 already exist in active
configuration' in /www/drupal8/core/lib/Drupal/Core/Config/PreExistingConfigException.php:70

I've tried several modules and with all of them happens the same, the configuration gets stuck when you try to install it the second time.

Hi,
I also receive same error after uninstalling the module.

It says that config object are still in active configuration and cannot install module again.

Any suggestions?
Thanx

Add this to your module install file:

function migrate_plus_uninstall() {
  db_query("DELETE FROM {config} WHERE name LIKE 'migrate.migration.custom_taxonomy_term%'");
  drupal_flush_all_caches();
}

Replace "migrate.migration.custom_taxonomy_term" with the name of your .yml file

Migrations are config entities, so you can also do something like this:

// Procedural code
entity_load('migration', 'your_migration_id')->delete();
 
// OO code
\Drupal::entityManager()->getStorage('migration')->load('your_migration_id')->delete();
// or:
\Drupal\migrate\Entity\Migration::load('your_migration_id')->delete();

skip_process_on_empty and skip_row_on_empty were combined into a single plugin. To do the equivalent:

# To skip processing:
plugin: skip_on_empty
method: process
 
# To skip the row
plugin: skip_on_empty
method: row

I hope this helps someone.

Hi,

I am going through this tutorial. I skipped part two since I don't need to import users, though I did create the manifest.yml file. When I run the taxonomy migration I get this error:

missing source provider taxonomy source_provider

The files I created are just as in the tutorial so I am not sure what I could be doing wrong. ideas?

Has anyone run across the following error when performing a migrate-import of the custom_taxnomy_term:

[error] Missing bundle for entity type taxonomy_term ({location_of_drupal_site}/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php:83)

Any help would be much appreciated.

I tried importing taxonomy terms from CSV, which worked, except that it would not import to a custom field on the vocabulary, even though I mapped the columns under 'process' in the YML file. I set the destination plugin to 'entity:taxonomy_term'. Any idea why my column for the custom field on the vocabulary would not be imported?

Here's the relevant excerpt from my migration YML file:

source:
plugin: csv
path: 'public://import/iso-3166-1.csv'
delimiter: ','
enclosure: '"'
header_row_count: 1
keys:
- aplha_2
fields:
name: The name of the country
alpha_2: ISO-3166-1 Alpha-2 Country code
column_names:
-
name: 'Country Short Name'
-
aplha_2: 'ISO 3166-1 Alpha-2 code'
process:
name: name
field_iso_3166_1_alpha_2: aplha_2
destination:
plugin: 'entity:taxonomy_term'
default_bundle: countries
migration_dependencies: null

Would be nice to see this working with i18n translations.

Hi, both migrations work but when I import the taxonomy terms they cannot be seen in the UI listed under the vocabulary. Vocabularies seem empty but the terms have been imported in the database. I have checked the actual mysql tables and it all seems fine. I was cracking my head and I finally got it working by copying the d7_taxonomy migration template from the Drupal 8 core Taxonomy module. This is the process key that works for me for the taxonomy_term migrations:

process:
tid: tid
vid:
plugin: migration_lookup
migration: cruk_content_taxonomy_vocabulary_migrate
source: vid
name: name
'description/value': description
'description/format': format
weight: weight
# Only attempt to stub real (non-zero) parents.
parent_id:
-
plugin: skip_on_empty
method: process
source: parent
-
plugin: migration_lookup
migration: cruk_content_taxonomy_term_migrate
parent:
plugin: default_value
default_value: 0
source: '@parent_id'
changed: timestamp

I've experienced the same thing. This was related to hierarchy: no rows were created in the taxonomy_term_hierarchy table for taxonomy terms that did not have a parent. Drupal expects a row in that table for every term, the value for parent has to be 0 if the term does not have a parent. The migration was not adding those rows. After using the above example, rows are created and the terms show up fine in the UI. I implemented a similar solution, the key part is the addition of:

parent_id:
-
plugin: skip_on_empty
method: process
source: parent
-
plugin: migration_lookup
migration: custom_taxonomy_term
parent:
plugin: default_value
default_value: 0
source: '@parent_id'

I have a custom image field added in vocabulary. But I couldn't manage to get the content for that field migrated from D7 to D8. I have added the field mapping in my plugin. Any help will be highly appreciated.

Hello, I followed this tutorial and it's very helpful. One fix, in case your terms aren't showing up under each vocabulary taxonomy. Remove line 31, "->distinct();" from your Term.php file. So it should look like this:

```
/**
* {@inheritdoc}
*/
public function query() {
$query = $this->select('taxonomy_term_data', 'td')
->fields('td', array('tid', 'vid', 'name', 'description', 'weight', 'format'));
return $query;
}
```
Instead, just a tip.

Add new comment

Restricted HTML

  • Allowed HTML tags: <a href hreflang> <em> <strong> <cite> <blockquote cite> <code> <ul type> <ol start type> <li> <dl> <dt> <dd> <h2 id> <h3 id> <h4 id> <h5 id> <h6 id>
  • You can enable syntax highlighting of source code with the following tags: <code>, <blockcode>, <cpp>, <java>, <php>. The supported tag styles are: <foo>, [foo].
  • Web page addresses and email addresses turn into links automatically.
  • Lines and paragraphs break automatically.

Ready for transformation?