Cómo migrar contenido a Drupal 8 desde un csv

Cuando encargan una web en drupal a Omitsis es muy habitual que sea una web ya existente y que por lo tanto tenga un contenido que quieran migrar.

En Drupal existe el módulo migrate que está incorporado en el core y que nos permite migrar contenido. Es un módulo muy potente, versátil y que facilita realizar rollbacks de serie. Los rollbacks nos permiten «desimportar», muy útil para cuando aún estamos desarrollando y hay errores.

El módulo migrate permite importar desde diferentes fuentes: un Drupal 7, una base de datos tenga la estructura que sea, desde un csv, un JSON, XML, SOAP, etc.

Es muy común que nos pasen un excel con todos los campos a migrar por cada tipo de contenido. En este post vamos a explicar como realizar este tipo de migración: desde un csv, ya que todas las hojas de cálculo (excel por ejemplo) permite guardar como csv y este es un formato estándar mejor soportado.

Todo lo que vamos a explicar es válido para Drupal 8 y para Drupal 9, ya que todos los módulos están disponibles para ambas versiones excepto el config_devel que parece que estará próximamente y no es un módulo imprescible.

Lo primero es instalar los módulos que serán necesarios. El migrate ya está en el code de Drupal pero vamos a necesitar algunos más: migrate_plus, migrate_tools, migrate_source_csv y config_devel.

Instalamos estos módulos usando composer:

composer require drupal/migrate_plus drupal/migrate_tools drupal/migrate_source_csv drupal/config_devel

Y los activamos, con drush sería así:

drush -y en migrate migrate_plus migrate_tools migrate_source_csv config_devel

Ahora hemos de crear los ficheros yml que definirán las migraciones. Esto lo hemos de hacer dentro de un módulo custom, que podemos tener ya o podemos crear uno específico para el migrate (opción más limpia).

Puedes generar un módulo de forma fácil con la consola, en nuestro caso lo vamos a llamar custom_migrate.

drupal gm

En el directorio de este módulo creamos una subcarpeta llamada config y dentro de esta otra llamada install. Ahí pondremos los ficheros yml’s que definirán las migraciones.

La nomenclatura de estos ficheros hay de ser:

migrate_plus.migration.[ID_MIGRACION].yml

Por ejemplo, si queremos importar productos el nombre del fichero podría ser este:

migrate_plus.migration.products.yml

En este ejemplo el archivo estaría en la siguiente ruta:

web/modules/custom/custom_migrate/config/install/migrate_plus.migration.products.yml

El módulo migrate_plus nos permite también crear grupos, para luego hacer las importaciones de golpe (y los rollbacks). Puedes ver en este post cómo hacerlo. En este ejemplo no los usaremos, pero es bastante práctico.

Ahora hay que decirle a Drupal que hay de tener en cuenta el fichero de importación que definimos. Eso se pone en el .info.yml de nuestro módulo de la siguiente manera:

config_devel:
  install:
    - migrate_plus.migration.products

El archivo entero sería así:

name: 'Custom migrate'
type: module
description: 'Módulo de importaciones de contenido'
core: 8.x
package: 'Custom'
dependencies:
  - migrate
  - migrate_plus
  - migrate_tools
  - config_devel
config_devel:
  install:
    - migrate_plus.migration.products
#   - migrate_plus.migration.otra_importacion

Esto lo podemos hacer gracias al módulo config_devel, ya que sin este módulo solo se pondría esta configuración cuando instalamos el módulo, y no es nada práctico tener que estar instalando y desinstalando.

Ahora nos falta lo más importante: el contenido del yml de importación. Esto daría para muchos posts, así que vamos a poner un ejemplo sencillo:

Primero es necesario definir el id y un label irá bien para ponerle un nombre bonito.

id: products
label: Import products

Luego va la sección del source. Aquí hay que decirle que plugin usaremos y para el caso del csv un path donde estará el csv. En este caso lo hemos puesto dentro del módulo para poderlo integrar en nuestro git.

También le hemos indicado los ids únicos, que en nuestro csv se llamaba «code».

Y finalmente indicamos los campos del csv.

source:
  plugin: 'csv'
  # Full path to the file.
  path: 'modules/custom/custom_migration/data/products.csv'
  # Column delimiter. Comma (,) by default.
  delimiter: ','
  # Field enclosure. Double quotation marks (") by default.
  enclosure: '"'
  # The row to be used as the CSV header (indexed from 0),
  # or null if there is no header row.
  header_offset: 0
  # The column(s) to use as a key. Each column specified will
  # create an index in the migration table and too many columns
  # may throw an index size error.
  ids:
    - code
  # Here we identify the columns of interest in the source file.
  # Each numeric key is the 0-based index of the column.
  # For each column, the key below is the field name assigned to
  # the data on import, to be used in field mappings below.
  # The label value is a user-friendly string for display by the
  # migration UI.
  fields:
    0:
      name: cat
      label: 'Categoria'
    1:
      name: code
      label: 'Product code'
    2:
      name: title
      label: 'Title'
    3:
      name: image
      label: 'Image'

Ahora la parte del process, dónde indicamos los campos a dónde hay que poner los datos, el plugin que vamos a usar y la fuente (source).

Si es algo muy simple podría ser tan sencillo como esto:

field_nombre_campo_en_nuestro_drupal: nombre_columna_source

Pero muchas veces no es tan simple. Por ejemplo nos pasan el título todo en mayúsculas, así que combinamos varios plugins. También verás la parte de importación a un campo media. Esto lo explicaremos en otro post en breve.

Un caso real podría ser este:

process:
  type:
    plugin: default_value
    default_value: producto
  field_cat:
    plugin: entity_lookup
    source: division
    value_key: name
    bundle_key: vid
    bundle: cat_producto
    entity_type: taxonomy_term
    ignore_case: true
  field_producto_codigo: code
  title:
    -
      source: title
      plugin: callback
      callable: mb_strtolower
    -
      plugin: callback
      callable: ucfirst
  field_imagen/target_id:
    plugin: entity_lookup
    value_key: name
    source: image
    bundle_key: bundle
    bundle: image
    entity_type: media
    ignore_case: 1
    access_check: 0

Hay muchos plugins disponibles, los puedes ver aquí. Además podemos crear plugins de forma muy fácil.

Ya solo falta indicar a dónde irá todo este contenido con destination. dependerá de la entidad que estemos importando, en este caso son nodos.

destination:
  plugin: entity:node

Y aquí todo entero:

id: products
label: Import products

source:
  plugin: 'csv'
  # Full path to the file.
  path: 'modules/custom/custom_migration/data/products.csv'
  # Column delimiter. Comma (,) by default.
  delimiter: ','
  # Field enclosure. Double quotation marks (") by default.
  enclosure: '"'
  # The row to be used as the CSV header (indexed from 0),
  # or null if there is no header row.
  header_offset: 0
  # The column(s) to use as a key. Each column specified will
  # create an index in the migration table and too many columns
  # may throw an index size error.
  ids:
    - code
  # Here we identify the columns of interest in the source file.
  # Each numeric key is the 0-based index of the column.
  # For each column, the key below is the field name assigned to
  # the data on import, to be used in field mappings below.
  # The label value is a user-friendly string for display by the
  # migration UI.
  fields:
    0:
      name: cat
      label: 'Categoria'
    1:
      name: code
      label: 'Product code'
    2:
      name: title
      label: 'Title'
    3:
      name: image
      label: 'Image'

process:
  field_cat:
    plugin: entity_lookup
    source: division
    value_key: name
    bundle_key: vid
    bundle: cat_producto
    entity_type: taxonomy_term
    ignore_case: true
  field_producto_codigo: code
  title:
    -
      source: title
      plugin: callback
      callable: mb_strtolower
    -
      plugin: callback
      callable: ucfirst
  type:
    plugin: default_value
    default_value: producto
  field_imagen/target_id:
    plugin: entity_lookup
    value_key: name
    source: image
    bundle_key: bundle
    bundle: image
    entity_type: media
    ignore_case: 1
    access_check: 0

destination:
  plugin: entity:node

migration_dependencies: {}

Ahora finalmente solo nos queda ejecutar los comandos desde drush para realizar la importación. También se puede hacer desde UI en admin/structure/migrate.

Lo primero es decirle a drupal que cargue todos los archivos de importación con el módulo config_devel:

drush cdi [nombre_del_modulo]

Que en nuestro caso sería:

drush cdi custom_migrate

Luego podemos ver las migraciones que tenemos y su estando con:

drush ms

Para realizar la importación:

drush mim [id_importacion]

Que en nuestro caso sería:

drush mim products

Si algo sale mal, que lo normal es que pase, podemos hacer un rollback con este comando:

drush mr products

También es posible que la migración se atasque, con lo cual primero antes de hacer el rollback hay que ponerla en idle:

drush mrs products

Y esto es más o menos todo, como introducción a una migración con Drupal desde un csv usando el módulo migrate.

by Carlos Rincón
Programador Drupal Senior con experiencia en SEO, SEM, analíticas web, aplicaciones móbil y usabilidad.