Cómo migrar contenido a Drupal moderno desde un csv

Cliente
  • Omitsis
Tecnologías
Servicios
Fecha
  • 04/05/2026

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 moderno. Cuando digo «Drupal moderno» me refiero a Drupal 8 y posteriores (9, 10, 11…), porque a partir del 8 cambió bastante la cosa con la adopción de Symfony y los upgrades entre versiones empezaron a ser más llevaderos. Hoy en día, si estás empezando un proyecto, deberías estar en Drupal 10 o Drupal 11. Drupal 7, 8 y 9 ya no tienen soporte, así que si aún tienes algo ahí… bueno, ya tienes otra cosa en la lista de tareas.

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

Instalamos estos módulos usando composer:

composer require drupal/migrate_plus drupal/migrate_tools drupal/migrate_source_csv

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

drush -y en migrate migrate_plus migrate_tools migrate_source_csv

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 drush, en nuestro caso lo vamos a llamar custom_migrate.

drush generate module

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 necesitamos un módulo más: config_devel. Este nos permite que Drupal vuelva a leer los ficheros de configuración cuando se lo pidamos, sin tener que estar instalando y desinstalando el módulo cada vez que cambiamos algo. Eso sí, config_devel es solo para entornos de desarrollo, no lo dejes activado en producción.

Para instalarlo:

composer require --dev drupal/config_devel

Y para activarlo:

drush -y en config_devel

Ahora hay que decirle a Drupal que tiene de tener en cuenta el fichero de importación que definimos. Eso se pone en el .info.yml de nuestro módulo. El archivo entero quedaría así:

name: 'Custom migrate'
type: module
description: 'Módulo de importaciones de contenido'
core_version_requirement: ^10 || ^11
package: 'Custom'
dependencies:
  - migrate
  - migrate_plus
  - migrate_tools
  - config_devel
config_devel:
  install:
    - migrate_plus.migration.products
#   - migrate_plus.migration.otra_importacion

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: cat
    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: cat
    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.

Un apunte antes de seguir: voy a usar los aliases cortos porque son los que uso yo en el día a día y los tengo metidos en los dedos. Si prefieres la forma larga, aquí tienes la equivalencia:

Alias cortoForma larga
drush mimdrush migrate:import
drush mrdrush migrate:rollback
drush msdrush migrate:status
drush mrsdrush migrate:reset-status
drush cdidrush config:devel-import
drush endrush pm:install

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

Recuerda que el YAML tiene que estar en config/install y listado en la sección config_devel: del .info.yml. Si falta cualquiera de las dos cosas, drush cdi no lo va a cargar.

Y apunta esto en algún sitio porque vas a tropezar con ello: cada vez que toques el YAML de la migración tienes que volver a ejecutar drush cdi. Si no, los cambios no se aplican y te volverás loco intentando entender por qué la migración sigue haciendo lo mismo de antes. Lo digo por experiencia.

Luego podemos ver las migraciones que tenemos y su estado 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

Algunos flags que vale la pena conocer

Cuando lleves unas cuantas migraciones a la espalda te encontrarás con que siempre acabas usando los mismos cuatro flags. Los pongo aquí para que no tengas que descubrirlos a hostias como hice yo.

--limit

Para probar la migración con unas pocas filas antes de lanzarla entera. Si tu CSV tiene 50.000 filas y sospechas que algo va a fallar, no te lances de cabeza:

drush mim products --limit=10

--update

Reimporta los registros que ya estaban importados. Útil cuando cambias algo en el process y quieres que se aplique también a lo que ya estaba en la base de datos, sin tener que hacer rollback completo:

drush mim products --update

--idlist

Para reimportar (o importar por primera vez) un id concreto o varios. Va perfecto cuando estás depurando un caso raro y quieres centrarte en una fila específica:

drush mim products --idlist=ABC123
drush mim products --idlist=ABC123,DEF456

--sync

Sincroniza el destino con el origen: importa lo nuevo, actualiza lo existente y borra del destino lo que ya no esté en el origen. Cuidado con este, porque si te equivocas borrando del CSV te cargas contenido en Drupal. Pero si lo que quieres es justo eso, te ahorra mucho lío:

drush mim products --sync

Y esto es más o menos todo. No pretende ser una guía exhaustiva del módulo migrate, daría para muchísimos posts (importar imágenes a media, paragraphs, taxonomías con jerarquía, plugins custom…), pero como introducción para hacer una migración desde un CSV creo que cubre lo básico.

Si te ha servido y te ha ahorrado un par de horas de cabezazos contra el teclado, pues genial. Y si lo compartes con alguien que esté empezando con migraciones, mejor que mejor.

Carlos Rincón

Carlos Rincón

developer