Drupal 8 - migration personnalisee

Drupal 8 migration custom

La migration avec Drupal 8... C'est un sacré morceau. Il s'avère que le noyau intègre tout un système de migration qui semble relativement bien développé. En gros, il suffit d'installer certains modules du noyau relatifs à la migration et de visiter une url précise pour se voir proposer un petit formulaire dans lequel nous allons indiquer la base de données à traiter, ainsi que le chemin vers l'ancien site.

Si tout ceci peut se faire de manière plus ou moins propre avec un petit site, ça s'avère beaucoup plus compliqué avec un site de grande envergure. J'ai fait une tentative sur un site contenant plusieurs centaines de milliers de contenus et j'ai assez rapidement abandonné, ça sembler pédaler dans la semoule après un certain temps.

Recherches

J'ai donc commencé à faire des recherches pour comprendre comment mettre en place son propre système de migration basé sur le système du noyau... Et là, ben... rien... Je n'ai trouvé aucun tutorial qui indiquait comment créer ses propres classes de migration. Bien entendu, on trouve un peu de tout... les plugins de process, des explications à n'en plus finir sur les processus des modules du noyau, mais pas la moindre explication sur comment "customiser" sa migration.

Analyse

J'ai fini par trouver quelques modules de contribution tels que migrate_tools et migrate_plus. Dans ce dernier, on peut trouver quelques exemples pas forcément simples à comprendre au premier abord, mais je m'y suis attelé histoire de pas mourir bête.

Et c'est bien à partir de migrate_plus que les choses se passent. migrate_tools, quant à lui, ajoute des commandes drush bien pratiques pour effectuer des migrations, tels que:

  • ms: migrate-status
  • mi: migrate-import
  • mr: migrate-rollback
  • mst: migrate-stop
  • mrs: migrate-reset-status

Vu que je n'ai pas trouvé de tutoriaux sur le sujet, je me suis dit que ça pourrait être intéressant de montrer une implémentation très simple d'un module permettant une migration customisée.

Je prendrai comme exemple une table utilisateur à partir d'un Drupal 6, en proposant quelques petites astuces pour que vous puissiez aller un peu plus loin par la suite.

Module de base

Comme vous pouvez l'imaginer si vous avez lu mes précédents articles, c'est avec Drupal Console que nous allons générer un module:

MacPro:rev8 titouille$ drupal generate:module
 
 Enter the new module name:
 > SampleMigration
 
 Enter the module machine name [samplemigration]:
 > 
 
 Enter the module Path [/modules/custom]:
 > 
 
 Enter module description [My Awesome Module]:
 > A Sample module to describe migration
 
 Enter package name [Custom]:
 > Migration
 
 Enter Drupal Core version [8.x]:
 > 
 
 Do you want to generate a .module file (yes/no) [yes]:
 > yes
 
 Define module as feature (yes/no) [no]:
 > no
 
 Do you want to add a composer.json file to your module (yes/no) [yes]:
 > 
 
 Would you like to add module dependencies (yes/no) [no]:
 > yes
 
 Module dependencies separated by commas (i.e. context, panels):
 > migrate, migrate_drupal, migrate_plus, migrate_tools
 
 
 Do you confirm generation? (yes/no) [yes]:
 > yes
 
Generated or updated files
 Site path: /Users/titouille/Dev/web/htdocs/watchonista/watchonista/rev8
 1 - modules/custom/samplemigration/samplemigration.info.yml
 2 - modules/custom/samplemigration/samplemigration.module
 3 - modules/custom/samplemigration/composer.json
MacPro:rev8 titouille$ 

Notez déjà que j'ai intégré des dépendances, à savoir:

  • migrate
  • migrate_drupal
  • migrate_plus
  • migrate_tools

Ces dépendances vont permettre d'activer les modules si ils ne sont pas déjà activés dans votre installation de Drupal 8.

Configuration

La configuration de la migration s'effectue dorénavant avec des fichiers YAML. Ces derniers sont placés dans le répertoire config/install pour être ajoutés à la configuration dès le départ.

Nous pouvons créer 2 fichiers de configuration:

  • config/install/migrate_plus.migration_group.myusers.yml
  • config/install/migrate_plus.migration.myusers.yml

Notez maintenant la nomenclature de ces 2 fichiers. Ils débutent par migrate_plus, le nom du module dont j'ai parlé avant. Il est nécessaire que chaque fichier de configuration pour la migration débute par migrate_plus, sous peine de ne pas être détecté dans le système.

Groupe de migration

Le premier fichier continue avec l'information migration_group. Cette information indique que c'est un fichier de configuration pour la migration, qu'il va définir un... "groupe", oui, bien entendu.

L'avantage d'utiliser des groupes de migration, c'est de pouvoir stocker dans le groupe certaines informations qui seront accessibles à toutes les classes de migration affiliées au groupe. Dans mon cas, le fichier de groupe contient la configuration suivante:

# A "migration group" is - surprise! - a group of migrations. It is used to
# group migrations for display by our tools, and to perform operations on a
# specific set of migrations. It can also be used to hold any configuration
# common to those migrations, so it doesn't have to be duplicated in each one.
 
# The machine name of the group, by which it is referenced in individual
# migrations.
id: myusers
 
# A human-friendly label for the group.
label: User Imports
 
# More information about the group.
description: User migration process 
 
# Short description of the type of source, e.g. "Drupal 6" or "WordPress".
source_type: Drupal 6
 
# Here we add any default configuration settings to be shared among all
# migrations in the group. For this example, the source tables are in the
# Drupal (default) database, but usually if your source data is in a
# database it will be external.
shared_configuration:
  # Specifying 'source' here means that this configuration will be merged into
  # the 'source' configuration of each migration.
  source:
    # A better practice for real-world migrations would be to add a database
    # connection to your external database in settings.php and reference its
    # key here.
    key: legacy

Le truc intéressant ici, c'est la partie shared_configuration:source:key. J'y ai placé la chaîne "legacy". Cette chaîne, je vais l'utiliser pour déclarer la base de données depuis laquelle est issue mon lot de données à migrer.

Settings.php

Dans le fichier sites/default/settings.php, je vais donc déclarer les bases de données suivantes:

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

Vous pouvez voir que la seconde déclaration de base de données utilise la clé "legacy" dans sa déclaration. Ainsi, les migrations affiliées au groupe "myusers" (id de groupe) utiliseront cette base de données pour extraire les données utilisées pour la migration.

Migration

Le fichier migate_plus.migration.myusers.yml, quant à lui, détient le "mapping" des champs. Voici son contenu:

id: myusers
label: My Users
migration_group: myusers
source:
  plugin: myusers
destination:
  plugin: entity:user
process:
  uid: uid
  pass: pass
  mail: mail
  status: status
  roles: roles # @see MyUsers::prepareRow to know how roles are applied
  langcode: 
    plugin: default_value
    source: language
    default_value: en
  preferred_langcode:
    plugin: default_value
    source: language
    default_value: en
  preferred_admin_langcode:
    plugin: default_value
    source: language
    default_value: en
  timezone: timezone
  login: login
  init: init
  name:
    plugin: dedupe_entity
    source: name
    entity_type: user
    field: name
    postfix: _
  created: created
  changed: '@created'
  access: access
  
  field_gender: 
    plugin: default_value
    source: gender
    default_value: male
  field_about: field_about

migration_dependencies: {}

Ici, ça se complique un peu. Les premières déclarations indiquent l'identifiant unique de la migration, son label ainsi que le groupe auquel elle est affiliée.

Nous trouvons ensuite la source, qui va utiliser le plugin myusers, ainsi que la destination, qui indique le plugin entity:user. La source correspondra à une classe que nous allons implémenter, tandis que la destination correspond au type d'entité "user" dans le système. Il serait tout à fait possible d'utiliser une destination customisée également, bien entendu.

Enfin, nous trouvons les champs à procéder.

  • Certains sont des champs très simples qui vont simplement être "mappés" (c'est le cas de uid, mail, pass, status, timezone, login, init, created, access);
     
  • d'autres vont utiliser des valeurs existantes (changed va utiliser la valeur du champ created);
     
  • d'autres encore vont utiliser des plugins de migration spécifiques pour migrer les valeurs proprement. Nous trouvons par exemple les champs langcode, preferred_langcode, preferred_admin_langcode qui se basent sur la même source (le champ language) et qui utilisent le plugin default_value pour indiquer un langage par défaut si il n'est pas spécifié explicitement. Le champ name, quant à lui, utilise un plugin de migration nommé dedupe_entity qui va créer une nouvelle entité dans le cas ou le même nom serait utilisé par deux utilisateurs dans l'ancienne base de données;
     
  • enfin, j'ai rajouté deux champs "Sexe" (field_gender) et "A propos" (field_about) dans mon installation pour montrer comment il est possible de migrer des champs supplémentaires, qui pourraient par exemple être issus d'un type de contenu "content_profile" de Drupal 6.

Je ne saurais que vous recommander de jeter un oeil aux exemples du module migrate_plus et de regarder également dans le module migrate (/core/modules/migrate/src/Plugin/migrate/process) pour vous faire une petite idée des processus de migration possibles. Il en existe bien d'autres, il est même possible de faire référence à des éléments qui ont déjà été migrés (par exemple migrer une taxonomie, puis utiliser cette référence pour migrer un champ appartenant à un contenu de type node:article).

{module}.install

Il nous reste une petite chose à faire pour finaliser cette première partie. Si nous effectuons des modifications sur la configuration, nous devons désinstaller puis réinstaller notre module. Le problème, c'est que la configuration est intégrée dans le système, et que si nous réinstallons notre module, Drupal va planter en indiquant que cette configuration existe déjà. Pour palier ce problème, nous pouvons rajouter un fichier samplemigration.install à la racine, avec le code suivant:

function samplemigration_install() {
 
}
 
function samplemigration_uninstall() {
  db_delete('config')
    ->condition('name', [
      'migrate_plus.migration_group.myusers',
      'migrate_plus.migration.myusers',
    ], 'IN')
    ->execute();
}

Ainsi, à chaque fois que nous désinstallerons notre module, la configuration sera supprimée et pourra être rajoutée à la réinstallation.

EventListener

Là, vous allez me dire... "Hein ?". Dans une autre vie, j'ai beaucoup travaillé avec ActionScript3. AS3, c'est le langage de développement pour Flash et Flex (et Air). Et avec Flash, les écouteurs d'évènement, c'est quasi "la base". Flash étant un système qui se déroule "dans le temps", il est très important de pouvoir générer des évènements à des moments précis, et de pouvoir les intercepter pour déclencher des actions particulières. En gros, les écouteurs d'évènement, c'est ça. On s'abonne (écouteur) à un évènement qui sera généré à un moment donné (déclencheur), et on peut exécuter du code qui va effectuer des actions précises.

Avec Drupal 7, nous avions drupal_alter pour altérer certaines informations. Avec Drupal 8, il est plutôt indiqué de générer des évènements et de les intercepter via des écouteurs. Et les écouteurs, on les déclare sous forme de services. Je rajoute donc un fichier samplemigration.services.yml à la racine et y inclut le code suivant:

services:
  samplemigration.myusers_subscriber:
    class: Drupal\samplemigration\EventSubscriber\MyUsersMigrateSubscriber
    tags:
      - { name: event_subscriber }
 

On indique la classe qui va implémenter l'écoute, et on ajoute le tag "event_subscriber" pour que Drupal sache que ce service est un service d'abonnement à un ou des évènements... un écouteur, quoi.

Je vais également implémenter la base de cette classe directement en créer un fichier src/EventSubscriber/MyUsersMigrateSubscriber.php et en y plaçant le code suivant:

/**
 * Example event subscriber.
 */
 
namespace Drupal\samplemigration\EventSubscriber;
 
use Drupal\migrate\Event\MigrateEvents;
use Drupal\migrate\Event\MigratePostRowSaveEvent;
use Drupal\migrate\Event\MigrateRowDeleteEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
 
/**
 * Subscribe to MigrateEvents.
 */
class MyUsersMigrateSubscriber implements EventSubscriberInterface {
 
 
  /**
   * {@inheritdoc}
   */
  static function getSubscribedEvents() {
    $events = [];
    $events[MigrateEvents::POST_ROW_SAVE][] = ['postRowSave'];
    $events[MigrateEvents::PRE_ROW_DELETE][] = ['preRowDelete'];
    $events[MigrateEvents::POST_ROW_DELETE][] = ['postRowDelete'];
    return $events;
  }
 
 
  /**
   * Post row save callback.
   * 
   * @param MigratePostRowSaveEvent $event
   */
  public function postRowSave(MigratePostRowSaveEvent $event) {
 
    if ($event->getMigration()->id() == 'myusers') {
    }
  }
 
  /**
   * Pre row delete callback.
   * 
   * @param MigrateRowDeleteEvent $event
   */
  public function preRowDelete(MigrateRowDeleteEvent $event) {
 
    if ($event->getMigration()->id() == 'myusers') {
    }
  }
 
  /**
   * Post row delete callback.
   * 
   * @param MigrateRowDeleteEvent $event
   */
  public function postRowDelete(MigrateRowDeleteEvent $event) {
 
    if ($event->getMigration()->id() == 'myusers') {
    }
  }
}

Pour le moment, tout ce qu'il y a de plus simple. Je déclare la méthode getSubscribedEvents et indique à quels évènements je veux m'abonner. Les différents évènements sont issus de la classe MigrateEvents. Pour chaque évènement, j'indique la ou les méthodes à déclencher. Chaque méthode prend en argument une instance de classe de type event qui va contenir différentes informations.

Nous verrons par la suite que c'est particulièrement utile de pouvoir s'abonner à certains évènements issus du processus de migration. C'est un peu comme les méthodes prepare et complete qu'on pouvait trouver dans le système de migration de Drupal 7.

La source

mettons maintenant en place notre nouvelle source. Pour ce faire, je crée un fichier src/Plugin/migrate/source/MyUsers.php dans lequel je place le code suivant:

namespace Drupal\samplemigration\Plugin\migrate\source;
 
use Drupal\migrate\Row;
use Drupal\user\Plugin\migrate\source\d6\User;
/**
 * Source plugin for my user accounts.
 *
 * @MigrateSource(
 *   id = "myusers"
 * )
 */
class MyUsers extends User {
 
  /**
   * {@inheritdoc}
   */
  public function query() {
 
    $query = parent::query();
    //$query->condition('u.uid', [119, 120, 1017, 1150, 4491], 'IN');
    return $query;
  }
 
  /**
   * {@inheritdoc}
   */
 
  public function fields() {
 
    $fields = parent::fields();
 
    $fields['gender'] = $this->t('Gender');
    $fields['about'] = $this->t('About me');
 
    return $fields;
  }
 
  /**
   * {@inheritdoc}
   */
  public function getIds() {
    return [
      'uid' => [
        'type' => 'integer',
        'alias' => 'u',
      ],
    ];
  }
 
  /**
   * {@inheritdoc}
   */
  public function prepareRow(Row $row) {
 
    $result = parent::prepareRow($row);
 
    if ($result) {
      $uid = $row->getSourceProperty('uid');
      $langcode = $row->getSourceProperty('language');
 
      $row->setSourceProperty('roles', ['member', 'blogger']);
 
      $select = $this->select('content_type_profile', 'ctp')
        ->fields('ctp');
      $select->join('node', 'n', 'ctp.nid = n.nid');
      $profile = $select->condition('n.uid', $uid)
        ->execute()
        ->fetchAll();
 
 
      $row->setSourceProperty('gender', $profile->field_gender_value);
 
      $about = $langcode == 'en' ? $profile->field_about_value : $profile->field_about_fr_value;
      $row->setSourceProperty('about', $about);
      $row->setSourceProperty('about_en', $profile->field_about_value);
      $row->setSourceProperty('about_fr', $profile->field_about_fr_value);
    }
    return $result;
  }
}

Bon, vous étiez prévenu, l'exemple est vraiment simple, mais il illustre bien comment customiser sa migration. Commençons par le commencement, c'est à dire l'annotation:

/**
 * Source plugin for my user accounts.
 *
 * @MigrateSource(
 *   id = "myusers"
 * )
 */

Nous trouvons ici l'identifiant de notre plugin @MigrateSource: myusers. C'est la valeur qui est indiquée dans le fichier de configuration, souvenez-vous:

...
source:
  plugin: myusers
...

C'est ainsi que Drupal sait que la migration myusers va utiliser la classe src/Plugin/migrate/source/MyUsers.

Passons maintenant à l'héritage de classe:

use Drupal\user\Plugin\migrate\source\d6\User;
// ...
class WUsers extends User {

Nous étendons la classe de base Drupal\user\Plugin\migrate\source\d6\User, qui elle même étend Drupal\migrate_drupal\Plugin\migrate\source\DrupalSqlBase, elle même étendant Drupal\migrate\Plugin\migrate\source\SqlBase.

  • SqlBase propose le nécessaire pour effectuer des migrations à partir de données issues d'une base de données;
     
  • DrupalSqlBase propose le nécessaire pour effectuer des migrations de données Drupal;
     
  • Enfin, User propose une base pour migrer des données utilisateur à partir d'une base de données Drupal6;

La classe User implémente tout le nécessaire pour récupérer les données de base utilisateur:

  • implémentation de query pour interroger la base de données et récupérer les données de la table utilisateur;
     
  • implémentation de fields pour intégrer tous les champs de base dans la liste des champs;
     
  • implémentation de prepareRow pour ajouter les rôles ainsi que d'autres données spécifiques;
     
  • implémentation de getIds pour indiquer l'identifiant de la table;

Dans ma classe finale, je surdéfinis la méthode query pour éventuellement intégrer des conditions à la requête de base. Sachant que ma base de données possèdent plus de 10'000 utilisateurs, je rajoute une condition sur les identifiants afin de ne tester qu'un fragment des données.

Je surdéfinis la méthode fields pour ajouter les champs supplémentaires que je vais récupérer d'un profil (content_profile).

Enfin, je surdéfinis la méthode prepareRow. C'est cette dernière qui permet de préparer la "ligne" qui va être migrée. Dans mon cas, je commence par récupérer l'identifiant utilisateur de la ligne en cours afin de pouvoir l'utiliser par la suite. J'écrase ensuite la propriété "roles" qui avait été affectée dans la méthode parent par mes propres valeurs, ici en dur, mais il serait tout a fait possible de rendre tout ça dynamique.

Je récupère enfin les données de mon profil issu de la base de données Drupal6. Admettons les éléments suivants: un champ "field_gender", un champ "field_about" et un champ "field_about_fr". Ce dernier correspond à la traduction française du champ "about".

J'affecte la valeur de chaque champ à une propriété qui va correspondre à la valeur indiquée dans la configuration.

Regardez bien l'affectation des valeurs:

      $row->setSourceProperty('gender', $profile->field_gender_value);
 
      $about = $langcode == 'en' ? $profile->field_about_value : $profile->field_about_fr_value;
      $row->setSourceProperty('about', $about);
      $row->setSourceProperty('about_en', $profile->field_about_value);
      $row->setSourceProperty('about_fr', $profile->field_about_fr_value);

Je valorise le genre, puis je crée une variable dans laquelle je vais stocker la valeur du champ "about" par rapport au langage de l'entité. Si ma ligne originale est en français, alors je récupère la valeur du champ "about" en français. Si ma ligne originale est en anglais, alors je récupère la valeur du champ "about" en anglais. Je valorise la valeur stockée dans l'alias "about". Je valorise ensuite la valeur en anglais dans l'alias "about_en", puis la valeur en français dans l'alias "about_fr". Nous verrons par la suite pourquoi.

Configuration des utilisateurs dans le système

Comme nous l'avons vu auparavant, nous avons un champ "about" qui va être traduisible. Mon site est en anglais par défaut, puis j'ai activé les modules de traduction et rajouté la langue française. J'ai ensuite indiqué que l'entité "User" est traduisible, et j'ai rendu le champ "field_about" traduisible via la configuration du système de traduction des contenus.

Générer l'entité traduite

Même si mon champ est traduisible, il n'empèche que pour le moment, la migration va simplement créer un nouvel utilisateur. J'ai fait beaucoup de recherches sur le sujet mais je n'ai pas vraiment trouvé de réponse satisfaisante qui pourrait être intégrée à la configuration de la migration directement. En fait, j'avais déjà mis en place mon écouteur d'évènement car je pensais en avoir besoin, et je suis tombé sur ce thread qui indiquait la problématique. digitaldonkey indique une utilisation des écouteurs d'évènement pour créer la traduction.

J'ai donc implémenté ma classe d'écoute sur l'évènement MigrateEvents::POST_ROW_SAVE pour créer la traduction une fois l'entité originale créée:

  /**
   * Post row save callback.
   * 
   * @param MigratePostRowSaveEvent $event
   */
  public function postRowSave(MigratePostRowSaveEvent $event) {
 
    if ($event->getMigration()->id() == 'myusers') {
 
      // Retrieve current row and useful data.
      $row = $event->getRow();
      $src_values = $row->getSource();
      $dest_values = $row->getDestination();
      $id = $event->destinationIdValues[0];
      $entity = \Drupal::entityTypeManager()->getStorage('user')->load($id);
 
      // Get available languages.
      $available_langcodes = ['en', 'fr'];
 
      // Get default current row language.
      $default_langcode = $row->getDestination()['langcode'];
 
      // Remove default language from available languages.
      $key = array_search($default_langcode, $available_langcodes);
      if ($key !== FALSE) {
        unset($available_langcodes[$key]);
      }
 
      foreach ($available_langcodes as $langcode) {
        // Build the basic array of values, with minima to
        // create entity.
        $values = [
          'uid' => $dest_values['uid'],
          'created' => $dest_values['created'],
          'status' => $dest_values['status'],
 
          // add specific field(s)
          'field_about' => [
            'value' => $src_values['about_' . $langcode],
            'format' => 'basic_html',
          ],
        ];
 
        /** @var \Drupal\Core\Entity\FieldableEntityInterface $translated_entity */
        $translated_entity = $entity->addTranslation($langcode, $values);
        $translated_entity->setChangedTime($dest_values['changed']);
        $translated_entity->save();
      }
      $map = $event->getMigration()->getIdMap();
      $map->saveIdMapping($row, [$id]);
    }
  }

C'est ici que la traduction sera générée. Je récupère les données utiles telles que la ligne en cours, les valeurs sources et destination, l'identifiant et l'entité qui vient d'être migrée (via le service entityTypeManager).

Je déclare ensuite les langages disponibles, puis je récupère le langage original de l'entité, et supprime ce langage de mon tableau de langages disponibles.

J'itère ensuite sur chaque langage disponible afin de créer un tableau contenant les valeurs à intégrer à la traduction. Si vous vous souvenez, j'avais intégré plusieurs alias:

  • about => valeur dans la langue originale de l'entité;
  • about_en => valeur en anglais;
  • about_fr => valeur en français;

La valeur "about" a bien été affectée à mon entité originale lors de la migration. Ainsi, si le langage préféré de l'utilisateur migré était le français alors l'entité a été créée en français, avec le texte "about" en français. Si le langage préféré de l'utilisateur migré était l'anglais alors l'entité a été créée en anglais, avec le texte "about" en anglais.

Maintenant que je crée la traduction, nous devons récupérer la valeur dans la bonne langue. Vu que j'ai le code de langage, je peux aisément l'utiliser pour récupérer la bonne valeur, via:

'field_about' => [
  'value' => $src_values['about_' . $langcode],
  'format' => 'basic_html',
],

J'utilise ensuite les méthodes de l'API translation pour créer la traduction et la sauvegarder. Enfin, je sauvegarde le mapping avec son identifiant. That's all !

Il ne reste plus qu'à activer le module via Drupal console:

drupal module:install samplemigration

Puis utiliser drush pour voir si notre migration est présente. Si tout se passe bien, vous devriez voir ça:

MacPro:rev8 titouille$ drush ms
 Group: myusers  Status  Total  Imported  Unprocessed  Last imported       
 myusers              Idle    1      1         0            2016-06-21 17:49:37
MacPro:rev8 titouille$ 

Pour démarrer la migration, vous pouvez utiliser la commande drush suivante:

drush mi myusers

et si vous voulez effectuer un rollback, la commande drush suivante:

drush mr myusers

Conclusion

Bien que l'exemple soit simple, il permet aisément de voir les possibilités offertes pour la migration de données. Dans mon cas particulier, bien plus qu'une migration de données, je tente de faire un refactoring complet d'un site et je dois grandement customiser la migration, ce qui demande beaucoup de code supplémentaire. Je mets en place des méthodes réutilisables, des "traits", des classes abstraites, des process personnalisés, etc.

Avec cet aperçu, vous êtes maintenant prêt pour débuter la migration de vos sites et passer à Drupal 8 ;-)

Commentaires

Si vous avez besoin de

Si vous avez besoin de traduire Drupal 8 site, je crois que vous pouvez essayer une pateforme de localisation comme https://poeditor.com/ qui peut simplifier le travail.

Merci David. Il existe

Merci David. Il existe plusieurs plateformes de ce type, dont certaines proposent des outils plus ou moins performants ou des services personnalisés de traduction. Pour ma part, je fais généralement le travail moi-même quand je le peux, et google propose un assez bon outil pour la gestion des fichiers .po ici: https://translate.google.com/toolkit

Ajouter un commentaire

CAPTCHA
Cette question permet de savoir si vous êtes un visiteur ou un robot de soumission de spams