Com migrar contingut a Drupal modern des d’un csv

Client
  • Omitsis
Tecnologies
Data
  • 04/05/2026

Quan ens encarreguen una web en drupal a Omitsis és molt habitual que sigui una web ja existent i que per tant tingui un contingut que vulguin migrar.

A Drupal existeix el mòdul migrate que està incorporat al core i que ens permet migrar contingut. És un mòdul molt potent, versàtil i que facilita fer rollbacks de sèrie. Els rollbacks ens permeten «desimportar», molt útil per quan encara estem desenvolupant i hi ha errors.

El mòdul migrate permet importar des de diferents fonts: un Drupal 7, una base de dades tingui l’estructura que tingui, des d’un csv, un JSON, XML, SOAP, etc.

És molt comú que ens passin un excel amb tots els camps a migrar per cada tipus de contingut. En aquest post explicarem com fer aquest tipus de migració: des d’un csv, ja que tots els fulls de càlcul (excel per exemple) permeten desar com a csv i aquest és un format estàndard més ben suportat.

Tot el que explicarem és vàlid per a Drupal modern. Quan dic “Drupal modern” em refereixo a Drupal 8 i posteriors (9, 10, 11…), perquè a partir del 8 va canviar bastant la cosa amb l’adopció de Symfony i les actualitzacions entre versions van començar a ser més portables. Avui en dia, si estàs començant un projecte, hauries d’estar a Drupal 10 o Drupal 11. Drupal 7, 8 i 9 ja no tenen suport, així que si encara tens alguna cosa allà… bé, ja tens una altra cosa a la llista de tasques.

El primer és instal·lar els mòduls que seran necessaris. El migrate ja està al core de Drupal però necessitarem alguns més: migrate_plusmigrate_tools i migrate_source_csv.

Instal·lem aquests mòduls fent servir composer:

composer require drupal/migrate_plus drupal/migrate_tools drupal/migrate_source_csv

I els activem, amb drush seria així:

drush -y en migrate migrate_plus migrate_tools migrate_source_csv

Ara hem de crear els fitxers yml que definiran les migracions. Això ho hem de fer dins d’un mòdul custom, que podem tenir ja o podem crear-ne un d’específic per al migrate (opció més neta).

Pots generar un mòdul de manera fàcil amb la drush, en el nostre cas el direm custom_migrate.

drush generate module

Al directori d’aquest mòdul creem una subcarpeta anomenada config i dins d’aquesta una altra anomenada install. Aquí posarem els fitxers yml que definiran les migracions.

La nomenclatura d’aquests fitxers ha de ser:

migrate_plus.migration.[ID_MIGRACIO].yml

Per exemple, si volem importar productes el nom del fitxer podria ser aquest:

migrate_plus.migration.products.yml

En aquest exemple l’arxiu estaria a la següent ruta:

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

El mòdul migrate_plus ens permet també crear grups, per després fer les importacions de cop (i els rollbacks). Pots veure en aquest post com fer-ho. En aquest exemple no els farem servir, però és bastant pràctic.

Ara necessitem un mòdul més: config_devel. Aquest ens permet que Drupal torni a llegir els fitxers de configuració quan li demanem, sense haver d’estar instal·lant i desinstal·lant el mòdul cada vegada que canviem alguna cosa. Això sí, config_devel és només per a entorns de desenvolupament, no el deixis activat en producció.

Per instal·lar-lo:

composer require --dev drupal/config_devel

I per activar-lo:

drush -y en config_devel

Ara cal dir-li a Drupal que ha de tenir en compte el fitxer d’importació que hem definit. Això es posa al .info.yml del nostre mòdul. L’arxiu sencer quedaria així:

name: 'Custom migrate'
type: module
description: 'Mòdul d''importacions de contingut'
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.altra_importacio

Ara ens falta el més important: el contingut del yml d’importació. Això donaria per a molts posts, així que posarem un exemple senzill:

Primer cal definir l’id i un label anirà bé per posar-li un nom bonic.

id: products
label: Import products

Després va la secció del source. Aquí cal dir-li quin plugin farem servir i per al cas del csv un path on hi haurà el csv. En aquest cas l’hem posat dins del mòdul per poder-lo integrar al nostre git.

També li hem indicat els ids únics, que al nostre csv s’anomenava «code».

I finalment indiquem els camps 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'

Ara la part del process, on indiquem els camps on cal posar les dades, el plugin que farem servir i la font (source).

Si és una cosa molt simple podria ser tan senzill com això:

field_nom_camp_al_nostre_drupal: nom_columna_source

Però moltes vegades no és tan simple. Per exemple ens passen el títol tot en majúscules, així que combinem diversos plugins. També veuràs la part d’importació a un camp media. Això ho explicarem en un altre post aviat.

Un cas real podria ser aquest:

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

Hi ha molts plugins disponibles, els pots veure aquí. A més podem crear plugins de manera molt fàcil.

Ja només falta indicar on anirà tot aquest contingut amb destination. Dependrà de l’entitat que estiguem important, en aquest cas són nodes.

destination:
  plugin: entity:node

I aquí tot sencer:

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: {}

Ara finalment només ens queda executar les comandes des de drush per fer la importació. També es pot fer des de la UI a admin/structure/migrate.

Un apunt abans de seguir: faré servir els alies curts perquè són els que faig servir jo en el dia a dia i els tinc ficats als dits. Si prefereixes la forma llarga, aquí tens l’equivalència:

Àlies curtForma llarga
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

El primer és dir-li a drupal que carregui tots els arxius d’importació amb el mòdul config_devel:

drush cdi [nom_del_modul]

Que en el nostre cas seria:

drush cdi custom_migrate

Recorda que el YAML ha d’estar a config/install i llistat a la secció config_devel: del .info.yml. Si falta qualsevol de les dues coses, drush cdi no el carregarà.

I apunta’t això en algun lloc perquè hi ensopegaràs: cada vegada que toquis el YAML de la migració has de tornar a executar drush cdi. Si no, els canvis no s’apliquen i et tornaràs boig intentant entendre per què la migració continua fent el mateix d’abans. T’ho dic per experiència.

Després podem veure les migracions que tenim i el seu estat amb:

drush ms

Per fer la importació:

drush mim [id_importacio]

Que en el nostre cas seria:

drush mim products

Si alguna cosa surt malament, que el normal és que passi, podem fer un rollback amb aquesta comanda:

drush mr products

També és possible que la migració s’encalli, amb la qual cosa primer abans de fer el rollback cal posar-la en idle:

drush mrs products

Alguns flags que val la pena conèixer

Quan portis unes quantes migracions a l’esquena et trobaràs amb que sempre acabes fent servir els mateixos quatre flags. Els poso aquí perquè no els hagis de descobrir a cops com vaig fer jo.

--limit

Per provar la migració amb unes poques files abans de llançar-la sencera. Si el teu CSV té 50.000 files i sospites que alguna cosa fallarà, no t’hi llencis de cap:

drush mim products --limit=10

--update

Reimporta els registres que ja estaven importats. Útil quan canvies alguna cosa al process i vols que s’apliqui també al que ja era a la base de dades, sense haver de fer rollback complet:

drush mim products --update

--idlist

Per reimportar (o importar per primera vegada) un id concret o diversos. Va perfecte quan estàs depurant un cas estrany i et vols centrar en una fila específica:

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

--sync

Sincronitza el destí amb l’origen: importa el nou, actualitza l’existent i esborra del destí el que ja no és a l’origen. Compte amb aquest, perquè si t’equivoques esborrant del CSV et carregues contingut a Drupal. Però si el que vols és just això, t’estalvia molt embolic:

drush mim products --sync

I això és més o menys tot. No pretén ser una guia exhaustiva del mòdul migrate, donaria per a moltíssims posts (importar imatges a media, paragraphs, taxonomies amb jerarquia, plugins custom…), però com a introducció per fer una migració des d’un CSV crec que cobreix el bàsic.

Si t’ha servit i t’ha estalviat un parell d’hores de cops de cap contra el teclat, doncs genial. I si ho comparteixes amb algú que estigui començant amb migracions, millor que millor.

Carlos Rincón

Carlos Rincón

developer