Migration de Drupal 7 à Drupal 8

Créé le 02/11/2015

Dernière mise à jour le 08/11/2015

Afin de faire l'expérience d'une migration de contenus et autres entités d'un site Drupal 7 à un site Drupal 8, j'ai créé un module de migration pour mon site.

Le développement a été fait en deux étapes, la première sur fin mai/début juin 2015 et la seconde début octobre 2015.

Voici un retour d'expérience dessus.

Introduction et pré-requis

C'est grâce à un article sur le blog de metaltoad que j'ai initié le module : http://www.metaltoad.com/blog/migrating-content-drupal-8

Et je me suis également penché sur les exemples et vrais templates de migration présents dans le core.

Le code source est disponible sur mon compte github : https://github.com/FlorentTorregrosa/migrate_ftorregrosa

En pré-requis, l'utilisation de Migrate plus et de Migrate upgrade aide au développement. L'interface utilisateur n'était pas complète au moment du développement, mais c'est pas mal pour avoir des indications sur le statut des migrations.

La migration s'est effectuée à partir d'une base de données comme source. Dans le fichier settings.php, il faut rajouter les informations de connexion suivantes :

$databases['migrate']['default'] = array(
  'database' => 'database',
  'username' => 'username',
  'password' => 'password',
  'prefix' => '',
  'host' => 'localhost',
  'port' => '3306',
  'namespace' => 'Drupal\\Core\\Database\\Driver\\mysql',
  'driver' => 'mysql',
);

L'important étant la clé "migrate".

Structure

Avec Migrate Drupal 7, pour effectuer une migration il faut écrire une classe de migration qui va faire le lien entre la source et la destination (même si des classes de source et de destination existent déjà) et va également contenir les fonctions de pré-traitement, traitement et post-traitement.

Avec Migrate Drupal 8, les informations de migration ; nom de la migration, source, destination, correspondance des champs, etc. sont contenus dans une entité de configuration créée via un fichier de configuration YAML et placés dans le répertoire config/install.

Afin de faire des traitements en plus de ceux fournit par Migrate et de récupérer les données, il est nécessaire (en tout cas dans mon cas) de faire des classes source de migration.

Afin de lancer vos migrations, il y a plusieurs méthodes. Celle utilisée initialement était celle du manifest migrate. Il s'agit d'un fichier YAML appelé manifest.yml placé dans le module. Il liste les migrations à exécuter dans l'ordre du fichier. Pour exécuter la migration il faut ensuite lancer la commande drush suivante :

drush migrate-manifest /home/florent/sites/drupal8/modules/migrate_ftorregrosa/manifest.yml --legacy-db-url=""

Problèmes rencontrés

Tout d'abord les problèmes ralentissant le développement :

  • Le fait que les migrations soient des entités de configuration sans interface graphique pour les modifier fait que si l'on doit changer quelque chose dans les fichiers YAML il est ensuite nécessaire de désinstaller/réinstaller le module.
  • Plus au début du développement la désinstallation ne supprimait pas les entités de configuration, les tables de migrate en base de données et le contenu, d'où des suppressions manuelles à faire.
  • Initialement, j'ai suivi la méthode du manifest migrate, sauf qu'elle ne permet pas de relancer des migrations en mode "update" pour mettre à jour le contenu déjà importé. D'où la création d'un petit script.
  • Pour les migrations des "livres Drupal", le type de contenu book, Il y avait un problème dans la table de la structure de livre lors de la création d'un contenu étant une racine de livre. En effet pas moyen qu'il y soit inséré le bon nid malgré le fait d'avoir renseigné d'utiliser la migration elle-même pour que migrate cherche le nid créé. J'avais également commencé à passer par les événements migrate, mais pas moyen de mettre à jour la donnée. Lors d'une réexécution de la migration en mode import, cette fois-ci Migrate allait insérer le bon nid, mais il y avait des erreurs mysql à cause des contraintes sur la table. Au lieu de mettre à jour les entrées de la table, il y avait tentative d'insertion de nouvelles entrées. Pour palier à cela, j'ai donc lancé une fois la migration des book puis fait un TRUNCATE sur la table (oui, c'est bourrin mais ça marche) puis relancé la migration en mode update et là magie, la structure de livre est bonne.
  • Mon site est (au moment de l'écriture de cet article en tout cas) uniquement en français, il faut que le français soit activé sur le site Drupal 8. C'est bête, mais à cause de ça les corps d'articles sont migrés mais pas affichés.

Détail d'un fichier de configuration migrate

Exemple de migrate.migration.ftorregrosa_website.yml (commentaire au fil du code) :

id: ftorregrosa_website
label: ftorregrosa website nodes
dependencies:                      # Dépendance de module pour cette migration.
  enforced:
    module:
      - language
      - migrate_ftorregrosa
migration_group: ftorregrosa      # Fonctionnallité ajoutée par migrate_plus, permet de grouper les migrations dans le back-office.
source:
  plugin: ftorregrosa_website     # La classe (plugin) de migration servant de source.
destination:
  plugin: entity:node             # La classe (plugin) de migration servant de destination. Fournie par le core.
  type: website
  bundle: website
migration_dependencies:           # Migration devant être exécutée auparavant.
  required:
    - ftorregrosa_taxonomy_term
    - ftorregrosa_user
process:                         # La correspondance de champ.
#  nid: nid
  vid: vid
  type: type
  langcode:
    plugin: static_map
    bypass: true
    source: language
    map:
      und: en
  title: title
  uid:
    -
      plugin: migration         # Indication d'aller chercher l'uid créé à partir de l'uid source de la migration ftorregrosa_user.
      migration: ftorregrosa_user
      source: uid
  status: status
  created: created
  changed: changed
  promote: promote
  sticky: sticky
  'body/value': body_value
  'body/summary': body_summary
  'body/format':
    plugin: static_map
    bypass: true
    source: body_format
    map:
      1: plain_text
      2: restricted_html
      3: full_html
      4: full_html
  field_website_type:
    -
      plugin: migration
      migration: ftorregrosa_taxonomy_term
      source: field_website_type
  field_website_technology:
    -
      plugin: migration
      migration: ftorregrosa_taxonomy_term
      source: field_website_technology
  field_website_image: field_website_image
  field_website_dev_date_start: field_website_dev_date_start
  field_website_dev_date_end: field_website_dev_date_end
  field_website_link: field_website_link

Détail d'une classe de source de migration

Exemple de Website.php (Fichier complet sur github) :

Annotation pour déclarer un plugin Migrate :

/**
 * ftorregrosa website node source plugin.
 *
 * @MigrateSource(
 * id = "ftorregrosa_website"
 * )
 */

Exemple de la récupération du body :

$nid = $row->getSourceProperty('nid');
// Body (compound field with value, summary, and format).
$result = $this->getDatabase()->query('SELECT fdb.body_value, fdb.body_summary, fdb.body_format FROM {field_data_body} fdb WHERE fdb.entity_id = :nid ', array(':nid' => $nid));
foreach ($result as $record) {
  $row->setSourceProperty('body_value', $record->body_value);
  $row->setSourceProperty('body_summary', $record->body_summary);
  $row->setSourceProperty('body_format', $record->body_format);
}

Exemple préparation du tableau de données pour des champs liens :

// field_website_link.
$result = $this->getDatabase()->query('SELECT fdfwl.field_website_link_url, fdfwl.field_website_link_title FROM {field_data_field_website_link} fdfwl WHERE fdfwl.entity_id = :nid ORDER BY fdfwl.delta ASC ', array(':nid' => $nid));
// Create an associative array for each row in the result. The keys // here match the last part of the column name in the field table.
$links = [];
foreach ($result as $record) {
  $links[] = [
    'uri' => $record->field_website_link_url,
    'title' => $record->field_website_link_title,
  ];
}
$row->setSourceProperty('field_website_link', $links);

Traitement sur la date : passage d'un stockage de la forme yyyy-MM-dd HH:mm:ss (où HH:mm:ss vaut 00:00:00) à yyyy-MM-dd :

// field_website_dev_date.
$result = $this->getDatabase()->query('SELECT fdfwdd.field_website_dev_date_value, fdfwdd.field_website_dev_date_value2 FROM {field_data_field_website_dev_date} fdfwdd WHERE fdfwdd.entity_id = :nid ', array(':nid' => $nid));
// Create an associative array for each row in the result. The keys
// here match the last part of the column name in the field table.
// Source: yyyy-MM-dd HH:mm:ss (where HH:mm:ss equals 00:00:00)
// Target: yyyy-MM-dd
foreach ($result as $record) {
  $row->setSourceProperty('field_website_dev_date_start', substr($record->field_website_dev_date_value, 0, 10));
  $row->setSourceProperty('field_website_dev_date_end', substr($record->field_website_dev_date_value2, 0, 10));
}

Pour les images, il faut récupérer les attributs title, alt, hauteur, largeur et les préparer comme pour les liens. Pour la récupérationdu fid migré. $this->setUpDatabase permet d'aller intéroger la base de données du site Drupal 8, sinon $this->getDatabase est une connexion à la base de données migrée :

// Images.
// Site using file_entity:
// alt text => field_data_field_file_image_alt_text
// title text => field_data_field_file_image_title_text
$result = $this->getDatabase()->query('SELECT ...
// Create an associative array for each row in the result. The keys
// here match the last part of the column name in the field table.
$images = [];
foreach ($result as $record) {
  // Retrieve the migrated fid from ftorregrosa_file migration.
  $migrated_fid = $this->setUpDatabase(array('key' =>'default', 'target' => 'default'))
    ->select('migrate_map_ftorregrosa_file')
    ->fields('migrate_map_ftorregrosa_file', array('destid1'))
    ->condition('sourceid1', $record->field_website_image_fid)
    ->execute()
    ->fetchField();
  // Skip file if not migrated yet.
  if (!is_null($migrated_fid)) {
    $images[] = [
      'target_id' => $migrated_fid,
      'alt' => $record->field_file_image_alt_text_value,
      'title' => $record->field_file_image_title_text_value,
      'width' => $record->field_website_image_width,
      'height' => $record->field_website_image_height,
    ];
  }
}
$row->setSourceProperty('field_website_image', $images);

Remarques

Pour les fichiers contribués et pour faire simple, il suffit de copier/coller les fichiers contribués dans le répertoire files de votre site Drupal 8 et la migration va servir à remplir les tables des fichiers contribués. Au moins il n'y a pas à faire des appels sur le site source pour récupérer les fichiers.

Je n'ai pas fait de traitement sur les textes riches pour gérer les liens internes. Je les gérerai manuellement lors de la migration réelle. Mais il est possible de faire un tel traitement dans la fonction prepareRow.

Conclusion

Ce module m'a permit de me familiariser avec Migrate D8 et de voir un peu en situation ce que cela donne avec du vrai contenu. Je pourrai toujours le peaufiner :).

Par exemple gérer le cas d'un compte existant sur le site de destination (compte admin par exemple), le compte créé par la migration est désactivé mais impossible de se logguer avec le premier compte.

EDIT : Pour le cas du nom utilisateur existant déjà sur le site destination, cela est désormais "gérer". Migrate va lever une erreur et ne migrera pas l'utilisateur source. Je pense que cela est néanmoins paramètrable afin de préciser à Migrate comment gérer les entités déjà existantes.

Commentaires

Ajouter un commentaire