Qué es la Migrate API
En resumen: Guía completa de la Migrate API en Drupal 11 para importar contenido desde JSON, CSV o APIs externas. Define source, process y destination en YAML, depura con drush y crea plugins de proceso personalizados.
- Importa contenido desde JSON con migrate_plus y el plugin url
- Define normalizadores para media, taxonomías y relaciones entre entidades
- Depura con drush migrate:import, —migrate-debug y migraciones incrementales
- Aplica rollback, reset-status y plugins de proceso personalizados
Cuando necesitas importar contenido a Drupal desde un sistema externo — una API REST, un fichero JSON, una base de datos heredada o un CSV — la Migrate API es la herramienta nativa para hacerlo. No es un módulo puntual: es un framework completo que gestiona el ciclo de vida de la importación, desde la extracción del dato hasta la creación de entidades en Drupal, pasando por transformaciones, validaciones y control de errores.
Es importante distinguir dos escenarios:
- Migraciones de contenido custom (lo que cubre esta guía): importas datos desde una fuente externa (JSON, XML, CSV) usando el plugin
urlde migrate_plus y ficheros YAML que defines tú. - Ruta de upgrade D7→D11: el módulo
migrate_upgradegenera automáticamente las migraciones para pasar de Drupal 7 a Drupal 11. Son dos cosas distintas: esta guía no cubre la ruta de upgrade, que será tema de otro artículo.
La Migrate API de Drupal funciona con un esquema simple: source (de dónde vienen los datos), process (cómo se transforman) y destination (a dónde van). Todo se define en ficheros YAML y se ejecuta con comandos drush.
Instalación
Necesitas tres módulos contrib que extienden el framework de migrate de Drupal core:
composer require drupal/migrate_plus:^6.0
composer require drupal/migrate_tools:^6.1
composer require drupal/migrate_devel:^3.0
| Módulo | Versión | Drupal | Función |
|---|---|---|---|
migrate_plus | ^6.0 | ^10.5 || ^11 | Source plugin url, parsers JSON/XML, entity_generate, entity_lookup |
migrate_tools | ^6.1 | ^9.1 || ^10 || ^11 | Comandos drush: status, import, rollback, messages, fields-source |
migrate_devel | ^3.0 | ^10 || ^11 | Depuración: —migrate-debug, plugin debug |
Dentro de un módulo personalizado, creamos la carpeta migrations justo en la raíz del módulo. Drupal descubre automáticamente los ficheros YAML que encuentre ahí:
modules/
mi_modulo/
mi_modulo.info.yml
mi_modulo.module
migrations/
noticias.json.yml
noticias_es.yml
Cada fichero YAML corresponde a una migración. El identificador del fichero (id) es único dentro del sistema y es lo que usas en los comandos drush.
El ciclo de vida de una importación
Antes de meternos en el YAML, es importante entender las operaciones básicas que vamos a ejecutar con drush.
Ver el estado de las migraciones
drush migrate:status
Cada fichero YAML genera un registro en la tabla resultante:
------------------- ------------------ -------- ------- ---------- ------------- ---------------------
Group Migration ID Status Total Imported Unprocessed Last Imported
------------------- ------------------ -------- ------- ---------- ------------- ---------------------
Default (default) noticias_json Idle 666 1 665 2025-07-16 14:46:36
Default (default) noticias_json_es Idle 666 0 666
------------------- ------------------ -------- ------- ---------- ------------- ---------------------
Las columnas son: Migration ID (identificador del YAML), Status (Idle = listo, Importing = en progreso, Idle después de error si se quedó bloqueado), Total (registros en la fuente), Imported (ya importados), Unprocessed (pendientes) y Last Imported (última ejecución).
Los mensajes de error no aparecen aquí. Para verlos:
drush migrate:messages noticias_json
Ejecutar una importación
drush migrate:import noticias_json --limit=1 --migrate-debug
| Parámetro | Función |
|---|---|
--limit=1 | Número máximo de entidades a importar en esta ejecución |
--migrate-debug | Salida detallada: muestra source, destination y ID de cada fila (necesita migrate_devel) |
--migrate-debug-pre | Igual pero antes de que se ejecute el process (no muestra el resultado del guardado) |
--update | Actualiza entidades ya importadas con los datos actuales de la fuente |
--execute-dependencies | Ejecuta primero las migraciones de las que depende esta |
Si no pasas --limit, se importan todos los pendientes. Si la migración es grande, usa --limit para ir probando.
Revertir importaciones
Si algo ha salido mal o necesitas volver a empezar:
drush migrate:rollback noticias_json
El rollback solo borra lo importado por esa migración. Usa la tabla migrate_map_noticias_json para saber qué entidades creó. Cada registro de la tabla migrate_map_* almacena la relación entre el ID de la fuente y el ID de la entidad creada en Drupal. El rollback recorre esa tabla y borra las entidades correspondientes.
Lo que no borra son entidades que ya existían antes de la migración o que fueron creadas por otra migración. Si dos migraciones crean la misma entidad (mismo source ID), el rollback de una no afecta a la otra.
Resetear un estado bloqueado
Si la migración se queda en estado “Importing” (por un error, un timeout o un kill del proceso):
drush migrate:reset-status noticias_json
Esto devuelve el estado a Idle. Necesario antes de volver a ejecutar la importación.
Cadena de pruebas
En desarrollo, esta es la cadena que uso para probar cambios en el YAML:
drush migrate:reset-status noticias_json && \
drush migrate:rollback noticias_json && \
drush cr && \
drush migrate:import noticias_json --limit=1 --migrate-debug
Reset → Rollback → Cache clear → Import con una fila y depuración. Si la fila se importa correctamente, amplía el --limit o quítalo para procesar todo.
Anatomía de un fichero de migración
Un fichero YAML de migración tiene tres secciones principales: source, process y destination.
id: noticias_json
label: 'Migración de noticias desde JSON'
migration_group: default
source:
process:
destination:
plugin: 'entity:node'
id: identificador único. Es lo que usas en drush. No tiene por qué coincidir con el nombre del fichero.label: nombre legible para humanos.migration_group: agrupación lógica. Puedes filtrar por grupo con--groupen drush.source: define de dónde y cómo vienen los datos.process: el procesamiento del origen de datos a campos de Drupal.destination: dónde se guardan los datos.
Drupal almacena una tabla de mapeo por cada migración: migrate_map_{id}. Esta tabla registra qué ID de la fuente corresponde con qué entidad de Drupal, y es la que permite el rollback y el --update.
Source: de JSON a filas
El source plugin url de migrate_plus permite obtener datos a través de una URL, sea remota o local. Para un fichero JSON local, la configuración es:
source:
plugin: url
data_fetcher_plugin: file
urls:
- public://import/noticias.json
data_parser_plugin: json
item_selector: ''
fields:
- name: id_contenido
label: 'ID del contenido original'
selector: ContentId
- name: titulo
label: 'Título'
selector: title
- name: body
label: 'Body'
selector: textNoticia
- name: imagen_url
label: 'Imagen principal'
selector: imatgePrincipal
ids:
id_contenido:
type: integer
| Clave | Función |
|---|---|
plugin: url | Source plugin de migrate_plus para datos vía URL |
data_fetcher_plugin: file | Usa el fetcher de ficheros locales (para URLs remotas usa http) |
data_parser_plugin: json | Parsea la respuesta como JSON |
item_selector: '' | Array raíz: cada elemento del array principal es una fila. Si el JSON tiene estructura anidada, usa un selector como items o /data/results |
fields | Define qué campos extraemos del JSON. name es la variable interna, selector es la ruta en el JSON (usa / para navegar la estructura) |
ids | Identificador único de cada fila en la fuente. Drupal lo usa para relacionar fuentes con entidades. No necesitas crear un campo para esto: la tabla migrate_map_* lo gestiona automáticamente |
Nota sobre item_selector: en migrate_plus ≥6.0.3, un item_selector vacío ('') significa “array raíz”. En versiones anteriores se usaba /. Si tu JSON tiene los datos dentro de una clave, usa esa clave como selector (por ejemplo, data o results).
Una vez ejecutas migrate:status, el Total corresponde al número de elementos que el parser encontró en la fuente.
Process: del dato al campo
La sección process mapea los datos del source a los campos de Drupal. Hay varios patrones:
Mapeo directo
El más sencillo: un campo de la fuente se guarda directamente en un campo de Drupal.
process:
type:
plugin: default_value
default_value: article
langcode:
plugin: default_value
default_value: es
title: titulo
body: body
type y langcode usan default_value porque no vienen de la fuente. title y body son mapeos directos: Drupal usa el plugin get implícitamente para copiar el valor.
Callback: funciones propias
El plugin callback permite llamar a cualquier función PHP y pasarle uno o más parámetros:
process:
field_debug_text:
plugin: callback
callable: basename
source: imagen_url
Esto ejecuta basename($imagen_url) y guarda el resultado en field_debug_text. Como callable puedes usar cualquier función PHP incluyendo funciones definidas en el .module de tu módulo.
Variables temporales
Puedes almacenar valores intermedios usando prefijo _. Estas variables no se guardan en campos de Drupal, sino que se usan como paso intermedio para otros plugins:
process:
_temp_filename:
plugin: callback
callable: basename
source: imagen_url
_temp_destination:
plugin: callback
callable: mi_modulo_generar_ruta
source: "@_temp_filename"
Las variables temporales se referencian con @ (por ejemplo, @_temp_filename). El prefijo _ le indica a Drupal que esto no es un campo destino real.
Pipeline: de URL a alias
Una de las funcionalidades más potentes de la Migrate API es el pipeline: encadenar varios plugins de proceso donde la salida de uno es la entrada del siguiente.
Ejemplo práctico: transformar una URL completa en un alias de Drupal:
process:
path:
- plugin: get
source: canonical_url
- plugin: callback
callable: parse_url
- plugin: extract
index:
- path
- plugin: single_value
El valor va pasando por cada paso:
- get: extrae
canonical_urlde la fuente →"https://www.ejemplo.org/noticias/la-mi-noticia" - callback: ejecuta
parse_url()→ devuelve un array con las partes de la URL - extract: extrae el valor del índice
pathdel array →["/noticias/la-mi-noticia"] - single_value: convierte el array de un solo elemento en un valor simple →
"/noticias/la-mi-noticia"
Los plugins de este pipeline son todos de Drupal core: get, callback, extract y single_value.
Imágenes: pipeline completo
Migrar imágenes es uno de los escenarios más complejos porque implica tres pasos: descargar el fichero, crear la entidad File y crear la entidad Media que referencia al fichero.
1. Descargar el fichero
El plugin download (de Drupal core) descarga un fichero desde una URL y lo guarda en el sistema de ficheros de Drupal. Necesita dos parámetros: la URL de origen y la ruta de destino.
process:
_temp_filename:
plugin: callback
callable: basename
source: imagen_url
_temp_destination:
plugin: callback
callable: mi_modulo_generar_ruta
source: "@_temp_filename"
_temp_file_uri:
plugin: download
source:
- imagen_url
- '@_temp_destination'
file_exists: rename
Opción file_exists | Comportamiento |
|---|---|
replace (por defecto) | Sobrecribe si ya existe |
rename | Añade _0, _1… hasta que el nombre sea único |
use existing | No descarga nada y devuelve FALSE si ya existe |
El plugin download devuelve la URI del fichero descargado (por ejemplo, public://import/foto.jpg), pero no crea la entidad File. Necesitas un paso adicional para eso.
2. Crear la entidad File
Aquí es donde tu módulo custom entra en juego. Necesitas una función que reciba la URI y devuelva el ID de la entidad File creada:
<?php
use Drupal\file\Entity\File;
/**
* Crea una entidad File a partir de una URI y devuelve su ID.
*/
function mi_modulo_crear_entidad_archivo(string $file_uri): int {
// File::create() no descarga nada: registra la URI como entidad.
$file = File::create([
'uri' => $file_uri,
'status' => 1, // Publicado.
]);
$file->save();
// Añadir uso para que no se marque como huérfana.
\Drupal::service('file.usage')
->add($file, 'mi_modulo', 'migration', 1);
return $file->id();
}
En el YAML, lo llamamos como callback:
_temp_file_id:
plugin: callback
callable: mi_modulo_crear_entidad_archivo
source: "@_temp_file_uri"
_temp_file_id ahora contiene el ID de la entidad File (por ejemplo, 42).
Alternativa oficial: en lugar de un callback custom, puedes usar destination: plugin: entity:file como destino de la migración. Esto crea la entidad File automáticamente. El approach con callback es útil cuando necesitas lógica adicional (validar tipo MIME, generar thumbnails, etc.).
3. Crear la entidad Media
Con el ID del fichero, creamos la entidad Media usando entity_generate:
field_imatge_principal:
plugin: entity_generate
entity_type: media
bundle: image
bundle_key: bundle
value_key: name
source:
- '@_temp_filename'
values:
field_media_image/target_id: '@_temp_file_id'
field_media_image/alt: '@_temp_filename'
| Clave | Función |
|---|---|
entity_type: media | Tipo de entidad a crear |
bundle: image | Bundle del Media (image, video, document…) |
bundle_key: bundle | Campo que almacena el bundle en la entidad media |
value_key: name | Campo usado para buscar si ya existe (evita duplicados) |
source | Valor a buscar en value_key |
values | Campos adicionales a setear en la entidad Media |
entity_generate es un plugin de migrate_plus que extiende entity_lookup. Primero busca si ya existe una entidad Media con ese name. Si la encuentra, devuelve su ID. Si no, la crea con los valores especificados.
Importante: los tipos de entidad con bundles (nodes, media, taxonomy_terms) requieren bundle_key explícito. Para media, el bundle_key es bundle. Para taxonomy terms, es vid. Para nodes, es type.
Listas y relaciones
sub_process para listas
Cuando el JSON contiene arrays (listas de elementos), el plugin sub_process itera sobre cada elemento:
process:
field_recursos:
plugin: sub_process
source: recursos
process:
title: Caption
uri: Link
'options/attributes/target':
plugin: static_map
source: NewWindow
default_value: ''
map:
'true': '_blank'
Para cada elemento del array recursos, ejecuta el bloque process interior. static_map convierte el valor booleano NewWindow en _blank o cadena vacía.
migration_lookup para referencias
Cuando necesitas referenciar entidades creadas por otra migración, usa el plugin migration_lookup de migrate_plus:
process:
field_autor:
plugin: migration_lookup
source: autor_id
migration: migracion_autores
no_stub: true
Esto busca en la tabla migrate_map_migracion_autores si el autor_id tiene una entidad asociada. Si la encuentra, devuelve el ID de Drupal. Si no, devuelve null (o crea un stub si no_stub es false).
Para que migration_lookup funcione, necesitas declarar la dependencia:
migration_dependencies:
required:
- migracion_autores
Drupal ejecutará primero migracion_autores antes de esta migración si usas --execute-dependencies.
Plugins de proceso personalizados
Cuando los plugins de core y migrate_plus no cubren tu caso, puedes crear los tuyos. Deben estar en src/Plugin/migrate/process/ dentro de tu módulo.
Ejemplo: un plugin que descarga una imagen desde una URL y devuelve el ID de la entidad Media creada:
<?php
namespace Drupal\mi_modulo\Plugin\migrate\process;
use Drupal\Core\File\FileSystemInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\media\Entity\Media;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\migrate\Row;
use Drupal\migrate_plus\Plugin\migrate\process\EntityLookup;
use GuzzleHttp\ClientInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Busca o crea una entidad Media desde una URL.
*
* @MigrateProcessPlugin(
* id = "mi_modulo_media_from_url"
* )
*/
class MediaFromUrl extends EntityLookup implements ContainerFactoryPluginInterface {
protected $fileSystem;
protected $httpClient;
public static function create(
ContainerInterface $container,
array $configuration,
$plugin_id,
$plugin_definition,
MigrationInterface $migration = NULL
) {
$instance = parent::create($container, $configuration, $plugin_id, $plugin_definition, $migration);
$instance->fileSystem = $container->get('file_system');
$instance->httpClient = $container->get('http_client');
return $instance;
}
public function transform(
$value,
\Drupal\migrate\MigrateExecutableInterface $migrate_executable,
Row $row,
$destination_property
) {
// Configurar la búsqueda de la entidad.
$this->configuration['entity_type'] = 'media';
$this->configuration['bundle'] = $this->configuration['bundle'] ?? 'image';
$this->configuration['value_key'] = 'name';
// Buscar si ya existe la entidad Media.
$media_id = parent::transform($value, $migrate_executable, $row, $destination_property);
if ($media_id) {
return $media_id;
}
// No existe: crear la entidad.
$entity = $this->entityTypeManager->getStorage('media')->create([
'name' => $value,
'bundle' => $this->configuration['bundle'],
'field_media_image/target_id' => $this->descargarYCrearArchivo($value),
]);
$entity->save();
return $entity->id();
}
protected function descargarYCrearArchivo($url) {
// Lógica de descarga y creación de File entity.
// ...
}
}
Los puntos clave del plugin:
@MigrateProcessPlugin: anotación con elidque usarás en el YAML.create(): inyecta servicios del contenedor (file_system, http_client, etc.).transform(): recibe el valor procesado ($value), el ejecutable de la migración, elrow(toda la fila de datos) y el nombre del campo destino.parent::transform(): llama aEntityLookup::transform(), que busca la entidad. Si existe, devuelve el ID. Si no, devuelve null.
En el YAML, lo usas así:
field_imatge_principal:
plugin: mi_modulo_media_from_url
source: imagen_url
bundle: image
Depuración
Ver errores de una migración
drush migrate:messages noticias_json
Los mensajes incluyen el source ID, el nivel de severidad y el texto del error. Si una migración falla en una fila, el proceso continúa con las siguientes (a menos que uses --stop-on-failure).
Depurar fila a fila
Con migrate_devel instalado:
drush migrate:import noticias_json --limit=1 --migrate-debug
La salida muestra: datos de la fuente (source), resultado del process pipeline (process) y el ID de la entidad creada en Drupal (destination). Si usas --migrate-debug-pre, ves el resultado antes de que se guarde en la base de datos.
Listar campos disponibles del source
drush migrate:fields-source noticias_json
Devuelve los campos que definiste en la sección fields del source, con su nombre interno y descripción. Muy útil cuando estás escribiendo el YAML y no recuerdas cómo se llama un campo.
Importación incremental con —update
Cuando la fuente cambia periódicamente (por ejemplo, un JSON que se actualiza cada día), usa --update para sincronizar sin duplicar:
drush migrate:import noticias_json --update
Drupal compara el source ID de cada fila con la tabla migrate_map_*. Si el ID ya existe, actualiza la entidad correspondiente. Si es nuevo, lo importa. Esto funciona gracias al high water mark (high_water_property en la configuración del source), que Drupal usa para saber dónde se quedó la última vez.
Tablas de debug
Cada migración crea dos tablas en la base de datos:
| Tabla | Función |
|---|---|
migrate_map_{id} | Relación source ID → entity ID. Controla importación, rollback y update. |
migrate_message_{id} | Mensajes de error, warning e info generados durante la importación. |
Puedes consultarlas directamente con SQL para diagnosticar problemas:
-- Ver entidades importadas por una migración
SELECT * FROM migrate_map_noticias_json;
-- Ver errores
SELECT * FROM migrate_message_noticias_json;
Lecciones aprendidas
-
Prueba con
--limit=1antes de importar todo. El 90% de los errores de migración aparecen en la primera fila: un campo mal nombrado, un selector JSON incorrecto, un campo requerido sin valor. Importa una fila, verifica que el nodo se creó correctamente en Drupal, y luego amplía. -
Las variables temporales con
_son tu mejor amigo. Te permiten encadenar transformaciones complejas (descarga + creación de entidad + entity_generate) sin ensuciar los campos de destino. Piensa en ellas como el equivalent a las variables intermedias en un programa. -
No te saltes el
bundle_keyen entity_generate. Sinbundle_key, entity_lookup no sabe en qué bundle buscar y puede encontrar duplicados en bundles equivocados o fallar directamente. Siempre fijaentity_type,value_key,bundle_keyybundle. -
Usa
--migrate-debugcon--limit=1, nunca con miles de registros. La salida de depuración es enorme. Si necesitas debuggear una fila concreta, usa--idlisten lugar de--limit. -
El rollback no es una papelera de reciclaje. Borra entidades permanentemente. Si vas a hacer cambios grandes en el YAML, haz un backup de la base de datos antes de ejecutar
migrate:rollback.
La Migrate API es una de las herramientas más potentes de Drupal para integración de datos. Configurar una migración bien estructurada te ahorra horas de trabajo manual y te da un sistema reproducible y depurable. Si estás pensando en sincronizar contenido desde una API externa o importar datos de un sistema legacy, merece la pena invertir tiempo en aprender bien source, process y destination.
Si necesitas gestionar colas de procesamiento asíncrono para acompañar las migraciones, tienes la guía de Queue API. Y si el rendimiento es tu prioridad, la guía de cache bins te explica cómo aislar la caché a bajo nivel.
Preguntas frecuentes
¿Qué es la Migrate API de Drupal?
La Migrate API es el framework nativo de Drupal para importar contenido desde sistemas externos: APIs REST, JSON, CSV o bases de datos heredadas. Gestiona el ciclo de vida completo de la importación con tres secciones definidas en YAML: source (de dónde vienen los datos), process (cómo se transforman) y destination (a dónde van). Incluye transformaciones, validaciones, control de errores y trazabilidad mediante tablas migrate_map.
¿Qué módulos necesito para importar JSON con la Migrate API?
Necesitas tres módulos contrib: `migrate_plus` (^6.0) que aporta el source plugin `url`, los parsers JSON/XML y los plugins entity_generate y entity_lookup; `migrate_tools` (^6.1) que añade los comandos drush de status, import, rollback y messages; y `migrate_devel` (^3.0) para depurar con `--migrate-debug`. Los ficheros YAML se colocan en la carpeta `migrations/` del módulo personalizado.
¿Cómo importar una migración de Drupal con drush?
La cadena de importación se ejecuta con `drush migrate:import id_migracion`. Para probar de forma segura usa `--limit=1` y `--migrate-debug`, que muestra el source y destination de cada fila. Para actualizar entidades ya importadas añade `--update`, y para ejecutar las dependencias usa `--execute-dependencies`. Antes de cada prueba conviene hacer reset-status, rollback y cache clear.
¿Cómo revertir o resetear una migración en Drupal?
`drush migrate:rollback id_migracion` borra solo las entidades creadas por esa migración, recorriendo la tabla `migrate_map_*`. No elimina entidades que ya existían antes ni las creadas por otras migraciones. Si la migración se queda bloqueada en estado Importing por un error o timeout, `drush migrate:reset-status id_migracion` la devuelve a estado Idle para poder reejecutarla.
¿Qué diferencia hay entre una migración custom y la ruta de upgrade D7 a D11?
Las migraciones custom, que cubre esta guía, importan datos desde una fuente externa (JSON, XML, CSV) con el plugin `url` de migrate_plus y ficheros YAML propios. La ruta de upgrade D7→D11 usa el módulo `migrate_upgrade`, que genera automáticamente las migraciones para pasar de Drupal 7 a Drupal 11. Son frameworks distintos y requieren enfoques diferentes.
¿Qué estructura tiene un fichero YAML de migración?
Un fichero YAML de migración tiene tres secciones principales: `source`, donde se define la fuente de datos y el parser (por ejemplo, un endpoint JSON con el plugin `url`); `process`, donde se mapean y transforman los campos con callbacks, variables temporales y plugins como sub_process, migration_lookup o entity_generate; y `destination`, que indica el tipo de entidad donde se guardará el resultado.