Migration des contenus Drupal

migrate

La migration des contenus vers Drupal est rendue facile grâce au module « Migrate ». Quelles que soient les sources, le module est très souple, complet et simple d’utilisation. Nous pouvons faire la migration des nœuds, des utilisateurs, des taxonomies, des fichiers, des menus, des commentaires etc. Une interface en Back Office, facile à manipuler, nous guide pour tout type d’action. Importer, annuler l’import, réinitialiser, stopper la migration en cours sont des actions possibles dans cette interface. Il y a même plusieurs modules spécialisés pour des sources précises, comme « drupal_d2d » pour une source Drupal (version 5, 6 ou 7), « commerce_migrate » pour Drupal commerce, « wordpress_migrate » pour wordpress, « g2migrate » pour les photos et données relatives au Gallery package (version 2), « typo3_migrate » pour TYPO3, « phpbb2drupal » pour phpBB2.

Nous n’allons pas entrer en détail sur ces dernières, qui d’ailleurs sont déjà riches en documentation sur chacun de leur site officiel. Nous allons plutôt mettre en relief les problèmes assez classiques rencontrés lors d’une migration drupal vers drupal, à savoir :

  •  Migration des champs personnalisés
  • Migration des « fields collections »
  • Migration des fichiers/images
  • Migration des books

Vue générale :

Bien entendu il y a déjà le module « drupal_d2d » pour migrer les contenus d’une source drupal vers une destination drupal. Après activation de ce module, nous observons un bouton « Import from Drupal »  dans l’onglet « MIGRATE » du menu « Contents ». Ce bouton redirige vers une page de configuration à 6 étapes (configuration de la base de données source, configuration des utilisateurs à migrer, des taxonomies, des fichiers, des types de contenus, un résumé des paramètres avec des actions « save configurations » et « run import »).

En revenant au menu « MIGRATE » nous avons aussi le bouton « Tableau de bord » qui redirige vers une page de suivi et gestion des migrations. Si nous avons enregistré les 6 étapes mentionnées plus haut, nous aurons une nouvelle ligne insérée dans le tableau de bord. Pour lancer la migration, il suffit de cocher cette nouvelle entrée et cliquer sur « Exécuter ».

dashboard

dashboard

Problèmes :

Une fois la migration achevée, nous observerons dans l’édition d’un des contenus que seules les données des champs natifs sont importées (nid, title, uid, created, changed, status, language, body, etc.). Si nous avons des champs personnalisés (exemple : field_image_media, field_document_associe, etc.), ces champs sont vides. Pour mieux comprendre, regardons dans le détail de chaque contenu à migrer.

pb

err-link

Il y a des problèmes de mapping entre la source et la destination. Les champs à fond rouge ne sont pas mappés correctement.

Dans l’onglet « Modifier », nous pouvons voir que les champs correspondants à la source sont vides.

source-vide

Il faut choisir dans la liste de sélection le champ correspondant. Malheureusement, après sauvegarde de ces paramètres, les données ne sont toujours pas importées.

source-select

Proposition de solutions :

Le plus conseillé est de créer une classe spécifique pour importer chaque type de contenu. Là, nous avons l’assurance de réussir sans aucun problème. Nous allons voir cela étape par étape.

Tout d’abord, il nous faut créer un module custom pour contenir les classes spécifiques. Ce module doit suivre la convention de nommage suivante : « migrate_{modulename} ». Comme tout module, il doit y avoir les fichiers de base « migrate_{modulename}.info », « migrate_{modulename}.module », ainsi que les classes spécifiques « migrate_{classname}.inc ». Un autre fichier indispensable à créer est le « migrate_{modulename}.migrate.inc ». Ce fichier est essentiel pour contenir l’implémentation  du « hook_migrate_api() », c’est là que seront déclarées les classes spécifiques nouvellement ajoutées. En principe, le fichier « .module » ne contient aucune ligne de code parce que les classes pour la migration sont déjà dans le hook du fichier « .migrate.inc ».

Une illustration pour simplifier :

arbre

La structure de la classe :

Le nom de la classe doit suivre la règle suivante : « {NomClasse}Migration ».

Elle doit hériter la classe abstraite « Migration » du module migrate.

Supposons un type de contenu « Article ».

Exemple :

class ArticleMigration extends Migration {}

Maintenant nous définissons la source, la destination, le mapping.

Voici le code complet :

/**
 * Classe de migration des articles
 */
class ArticleMigration extends Migration {

  public function __construct($arguments = array()) {
    parent::__construct($arguments);
    // Description
    $this->description = t('Article migration from another database source.');
    // Connexion à la source
    $this->bdd_connexion = Database::getConnection('default', 'migration');
    $query = $this->bdd_connexion->select('node', 'n');
    $query->fields('n', array('nid', 'title', 'language'));
    $query->leftJoin('field_data_body', 'b', 'n.nid = b.entity_id');
    $query->leftJoin('field_data_field_adresse_du_lien_article', 'lnk', 'n.nid = lnk.entity_id');
    $query->fields('b', array('body_value'));
    $query->fields('lnk', array('field_adresse_du_lien_article_url', 'field_adresse_du_lien_article_title', 'field_adresse_du_lien_article_attributes'));
    $query->condition('n.type', 'article', '=');
    // Source
    $this->source = new MigrateSourceSQL($query);
    // Destination
    $this->destination = new MigrateDestinationNode('article');
    // Mapping
    $this->map = new MigrateSQLMap($this->machineName, array(
      'nid' => array(
        'type' => 'int',
        'not null' => true,
      ),
        ), MigrateDestinationNode::getKeySchema()
    );
    // uid
    $this->addFieldMapping('uid', NULL)->defaultValue(1);
    // Titre et langue
    $this->addSimpleMappings(array(
      'title',
      'language',
    ));
    // Contenu
    $this->addFieldMapping('body', 'body_value');
    $this->addFieldMapping('body:format')->defaultValue('full_html');
    // Adresse du lien
    $this->addFieldMapping('field_adresse_du_lien_article', 'field_adresse_du_lien_article_url');
    $this->addFieldMapping('field_adresse_du_lien_article:title', 'field_adresse_du_lien_article_title');
    $this->addFieldMapping('field_adresse_du_lien_article:attributes', 'field_adresse_du_lien_article_attributes');
  }

}

Le contenu du fichier « .migrate.inc » :

/**
 * Implement hook_migrate_api()
 * 
 * @return int
 */
function migrate_example_migrate_api() {
  $api = array(
    'api' => 2,
    'migrations' => array(
      'Article' => array(
        'class_name' => 'ArticleMigration',
      ),
    ),
  );
  return $api;
}

Migration des champs personnalisés :

Ce qu’il faut bien préciser est le type d’entité de destination. Dans notre exemple, il s’agit d’un champ personnalisé pour un type de contenu. Donc nous utilisons, pour la destination, la classe « MigrateDestinationNode ».

Code :

$this->destination = new MigrateDestinationNode('article');

Vient ensuite le mapping du champ personnalisé. Pour notre exemple, nous avons un champ de type « lien », donc il ne faut pas oublier le titre du lien, les attributs.

Code :

$this->addFieldMapping('field_adresse_du_lien_article', 'field_adresse_du_lien_article_url');
$this->addFieldMapping('field_adresse_du_lien_article:title', 'field_adresse_du_lien_article_title');
$this->addFieldMapping('field_adresse_du_lien_article:attributes', 'field_adresse_du_lien_article_attributes');

Migration des « fields collections » :

Les « fields collections » sont des entités à part, liées à une autre entité. De ce fait, nous devons créer une autre classe spécifique pour les migrer. Il y a donc une notion de dépendance, d’ordre d’import. L’entité hôte doit être importée avant de lancer la migration des « fields collections ».

Supposons que nous avons un champ field collection « bloc_video » rattaché au type de contenu article. Celui-ci a 3 champs « titre_video », « description_video » et « lien_video ».

Spécification de la dépendance :

// Si la classe de migration du contenu Article est ArticleMigration,
// la dépendance est « Article »

$this->dependencies = array(‘Article’);

Ici la destination est un field collection, donc :

$this->destination = new MigrateDestinationFieldCollection(
'field_bloc_video, array('host_entity_type' => 'node')
);

Et le mapping :

$this->addFieldMapping('host_entity_id', 'entity_id')->sourceMigration('Article');
// Titre
$this->addFieldMapping('field_titre_video', 'field_titre_video_value');
// Description
$this->addFieldMapping('field_description_video', 'field_description_video_value');
$this->addFieldMapping('field_description_video:format')->defaultValue('full_html');
// Video
$this->addFieldMapping('field_lien_video', 'field_lien_video_input');

Le code complet sera donc:

/**
 * Classe de migration du field collection bloc video pour les articles
 */
class BlocVideoMigration extends Migration {

  public function __construct($arguments) {
    parent::__construct($arguments);

    $this->description = t('Field collection Bloc Video Article.');
    $this->dependencies = array('Article');

    $this->bdd_connexion =Database::getConnection('default', 'migration');

    $query = $this->bdd_connexion->select('field_data_field_bloc_video', 'b');
    $query->fields('b', array('entity_id', 'field_bloc_video_value'));
    $query->leftJoin('node', 'n', 'p.entity_id = n.nid');
    $query->leftJoin('field_data_field_titre_video', 't', 'b.field_bloc_video_value = t.entity_id');
    $query->leftJoin('field_data_field_description_video', 'd', 'b.field_bloc_video_value = d.entity_id');
    $query->leftJoin('field_data_field_lien_video', 'v', 'b.field_bloc_video_value = v.entity_id');
    $query->fields('n', array('type'));
    $query->fields('t', array('field_titre_video_value'));
    $query->fields('d', array('field_description_video_value'));
    $query->fields('v', array('field_video_video_input'));
    $query->condition('b.bundle', 'article', '=');

    $this->source = new MigrateSourceSQL($query);

    $this->destination = new MigrateDestinationFieldCollection(
        'field_bloc_video', array('host_entity_type' => 'node')
    );

    $this->map = new MigrateSQLMap($this->machineName, array(
      'field_bloc_video_value' => array(
        'type' => 'int',
        'length' => 11,
        'not null' => false,
        'alias' => 'b',
      ),
        ), MigrateDestinationFieldCollection::getKeySchema()
    );

    $this->addFieldMapping('host_entity_id', 'entity_id')->sourceMigration('Article');
    // Titre
    $this->addFieldMapping('field_titre_video', 'field_titre_video_value');
    // Description
    $this->addFieldMapping('field_description_video', 'field_description_video_value');
    $this->addFieldMapping('field_description_video:format')->defaultValue('full_html');
    // Video
    $this->addFieldMapping('field_lien_video', 'field_lien_video_input');
  }

}

Ne pas oublier d'ajouter cette classe dans migrate_example_migrate_api().

  'migrations' => array(
      'Article' => array(
        'class_name' => 'ArticleMigration',
      ),
      'BlocVideo' => array(
        'class_name' => 'BlocVideoMigration',
      ),

Migration des fichiers/images :

Le principe est le même que celui mentionné plus haut, dans la migration des champs personnalisés.

Il faut préciser le répertoire de destination. Comme les « liens », les images ont des attributs « title », « alt », etc.

Le code sera comme suit :

// Image
$this->addFieldMapping('field_image', 'uri_image')->separator(',');
$this->addFieldMapping('field_image:destination_dir')->defaultValue('public://');
$this->addFieldMapping('field_image:alt', 'field_image_alt');
$this->addFieldMapping('field_image:title', 'field_image_title');

Avant de lancer la migration des fichiers/images, il faut que les fichiers physiques soient synchronisés d’avance dans le bon répertoire.

Migration des books :

Garder la structure des livres lors de l’import des contenus est plutôt difficile, voire impossible, vu qu’on ne pourra pas maitriser l’ordre hiérarchique lors de l’import. Il se peut que des contenus de nième niveau soient importés avant même que  leurs parents ou grands-parents ne soient déjà importés. L'idée est donc de d'abord terminer toutes les migrations sans se soucier de la hiérarchie.

Ensuite on crée un batch qui reconstruit le livre en se référant de la source. Il s’agit donc de travailler directement dans la table source « book » et la table de destination « menu_links ».

 Structure de la table book:

book

Table menu_links:

menu_links

Le travail consiste à reconstruire la nouvelle table book, image de l’ancienne et refaire une sauvegarde des contenus livre avec leur parent respectif.

Notons que dans la table « menu_links », le champ « plid » indique le nid parent direct du contenu concerné, le champ « depth » indique son niveau hiérarchique (Ex : deph = 3, le contenu est au 3è niveau), les champs « p1 p2 à p9 » sont respectivement les parents de niveau 1, 2 à 9.

Heureusement, une table stocke la correspondance des « ids » source et destination. Par exemple, pour le type de contenu « Article », on a une table « migrate_map_article » créée automatiquement lors de l’import.

map

Une simple requête permet d’obtenir les nouveaux « nid » et « bid » pour la table « book », en jouant sur les champs « sourceid1 » et « destid1 ».

// nouveau nid
$q1 = db_select('migrate_map_pagestandard', 'map');
$q1->fields('map', array('destid1'));
$q1->condition('map.sourceid1', $old_nid, '=');
$new_nid = $q1->execute()->fetchField();

De même pour obtenir le nouveau « plid » à partir de l’ancien « mlid ».

Une fois qu’on a le nouveau nid, bid et plid, on fait une mise à jour du contenu.

Code :

// node concerné
$node = node_load($new_nid);
$node->book['bid'] = $new_bid;
$node->book['nid'] = $new_nid;
$node->book['plid'] = $new_plid;
_book_update_outline($node);
node_save($node);

Note :

Il faut bien noter que si le type de notre champ personnalisé  est un « entity reference », les « ids » pourraient être faussés. Nous devons faire une requête qui cherche la correspondance de l’ancien « id » dans la nouvelle table pour avoir l’id exact de l’entité liée. Pour cela il suffit de passer par les tables « migrate_map_{entityname} ». Si par exemple nous avons un champ « node reference » qui remonte un type de contenu « Mise en avant » dans le type de contenu « Article », il faut impérativement que les contenus « Mise en avant » soient importés avant les « Articles ». Ensuite, lors de l’import des « Articles », il faut faire référence aux nouveaux « ids » des « Mise en avant ».

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Captcha *

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.