[{"categories":["Migraciones"],"contents":"En los artículos anteriores de esta serie he explorado la API Migrate de Drupal desde distintos ángulos: primero como una caja de herramientas, después mediante ejemplos sencillos con datos incrustados y archivos CSV, más tarde con una hoja de cálculo de Google expuesta como JSON y, finalmente, dando un paso atrás para depurar el proceso cuando la realidad empieza a dar guerra. Porque siempre acaba dando guerra.\nEsta sexta entrega vuelve al lado práctico de la migración de datos: poblar Drupal con datos remotos y estructurados obtenidos de varias API. El objetivo no es construir de una sentada la migración más espectacular del mundo, sino diseñar un proceso lo bastante sencillo para entenderlo y lo bastante complejo para resultar realmente útil.\n¿Qué demuestra esto sobre Drupal? Que puede incorporar datos de varias API a un modelo de contenido estructurado, conservar las relaciones entre registros y convertir ese modelo en páginas que las personas puedan recorrer y consultar. Para quienes desarrollan directorios, catálogos u otros sitios que combinan datos externos, la experiencia Rick and Migraty ofrece un ejemplo práctico con la API Migrate, entidades de contenido personalizadas y Views. El repositorio enlazado y la configuración de DDEV permiten ejecutar el proyecto en local y examinar cómo encajan esas piezas.\nPara conseguirlo, nos conectaremos a API externas, abiertas y de acceso gratuito. Recursos como la API de Rick and Morty, la API de TVmaze y la wiki de Rick and Morty en Fandom nos proporcionarán los datos necesarios para poblar nuestras entidades de Drupal con personajes, localizaciones, episodios, puntuaciones y descripciones.\nLa API de Rick and Morty será nuestra fuente principal y proporcionará las entidades básicas y sus relaciones. TVmaze se unirá a la fiesta como fuente secundaria de enriquecimiento, aportando puntuaciones de episodios que integraremos en las entidades de Drupal existentes. Por último, Fandom contribuirá con descripciones narrativas, detalles biográficos y otra información contextual, para construir perfiles más ricos de los personajes y lugares de nuestro universo animado favorito.\nNota: A fecha de 14 de octubre de 2026, la versión original de este artículo supera los 52.000 caracteres y ronda las 10.000 palabras, con un tiempo de lectura estimado de entre 50 y 60 minutos.\n Fotografía de Unsplash, del usuario Julian Kraus, @juliankraus\nÍndice de contenidos\n 0. TL;DR (solo quiero jugar con Drupal)\n1. De dónde salió la idea (orígenes)\n2. Qué buscamos (objetivos)\n3. Analizar los datos (comprender)\n4. Algunas restricciones iniciales (espera)\n5. Contenedores de destino en Drupal (entidades)\n6. Diseñar la migración (cocinar)\n7. Definir plugins de migración (soluciones)\n8. Más información de las fuentes (enriquecimiento)\n9. De los datos al contenido (navegar)\n10. La migración como operación (ejecutar)\n11. Perder la cabeza (depurar)\n12. Decisiones que merece la pena destacar (decidir)\n13. Qué he aprendido (reflexionar)\n14. Próximos pasos (planificar)\n15. :wq!\n0. TL;DR (solo quiero jugar con Drupal) Vale, vale, no te juzgo. Asegúrate de cumplir los requisitos previos del proyecto:\n Tener Git instalado en tu equipo (git v2.43.0). Tener DDEV instalado (ddev v1.25.4, Docker v29.9.0). Disponer de una terminal de Linux.  Una vez comprobado todo, sigue estos pasos:\ngit clone https://gitlab.com/davidjguru/rick-and-migraty.git cd rick-and-migraty/ ddev start ddev composer install Con ello habrás configurado el proyecto en tu entorno de desarrollo local (LDE) utilizando DDEV. Ahora vamos a preparar la instalación de Drupal:\nddev drush site:install --existing-config \\  --account-name=admin \\  --account-pass=admin \\  -y ddev launch Así se completará la configuración del sitio y se abrirá el proyecto en tu navegador, en https://rick-and-migraty.ddev.site:\n Vista principal de Rick and Migraty en el navegador  Lo que realmente importa en la portada son las acciones que permite realizar el proyecto:\n Menú de acciones: aprender, explorar y borrar cosas  ¡Recuerda! Las credenciales de instalación son user: admin, password: admin. También puedes acceder a la documentación del proyecto en la URL local https://docs.rick-and-migraty.ddev.site. ¡Disfrútalo!\n¿Te gusta lo que ves? Puedes apoyarme compartiendo este artículo, dando a conocer el repositorio (https://gitlab.com/davidjguru/rick-and-migraty) o añadiéndole una estrella en GitLab:\n ¡Apoya el proyecto con una estrella! ⭐  1. De dónde salió la idea (orígenes) Durante una temporada estuve trabajando en procesos de migración en Drupal basados en la API Migrate. Algunas de aquellas experiencias profesionales terminaron convirtiéndose en la serie de artículos sobre migraciones en Drupal publicada aquí.\nPor esas mismas fechas llegó a mi vida la serie de animación de Adult Swim Rick and Morty (2013–actualidad). Durante un tiempo fue una de mis series favoritas, a pesar de sus altibajos, gracias a su ritmo frenético, su absoluta falta de filtros, sus colores vibrantes y la excéntrica relación entre un científico loco (Rick Sanchez) y su nieto (Morty Smith).\nQuería hacer algo con todo aquello: preparar una poción mágica con un poco de Drupal, mucho Rick and Morty y una dosis generosa de la API Migrate de Drupal. Llevaba tiempo dando vueltas a la idea de reunir información útil sobre la serie en una plataforma web que indexara su enorme cantidad de personajes, una cifra que parece crecer exponencialmente. Decidí probar… y así descubrí The Rick and Morty API, que me entusiasmó bastante.\nY así nació The Rick and Migraty Experience.\n La nueva experiencia Rick and Migraty: ¡Wubba Lubba Dub-Dub!  Algunos acontecimientos recientes me han dejado algo más de tiempo libre para retomar la idea justo donde la había dejado. Por suerte había conservado el trabajo anterior: investigación de experiencia de usuario, maquetas, esquemas de interfaz, diseños de entidades de Drupal y casos de uso para consultas. Montones de consultas, preparadas para recibir montones de respuestas JSON.\nEl tercer artículo de esta serie ya utilizaba una fuente con formato JSON, pero lo hacía mediante una transformación de una hoja de cálculo de Google y un analizador de un módulo contribuido que sabía interpretar aquella estructura especial. Esta vez quería centrarme en un escenario más propio del trabajo con API.\nLa API de Rick and Morty es un buen campo de pruebas para las migraciones en Drupal: es pública, lo bastante estable para un artículo, paginada, relacional y un poco disparatada en el buen sentido. Esa combinación vale más de lo que parece. Un ejemplo de migración no debería limitarse a importar tres filas planas y dar el trabajo por terminado; debería mostrar algunos de los problemas estructurales que encontramos en proyectos reales.\nEn este caso, el objetivo es diseñar el proceso de migración antes de escribir todo el código. Identificaremos los recursos de origen, propondremos un modelo de destino en Drupal, definiremos el orden de las operaciones y enumeraremos los plugins de proceso o transformaciones que probablemente necesitaremos.\n2. Qué buscamos (objetivos) La idea principal, el objetivo central, es construir una plataforma unificada para explorar y seguir tanto a los personajes que pueblan los episodios como a los propios episodios. Un lugar donde consultar perfiles de personajes como este:\n Esquema de baja fidelidad de un perfil de personaje  Un sitio que resulte útil para consultar información sobre los aproximadamente mil personajes existentes —las cifras varían según la fuente, pues muchos habitan en distintos lugares y no siempre hay consenso— y sobre los numerosos episodios y temporadas. Todo envuelto en una apariencia coherente, sencilla y atractiva. Se trata de convertir los bocetos originales en algo más interesante:\n Evolución del perfil: del esquema a la maqueta y al diseño  En un plano más concreto necesitamos una navegación básica, directa e intuitiva; una pequeña herramienta de búsqueda fácil de usar, con la que localizar y filtrar información; y, junto a la plataforma de datos, un espacio centralizado para reunir la documentación y el conocimiento del proyecto.\nComo ya existen plataformas de este tipo, necesitamos ir un paso más allá: reunir información que actualmente no se encuentra en un único lugar.\nPara conseguirlo, diseñaremos un proceso de migración capaz de combinar datos de varias fuentes cuando sea necesario. Pero antes de escribir archivos YAML debemos comprender bien la naturaleza del problema —o, mejor dicho, del desafío tecnológico— que pretendemos resolver.\n3. Analizar los datos (comprender) Lo primero es pensar cómo modelar lo que buscamos: qué información queremos obtener, qué novedad queremos introducir para ofrecer algo distintivo, como comentábamos antes, y cómo vamos a hacerlo.\nLa primera pregunta es: ¿qué consideraríamos información valiosa? Aquí van algunas ideas:\n Disponer de una sinopsis de cada episodio que contextualice lo que sucede. Obtener una puntuación o valoración de cada episodio. Tener una lista de los personajes que aparecen en cada episodio. Contar con información de contexto sobre los personajes, ya sea biográfica o relacionada con su papel en la historia. Consultar la lista de episodios en los que aparece un personaje concreto.  Aquí comienza una breve fase de selección de fuentes de datos. Por razones obvias —forma parte de la motivación original del proyecto— queremos que la fuente principal sea The Rick and Morty API. Sin embargo, frente a los requisitos anteriores, esta API presenta algunas limitaciones:\n Los episodios no incluyen sinopsis ni descripción. Los episodios no incluyen puntuación ni valoración. Los personajes no incluyen descripción.  Esta API será la fuente canónica del dominio principal. Es decir, decidirá qué personajes, localizaciones y episodios existen en nuestro modelo de Drupal. Otras API podrán enriquecer los registros importados, pero no deberían alterar silenciosamente la identidad principal del conjunto de datos. Como puedes comprobar en la propia fuente (https://rickandmortyapi.com/documentation#get-a-single-episode), aún falta información para completar el modelo:\n Ejemplo de datos obtenidos de un episodio  Por tanto, necesitamos fuentes adicionales que aporten lo que falta y enriquezcan los datos existentes: descripciones de episodios y personajes, y puntuaciones de episodios.\nPara obtener las biografías y las sinopsis podemos utilizar la API de la wiki de Fandom, que ofrece esa información.\n¿Y las puntuaciones de los episodios? Fandom puede ofrecer un campo llamado ratings, pero no se refiere a valoraciones o reseñas. En las páginas de episodios de la wiki de Rick and Morty en Fandom, ese campo parece representar cifras de audiencia, en millones de espectadores, y no una puntuación numérica.\nAsí que necesitamos otra fuente para obtener esas puntuaciones. Utilizaremos la API de TVmaze, una base de datos y plataforma comunitaria de seguimiento de series de televisión y series web.\nEsto no consiste solo en «leer JSON y crear contenido en Drupal». Esa frase siempre se queda corta. Estamos modelando un pequeño dominio de contenido a partir de API remotas: personajes, lugares, episodios, puntuaciones, descripciones, referencias entre registros y reglas operativas para que las importaciones repetidas sean predecibles.\nEn resumen… En este punto ya trabajamos con tres API al modelar el contenido y la migración:\n   API Papel en la migración     https://rickandmortyapi.com/api Fuente canónica de personajes, localizaciones y episodios. Su punto de acceso raíz expone las tres colecciones principales: character, location y episode.   https://rickandmorty.fandom.com/api.php Interfaz de la wiki/MediaWiki para el contexto editorial que no forma parte de la identidad canónica de las entidades. Necesita un contrato explícito antes de permitirle escribir en campos de Drupal.   https://www.tvmaze.com/api API de metadatos televisivos utilizada como fuente secundaria para enriquecer las puntuaciones y conservar su trazabilidad mediante los puntos de acceso de series y episodios de TVmaze.    Esta separación importa. Cuando varias API participan en el mismo modelo de destino, cada fuente necesita una responsabilidad clara. De lo contrario, la migración se convierte en una discusión silenciosa entre sistemas remotos y a Drupal le toca recoger el desastre.\nLos tres recursos principales de la API de Rick and Morty deberían corresponderse de forma natural con entidades de contenido de Drupal:\n Personajes: son los registros más visibles. Contienen estado, especie, género, origen, última localización conocida, URL del avatar y referencias a episodios. También son lo primero que la mayoría querrá consultar. Localizaciones: proporcionan la estructura espacial del multiverso: planetas, cúmulos, dimensiones y otros lugares. No son meras etiquetas de los personajes, sino entidades con sus propios metadatos y relaciones inversas a través de sus residentes. Episodios: aportan la estructura narrativa. Conectan muchos personajes, tienen códigos de temporada y episodio y son el mejor lugar para integrar las puntuaciones de TVmaze, porque ambas API pueden ponerse de acuerdo sobre temporada y número de episodio.  En este artículo quiero tratarlos como datos estructurados reales del dominio, y no como JSON de ejemplo de usar y tirar. Por eso no nos limitaremos a importar personajes como nodos ignorando lo demás. De hecho, lo interesante es el grafo de relaciones:\n Relaciones entre conceptos y entidades del proceso de migración  Como muestra la imagen:\n Los personajes tienen un origen y una última localización conocida. Los personajes aparecen en episodios. Los episodios contienen muchos personajes. Las localizaciones contienen residentes. Los residentes son personajes.  Y sí: esto implica relaciones circulares.\n4. Algunas restricciones iniciales (espera) Esas relaciones circulares son la primera restricción arquitectónica. Nos indican que la migración no puede resolverse con una única importación heroica. Pongamos un poco de orden.\nLa primera restricción es el orden de migración. Las localizaciones y los episodios deben existir antes de que los personajes puedan referenciarlos. Los personajes deben existir antes de rellenar characters en los episodios y residents en las localizaciones. El orden no es un detalle técnico que dejamos para el final: forma parte del modelo de contenido.\nLa segunda restricción es la autoridad de las fuentes. Los registros de la API de Rick and Morty incluyen identificadores numéricos estables y URL de los recursos; esos valores serán nuestra capa de identidad externa mediante campos como api_id y api_url. TVmaze puede enriquecer episodios, pero no debería crear episodios adicionales en Drupal que no existan en la fuente canónica. Fandom puede aportar contexto editorial de la wiki, pero tampoco debe sobrescribir alegremente la fuente principal. Antes de introducir un valor en un campo de destino, debemos decidir si representa identidad de origen, enriquecimiento editorial, ayuda para la presentación o simples metadatos de diagnóstico.\nLa tercera restricción es la repetibilidad. Durante el desarrollo ejecutaremos muchas importaciones, reversiones y reinicios; limitaremos páginas, almacenaremos respuestas en caché y, a veces, utilizaremos fixtures. No es solo un ejercicio de modelado, sino también de operaciones: ¿podremos volver a ejecutar la migración mañana sin tener que adivinar el estado de la base de datos, las tablas de correspondencias o la API remota?\nBienvenidos a las migraciones. Los datos son un desastre, pero al menos vienen en JSON.\n5. Contenedores de destino en Drupal (entidades) Mi primera reacción es evitar los nodos. No porque tengan nada de malo, sino porque estos datos son importados, estructurados, externos y fuertemente relacionales. Parece más fiel a su naturaleza modelarlos como entidades de contenido personalizadas.\nEn el proyecto habrá un módulo personalizado dedicado a la API de entidades: rick_and_migraty_api.\nSu función no es obtener datos remotos. Su función es proporcionar el modelo permanente de Drupal que rellenará la capa de migración y que después el sitio expondrá mediante rutas, Views, plantillas y navegación. Definirá tres entidades de contenido personalizadas:\n rick_and_migraty_character rick_and_migraty_location rick_and_migraty_episode  Las tres extienden la misma clase base abstracta:\nDrupal\\rick_and_migraty_api\\Entity\\RickAndMigratyEntityBase Esta clase centraliza los campos necesarios para cualquier recurso importado: api_id, name, api_url, api_created, uid, created, changed y path. El fundamental es api_id: es obligatorio, tiene una restricción de unicidad y representa el identificador numérico estable de la API de Rick and Morty. En la práctica, es la identidad externa que utilizamos para actualizaciones, búsquedas de relaciones y depuración.\nEl campo name actúa también como etiqueta de la entidad mediante entity_keys, mientras que api_url y api_created permiten rastrear la entidad hasta el recurso remoto original. Los campos locales created y changed indican cuándo creó o actualizó Drupal su copia. Existe un campo uid porque son entidades de contenido reales y Drupal sigue necesitando un propietario para comprobar el acceso y gestionar los formularios de edición.\nLa entidad base conserva además los campos de enriquecimiento para una pasada posterior de Fandom: fandom_description, fandom_page_id, fandom_url y fandom_source. Es deliberado: la identidad canónica procede de la API de Rick and Morty, pero el contexto editorial puede llegar después desde la API de Wiki/Fandom, mediante una actualización independiente. El modelo hace visible esa diferencia, en lugar de fingir que todos los campos importados tienen la misma autoridad.\nPor último, cada entidad incluye un campo base path. Así Pathauto tiene dónde guardar alias legibles, mientras las rutas canónicas internas permanecen estables.\nLos tres tipos de entidad deben compartir también la misma estructura administrativa: rutas canónicas, de colección, creación, edición y borrado; un constructor de listados personalizado para los resúmenes administrativos; datos disponibles para Views; y un permiso común:\nadminister rick and migraty api entities Un detalle pequeño pero importante: la visualización es pública, mientras las operaciones administrativas permanecen protegidas. Encaja con el objetivo del módulo: cualquiera puede recorrer el universo importado, pero la gestión de sus registros sigue sometida a los permisos de Drupal.\nLocalizaciones Campos de rick_and_migraty_location:\n   Campo Tipo Notas     location_type string Asigna la propiedad type de la API sin utilizar el ambiguo nombre de campo type.   dimension string Se almacena como texto porque las dimensiones no son un recurso normalizado independiente de la API.   residents referencia a entidad, múltiple Referencia a rick_and_migraty_character. Se rellena cuando ya existen los personajes.    Las localizaciones se importan pronto porque los personajes las referencian mediante origin y location.url. La relación inversa residents puede esperar a otra pasada. Así evitamos pedir a la migración que referencie personajes todavía inexistentes.\nEpisodios Campos de rick_and_migraty_episode:\n   Campo Tipo Notas     air_date_raw string Conserva el valor legible original, como December 2, 2013.   air_date datetime, fecha Fecha normalizada opcional para ordenar y filtrar cuando la conversión resulta fiable.   episode_code string Guarda códigos como S01E01.   season integer Se obtiene de episode_code.   episode_number integer Se obtiene de episode_code.   tvmaze_id integer Identificador del episodio en TVmaze para rastrear la puntuación.   tvmaze_url uri URL canónica del episodio en TVmaze.   rating_average decimal Puntuación media importada de TVmaze, con un decimal.   rating_source string Etiqueta del proveedor de la puntuación. El valor esperado es TVmaze.   characters referencia a entidad, múltiple Referencia a rick_and_migraty_character. Se rellena después de crear los personajes.    Con los episodios empezamos a ir más allá de la API de Rick and Morty. Esta nos da la identidad, el título, el código y las URL de los personajes. TVmaze enriquece ese mismo episodio con su puntuación. Por eso la entidad almacena tanto los campos canónicos como los de trazabilidad de TVmaze.\nConservamos deliberadamente episode_code como texto de origen y lo descomponemos en season y episode_number. Estos enteros derivados resultan más útiles para filtrar, ordenar, emparejar episodios con TVmaze y construir la navegación.\nPersonajes Campos de rick_and_migraty_character:\n   Campo Tipo Notas     status list_string Valores permitidos: Alive, Dead, unknown.   species string Texto libre porque la API ofrece un conjunto abierto de valores.   character_type string Asigna la propiedad type sin utilizar el ambiguo nombre de campo type.   gender list_string Valores permitidos: Female, Male, Genderless, unknown.   origin referencia a entidad Referencia a rick_and_migraty_location, resuelta desde origin.url cuando está disponible.   last_location referencia a entidad Referencia a rick_and_migraty_location, resuelta desde location.url cuando está disponible.   image_url uri Conserva la URL remota del avatar para mantener la trazabilidad.   avatar image Imagen local descargada del avatar, guardada en public://rick-and-migraty/characters.   episodes referencia a entidad, múltiple Referencia a rick_and_migraty_episode.    Los personajes son las entidades más conectadas: apuntan a localizaciones y episodios y aparecen en sus listas inversas. Por eso su migración no debe ejecutarse primero. Necesita que las localizaciones y los episodios existan para resolver con seguridad origin, last_location y episodes.\nLa URL remota image_url y el campo local avatar están separados intencionadamente. La primera conserva el dato de la API; el segundo representa un archivo gestionado por Drupal que podemos mostrar de manera uniforme en las páginas de personajes.\nEl módulo mantiene así un grafo que refleja la API de origen, sin obligarnos a resolver todo en una única pasada:\n rick_and_migraty_character.origin apunta a rick_and_migraty_location. rick_and_migraty_character.last_location apunta a rick_and_migraty_location. rick_and_migraty_character.episodes apunta a rick_and_migraty_episode. rick_and_migraty_episode.characters vuelve a apuntar a rick_and_migraty_character. rick_and_migraty_location.residents vuelve a apuntar a rick_and_migraty_character.  Al ser entidades de contenido personalizadas, participan de forma natural en Views, rutas de entidad, plantillas y permisos, sin hacerse pasar por nodos editoriales normales. Puedes ver las entidades implementadas en el repositorio.\nEl proyecto define además patrones legibles de Pathauto:\n   Tipo de entidad Patrón del alias     Personaje /rick-and-migraty/characters/[rick_and_migraty_character:name]   Localización /rick-and-migraty/locations/[rick_and_migraty_location:name]   Episodio /rick-and-migraty/episodes/[rick_and_migraty_episode:name]    Estos alias son deliberadamente plurales y fáciles de leer. Por debajo, las rutas canónicas siguen siendo rutas estables de Drupal, como /rick-and-migraty/character/{rick_and_migraty_character}. Esta separación mantiene predecible la implementación y agradables las URL públicas.\n6. Diseñar la migración (cocinar) Una vez resuelto —o casi— el modelo de destino, el siguiente paso no es escribir archivos YAML a toda velocidad. Ahora toca cocinar el diseño: comprender cómo se organizan los datos de origen, decidir el orden de importación, determinar qué problemas podemos resolver con YAML y cuáles requieren plugins personalizados y, solo entonces, conectar origen, proceso y destino.\nTambién debemos asumir que aparecerán dificultades y retos nuevos por el camino. Habrá que iterar, experimentar y encontrar soluciones mediante ensayo y error. Paciencia.\nPara empezar a cocinar, recordemos que la API de Rick and Morty es una API REST que devuelve JSON. No es JSON:API en el sentido estricto del módulo del núcleo de Drupal. Sin embargo, desde la perspectiva de una migración, la idea fundamental es la misma: recibir registros estructurados de una fuente remota y transformarlos en entidades de Drupal.\nLa API utiliza peticiones GET sobre HTTPS y devuelve respuestas JSON. Los puntos de acceso de los listados están paginados e incluyen un objeto info con los metadatos de paginación:\n{ \u0026#34;info\u0026#34;: { \u0026#34;count\u0026#34;: 826, \u0026#34;pages\u0026#34;: 42, \u0026#34;next\u0026#34;: \u0026#34;https://rickandmortyapi.com/api/character/?page=2\u0026#34;, \u0026#34;prev\u0026#34;: null }, \u0026#34;results\u0026#34;: [] } La fuente proporciona tres colecciones principales:\n   Recurso Punto de acceso Cantidad aproximada     Personajes /api/character 826   Localizaciones /api/location 126   Episodios /api/episode 51    Las cifras exactas importan menos que la estructura de los datos de origen:\n Los resultados de los listados están en results. La paginación está en info.next. Las relaciones se representan mediante URL. Cada objeto tiene un identificador numérico estable. Desde nuestro punto de vista, la fuente es de solo lectura.  Un registro de personaje permite ver el principal problema de la migración:\n{ \u0026#34;id\u0026#34;: 1, \u0026#34;name\u0026#34;: \u0026#34;Rick Sanchez\u0026#34;, \u0026#34;status\u0026#34;: \u0026#34;Alive\u0026#34;, \u0026#34;species\u0026#34;: \u0026#34;Human\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;gender\u0026#34;: \u0026#34;Male\u0026#34;, \u0026#34;origin\u0026#34;: { \u0026#34;name\u0026#34;: \u0026#34;Earth (C-137)\u0026#34;, \u0026#34;url\u0026#34;: \u0026#34;https://rickandmortyapi.com/api/location/1\u0026#34; }, \u0026#34;location\u0026#34;: { \u0026#34;name\u0026#34;: \u0026#34;Citadel of Ricks\u0026#34;, \u0026#34;url\u0026#34;: \u0026#34;https://rickandmortyapi.com/api/location/3\u0026#34; }, \u0026#34;image\u0026#34;: \u0026#34;https://rickandmortyapi.com/api/character/avatar/1.jpeg\u0026#34;, \u0026#34;episode\u0026#34;: [ \u0026#34;https://rickandmortyapi.com/api/episode/1\u0026#34;, \u0026#34;https://rickandmortyapi.com/api/episode/2\u0026#34; ], \u0026#34;url\u0026#34;: \u0026#34;https://rickandmortyapi.com/api/character/1\u0026#34;, \u0026#34;created\u0026#34;: \u0026#34;2017-11-04T18:48:46.250Z\u0026#34; } El contenido es fácil de leer, pero no es plano. Los personajes apuntan a localizaciones y episodios mediante URL. Los episodios vuelven a apuntar a personajes. Las localizaciones apuntan a residentes, que también son personajes. En otras palabras, la fuente es lo bastante pequeña para explicarla en un artículo, pero lo bastante relacional para obligarnos a tomar decisiones reales de migración.\nLa primera decisión es el orden de importación. Si intentamos resolver todo el grafo en una única importación heroica, nos estamos ofreciendo voluntarios para sufrir.\nEl proceso que implementaremos estará dividido en fases:\n Importar las localizaciones. Importar los episodios, ya enriquecidos con las puntuaciones de TVmaze cuando estén disponibles. Descargar los archivos de los avatares. Importar los personajes, resolviendo localizaciones, episodios y archivos de avatar. Actualizar los episodios para resolver su campo characters. Actualizar las localizaciones para resolver su campo residents.  Esta secuencia es la respuesta práctica al grafo circular. Algunos campos de relación ya figuran en el YAML de la primera migración, pero migration_lookup utiliza no_stub: true, de modo que las referencias no resueltas no crean entidades provisionales. Las referencias inversas pendientes se completan después con --update, cuando ya existen las entidades locales de los personajes.\nAhora que la secuencia está clara, por fin toca implementar los pasos que acabamos de describir. Sigamos.\nPrimero, las localizaciones En esta migración, una localización es un dato fundamental sobre un personaje: nos dice de dónde procede y dónde estaba establecido, al menos al principio, porque los personajes importantes suelen moverse entre dimensiones, planetas y demás.\nLas localizaciones son el primer contenedor estable, ya que los personajes las necesitan para origin y last_location.\n Punto de acceso: /api/location.  Cargaremos:\n api_id name location_type dimension api_url api_created  Esta es la definición completa de la migración de localizaciones:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49  id:rick_and_migraty_locationlabel:\u0026#39;Rick and Migraty locations\u0026#39;migration_group:rick_and_migratysource:# Custom source plugin that follows the Rick and Morty API info.next pager.plugin:rick_and_morty_apiresource:location# Avoid status/listing commands walking the public API just to count rows.skip_count:true# Cache each API page by URL so repeated imports and retries are gentle.cache:truecache_ttl:86400# Keep this at 0 by default; raise it locally if the public API throttles.request_delay_ms:0process:# Store the external ID as a first-class field. The migrate map still owns# the Drupal entity ID mapping.api_id:idname:namelocation_type:typedimension:dimensionresidents:- # Location rows contain an array of resident character URLs.plugin:rick_and_migraty_url_idsource:residents- plugin: migration_lookupmigration:rick_and_migraty_characterno_stub:trueapi_url:urlapi_created:plugin:format_datesource:createdfrom_format:\u0026#39;Y-m-d\\TH:i:s.v\\Z\u0026#39;to_format:\u0026#39;Y-m-d\\TH:i:s\u0026#39;from_timezone:UTCto_timezone:UTCuid:plugin:default_valuedefault_value:1destination:plugin:\u0026#39;entity:rick_and_migraty_location\u0026#39;validate:truemigration_dependencies:{}   Source: Archivo fuente en GitLab  El primer bloque es la definición del origen. En lugar de enumerar URL concretas, la migración pide al plugin personalizado rick_and_morty_api el recurso location. Ese pequeño ajuste, resource: location, basta para que construya el primer punto de acceso, siga la paginación, almacene las respuestas en caché y exponga cada localización de results como una fila de migración.\nLas asignaciones directas de campos son deliberadamente aburridas, y eso es bueno. api_id guarda el identificador numérico externo; name, la etiqueta visible; location_type recibe el valor type de la API; y dimension conserva el espacio ficticio al que pertenece la localización. Por su parte, api_url y api_created permiten rastrear el registro hasta la respuesta original.\nHay una transformación que merece atención: api_created utiliza format_date. La fuente proporciona una marca temporal como 2017-11-10T12:42:04.162Z; Drupal la guarda en UTC con el formato más limpio Y-m-d\\TH:i:s. No es una transformación espectacular, pero estas pequeñas normalizaciones facilitan consultar los datos después.\nLa asignación de residents resulta más interesante porque es un campo de relación. Las filas contienen un array de URL de personajes, no identificadores locales de Drupal. Por eso el proceso utiliza primero rick_and_migraty_url_id para extraer los identificadores numéricos y después migration_lookup contra rick_and_migraty_character.\nEn la primera pasada, esas búsquedas todavía no pueden resolverse porque no existen los personajes. No es un fallo del YAML, sino parte del diseño. no_stub: true indica a Drupal que no cree personajes provisionales solo para satisfacer la relación antes de tiempo. Más adelante, cuando la migración de personajes haya creado las entidades reales, se ejecutará esta misma migración con --update para rellenar residents.\nPor último, el destino es explícito: entity:rick_and_migraty_location. No creamos nodos, términos de taxonomía ni registros genéricos. Rellenamos la entidad personalizada definida por rick_and_migraty_api, con la validación activada para que las filas incorrectas fallen de forma visible durante la importación y no creen silenciosamente contenido incompleto.\nDespués, los episodios Los episodios son el segundo contenedor estable del grafo. Los personajes pueden aparecer en muchos episodios y las páginas de episodios necesitarán enumerarlos, pero los propios episodios pueden crearse antes de que existan los personajes. Por eso podemos importarlos con seguridad antes de su migración.\n Punto de acceso: /api/episode.  La migración guarda tanto los datos originales del episodio como los valores normalizados que necesitaremos después.\nCargaremos:\n api_id name air_date_raw air_date episode_code season episode_number tvmaze_id tvmaze_url rating_average rating_source api_url api_created  Esta es la definición completa:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66  id:rick_and_migraty_episodelabel:\u0026#39;Rick and Migraty episodes\u0026#39;migration_group:rick_and_migratysource:# Episodes are imported before character relationships are backfilled.plugin:rick_and_morty_apiresource:episode# Avoid status/listing commands walking the public API just to count rows.skip_count:true# Cache each API page by URL so repeated imports and retries are gentle.cache:truecache_ttl:86400# Keep this at 0 by default; raise it locally if the public API throttles.request_delay_ms:0process:api_id:idname:nameair_date_raw:air_dateair_date:plugin:format_datesource:air_datefrom_format:\u0026#39;F j, Y\u0026#39;to_format:\u0026#39;Y-m-d\u0026#39;from_timezone:UTCto_timezone:UTCepisode_code:episodeseason:plugin:rick_and_migraty_episode_partsource:episodepart:seasonepisode_number:plugin:rick_and_migraty_episode_partsource:episodepart:episodetvmaze_id:tvmaze_idtvmaze_url:tvmaze_urlrating_average:rating_averagerating_source:rating_sourcecharacters:- # Episode rows contain an array of character URLs.plugin:rick_and_migraty_url_idsource:characters- plugin: migration_lookupmigration:rick_and_migraty_characterno_stub:trueapi_url:urlapi_created:plugin:format_datesource:createdfrom_format:\u0026#39;Y-m-d\\TH:i:s.v\\Z\u0026#39;to_format:\u0026#39;Y-m-d\\TH:i:s\u0026#39;from_timezone:UTCto_timezone:UTCuid:plugin:default_valuedefault_value:1destination:plugin:\u0026#39;entity:rick_and_migraty_episode\u0026#39;validate:truemigration_dependencies:{}   Source: Archivo fuente en GitLab  Lo primero que observamos es que el plugin de origen sigue siendo rick_and_morty_api, pero resource cambia a episode. Así conservamos la paginación, la caché y las pausas entre peticiones en un único plugin reutilizable, mientras el YAML describe la entidad concreta que queremos rellenar.\nLa sección de proceso realiza tres tipos de trabajo. Algunos campos son copias directas de la fila de origen: api_id, name, episode_code y api_url. Otros se interpretan o normalizan: air_date convierte una fecha legible como December 2, 2013 a Y-m-d; season y episode_number utilizan el plugin de proceso personalizado rick_and_migraty_episode_part para descomponer valores como S01E01.\nLos campos de TVmaze ya están presentes en la fila de origen cuando se ejecuta el proceso. Con resource: episode, el plugin puede cargar los episodios de la serie 216 de TVmaze, construir una tabla de consulta por temporada y número de episodio y añadir tvmaze_id, tvmaze_url, rating_average y rating_source a la fila. Es un límite importante: TVmaze enriquece las filas canónicas, pero no crea episodios.\nPor último, characters aparece en el YAML aunque los personajes aún no existan durante la primera pasada. La pareja rick_and_migraty_url_id + migration_lookup repite el mismo patrón de relaciones utilizado en las localizaciones. Con no_stub: true, las referencias no resueltas no se convierten en entidades provisionales. Después de crear los personajes, el proyecto vuelve a ejecutar esta migración con --update.\nLos avatares antes que los personajes Los avatares se gestionan deliberadamente en una migración de archivos separada. Puede parecer un paso adicional, pero mantiene una operación frágil fuera de la importación principal de personajes. Descargar archivos remotos es distinto de asignar campos JSON: puede ser lento, fallar o verse limitado por el servidor.\nLa migración lee las filas de /api/character, toma la URL remota image y descarga el archivo en el sistema público de archivos de Drupal, bajo:\npublic://rick-and-migraty/characters/ Esta es la migración de los archivos de avatar:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43  id:rick_and_migraty_character_avatarlabel:\u0026#39;Rick and Migraty character avatars\u0026#39;migration_group:rick_and_migratysource:plugin:rick_and_morty_apiresource:characterskip_count:truecache:truecache_ttl:86400request_delay_ms:0constants:avatar_directory:\u0026#39;public://rick-and-migraty/characters/\u0026#39;avatar_extension:\u0026#39;.jpeg\u0026#39;process:filename:plugin:concatsource:- id- constants/avatar_extensiondestination_uri:plugin:concatsource:- constants/avatar_directory- id- constants/avatar_extensionuri:plugin:downloadsource:- image- \u0026#39;@destination_uri\u0026#39;file_exists:replaceuid:plugin:default_valuedefault_value:1status:plugin:default_valuedefault_value:1destination:plugin:\u0026#39;entity:file\u0026#39;validate:true   Source: Archivo fuente en GitLab  El origen vuelve a ser rick_and_morty_api, ahora con resource: character, porque la URL del avatar está en los datos del personaje. El bloque constants define dos valores que no proceden de la API: el directorio de destino y la extensión del archivo. Mantenerlos en constants facilita leer la sección de proceso.\nLos campos filename y destination_uri se construyen con concat. Un personaje con identificador de origen 1 se convierte en 1.jpeg, y la URI final será public://rick-and-migraty/characters/1.jpeg. Ese nombre determinista ayuda a repetir la operación: la misma fila apunta siempre a la misma ruta local.\nLa descarga se realiza en el proceso del campo uri. El plugin download recibe dos valores: la URL remota y la URI de destino calculada. Con file_exists: replace, las importaciones repetidas pueden actualizar el archivo en vez de fallar porque un intento anterior ya lo creó.\nEl destino es entity:file, no una de nuestras entidades de contenido de Rick and Migraty. Esta migración solo crea entidades de archivo reutilizables. Después, la migración de personajes resolverá avatar/target_id mediante migration_lookup.\nLos personajes después de las localizaciones, los episodios y los avatares Los personajes son el centro de la experiencia de navegación, pero llegan después de las localizaciones, los episodios y los archivos de avatar. El orden no es arbitrario: necesitan localizaciones para origin y last_location, episodios para episodes y una entidad de archivo para avatar.\nEl punto de acceso es /api/character, y la migración rellena:\n Campos básicos como api_id, name, status, species, character_type y gender. Referencias a localizaciones mediante origin y last_location. La URL remota image_url. El campo de imagen local avatar. Referencias a episodios mediante episodes. Campos de trazabilidad de la API y de propiedad de la entidad.  Esta es la migración implementada:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78  id:rick_and_migraty_characterlabel:\u0026#39;Rick and Migraty characters\u0026#39;migration_group:rick_and_migratysource:# Characters depend on locations and episodes for entity references.plugin:rick_and_morty_apiresource:character# Avoid status/listing commands walking the public API just to count rows.skip_count:true# Cache each API page by URL so repeated imports and retries are gentle.cache:truecache_ttl:86400# Keep this at 0 by default; raise it locally if the public API throttles.request_delay_ms:0process:api_id:idname:namestatus:statusspecies:speciescharacter_type:typegender:genderorigin:- # The API nests the origin URL under origin.url.plugin:rick_and_migraty_url_idsource:origin/url- plugin: migration_lookupmigration:rick_and_migraty_locationno_stub:truelast_location:- # The API nests the last known location URL under location.url.plugin:rick_and_migraty_url_idsource:location/url- plugin: migration_lookupmigration:rick_and_migraty_locationno_stub:trueimage_url:imageavatar/target_id:plugin:migration_lookupmigration:rick_and_migraty_character_avatarsource:idno_stub:trueavatar/alt:nameepisodes:- # Character rows contain an array of episode URLs.plugin:rick_and_migraty_url_idsource:episode- plugin: migration_lookupmigration:rick_and_migraty_episodeno_stub:trueapi_url:urlapi_created:plugin:format_datesource:createdfrom_format:\u0026#39;Y-m-d\\TH:i:s.v\\Z\u0026#39;to_format:\u0026#39;Y-m-d\\TH:i:s\u0026#39;from_timezone:UTCto_timezone:UTCuid:plugin:default_valuedefault_value:1destination:plugin:\u0026#39;entity:rick_and_migraty_character\u0026#39;validate:truemigration_dependencies:required:- rick_and_migraty_character_avatar- rick_and_migraty_location- rick_and_migraty_episode   Source: Archivo fuente en GitLab  El primer grupo de asignaciones es directo: api_id, name, status, species, character_type y gender se copian de los datos del personaje. Observa que el campo type de la API pasa a llamarse character_type en Drupal. Así evitamos confundirlo con el concepto habitual de «tipo» en Drupal y conservamos su valor original.\nLas asignaciones de origin y last_location muestran el proceso de resolución de relaciones en su forma más útil. La API anida las URL bajo origin/url y location/url. El plugin rick_and_migraty_url_id extrae su identificador numérico y migration_lookup lo resuelve mediante la tabla de correspondencias de rick_and_migraty_location.\nEl avatar es algo distinto porque apunta a la migración de archivos, no a una entidad de contenido personalizada. avatar/target_id busca la entidad creada por rick_and_migraty_character_avatar utilizando el mismo identificador de origen del personaje. avatar/alt se obtiene de su nombre para proporcionar un texto alternativo útil.\nEl campo episodes utiliza el mismo patrón de extracción del identificador de una URL, pero sobre un array de URL de episodios. Como estos ya se importaron, las búsquedas pueden resolverse en la primera pasada de personajes.\nEl bloque final de dependencias documenta explícitamente el orden de ejecución: los personajes requieren archivos de avatar, localizaciones y episodios. Así Migrate dispone de información suficiente para entender por qué esta migración ocupa una posición posterior en el proceso básico.\nRelaciones inversas después del grafo básico Llegados a este punto, ya existen las relaciones de ida:\n Los personajes apuntan a su origen y a su última localización conocida. Apuntan a los episodios en los que aparecen. Apuntan a su archivo local de avatar.  Pero quedan dos relaciones inversas útiles por completar:\n rick_and_migraty_episode.characters rick_and_migraty_location.residents  Estas asignaciones ya existen en los YAML de episodios y localizaciones. La razón por la que no se resuelven por completo en la primera pasada es temporal: ambos se importan antes que los personajes. Sus filas ya contienen las URL de los personajes, pero las entidades locales todavía no existen.\nPor eso ambas asignaciones utilizan la misma pareja de plugins con precaución:\n- plugin: rick_and_migraty_url_id- plugin: migration_lookupmigration:rick_and_migraty_characterno_stub:trueEl plugin personalizado extrae los identificadores de personajes de las URL. migration_lookup consulta la tabla de correspondencias de Migrate para averiguar dónde terminaron esos personajes en Drupal. no_stub: true evita crear personajes provisionales incompletos cuando la búsqueda todavía no puede resolverse.\nUna vez ejecutada la migración de personajes, se realizan pasadas de actualización:\nddev drush migrate:import rick_and_migraty_episode --update ddev drush migrate:import rick_and_migraty_location --update Es entonces cuando el grafo se vuelve bidireccional en Drupal, sin obligar a la primera importación a conocerlo todo a la vez. El mismo YAML sirve para la importación inicial y para completar las relaciones más adelante.\nEstrategia de origen: no fijar la paginación a mano Todas las definiciones anteriores comparten un supuesto discreto: ninguna quiere saber cuántas páginas tiene hoy la API remota. El YAML debe indicar qué recurso necesita, no mantener manualmente una lista de URL.\nEn Drupal, esa responsabilidad vive en un módulo de migración dedicado:\nrick_and_migraty_migrate Este módulo contiene el plugin de origen, los YAML, los plugins de proceso y las utilidades Drush del proyecto. La pregunta interesante es cuánto comportamiento de la API debe vivir en PHP reutilizable y cuánto debería repetirse en cada archivo de migración.\nLa API de Rick and Morty devuelve respuestas paginadas. Cada respuesta incluye info.next y un array results. Un origen genérico url con un analizador JSON puede leer un punto de acceso, pero seguimos necesitando recorrer cómodamente todas las páginas y exponer cada elemento de results como una fila.\nUna posibilidad sería utilizar un origen URL genérico y enumerar explícitamente todas las páginas:\nsource:plugin:urldata_fetcher_plugin:httpdata_parser_plugin:jsonurls:- \u0026#39;https://rickandmortyapi.com/api/location?page=1\u0026#39;- \u0026#39;https://rickandmortyapi.com/api/location?page=2\u0026#39;- \u0026#39;https://rickandmortyapi.com/api/location?page=3\u0026#39;item_selector:resultsPuede funcionar en una pequeña demostración, pero es frágil. Si cambia el número de páginas, debemos cambiar la definición. Si necesitamos migraciones de personajes, localizaciones y episodios, el mismo problema de paginación se extiende a varios YAML. Y si queremos controles locales, como caché o limitar una prueba a una página, habrá que repetir la lógica o envolverla en otra capa.\nEl proyecto adopta una opción mejor: un plugin de origen personalizado de Migrate, llamado rick_and_morty_api.\nCada migración elige el recurso con un pequeño ajuste:\nsource:plugin:rick_and_morty_apiresource:locationEl mismo plugin puede leer character, location o episode. Internamente construye la primera URL a partir del recurso, obtiene la respuesta, reúne las filas de results, sigue info.next y se detiene cuando no hay más páginas o cuando un límite local dice «suficiente por esta vez».\nEn la práctica, el plugin realiza cuatro tareas por los archivos YAML:\n Comienza por el primer punto de acceso. Lee info.next. Entrega cada elemento de results. Se detiene cuando next es null.  Así las definiciones son más pequeñas y la paginación se convierte en código reutilizable. El YAML se centra en el contrato de la migración: recurso de origen, asignaciones de proceso, plugin de destino y dependencias.\nEl plugin incluye también controles para el desarrollo local. Importan porque las migraciones no son solo código: son operaciones que ejecutaremos una y otra vez mientras desarrollamos, depuramos y revertimos:\n   Ajuste Valor predeterminado Para qué sirve     base_url https://rickandmortyapi.com/api Permite apuntar a otra URL base compatible.   skip_count true Evita recorrer toda la API solo para calcular los totales.   cache true Reutiliza respuestas remotas en ejecuciones locales repetidas.   cache_ttl 86400 Conserva las respuestas durante un día.   request_delay_ms 0 Introduce una pausa entre peticiones sin caché cuando aparecen limitaciones.   max_pages sin definir Limita las pruebas básicas a unas pocas páginas.   fixture_base_path sin definir Utiliza archivos JSON locales deterministas en lugar de HTTP remoto.   enrich_tvmaze_ratings true Integra las puntuaciones de TVmaze en las filas de episodios.   tvmaze_base_url https://api.tvmaze.com Permite redirigir las llamadas a TVmaze en entornos controlados.   tvmaze_show_id 216 Identificador fijo de Rick and Morty en TVmaze.    Para las pruebas locales, podemos sobrescribir varios ajustes mediante variables de entorno, sin editar los YAML versionados:\nddev exec env RICK_AND_MIGRATY_API_MAX_PAGES=1 \\  RICK_AND_MIGRATY_API_REQUEST_DELAY_MS=250 \\  drush migrate:import rick_and_migraty_location La implementación permite sobrescribir la URL base de Rick and Morty, la pausa entre peticiones, el límite de páginas, la ruta de los archivos de prueba, la URL base de TVmaze y su identificador de serie. Las migraciones versionadas permanecen estables, mientras las ejecuciones locales pueden ser más rápidas, más lentas, basarse en archivos locales o apuntar a otro servidor.\nAquí es también donde encaja arquitectónicamente el enriquecimiento de TVmaze. El YAML de episodios asigna tvmaze_id, tvmaze_url, rating_average y rating_source, pero no necesita saber cómo se obtuvieron. El plugin enriquece las filas antes de entregarlas al proceso. Cuando se ejecuta el YAML, los datos de TVmaze se comportan como columnas adicionales de la fila canónica.\nEs una decisión arquitectónica pequeña pero importante. Permite tratar con consideración a la API pública sin perder rapidez al probar cambios. También mantiene legibles las definiciones: localizaciones, episodios, avatares y personajes comparten el plugin, pero cada migración declara únicamente el recurso y los campos que le interesan.\nLlegados aquí, el diseño ya no consiste simplemente en «importar JSON a Drupal». Es una secuencia:\n Importar los contenedores canónicos. Derivar campos normalizados. Resolver referencias a partir de URL. Descargar avatares con una migración de archivos ejecutable por lotes. Enriquecer determinados registros desde fuentes secundarias. Permitir almacenar en caché, limitar y repetir las peticiones remotas. Completar las relaciones inversas mediante actualizaciones cuando ya exista el grafo.  Esta es la receta. Ahora podemos examinar los plugins que convierten esas decisiones en código reutilizable. Las definiciones implementadas están en el repositorio.\n7. Definir plugins de migración (soluciones) La sección anterior describe el orden de la migración. Esta trata sobre las pequeñas piezas de código personalizado que hacen legible ese orden.\nLlega un momento en que una migración deja de ser solo YAML. No es un fallo de la API Migrate, sino una de sus virtudes. YAML es excelente para declarar cómo una fila se convierte en una entidad, pero las reglas específicas del proyecto necesitan nombres, límites y pruebas. En Rick and Migraty, los plugins son precisamente eso: soluciones con nombre para problemas que se repiten.\nAquí la implementación se vuelve más honesta. Si forzamos todas las reglas dentro del YAML, las definiciones se vuelven ruidosas y frágiles. Si trasladamos cada pequeña asignación a PHP, el YAML deja de documentar la importación. La pregunta útil no es «¿YAML o PHP?», sino «¿qué responsabilidad corresponde a cada lugar?».\nEl proyecto utiliza cuatro plugins personalizados:\n   Plugin Tipo Qué problema evita     rick_and_morty_api Origen Fijar a mano las URL paginadas, repetir la lógica de caché y obligar a cada migración a conocer info.next.   rick_and_migraty_fandom Origen Tratar Fandom como un segundo conjunto canónico en vez de como una capa de enriquecimiento sobre entidades existentes.   rick_and_migraty_url_id Proceso Repetir el análisis de URL cada vez que una relación de la API debe convertirse en referencia a entidad.   rick_and_migraty_episode_part Proceso Mantener S03E07 eternamente como cadena cuando necesitamos números de temporada y episodio.    Ninguno es especialmente grande. Esa es parte de la idea: resolver problemas pequeños y repetidos, y mantener las definiciones centradas en el modelo de contenido, no en los detalles de bajo nivel.\nAquí conviene hacer autocrítica: los plugins personalizados no deberían ser la respuesta por defecto. El núcleo y los módulos contribuidos ya ofrecen muchas herramientas. Utilizamos plugins estándar como format_date, concat, download, default_value y migration_lookup; además, migrate_plus aporta el origen genérico url, analizadores JSON y plugins de proceso como preg_match.\n¿Por qué no usar migrate_plus:url para todo? En una prueba de concepto pequeña podríamos hacerlo. Pero la fuente canónica de Rick and Morty no es solo «un archivo JSON en una URL». Tiene paginación mediante info.next, caché, pausas entre peticiones, soporte para archivos locales, límites de páginas para pruebas y enriquecimiento de episodios con TVmaze. Si todo ese comportamiento vive en YAML, cada migración acaba describiendo políticas operativas además de asignaciones de datos.\nLa fuente de Fandom también tiene motivos para ser personalizada. Parte de entidades existentes, resuelve una página mediante identificadores, alias o coincidencias exactas, omite resultados ambiguos, analiza HTML de MediaWiki y genera filas de actualización para entidades de destino existentes. Un plugin URL genérico puede obtener JSON, pero no sabe que Fandom es una capa de enriquecimiento sobre registros que ya existen en Drupal.\nEl compromiso es este: los plugins de origen son personalizados porque definen contratos con las fuentes, no porque nos apeteciera escribir PHP. Los pequeños plugins de proceso merecen un examen más crítico. rick_and_migraty_url_id sigue teniendo sentido porque maneja tanto URL individuales como arrays en un paso legible. Sin embargo, rick_and_migraty_episode_part podría sustituirse por preg_match de migrate_plus:\nseason:plugin:preg_matchsource:episodepattern:\u0026#39;/^S(\\d+)E(\\d+)$/\u0026#39;group_index:1episode_number:plugin:preg_matchsource:episodepattern:\u0026#39;/^S(\\d+)E(\\d+)$/\u0026#39;group_index:2Esa sustitución reduciría el código propio. Mantener el plugin se justificaría por la legibilidad y la intención de los tipos, no por una necesidad técnica. Precisamente quiero hacer visible esa diferencia: el código personalizado es aceptable cuando da nombre a un contrato real del proyecto; resulta sospechoso cuando solo oculta una transformación que ya ofrecen el núcleo o los módulos contribuidos.\nEl plugin de la fuente canónica rick_and_morty_api se ocupa de la fuente principal de datos. Recibe un nombre de recurso, comienza en su punto de acceso, sigue la paginación mediante info.next y entrega cada elemento de results como una fila de Migrate.\nPor tanto, el YAML puede decir:\nsource:plugin:rick_and_morty_apiresource:episodeskip_count:truecache:truecache_ttl:86400request_delay_ms:0Es mucho más fácil de mantener que enumerar todas las páginas a mano. Y, sobre todo, ofrece un único lugar para gestionar las cuestiones operativas:\n   Ajuste Para qué sirve     skip_count Evita recorrer toda la API pública solo porque migrate:status quiere calcular totales.   cache y cache_ttl Hacen las importaciones locales repetidas más rápidas y menos exigentes con la fuente.   request_delay_ms Permite ralentizar las ejecuciones cuando la API empieza a limitar peticiones.   max_pages Permite realizar pruebas básicas sin importar todos los datos.   fixture_base_path Permite leer instantáneas JSON locales en vez de hacer peticiones HTTP.   tvmaze_base_url Permite redirigir las llamadas a TVmaze en entornos controlados.   tvmaze_show_id Vincula el enriquecimiento a una serie de TVmaze fija y explícita.    Es uno de esos detalles modestos que cambian el día a día. Ejecutar una migración diez veces durante el desarrollo no debería implicar pedir diez veces las mismas páginas a una API pública.\nEl plugin completo está en RickAndMortyApi.php. Para este artículo importa menos cada línea de la clase que el contrato que ofrece al YAML: elige un recurso, recibe filas y mantén centralizada la relación con el sistema remoto.\nEl problema del lenguaje de las URL La API de Rick and Morty expresa las relaciones como URL:\nhttps://rickandmortyapi.com/api/location/20 https://rickandmortyapi.com/api/episode/1 https://rickandmortyapi.com/api/character/42 Las referencias a entidades de Drupal no necesitan esas URL. Necesitan identificadores locales de entidades de destino. El puente es el plugin de proceso rick_and_migraty_url_id.\nEsta es su implementación:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79  \u0026lt;?php declare(strict_types=1); namespace Drupal\\rick_and_migraty_migrate\\Plugin\\migrate\\process; use Drupal\\migrate\\Attribute\\MigrateProcess; use Drupal\\migrate\\MigrateExecutableInterface; use Drupal\\migrate\\ProcessPluginBase; use Drupal\\migrate\\Row; /** * Extracts numeric Rick and Morty API IDs from resource URLs. * * The API represents many relationships as URLs, for example * `https://rickandmortyapi.com/api/location/20`. Migrate lookup plugins need * the stable source ID (`20`), not the full URL, so this plugin converts either * a single URL or an array of URLs into integer IDs. * * Example: * * @code * process: * origin: * plugin: migration_lookup * migration: rick_and_migraty_location * source: * plugin: rick_and_migraty_url_id * source: origin/url * @endcode */ #[MigrateProcess(\u0026#39;rick_and_migraty_url_id\u0026#39;)] final class RickAndMigratyUrlId extends ProcessPluginBase { /** * {@inheritdoc} */ public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property): mixed { if (is_array($value)) { // Relationship arrays such as episode.characters should stay arrays, but  // empty or malformed URLs are filtered out before lookup.  return array_values(array_filter(array_map( static fn (mixed $item): ?int =\u0026gt; self::extractId($item), $value, ))); } return self::extractId($value); } /** * Extracts the trailing numeric segment from an API URL. * * @param mixed $value * A URL-like scalar value from the source row. * * @return int|null * The numeric API ID, or NULL when the value cannot identify a resource. */ private static function extractId(mixed $value): ?int { if (!is_scalar($value)) { return NULL; } $value = trim((string) $value); if ($value === \u0026#39;\u0026#39;) { return NULL; } // The API uses resource URLs ending in the numeric ID. Matching only the  // trailing segment keeps the plugin independent from the configured host.  if (preg_match(\u0026#39;/\\/(\\d+)$/\u0026#39;, $value, $matches) !== 1) { return NULL; } return (int) $matches[1]; } }    Source: Archivo fuente en GitLab  Hace una sola cosa: extraer el segmento numérico final de una URL de la API de Rick and Morty. Si recibe una URL, devuelve un entero. Si recibe un array, aplica la misma operación a cada elemento y filtra los valores vacíos o mal formados.\nPara una relación individual, el proceso es fácil de leer:\norigin:- plugin: rick_and_migraty_url_idsource:origin/url- plugin: migration_lookupmigration:rick_and_migraty_locationno_stub:trueEl primer plugin extrae el identificador de origen. El segundo pregunta a la tabla de correspondencias de la API Migrate qué entidad local se creó a partir de ese identificador. El mismo patrón funciona con arrays como episode.characters, location.residents y character.episode.\nEs un buen ejemplo de un plugin de proceso que hace lo justo. No sabe de personajes, localizaciones, episodios ni campos. Solo traduce el lenguaje de relaciones de la API a identificadores que Drupal puede buscar.\nConvertir los códigos de episodio en estructura Los códigos de episodio requieren otra transformación pequeña pero importante. La API proporciona valores como:\nS03E07 Son útiles para las personas, pero insuficientes para ordenar, filtrar o cruzar los datos con TVmaze. El modelo guarda tanto el código original como sus partes estructuradas:\nseason:plugin:rick_and_migraty_episode_partsource:episodepart:seasonepisode_number:plugin:rick_and_migraty_episode_partsource:episodepart:episodeEl plugin implementado es deliberadamente estricto:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44  \u0026lt;?php declare(strict_types=1); namespace Drupal\\rick_and_migraty_migrate\\Plugin\\migrate\\process; use Drupal\\migrate\\Attribute\\MigrateProcess; use Drupal\\migrate\\MigrateExecutableInterface; use Drupal\\migrate\\MigrateException; use Drupal\\migrate\\ProcessPluginBase; use Drupal\\migrate\\Row; /** * Extracts season or episode numbers from an API episode code. * * The source API stores episode position as a compact string like `S03E07`. * Drupal Views and filters are easier to build when season and episode number * are available as separate integer fields. * * Configuration keys: * - part: Required. Either `season` or `episode`. */ #[MigrateProcess(\u0026#39;rick_and_migraty_episode_part\u0026#39;)] final class RickAndMigratyEpisodePart extends ProcessPluginBase { /** * {@inheritdoc} */ public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property): ?int { $part = $this-\u0026gt;configuration[\u0026#39;part\u0026#39;] ?? NULL; if (!in_array($part, [\u0026#39;season\u0026#39;, \u0026#39;episode\u0026#39;], TRUE)) { throw new MigrateException(\u0026#39;The rick_and_migraty_episode_part plugin requires part: season or part: episode.\u0026#39;); } if (!is_scalar($value) || preg_match(\u0026#39;/^S(\\d+)E(\\d+)$/\u0026#39;, (string) $value, $matches) !== 1) { return NULL; } // Match groups are stable because the expression is anchored to the whole  // API code: group 1 is season, group 2 is episode number.  return (int) ($part === \u0026#39;season\u0026#39; ? $matches[1] : $matches[2]); } }    Source: Archivo fuente en GitLab  Exige part: season o part: episode y acepta valores con la forma S03E07. Si el valor no encaja, devuelve NULL en vez de inventarse un número. Importa porque los valores incorrectos de origen no deberían convertirse silenciosamente en una estructura falsa.\nEl resultado es sencillo:\nepisode_code = S03E07 season = 3 episode_number = 7 Este tipo de transformación facilita el resto del proyecto. Views puede agrupar por temporada, las páginas pueden ordenar los episodios de forma natural y las puntuaciones de TVmaze pueden emparejarse por temporada y número, en lugar de por título.\nPuedes consultar los plugins personalizados de origen y proceso en el repositorio.\n8. Más información de las fuentes (enriquecimiento) El proyecto utiliza tres fuentes externas, pero no todas tienen la misma autoridad.\nLa API de Rick and Morty es la fuente canónica del conjunto básico: personajes, localizaciones, episodios y su grafo de relaciones. TVmaze y Fandom añaden información que la fuente principal no proporciona, pero no deben redefinir silenciosamente qué considera el proyecto un personaje, una localización o un episodio.\nEsa distinción mantiene la migración bajo control. Nos ofrece una regla que podemos repetir durante toda la implementación: las fuentes secundarias enriquecen registros existentes; no crean el conjunto canónico.\nTVmaze enriquece las filas de episodios La API de Rick and Morty proporciona la identidad del episodio, su código, la fecha de emisión y la lista de personajes que aparecen. No ofrece puntuaciones. TVmaze cubre esa carencia.\nLa decisión importante es evitar las búsquedas aproximadas durante las importaciones normales. La serie ya está seleccionada y su identificador de TVmaze es explícito:\n   Campo Valor     Identificador de la serie en TVmaze 216   Nombre de la serie Rick and Morty   URL de la serie https://www.tvmaze.com/shows/216/rick-and-morty    El plugin puede cargar:\nhttps://api.tvmaze.com/shows/216/episodes Y construir una tabla de consulta por temporada y número de episodio. Así, cada fila de Rick and Morty llega al proceso enriquecida con:\nprocess:tvmaze_id:tvmaze_idtvmaze_url:tvmaze_urlrating_average:rating_averagerating_source:rating_sourceFíjate en la clave de unión: temporada y número de episodio, no el título. Los títulos dependen demasiado de decisiones editoriales para ser claves seguras. Los cambios de puntuación, los títulos alternativos y las pequeñas diferencias de redacción pueden generar fallos que parecen inocentes durante demasiado tiempo.\nLa asociación queda así:\nRick and Morty API: S01E01 season: 1 episode_number: 1 TVmaze show: 216 TVmaze episode: season 1, number 1 Obtenemos puntuaciones mientras la API de Rick and Morty sigue siendo responsable de la identidad del episodio.\nFandom enriquece entidades existentes Fandom es otra clase de fuente. Es rica, útil y editorial, pero también desordenada, como corresponde a una wiki. Los resultados de búsqueda pueden ser ambiguos, los títulos de las páginas pueden diferir de los nombres de la API y un episodio llamado Pilot no es precisamente una expresión única en Internet.\nPor eso las migraciones de Fandom se ejecutan después de crear las entidades básicas:\nrick_and_migraty_character_fandom rick_and_migraty_location_fandom rick_and_migraty_episode_fandom Actualizan entidades existentes de Drupal. No crean registros canónicos nuevos.\nEl plugin de origen rick_and_migraty_fandom parte de las entidades de Drupal, resuelve una página de Fandom mediante un identificador explícito, un alias o una búsqueda exacta, lee la API de MediaWiki, extrae texto descriptivo útil y produce filas que contienen el identificador de la entidad existente y los metadatos de Fandom.\nLa migración de enriquecimiento de personajes es un ejemplo compacto:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26  id:rick_and_migraty_character_fandomlabel:\u0026#39;Rick and Migraty character Fandom enrichment\u0026#39;migration_group:rick_and_migratysource:plugin:rick_and_migraty_fandomentity_type:rick_and_migraty_characterskip_count:truepage_id_map:1:20732:2074process:id:entity_idfandom_description:fandom_descriptionfandom_page_id:fandom_page_idfandom_url:fandom_urlfandom_source:fandom_sourcedestination:plugin:\u0026#39;entity:rick_and_migraty_character\u0026#39;validate:truemigration_dependencies:required:- rick_and_migraty_character   Source: Archivo fuente en GitLab  El plugin está en RickAndMigratyFandom.php. Utiliza dos servicios de apoyo: FandomClient.php, para las peticiones a MediaWiki con caché, y FandomDescriptionExtractor.php, para convertir el HTML interpretado de la wiki en párrafos introductorios legibles.\nEl destino sigue siendo el mismo tipo de entidad:\ndestination:plugin:\u0026#39;entity:rick_and_migraty_character\u0026#39;Y las asignaciones actualizan los campos de enriquecimiento:\nprocess:id:entity_idfandom_description:fandom_descriptionfandom_page_id:fandom_page_idfandom_url:fandom_urlfandom_source:fandom_sourceLa regla es deliberadamente conservadora: si la coincidencia es ambigua, se omite y se registra. Es más fácil corregir una descripción ausente que una equivocada que parece fiable.\nEl YAML hace visible ese contrato. Las entradas explícitas de page_id_map permiten fijar casos difíciles conocidos. aliases ayuda cuando el título de Fandom no coincide con la etiqueta de la API. La búsqueda exacta resuelve los casos sencillos. Y el destino sigue apuntando a la entidad personalizada existente mediante id: entity_id, por lo que el enriquecimiento llega al registro ya importado.\n9. De los datos al contenido (navegar) Una migración no termina cuando hay filas en las tablas. Se vuelve útil cuando las personas pueden recorrer, consultar y entender los datos importados.\nPor eso importa el modelo de entidades. Una vez que personajes, localizaciones y episodios existen como entidades reales de Drupal, el sitio puede exponerlos como páginas, rutas, Views y navegación basada en relaciones.\n Listado completo con filtros y cuadro de búsqueda  En una página de personaje, las preguntas útiles son inmediatas:\n ¿Quién es este personaje? ¿Cuál es su estado, especie y género? ¿De dónde procede? ¿Dónde fue visto por última vez? ¿En qué episodios aparece? ¿Qué contexto adicional aporta Fandom?   Ejemplo de personaje: Alien Rick  Para una localización, las preguntas cambian:\n ¿Qué clase de lugar es? ¿A qué dimensión pertenece? ¿Qué personajes residen allí?   Ejemplo de localización: los recuerdos de Birdperson  Y para un episodio:\n ¿A qué temporada pertenece y qué número tiene? ¿Cuándo se emitió? ¿Qué puntuación ofrece TVmaze? ¿Qué personajes aparecen?   Página del episodio Anatomy Park  Los mismos datos pueden convertirse en vistas: personajes por dimensión, episodios por temporada, localizaciones con muchos residentes o un pequeño explorador de personajes por estado. Aquí la migración deja de ser un ejercicio de backend y se convierte en un producto de contenido.\nLas plantillas del tema muestran esa idea:\n rick-and-migraty-character.html.twig muestra las insignias del perfil, el avatar, la descripción de Fandom, los enlaces a episodios y los metadatos de origen. rick-and-migraty-location.html.twig muestra los metadatos de la localización, sus residentes y el contexto de Fandom. rick-and-migraty-episode.html.twig muestra temporada y episodio, las insignias de puntuación de TVmaze y las apariciones de personajes.  La migración no se limita a rellenar campos: alimenta una experiencia de navegación. Los campos elegidos se convierten en variables que el tema puede utilizar: avatar_url, episode_links, resident_cards, appearance_cards, fandom.description, profile.api_url, etc.\nEl proyecto también incluye una ruta de exploración:\nrick_and_migraty_migrate.browse:path:\u0026#39;/browse-all-content\u0026#39;defaults:_controller:\u0026#39;\\Drupal\\rick_and_migraty_migrate\\Controller\\BrowseContentController::browse\u0026#39;_title:\u0026#39;Browse all content\u0026#39;requirements:_access:\u0026#39;TRUE\u0026#39;Su controlador, BrowseContentController.php, hace dos cosas interesantes para este artículo. Primero, ofrece una matriz de entidades importadas con búsqueda mediante /browse-all-content. Segundo, expone pasos de migración y puntos de consulta de estado visibles desde el navegador, para mostrar la importación como una operación y no solo como un comando de terminal.\nEsto explica también por qué reducir todo a campos de texto habría sido un mal atajo. Lo interesante del conjunto de datos es el grafo. Los personajes enlazan a episodios, estos vuelven a enlazar a personajes, las localizaciones enumeran residentes y los campos de enriquecimiento añaden contexto sin quitarle la identidad a la fuente canónica.\n10. La migración como operación (ejecutar) La implementación es una mitad de la migración. La operación es la otra mitad.\nEsto resulta especialmente cierto cuando intervienen varios servicios remotos, se descargan archivos, se enriquecen entidades en pasadas posteriores y debemos repetir el proceso durante el desarrollo. Llegados ahí, necesitamos algo más que definiciones YAML: puntos de entrada repetibles.\nEl proyecto admite varias formas de ejecución, cada una con su finalidad:\n   Punto de entrada Uso más adecuado     scripts/rick-and-migraty-migrate.sh Ejecuciones locales repetibles, especialmente con archivos de prueba.   ddev drush ramt:migrate:* Operaciones de desarrollo: reinicio, importación y lotes de avatares.   Ventana modal del navegador Panel visual para ejecutar y observar la migración paso a paso.    El script está en scripts/rick-and-migraty-migrate.sh. Es el ejecutor local de más alto nivel: reconstruye cachés, elimina opcionalmente el contenido importado anterior, elige entre archivos locales y llamadas reales, importa el contenido básico, genera alias, ejecuta el enriquecimiento de Fandom e imprime los recuentos de validación.\nPara una ejecución local con archivos de prueba existentes:\nscripts/rick-and-migraty-migrate.sh --fixtures Si primero hay que generarlos:\nscripts/rick-and-migraty-migrate.sh --build-fixtures Estos archivos viven en:\n.ddev/.tmp-fixtures/rick-and-migraty-api Permiten leer las filas JSON canónicas de Rick and Morty desde archivos locales estables, en lugar de hacerlo desde la API pública. Así separamos dos problemas que de otro modo se mezclan:\n ¿Está fallando el código de la migración? ¿O el servicio remoto está lento, limita peticiones, no está disponible o ha cambiado?  Para comprobar la estructura más deprisa, podemos omitir las fases costosas:\nscripts/rick-and-migraty-migrate.sh --fixtures --no-avatars --no-fandom El ciclo normal con Drush es intencionadamente breve:\nddev drush ramt:migrate:reset ddev drush ramt:migrate:import Estos comandos se implementan en RickAndMigratyMigrateCommands.php. La utilidad de importación sigue el orden diseñado: localizaciones, episodios, avatares, personajes, actualización de relaciones de episodios y actualización de relaciones de localizaciones.\nEl comando de reinicio es importante. Limpia las entidades importadas, las entidades de archivo de avatares, sus archivos físicos, los alias y las filas de correspondencias y mensajes de Migrate. Esto último importa porque borrar entidades desde la interfaz no limpia el seguimiento interno de la API Migrate. Drupal puede parecer vacío mientras migrate:status sigue considerando importadas las filas.\nLa limpieza se delega en MigrationResetter.php, que hace explícita la operación: eliminar entidades personalizadas, entidades de archivo y archivos físicos de avatares, borrar alias /rick-and-migraty/%, vaciar tablas migrate_map_* y migrate_message_* e invalidar cachés.\nLas descargas de avatares se gestionan como una operación propia:\nddev drush ramt:migrate:avatars --batch-size=10 --sleep=12 --retries=3 La forma del comando refleja la naturaleza del trabajo. Descargar cientos de archivos remotos no equivale a asignar campos JSON: requiere lotes, reintentos y pausas. La utilidad importa avatares mediante rangos --idlist, añade una pausa local entre peticiones y reintenta los lotes fallidos antes de rendirse.\nLa separación ofrece una vía de recuperación práctica: si las descargas fallan a mitad del proceso, podemos repetir la fase de archivos sin dar por roto todo el modelo de migración.\nLos límites de peticiones forman parte del diseño Durante el desarrollo apareció un recordatorio muy concreto de que las API remotas no son infinitas: respuestas 429 Too Many Requests. Es fácil considerarlas una molestia, pero en una migración aportan información útil. Nos dicen que pedimos demasiado, demasiado deprisa o demasiadas veces a un servicio que no controlamos.\nLa respuesta no fue ocultar el error, sino convertir el límite de peticiones en una restricción de diseño:\n Las respuestas se almacenan en caché por URL para no pedir las mismas páginas en cada ejecución. Los archivos de prueba pueden sustituir las llamadas remotas cuando comprobamos la lógica de asignación. RICK_AND_MIGRATY_API_MAX_PAGES limita las pruebas básicas a un subconjunto pequeño. request_delay_ms ralentiza las peticiones que no tienen respuesta en caché. Las descargas de avatares se ejecutan por lotes con --sleep y --retries. Las importaciones de avatares usan rangos --idlist, de modo que podemos repetir un lote fallido sin repetir toda la migración.  Por eso la fase de avatares tiene su propia secuencia y no queda escondida dentro de la importación de personajes. Las descargas remotas son la parte más agresiva del proceso: cientos de peticiones HTTP, transferencias binarias, entidades de archivo locales, escrituras en disco y reintentos. Separarlas hace la migración más respetuosa y mucho más fácil de recuperar.\nDicho de otro modo: el 429 no fue solo un código de error; fue una revisión de diseño enviada por la API remota.\nEl navegador es un panel de control El flujo del navegador resulta útil, pero no debería pretender ser el motor de migración.\nLa primera versión intentó realizar demasiado trabajo en una petición larga. Eso es frágil, porque PHP-FPM, nginx, los proxies y los navegadores tienen sus propias opiniones sobre las peticiones HTTP prolongadas, y ninguna es sentimental. 😉\nEl flujo actual utiliza pasos POST más pequeños:\n/rick-and-migraty/run-migration-step/{step} La ruta se declara en rick_and_migraty_migrate.routing.yml y la gestiona BrowseContentController.php.\nEsta es la ventana modal:\n Progreso y trazas de la migración en la ventana modal  Muestra el progreso, las trazas recientes y un resumen final. La misma traza se escribe en watchdog mediante el canal rick_and_migraty_migrate, de modo que un síntoma observado en el navegador puede investigarse con Drush:\nddev drush watchdog:show --type=rick_and_migraty_migrate --count=50 El trabajo de avatares se divide internamente en rangos como avatars-1, avatars-2, etc. El usuario ve una sola fase, «Procesando avatares», pero el backend recibe unidades de trabajo más pequeñas cuyos fallos, registros y recuperación son más claros.\nLa distinción importa: el navegador no sustituye a Drush. Es un panel guiado sobre los mismos pasos operativos, útil para observar el proceso y hacer visible la experiencia de migración.\n11. Perder la cabeza (depurar) Cuando falla una migración, resulta tentador mirar fijamente el YAML hasta que confiese. A veces funciona. A menudo solo conseguimos poner nervioso al YAML. 😬\nUna rutina mejor es depurar de fuera hacia dentro.\n Primero, inspecciona el origen:  curl -s \u0026#39;https://rickandmortyapi.com/api/character\u0026#39; | jq \u0026#39;.info\u0026#39; curl -s \u0026#39;https://rickandmortyapi.com/api/character\u0026#39; | jq \u0026#39;.results[0]\u0026#39; curl -s \u0026#39;https://rickandmortyapi.com/api/location\u0026#39; | jq \u0026#39;.results[0].residents\u0026#39; curl -s \u0026#39;https://rickandmortyapi.com/api/episode\u0026#39; | jq \u0026#39;.results[0].characters\u0026#39; curl -s \u0026#39;https://api.tvmaze.com/shows/216/episodes\u0026#39; | jq \u0026#39;.[0] | {id, name, season, number, rating}\u0026#39; Todavía no pretendemos depurar Drupal. Antes hay que responder una pregunta más sencilla: ¿la fuente sigue teniendo la estructura que espera la migración? Si ha cambiado o un punto de acceso está lento o caído, el YAML no es el primer sospechoso.\n Después, inspecciona el estado de la migración:  ddev drush migrate:status ddev drush migrate:messages rick_and_migraty_location ddev drush migrate:messages rick_and_migraty_episode ddev drush migrate:messages rick_and_migraty_character migrate:status indica qué cree Drupal que se ha importado. Las tablas de mensajes explican qué filas fallaron y por qué. Aquí cobra sentido lo explicado sobre el reinicio: el estado de la API Migrate no equivale al contenido visible.\nSi faltan relaciones inversas tras una ejecución parcial, repite directamente las pasadas de actualización:\nddev drush migrate:import rick_and_migraty_episode --update ddev drush migrate:import rick_and_migraty_location --update Estos comandos tienen un alcance limitado a propósito. No vuelven a descargar avatares, reconstruir archivos de prueba ni importar todo el grafo. Solo piden a las migraciones ya definidas que completen relaciones cuyos personajes de destino ahora sí pueden resolverse.\nSi el problema está en el enriquecimiento de Fandom, depura esa fase sin repetir las importaciones básicas:\nddev drush migrate:messages rick_and_migraty_character_fandom ddev drush migrate:messages rick_and_migraty_location_fandom ddev drush migrate:messages rick_and_migraty_episode_fandom Y repite únicamente las migraciones de enriquecimiento:\nddev drush migrate:import rick_and_migraty_character_fandom --update --feedback=50 ddev drush migrate:import rick_and_migraty_location_fandom --update --feedback=25 ddev drush migrate:import rick_and_migraty_episode_fandom --update --feedback=25 Esta es otra razón para ejecutar el enriquecimiento por separado. Un problema de análisis de Fandom no debería obligarnos a importar de nuevo todo el contenido canónico. El grafo básico y el enriquecimiento editorial son ámbitos de depuración distintos.\nCuando una fila se resiste, la lista de comprobación resulta familiar:\n Revisa la indentación del YAML. Inspecciona los selectores del origen. Inspecciona las tablas migrate_map_*. Inspecciona las tablas migrate_message_*. Confirma que las dependencias se ejecutaron en el orden esperado. Utiliza Xdebug en DDEV cuando sospeches de un plugin de proceso personalizado.  Para este proyecto añadiría unas comprobaciones:\n Confirma RICK_AND_MIGRATY_API_FIXTURE_BASE_PATH si utilizas archivos de prueba. Confirma que RICK_AND_MIGRATY_API_MAX_PAGES no limita inesperadamente la ejecución. Inspecciona las entradas de watchdog de rick_and_migraty_migrate. Determina si la fila fallida pertenece a la importación básica, la descarga de avatares, la cumplimentación de relaciones o el enriquecimiento de Fandom.  En los lugares aburridos suelen estar las pistas útiles: un selector, una tabla de correspondencias, una dependencia ausente, una cadena vacía o un valor que parecía un entero hasta que conoció los datos reales.\n12. Decisiones que merece la pena destacar (decidir) Hay varias decisiones que conviene documentar porque determinan la implementación más que cualquier YAML individual. Si alguien retoma el proyecto más adelante, me gustaría que las conociera antes de modificar las migraciones.\n  En primer lugar, las relaciones circulares son previsibles. Los personajes apuntan a localizaciones y episodios, y estos vuelven a apuntar a personajes. No intentamos resolverlo todo de una pasada heroica: primero importamos el grafo básico y después completamos las referencias inversas mediante actualizaciones.\n  En segundo lugar, la paginación forma parte del contrato del origen. Si una migración solo importa la primera página, no es una migración: es una demostración que se ha cansado. El plugin de origen se encarga de paginar porque todos los recursos canónicos necesitan el mismo comportamiento y al YAML no debería importarle cuántas páginas tiene hoy la API.\n  En tercer lugar, los identificadores externos no son opcionales. El id de origen se guarda como api_id y se utiliza como puente estable entre registros remotos, tablas de correspondencias, archivos de prueba, actualizaciones y depuración. Los identificadores de entidad de Drupal son detalles locales de implementación; el de la API es la identidad que podemos explicar.\n  En cuarto lugar, las imágenes merecen su propia fase. Los avatares se descargan mediante una migración separada y pueden procesarse por lotes. La entidad conserva image_url, pero gestionar archivos locales es una operación distinta de asignar campos y debe tratarse como tal.\n  En quinto lugar, debemos tratar con consideración a las API públicas. La caché, los archivos de prueba, los límites de páginas y las pausas no son adornos. Los errores 429 Too Many Requests lo dejaron claro: la consideración no es solo una cuestión de educación; hace posible repetir la migración.\n  Por último, las fuentes de enriquecimiento no son fuentes canónicas. TVmaze aporta puntuaciones. Fandom aporta descripciones. Ninguna puede redefinir el conjunto básico de Rick and Morty.\n  La lista no es solo documentación: también protege el diseño. Cuando el proyecto crezca, cada nueva migración debería contrastarse con estas decisiones:\n ¿Conserva la autoridad de cada fuente? ¿Mantiene explícitas las relaciones? ¿Sigue siendo repetible? ¿Evita que los servicios remotos se conviertan en un peaje constante para desarrollar?  13. Qué he aprendido (reflexionar) Si tuviera que extraer algunos principios reutilizables de la experiencia Rick and Migraty, serían estos.\nElabora un perfil de la fuente antes de escribir demasiado YAML. Conoce sus páginas, campos, valores vacíos, formas de relación y casos difíciles antes de dar por evidente el modelo de Drupal. No hace falta un documento formal, pero sí responder preguntas prácticas: dónde están los identificadores, las relaciones, las fechas, los archivos y los valores vacíos, y qué campos son meras etiquetas de presentación.\nConserva el grafo cuando el valor está en el grafo. Personajes, localizaciones y episodios no son simplemente tres listas. Lo interesante son sus relaciones, así que el modelo de destino debe permitir consultarlas. Si las aplana hasta convertirlas en texto, la migración puede tener éxito técnico y, al mismo tiempo, perder el sentido del conjunto de datos.\nMantén acotadas las transformaciones. Un plugin extrae identificadores de URL. Otro obtiene la temporada o el episodio. Un plugin de origen se ocupa de la paginación. Las piezas pequeñas son más fáciles de explicar, probar y sustituir. Además, hacen el YAML más legible porque cada nombre expresa una intención.\nOrdena las importaciones deliberadamente. Las localizaciones y los episodios deben existir antes de que los personajes puedan referenciarlos. Los personajes deben existir antes de completar las relaciones inversas. El orden no es un detalle de implementación, sino parte del modelo. Si resulta incómodo, probablemente el grafo está intentando decirnos algo.\nConsidera las operaciones parte del diseño. Reinicios, archivos de prueba, lotes de avatares, actualizaciones y trazas en el navegador no son extras simpáticos. Son lo que permite utilizar la migración después de la primera importación exitosa.\nY quizá lo más importante: no permitas que las fuentes secundarias difuminen el contrato. Podemos combinar datos de varios lugares sin volver ambiguo quién es responsable de cada información. Aquí, la API de Rick and Morty controla el grafo canónico; TVmaze enriquece las puntuaciones; y Fandom, el contexto narrativo.\nEso es lo que reutilizaría en otro proyecto aunque su temática fuera completamente distinta. Cambiarían los nombres, las API y los campos, pero se mantendría el patrón:\n Elegir la fuente canónica. Modelar el grafo. Aislar los enriquecimientos. Hacer que repetir la ejecución sea aburrido.  14. Próximos pasos (planificar) Este proyecto ha reunido paginación, relaciones, enriquecimiento, avatares, archivos de prueba y ejecución repetible en una migración funcional de Drupal. Los próximos pasos parten de esa implementación:\n Mantener el artículo y la documentación de MkDocs alineados con el código a medida que evolucione el proyecto, actualizando ejemplos y capturas cuando cambie el comportamiento. Explorar la migración de avatares en otro artículo, porque la gestión de archivos remotos, los lotes y la recuperación merecen un examen más detallado. Revisar la cobertura del conjunto canónico y decidir cómo tratar episodios y personajes ausentes, conservando identificadores externos estables y responsabilidades explícitas de las fuentes.  También hay otro posible artículo a la vista: el panel de control del navegador. Conecta la ejecución, el estado de las trazas, la consulta periódica del progreso, los comandos Drush y la información que recibe el usuario. No forma parte estrictamente de la API Migrate, pero sí de conseguir que una migración resulte utilizable por personas.\nPor ahora, lo importante es que el diseño sea visible. Tenemos una fuente canónica, un modelo de destino, un orden de importación por fases, plugins para problemas repetidos, pasadas de enriquecimiento, utilidades operativas y una rutina de depuración. Es suficiente para esta sexta parada de la serie.\nIntentar incluir todos los detalles de implementación convertiría el artículo en un manual. La versión útil tiene un alcance más acotado: código suficiente para concretar el diseño, explicación suficiente para comprender los compromisos y detalle operativo suficiente para no fingir que las migraciones terminan cuando el YAML valida.\nNota sobre la documentación: La estructura y la intención de este artículo siguen los principios de Diátaxis, distinguiendo tutoriales, guías prácticas, explicaciones y material de referencia para situar las instrucciones y el contexto conceptual donde corresponden.\nNota sobre el idioma: Se utilizó asistencia de IA para revisar la gramática y pulir las expresiones del original inglés. Las decisiones técnicas, la implementación y la experiencia descrita siguen siendo propias del autor. Esta versión castellana también se ha preparado con asistencia de IA.\n15. :wq! Canción recomendada: Fabrizio De André - Ballata dell\u0026rsquo;amore cieco   ","permalink":"https://www.therussianlullaby.dev/es/blog/drupal-migrations-six-rick-and-migraty-experience-merging-data-from-different-sources/","tags":["Drupal Migrations","Migrate API","JSON APIs","Drupal Development","ETL","Rick and Morty API","TVmaze"],"title":"Migraciones en Drupal (VI): la experiencia Rick and Migraty, combinando datos de distintas fuentes"},{"categories":["Migraciones"],"contents":"Siguiendo la entrega anterior, quiero compartir más pautas para depurar migraciones en Drupal. Si entonces tratamos módulos, plugins y gestión de objetos de configuración, ahora me apetece detenerme en algo más orientado al código. He elegido explicar cómo configurar Xdebug en un IDE —en este caso PhpStorm— para migraciones ejecutadas en una instalación local de Drupal con DDEV. Aunque el procedimiento es transversal y también sirve para otros desarrollos, lo aplicaremos a una migración. Esta es la quinta entrega de la serie. Espero que te resulte útil.\n Foto de Unsplash, de Neenu Vimalkumar, @dreamquest12610\n Índice\n  1. Introducción a Xdebug 2. Xdebug en DDEV 3. Configurar el IDE paso a paso 4. Errores al ejecutar Drush en contenedores 5. :wq!  1. Introducción a Xdebug Vamos a complementar las técnicas de la entrega anterior. Ya sabemos revisar archivos de configuración y utilizar plugins de proceso para obtener información. Ahora añadiremos la tercera pieza: depurar una migración desde el código que interviene en ella, siguiendo las clases PHP que hacen funcionar todo el mecanismo.\nXdebug es una extensión clásica de PHP que permite, entre otras cosas, seguir las variables y observar sus valores durante la ejecución del software. No explicaremos aquí su instalación y administración general; nos centraremos en un caso concreto. Para desplegar Drupal localmente y probar las migraciones podemos usar DDEV, una herramienta basada en contenedores Docker que ya incluye Xdebug preparado para utilizarlo.\nDDEV evita tener que montar un entorno LAMP clásico: organiza redes de contenedores preconfigurados con las herramientas de desarrollo, incluido Xdebug. Encontrarás más información, artículos y recursos en awesome-ddev.\n2. Xdebug en DDEV Si partimos de un entorno DDEV, podemos activar Xdebug de dos maneras.\nLa primera es editar config.yaml, en your_project/.ddev/config.yaml, y activar Xdebug. En el archivo mostrado en la captura, la opción aparece en la línea 9:\n La segunda es utilizar los comandos de DDEV desde fuera del contenedor:\nddev exec enable_xdebug ddev exec disable_xdebug O también:\nddev xdebug ddev xdebug on ddev xdebug off Estos comandos permiten activar y desactivar el recurso. Por rendimiento, Xdebug está desactivado inicialmente. Como trabaja del lado del servidor, ya se encuentra en el contenedor web y, en este escenario, nos queda preparar el IDE.\nEn esencia, los IDE funcionan de forma parecida con Xdebug: escuchan en un puerto y reaccionan al recibir una conexión. En la configuración descrita en el artículo original, Xdebug utiliza habitualmente el puerto 9000. Puedes consultar ejemplos de configuración en:\n Depuración paso a paso con DDEV y Xdebug. Xdebug desde la línea de comandos con DDEV.  El equipo de PhpStorm ya explicó la técnica básica en este artículo sobre depuración remota de scripts PHP de consola. Utilizaremos PhpStorm para el ejemplo.\n3. Configurar el IDE paso a paso Veremos los pasos básicos en PhpStorm, aunque el enfoque es similar en otros editores como VSCode. Al activar Xdebug queda disponible para los scripts PHP ejecutados desde la línea de comandos. Esto lo hace especialmente útil para depurar Drush o Drupal Console —la antigua y abandonada Drupal Console, por cierto—. DDEV facilita además su activación automática, lo que simplifica la depuración de nuestra migración.\nSigamos estos pasos:\n 1. Servidor del proyecto Abre File → Settings → Languages \u0026amp; Frameworks → PHP → Servers y crea un servidor nuevo. Asígnale un nombre; utilizar el nombre del proyecto DDEV facilita reconocerlo. Indica el puerto 80 y selecciona Xdebug como depurador.\n2. Configuración inicial Cuando el servidor no es local para PhpStorm, necesitamos definirlo como servidor externo y establecer las correspondencias de archivos. En un entorno Docker o DDEV, el código se ejecuta dentro de un contenedor. DDEV sirve la instalación desde /var/www/html/, mientras que los archivos del equipo pueden estar en una ruta como /home/user/folder/projects/my_project. Configuraremos la correspondencia entre ambas rutas.\n3. Escuchar conexiones de depuración Activa el botón de escucha de conexiones de depuración del IDE.\n4. Cargar una variable de entorno en el contenedor Tenemos dos opciones. Podemos definirla para cada sesión entrando en el contenedor con ddev ssh y ejecutando allí:\nexport PHP_IDE_CONFIG=\u0026quot;serverName=your.project.name.ddev.site\u0026quot;\nTambién podemos establecerla de forma persistente mediante un archivo complementario docker-compose.env.yaml en .ddev/, junto a los demás archivos de configuración de Docker Compose:\nversion: '3.6' services: web: environment: - PHP_IDE_CONFIG=serverName=your.project.name.ddev.site Al detener y arrancar la red de contenedores con ddev stop y ddev start, este archivo se combinará con los demás y la variable se cargará automáticamente.\n5. Puntos de interrupción Podemos colocar puntos de interrupción en el código que interviene en la migración. Cuando un campo solo pasa del origen al destino, sin transformaciones, sabemos que se utiliza el plugin de proceso predeterminado: la clase Get.php, situada en /core/modules/migrate/src/Plugin/migrate/process/Get.php.\nEs un buen lugar para detener algunos ejemplos de migración. Podemos añadir un punto de interrupción en transform(), que en la versión de la captura aparece en la línea 106:\n 4. Errores al ejecutar Drush en contenedores Con el IDE escuchando y los puntos de interrupción preparados, al ejecutar Drush en DDEV puede aparecer un error de correspondencia de archivos. El motivo es que se está ejecutando el lanzador global de Drush del contenedor, instalado en /usr/local/bin/drush, en lugar del ejecutable del proyecto.\n Tenemos dos opciones.\nPodemos ejecutar Drush directamente desde el directorio vendor del proyecto, que ya está incluido en el mapeo de rutas del servidor remoto. El original muestra estas formas:\n Desde fuera del contenedor: ddev exec /vendor/bin/drush migrate-status. Desde dentro, tras ddev ssh: /vendor/bin/drush migrate-status.  La otra opción la recomienda Randy Fay, @randyfay, uno de los creadores de DDEV, en este comentario de una incidencia de GitHub.\nConsiste en aprovechar el comando de ejemplo del archivo drush.example, en /your-name-project/.ddev/commands/web/, y sustituir la llamada a Drush por la ruta del ejecutable del proyecto: pasar de drush $@ a /var/www/html/vendor/bin/drush $@.\nAsí podemos depurar las migraciones con Xdebug en DDEV. Encontrarás más información en:\n Configuración de la depuración con PhpStorm. Incidencia 127 de docker4drupal. Incidencia 2341 de DDEV.  ¡Ahora puedes ejecutar tu migración cómodamente! Lanza los comandos Drush de la migración, comprueba que se alcanzan los puntos de interrupción y empieza a seguir su ejecución.\n5. :wq! Canción recomendada   ","permalink":"https://www.therussianlullaby.dev/es/blog/drupal-migrations-five-debugging-migrations-ii/","tags":["Migraciones en Drupal","Migrate API","Desarrollo Drupal","Plugins de Drupal","ETL"],"title":"Migraciones en Drupal (V): depurar migraciones II"},{"categories":["Migraciones"],"contents":"Las migraciones en Drupal, aunque sus definiciones sean lineales, contienen mucha complejidad heredada. La razón es intuitiva: Migrate API ofrece una interfaz de interacción relativamente sencilla a quien desarrolla migraciones, pero durante el proceso colaboran numerosos subsistemas: entidades, base de datos, plugins… Incluso en la migración más sencilla intervienen muchas clases. Si añadimos que una migración suele producir errores hasta que la afinamos, está claro que una de nuestras primeras necesidades será aprender a depurarlas.\n Foto de Unsplash, de Krzysztof Niewolny, @epan5\n Índice\n  1. Introducción 2. Depuración básica: revisa tus archivos 3. Depuración intermedia con Migrate Devel 4. :wq!  1. Introducción Siguiendo los últimos artículos, quiero ampliar la información sobre migraciones en Drupal. He pensado en una pequeña subserie sobre depuración dentro de esta serie principal. Esta es la primera parte: un conjunto de pasos básicos para obtener toda la información disponible de un proceso de migración. Los ejemplos proceden del módulo migration_google_sheet de mi cuenta de GitLab.\n2. Depuración básica: revisa tus archivos Empezaremos por una aproximación muy elemental a la detección de errores. Es fundamental reducir las posibles causas abordando la migración de forma iterativa e incremental: avanzaremos paso a paso y ampliaremos progresivamente los datos migrados.\n2.1. Revisar el archivo de definición de la migración Comenzamos con el paso más intuitivo. Hay errores que están a la vista y, por habituales que sean, no siempre resultan evidentes.\nAntes de nada, revisaremos dos cuestiones que aparecen con frecuencia:\nEspacios en blanco: un espacio adicional puede causar problemas en el archivo de definición. Recorre sus líneas para localizar espacios sobrantes.\nErrores de sangrado: el formato YAML organiza los datos mediante claves y valores, con niveles padre e hijo y sangrado de dos espacios en cada nivel del ejemplo. Un sangrado incorrecto puede provocar un error al procesar el archivo. Revisa todos los niveles, igual que has revisado los espacios.\nPuedes apoyarte en un validador de sintaxis YAML como www.yamllint.com, aunque también tendrás que comprobar el resultado.\n2.2. Revisar los registros de la base de datos En la instalación básica utilizada en el ejemplo, con el perfil estándar, tenemos setenta y tres tablas. Tras activar migrate, migrate_plus, migrate_tools y el módulo personalizado migration_google_sheet_wrong, pasan a ser setenta y cinco. Se han generado dos tablas nuevas:\ncache_migrate cache_discovery_migration Después de ejecutar la migración con identificador taxonomy_google_sheet_wrong, incluida en el módulo personalizado, aparecen otras dos tablas relacionadas con la migración:\n migrate_map_taxonomy_google_sheet. Contiene la información sobre el recorrido de cada fila de datos: las migraciones trabajan fila a fila. Migrate API almacena el identificador de origen, el de destino y un hash relacionado con la fila. Estas correspondencias permiten seguir cambios, actualizar información progresivamente y resolver dependencias entre migraciones. Las búsquedas o lookups se apoyan en estos datos. Por ejemplo, para cargar un término de taxonomía puede ser necesario buscar antes su término padre y mantener así la jerarquía. Si, después de lanzar la migración, no encontramos resultados registrados, no se han almacenado los datos y tendremos que depurar el proceso. migrate_message_taxonomy_google_sheet. Guarda los mensajes asociados a la ejecución, también organizados por fila. Cada mensaje tiene su propio identificador y está asociado al id_hash de la fila de origen:   Podemos obtener esta información con Drush: el contenido de la tabla es lo que aparece al ejecutar:\ndrush migrate:messages id-migration Es una forma útil de empezar a recopilar información sobre los errores de la migración.\n2.4. Recargar los objetos de configuración Durante la depuración también necesitamos actualizar los cambios realizados en el objeto de configuración creado a partir del archivo de definición de config/install.\nCada vez que instalamos el módulo se genera un objeto de configuración disponible en Drupal. Mientras depuramos tendremos que modificar el archivo y recargarlo para comprobar el efecto de los cambios. ¿Cómo podemos facilitar este ciclo?\nPrimero vincularemos el ciclo de vida del objeto de configuración de migración a la instalación de nuestro módulo. Como vimos al tratar las migraciones como configuración, declararemos el módulo personalizado como dependencia forzada:\ndependencies: enforced: module: - migration_google_sheet Podemos realizar importaciones específicas con Drush o Drupal Console, aprovechando las opciones de importación individual.\nCon Drupal Console:\ndrupal config:import:single --directory=\u0026#34;/modules/custom/migration_google_sheet/config/install\u0026#34; --file=\u0026#34;migrate_plus.migration.taxonomy_google_sheet.yml\u0026#34; Con Drush:\ndrush cim --partial --source=/folder/ También podemos eliminar objetos de la configuración activa con ambas herramientas:\ndrush config-delete \u0026#34;migrate_plus.migration.taxonomy_google_sheet\u0026#34; drupal config:delete active \u0026#34;migrate_plus.migration.taxonomy_google_sheet\u0026#34; Si preferimos la interfaz de Drupal, el módulo contribuido Config Delete incorpora opciones al menú de sincronización para eliminar elementos de configuración. Basta con descargarlo con Composer y activarlo con Drush o Drupal Console:\ncomposer require drupal/config_delete drush en config_delete -y  Así podemos volver a importar objetos sin chocar con versiones ya existentes en la base de datos. Si prefieres actualizar y comparar versiones, puedes valorar Configuration Update Manager.\n3. Depuración intermedia con Migrate Devel Hemos revisado los datos detenidamente y, aun así, la migración de términos desde Google Spreadsheet no parece funcionar.\nNecesitamos técnicas intermedias para obtener más información. En esta fase nos ayudarán módulos y plugins que permiten observar mejor lo que ocurre durante la migración.\n3.1. Migrate Devel Migrate Devel es un módulo contribuido que añade funcionalidades mediante opciones nuevas de Drush. Funciona con migrate_tools y migrate_run.\nActualización del 03/07/2020: versión 8.x-2.0-alpha2 Justo cuando publiqué este artículo, Andrew Macpherson, nuevo mantenedor de Migrate Devel y uno de los mantenedores de accesibilidad del core de Drupal, dejó un comentario al pie del original con novedades importantes. Desde que empecé el borrador se había publicado una versión nueva, el 28 de junio, compatible con Drush 9, ¡y no lo sabía! Esa versión evita instalar el parche que se describe más abajo.\nPara instalarla, la descargamos con Composer y la activamos con Drush: Migrate Devel 8.x-2.0-alpha2.\ncomposer require drupal/migrate_devel # To install the 2.0 branch of the module: composer require drupal/migrate_devel:^2.0 drush en migrate_devel -y Para versiones anteriores a 8.x-2.0-alpha2 Si trabajas con versiones anteriores, hay algunas particularidades. Estaban preparadas para Drush 8 y la adaptación a Drush 9 y posteriores aún no parecía cerrada.\nEn la rama 8.x-1.x había una etiqueta publicada dos semanas antes: migrate_devel/tree/8.x-1.4.\nPara utilizar esas versiones con Drush 9 o superior, el parche de esta incidencia resultaba prácticamente imprescindible. Aunque tampoco parecía definitivo, permitía ejecutar de forma controlada algunas funciones del módulo. Veremos sus aportaciones.\nPodemos descargar el parche con wget y aplicarlo con git apply:\ncd /web/modules/contrib/migrate_devel/ wget https://www.drupal.org/files/issues/2018-10-08/migrate_devel-drush9-2938677-6.patch git apply migrate_devel-drush9-2938677-6.patch Otra posibilidad es añadirlo a la sección de parches de composer.json, si tenemos activada la extensión composer-patches.\nLa instalamos con:\ncomposer require cweagans/composer-patches Y añadimos el parche a la sección extra de composer.json:\n Cómo funciona Al ejecutar una migración con las opciones de Migrate Devel obtenemos valores por consola que podemos inspeccionar. Por ejemplo, con \u0026ndash;migrate-debug:\n Es una vista parcial del procesamiento de una fila: muestra la fuente, los valores asociados y el identificador final de destino, almacenado en la tabla de correspondencias para seguir el proceso:\n Vemos que al valor 1 de origen, en el primer array, se le asignó el identificador 117 en destino. También será el identificador interno de la nueva entidad de Drupal, aquí un término de taxonomía. Así relacionamos el registro de migración con la entidad creada y almacenada.\n¿Y los suscriptores de eventos? Migrate Devel crea una clase que implementa EventSubscriberInterface y escucha eventos de Migrate API, disponibles en el módulo migrate del core:\nCalled from +56 /var/www/html/web/modules/contrib/migrate_devel/src/EventSubscriber/MigrationEventSubscriber.php La llamada procede de la clase que escucha estos eventos. En modules/migrate/src/Event se definen muchos, pero Migrate Devel presta atención especialmente a dos:\n MigratePostRowSaveEvent.php. MigratePreRowSaveEvent.php.  Estos corresponden a las dos opciones Drush del módulo, que originalmente acababan llamando a dump() de Kint, proporcionado por Devel, para imprimir los mensajes. En la versión 8.x-2.0-alpha2 se sustituyen esas llamadas por Symfony VarDumper. Donde antes teníamos:\n /** * Pre Row Save Function for --migrate-debug-pre. * * @param \\Drupal\\migrate\\Event\\MigratePreRowSaveEvent $event * Pre-Row-Save Migrate Event. */ public function debugRowPreSave(MigratePreRowSaveEvent $event) { $row = $event-\u0026gt;getRow(); $using_drush = function_exists('drush_get_option'); if ($using_drush \u0026amp;\u0026amp; drush_get_option('migrate-debug-pre')) { // Start with capital letter for variables since this is actually a label. $Source = $row-\u0026gt;getSource(); $Destination = $row-\u0026gt;getDestination(); // We use kint directly here since we want to support variable naming. kint_require(); \\Kint::dump($Source, $Destination); } } Ahora tenemos:\n /** * Pre Row Save Function for --migrate-debug-pre. * * @param \\Drupal\\migrate\\Event\\MigratePreRowSaveEvent $event * Pre-Row-Save Migrate Event. */ public function debugRowPreSave(MigratePreRowSaveEvent $event) { if (PHP_SAPI !== 'cli') { return; } $row = $event-\u0026gt;getRow(); if (in_array('migrate-debug-pre', \\Drush\\Drush::config()-\u0026gt;get('runtime.options'))) { // Start with capital letter for variables since this is actually a label. $Source = $row-\u0026gt;getSource(); $Destination = $row-\u0026gt;getDestination(); // Uses Symfony VarDumper. // @todo Explore advanced usage of CLI dumper class for nicer output. // https://www.drupal.org/project/migrate_devel/issues/3151276 dump( '---------------------------------------------------------------------', '| $Source |', '---------------------------------------------------------------------', $Source, '---------------------------------------------------------------------', '| $Destination |', '---------------------------------------------------------------------', $Destination ); } } Puedes consultar los cambios en MigrationEventSubscriber.php de 8.x-2.0-alpha2.\nTambién encontrarás más información sobre eventos y suscriptores en el artículo de The Russian Lullaby Building Symfony events for Drupal.\n3.2. El plugin de proceso Debug Migrate Devel también incorpora un plugin de proceso llamado debug, definido en Debug.php. Lo encontrarás en /web/modules/contrib/migrate_devel/src/Plugin/migrate/process/Debug.php. Su documentación explica su responsabilidad:\n/** * Debug the process pipeline. * * Prints the input value, assuming that you are running the migration from the * command line, and sends it to the next step in the pipeline unaltered. * * Available configuration keys: * - label: (optional) a string to print before the debug output. Include any * trailing punctuation or space characters. * - multiple: (optional) set to TRUE to ask the next step in the process * pipeline to process array values individually, like the multiple_values * plugin from the Migrate Plus module. El método transform(), heredado de la clase abstracta ProcessPluginBase, no transforma el valor: utiliza print_r de PHP para mostrar por consola tanto valores escalares como arrays.\nPodemos incluir el plugin de forma autónoma en la cadena de procesamiento para que imprima resultados al procesar cada fila. En nuestro caso modificaremos el bloque process de la migración de términos para revisar los valores que se están migrando.\nPrimero cambiaremos la estructura. Como sabemos por las entregas anteriores, esta forma:\nprocess: name: name description: description path: url status: published Utiliza implícitamente el plugin Get.php y equivale a esta otra:\nprocess: name: plugin: get source: name description: plugin: get source: description path: plugin: get source: url status: plugin: get source: published Añadimos Debug a la cadena con una etiqueta informativa para cada campo:\nprocess: name: plugin: debug label: \u0026#39;Processing name field value: \u0026#39; plugin: get source: name description: plugin: debug label: \u0026#39;Processing description field value: \u0026#39; plugin: get source: description path: plugin: debug label: \u0026#39;Processing path field value: \u0026#39; plugin: get source: url status: plugin: debug label: \u0026#39;Processing status field value: \u0026#39; plugin: get source: published Después recargamos el objeto de configuración desinstalando e instalando nuestro módulo. Como figura como dependencia, al desinstalarlo también se elimina la configuración de la migración:\ndrush pmu migration_google_sheet \u0026amp;\u0026amp; drush en migration_google_sheet -y Al ejecutarla de nuevo aparecerá información sobre los valores:\n Así obtenemos información más detallada de los datos que vamos a migrar. Para escenarios más avanzados podemos combinar argumentos y opciones. Por ejemplo, si queremos revisar un solo elemento:\n drush migrate-import --migrate-debug taxonomy_google_sheet --limit=\u0026quot;1 items\u0026quot; Esto combina la salida posterior al almacenamiento —a diferencia de --migrate-debug-pre— con la información del plugin, los valores mostrados por la herramienta de volcado y el identificador final de la única entidad procesada.\nAquí los valores son sencillos y apenas los transformamos: los extraemos y los cargamos en destino. En migraciones posteriores introduciremos tratamientos más complejos y esta información resultará aún más valiosa. Piensa en valores que hay que concatenar, recortar, ampliar o adaptar: si mostramos el resultado en cada paso podremos observar mejor toda la secuencia de transformación.\nPuedes revisar el código de Debug.php y la incidencia de Drupal.org en la que surgió la propuesta de este plugin.\nCon esta aproximación empezamos la subserie de depuración de migraciones. ¡Pronto, más experiencias!\n4. :wq! Canción recomendada   ","permalink":"https://www.therussianlullaby.dev/es/blog/drupal-migrations-four-debugging-migrations-i/","tags":["Migraciones en Drupal","Migrate API","Módulos contribuidos","Plugins de Drupal","ETL"],"title":"Migraciones en Drupal (IV): depurar migraciones I"},{"categories":["Migraciones"],"contents":"Los sistemas y subsistemas relacionados con la API de migraciones de Drupal resultan apasionantes. En las entregas anteriores quise trazar un mapa lo más completo posible de recursos, posibilidades y personas de referencia en la primera parte, y explicar algunos mecanismos básicos de las migraciones en la segunda. Sabiendo que todo ello abre la puerta a miles de opciones, posibilidades y técnicas, no quería dejar pasar una tercera entrega sin compartir experiencias migrando datos desde un formato tan habitual como una hoja de cálculo de Google: una de las formas en que solemos recibir los datos.\n Foto de Unsplash, de Pan Xiaozhen, @zhenhappy\n Índice\n  1. Introducción: recordemos los procesos ETL 2. Exponer datos mediante Google Spreadsheet 3. Particularidades de la transformación a JSON 4. Características de la migración 5. Módulo personalizado para la migración 6. :wq!  1. Introducción: recordemos los procesos ETL Empecemos planteando un escenario: tenemos que migrar una lista de términos de taxonomía a nuestra instalación Drupal. El origen de los datos importa, porque determina qué plugins utilizaremos para la extracción y puede influir también en el procesamiento y en la carga final en el destino.\nEn este artículo imaginaremos que hemos «heredado» una tarea de migración que había iniciado otra persona. Consiste en poblar con términos de taxonomía un vocabulario ya existente en Drupal. Para practicar con otros plugins y modelos de migración, utilizaremos una hoja de Google como fuente. No olvidemos el marco en el que trabajamos:\n Nuestro objetivo es rellenar los campos de un término de taxonomía con los datos de una hoja externa de Google. Estos son los campos que debemos completar:\n Lo haremos describiendo un proceso Migrate y ejecutándolo como configuración. ¿Conoces las diferencias entre migraciones como código y como configuración? Encontrarás algunas claves en el artículo anterior de la serie.\n2. Exponer datos mediante Google Spreadsheet Para extraer los datos necesitamos una hoja cuyos valores estén expuestos. He creado esta hoja de cálculo de Google, con columnas que corresponden a los campos de un término de taxonomía y están preparadas para la migración.\nPara procesar la fuente utilizaremos el módulo contribuido Migrate Google Sheets. Descárgalo con Composer y actívalo:\ncomposer require drupal/migrate_plus #If applicable composer require drupal/migrate_google_sheets drush en migrate_plus migrate_google_sheet -y Este módulo trata el recurso como un archivo JSON, pero antes tenemos que realizar algunas tareas: exponer la hoja como fuente JSON utilizando las herramientas de Google.\n Primero extraemos el workbook-id de la dirección de la hoja: docs.google.com/spreadsheets/d/1bKGbPbgeuXaBfcKetaDqoDimmYcerQY_hT1rqzw4TbM/. En este caso, workbook-id: 1bKGbPbgeuXaBfcKetaDqoDimmYcerQY_hT1rqzw4TbM. También necesitamos el worksheet-index, el índice de la pestaña que contiene los datos. Aquí es 1. Construimos la URL JSON con el patrón spreadsheets.google.com/feeds/list/[workbook-id]/[worksheet-index]/public/values?alt=json. Para nuestro ejemplo: URL de los valores en JSON.  ¡Eso es todo! La hoja queda expuesta como un archivo JSON y podemos obtener los valores mediante los plugins de origen elegidos para Migrate API.\n 3. Particularidades de la transformación a JSON Ahora podemos ver la fuente JSON en el navegador. Puede resultar útil una extensión que presente los datos con formato, como hace Firefox de forma predeterminada:\n { \u0026quot;gsx$id\u0026quot;: { \u0026quot;$t\u0026quot;: \u0026quot;1\u0026quot; }, \u0026quot;gsx$name\u0026quot;: { \u0026quot;$t\u0026quot;: \u0026quot;Term 1\u0026quot; }, \u0026quot;gsx$description\u0026quot;: { \u0026quot;$t\u0026quot;: \u0026quot;Descrip 1\u0026quot; }, \u0026quot;gsx$url\u0026quot;: { \u0026quot;$t\u0026quot;: \u0026quot;/t1\u0026quot; } \u0026quot;gsx$published\u0026quot;: { \u0026quot;$t\u0026quot;: \u0026quot;1\u0026quot; } } Algunos elementos han cambiado respecto al formato original. Fíjate en la comparación:\n La transformación a JSON introduce cambios respecto a la hoja. Por ejemplo, Google modifica los identificadores de los campos añadiendo prefijos:\n gsx$ para los nombres de campo. $t para los valores almacenados en los campos.  Las cabeceras cambian así:\n Id pasa a ser gsx$id. Name pasa a ser gsx$name. Description pasa a ser gsx$description. Url pasa a ser gsx$url. Published pasa a ser gsx$published.  Como estos cambios son estables, el plugin se encarga de procesarlos:\n// Class GoogleSheets.php // Module migrate_google_sheets // @see https://git.drupalcode.org/project/migrate_google_sheets/ // Actual values are stored in gsx$\u0026lt;field\u0026gt;['$t']. $this-\u0026gt;currentItem[$field_name] = $current['gsx$' . $selector]['$t']; Esto nos permite utilizar los nombres de los datos en su forma «plana»: en minúsculas y sin espacios ni caracteres especiales. Además, las filas se encuentran en la ruta feed/entry, que el módulo también resuelve:\n// For Google Sheets, the actual row data lives under feed-\u0026gt;entry. if (isset($array['feed']) \u0026amp;\u0026amp; isset($array['feed']['entry'])) { $array = $array['feed']['entry']; } 4. Características de la migración La primera observación es que el plugin de Google Spreadsheet extiende el analizador JSON, como vemos en la clase:\nuse Drupal\\Core\\Plugin\\ContainerFactoryPluginInterface; use Drupal\\migrate\\MigrateException; use Drupal\\migrate_plus\\Plugin\\migrate_plus\\data_parser\\JSON; use GuzzleHttp\\Exception\\RequestException; /** * Obtain Google Sheet data for migration. * * @DataParser( * id = \u0026quot;google_sheets\u0026quot;, * title = @Translation(\u0026quot;Google Sheets\u0026quot;) * ) */ class GoogleSheets extends JSON implements ContainerFactoryPluginInterface { ... } Su bloque de anotaciones lo identifica como un Data Parser, así que necesitamos más piezas: un plugin de origen y un Data Fetcher. El plugin de Google Spreadsheet será la tercera pieza del proceso.\n¿Qué plugins de origen tenemos disponibles en nuestra instalación Drupal? Vamos a consultarlos:\ndrupal debug:plugin migrate.source La consola mostrará:\ndrupal@migrations-web:/var/www/html$ drupal debug:plugin migrate.source --------------- --------------------------------------------------------- Plugin ID Plugin class --------------- --------------------------------------------------------- embedded_data Drupal\\migrate\\Plugin\\migrate\\source\\EmbeddedDataSource empty Drupal\\migrate\\Plugin\\migrate\\source\\EmptySource url Drupal\\migrate_plus\\Plugin\\migrate\\source\\Url --------------- --------------------------------------------------------- drupal@migrations-web:/var/www/html$ Podemos utilizar la clase Url.php como plugin de origen, ya que el archivo se expone mediante una URL. En la clase Url.php vemos que necesita un analizador de datos; en nuestro caso será la clase de Google Spreadsheet:\n /** * The data parser plugin. * * @var \\Drupal\\migrate_plus\\DataParserPluginInterface */ protected $dataParserPlugin; Al buscar un componente que recupere los datos encontramos la clase Http.php, preparada para trabajar con el plugin de origen URL mediante HTTP:\n/** * Retrieve data over an HTTP connection for migration. * * Example: * * @code * source: * plugin: url * data_fetcher_plugin: http * headers: * Accept: application/json * User-Agent: Internet Explorer 6 * Authorization-Key: secret * Arbitrary-Header: foobarbaz * @endcode * * @DataFetcher( * id = \u0026quot;http\u0026quot;, * title = @Translation(\u0026quot;HTTP\u0026quot;) * ) */ class Http extends DataFetcherPluginBase implements ContainerFactoryPluginInterface { Por otra parte, el constructor de Url.php recibe las direcciones de origen. Podemos proporcionar una URL o un conjunto de ellas:\n /** * {@inheritdoc} */ public function __construct(array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration) { if (!is_array($configuration['urls'])) { $configuration['urls'] = [$configuration['urls']]; } parent::__construct($configuration, $plugin_id, $plugin_definition, $migration); $this-\u0026gt;sourceUrls = $configuration['urls']; } Al reunir las piezas, debemos darles una estructura según su función y posición. Nuestro bloque de origen quedará así:\nsource: plugin: url data_fetcher_plugin: http data_parser_plugin: google_sheets urls: 'http://spreadsheets.google.com/feeds/list/1bKGbPbgeuXaBfcKetaDqoDimmYcerQY_hT1rqzw4TbM/1/public/values?alt=json' 5. Módulo personalizado para la migración Crearemos un módulo llamado migration_google_sheet con esta estructura:\n/project/web/modules/custom/ \\__migration_google_sheet/ \\__migration_google_sheet.info.yml \\__config/ \\__install/ \\__migrate_plus.migration.migration.taxonomy_google_sheet.yml Archivo de definición de la migración: migrate_plus.migration.taxonomy_google_sheet.yml.\nlangcode: en status: true dependencies: enforced: module: - migration_google_sheet id: taxonomy_google_sheet label: 'Migrating Taxonomy' source: plugin: url data_fetcher_plugin: http data_parser_plugin: google_sheets urls: 'http://spreadsheets.google.com/feeds/list/1bKGbPbgeuXaBfcKetaDqoDimmYcerQY_hT1rqzw4TbM/1/public/values?alt=json' fields: - name: id label: 'Id' selector: id - name: name label: 'Name' selector: name - name: description label: 'Description' selector: description - name: url label: 'Url' selector: url - name: published label: 'Published' selector: published ids: id: type: integer process: name: name description: description path: url status: published destination: plugin: 'entity:taxonomy_term' default_bundle: tags Ejecutamos la migración desde la consola:\ndrush en migration_google_sheet -y drush migrate:import taxonomy_google_sheet [notice] Processed 30 items (30 created, 0 updated, 0 failed, 0 ignored) - done with 'taxonomy_google_sheet' ¡Hecho! Hemos migrado treinta términos de taxonomía desde una hoja de Google:\n Puedes descargar o clonar el módulo personalizado desde mi repositorio de GitLab.\n6. :wq! Canción recomendada   ","permalink":"https://www.therussianlullaby.dev/es/blog/drupal-migrations-three-migrating-from-google-spreadsheet/","tags":["Migraciones en Drupal","Migrate API","Módulos contribuidos","Plugins de Drupal","ETL"],"title":"Migraciones en Drupal (III): migrar desde Google Spreadsheet"},{"categories":["Migraciones"],"contents":"Como comentaba en el artículo anterior, durante estos meses estaré experimentando con migraciones y preparando algunos casos para un futuro libro, eso espero. Durante estos días de confinamiento quiero seguir publicando pequeños artículos para compartir experiencias relacionadas con las migraciones.\nEn la entrega anterior abordé las migraciones en Drupal desde el punto de vista de una caja de herramientas: un conjunto de recursos básicos que nos ayudan a plantear una migración.\nHay mucha información que procesar y, por supuesto, intervienen más conceptos, técnicas y tácticas a la hora de resolver una migración. Por eso, este mes quiero escribir algo que me permita experimentar con ellas de una forma más práctica.\n Foto de Unsplash, de Émile Séguin, @emileseguin\nÍndice\n  1. Introducción 2. Preparativos 3. Enfoques 4. Migraciones 5. Conceptos clave 6. Recursos 7. :wq!  1. Introducción La API de migraciones de Drupal puede ser una de las más interesantes, pero también una de las más complejas: sus operaciones suelen estar relacionadas con clases y métodos de otras APIs de Drupal, algo que se hace especialmente evidente al depurar. En cualquier caso, como la cantidad de conceptos puede resultar abrumadora, creo que podemos practicar la mecánica de las migraciones mediante un par de ejercicios.\nPara este artículo me había propuesto modelar dos procesos de migración diferentes siguiendo una idea que podría resumirse como «primum vivere, deinde philosophari»: primero experimentar y después teorizar. Por eso he decidido organizarlo de una manera particular:\n Los dos procesos se dividen en apartados comunes. En lugar de terminar uno y empezar el siguiente, avanzaremos con ambos en paralelo: tú eliges tu propia aventura. Solo al final del artículo encontrarás algunos de los conceptos clave que utilizaremos. Primero vamos a jugar con las estructuras y después las entenderemos.  En los siguientes pasos trabajaremos con dos experiencias concretas:\n Migrar datos desde un formato embebido, quizá el ejemplo más sencillo de una migración en Drupal. Migrar datos desde un archivo CSV, un caso algo más complejo que el anterior.  Ambos son, probablemente, de los escenarios más básicos de migración. Por eso recomiendo este artículo a quien quiera iniciarse en su mecánica, como complemento práctico al estudio de las migraciones en Drupal.\n2. Preparativos Primer caso: migrar datos embebidos Para el primer caso necesitaremos, por una parte, activar el módulo Migrate del core de Drupal y, por otra, descargar e instalar un módulo contribuido con el que gestionar las migraciones.\nEntre las opciones disponibles elegiremos migrate_run, que ya mencionamos en la entrega anterior. Podemos entenderlo como una versión ligera de migrate_tools, aunque en realidad es un fork del proyecto. Ambos proporcionan comandos Drush para ejecutar migraciones, por lo que, si tienes migrate_tools instalado, debes desinstalarlo para evitar conflictos con migrate_run.\nLa primera lección, como detalle curioso, es que ni migrate_plus ni migrate_tools son dependencias obligatorias para ejecutar migraciones en Drupal: podemos implementar migraciones sin tener estos módulos activados en nuestra instalación.\nConviene saber, además, que migrate_run está optimizado para Drush 9 y posteriores. Si utilizas Drush 8, tendrás que recurrir a una versión adaptada, como la Alpha 4, que todavía estaba preparada para Drush 8.\nCon Composer y Drush:\ncomposer require drupal/migrate_run drush pmu migrate_tools # If you need drush en migrate migrate_run -y drush cr Con Drupal Console:\ncomposer require drupal/migrate_run drupal mou migrate_tools # If you need drupal moi migrate migrate_run En la ruta /admin/modules verás lo siguiente:\n Construcción de los recursos Crearemos un módulo personalizado para nuestra primera migración:\ncd project/web/modules/custom mkdir migration_basic_module Después, el archivo migration_basic_module.info.yml, con este contenido:\nname: \u0026#39;Migration Basic Module\u0026#39; type: module description: \u0026#39;Just a basic example of basic migration process.\u0026#39; package: \u0026#39;Migrations Examples 2000\u0026#39; core: 8.x dependencies: - drupal:migrate Crea el archivo de definición de la migración en la ruta /migration_basic_module/migrations/basic_migration_one.yml.\nNuestro archivo declarativo basic_migration_one.yml describe la migración mediante una lista de parámetros y valores en un archivo estático de tipo YAML. Incluiremos los datos embebidos de dos nodos del tipo de contenido «página básica», cargando únicamente dos valores:\n Un título: una cadena de texto. Un cuerpo: un texto basado en el generador ChiquitoIpsum*, http://www.chiquitoipsum.com.  *Chiquito de la Calzada fue una figura popular en España, un humorista legendario.\nbasic_migration_one.yml\nid: basic_migration_one label: \u0026#39;Custom Basic Migration 2000\u0026#39; source: plugin: embedded_data data_rows: - unique_id: 1 page_title: \u0026#39;Title for migrated node - One\u0026#39; page_content: \u0026#39;Lorem fistrum mamaar se calle ustée tiene musho pelo.\u0026#39; - unique_id: 2 page_title: \u0026#39;Title for migrated node - Two\u0026#39; page_content: \u0026#39;Se calle ustée caballo blanco caballo negroorl.\u0026#39; ids: unique_id: type: integer process: title: article_title body: article_content destination: plugin: \u0026#39;entity:node\u0026#39; default_bundle: page Esta será la estructura del nuevo módulo personalizado del ejemplo de migración básica:\n/project/web/modules/custom/ \\__migration_basic_module/ \\__migration_basic_module.info.yml \\__migrations/ \\__basic_migration_one.yml Activación de todos los módulos necesarios con Drush:\ndrush pm:enable -y migrate migrate_run migration_basic_module drush cr O con Drupal Console:\ndrupal moi migrate migrate_run migration_basic_module Segundo caso: migrar desde archivos CSV Para el segundo caso desactivaremos migrate_run, si procede, y activaremos el conjunto de módulos migrate, migrate_plus y migrate_tools. Para tratar los archivos CSV utilizaremos un plugin de origen incluido en el módulo contribuido Migrate Source CSV, migrate_source_csv. En su versión 3.x, este módulo utiliza league/csv para procesar los archivos CSV. Vamos allá, con Composer y Drush:\ncomposer require drupal/migrate_plus drupal/migrate_tools drupal/migrate_source_csv drush pmu migrate_run # If you need drush en migrate migrate_plus migrate_tools migrate_source_csv -y drush cr Ahora, en la ruta /admin/modules/:\n Construcción de los recursos Crearemos otro módulo personalizado para la segunda migración:\ncd project/web/modules/custom mkdir migration_csv_module Con un nuevo archivo migration_csv_module.info.yml:\nname: \u0026#39;Migration CSV Module\u0026#39; type: module description: \u0026#39;Just a basic example of basic migration process with a CSV source.\u0026#39; package: \u0026#39;Migrations Examples 2000\u0026#39; core: 8.x dependencies: - drupal:migrate - drupal:migrate_tools - drupal:migrate_plus En este ejemplo también necesitaremos un archivo declarativo de la migración, como en el caso anterior, pero lo ubicaremos en otro lugar: la ruta /migration_csv_module/config/install/.\nLa estructura tendrá este aspecto:\n/project/web/modules/custom/ \\__migration_csv_module/ \\__migration_csv_module.info.yml \\__csv/ \\_migration_csv_articles.csv \\__config/ \\__install/ \\__migrate_plus.migration.article_csv_import.yml Necesitamos un CSV con los datos originales que vamos a migrar. Podemos generarlo fácilmente con herramientas web como Mockaroo, un generador de datos aleatorios bastante bueno. He creado un CSV con los campos id, title, body, tags e image. Los recursos relacionados se mantienen ahora en mis snippets de GitLab.\nEste archivo será la fuente de datos del proceso de migración. De momento, crea los directorios del módulo y coloca el CSV en la ruta /csv:\n Y ahora, nuestro archivo migrate_plus.migration.article_csv_import.yml. En apartados posteriores explicaremos su construcción y sus secciones:\nuuid: 1bcec3e7-0a49-4473-87a2-6dca09b91aba langcode: en status: true dependencies: { } id: article_csv_import label: \u0026#39;Migrating articles\u0026#39; source: plugin: csv path: modules/custom/migration_csv_module/csv/migration_csv_articles.csv delimiter: \u0026#39;,\u0026#39; enclosure: \u0026#39;\u0026#34;\u0026#39; header_offset: 0 ids: - id fields: - name: id label: \u0026#39;Unique Id\u0026#39; - name: title label: Title - name: body label: \u0026#39;Post Body\u0026#39; - name: tags label: \u0026#39;Taxonomy Tag\u0026#39; - name: image label: \u0026#39;Image Field\u0026#39; process: title: title body: body tags: field_tags image: field_image type: plugin: default_value default_value: article destination: plugin: \u0026#39;entity:node\u0026#39; Ya tenemos todos los recursos necesarios para crear la nueva migración. Veamos cómo vamos a abordar el proceso.\n3. Enfoques Describiremos los diferentes enfoques que aplicaremos a nuestros ejemplos para entenderlos mejor.\nPrimer caso: migrar datos embebidos En el primer caso hemos planteado una migración tan ligera como sea posible: solo dos nodos con dos campos básicos cada uno y los datos embebidos. Lo más sencillo posible.\nAdemás, en las tres fases ETL de la migración —extracción, transformación y carga— utilizaremos plugins que Drupal ya proporciona. No desarrollaremos ningún plugin personalizado. Si todavía no conoces el concepto de plugin de migración, detente un momento y lee la introducción del primer artículo de esta serie.\nPara aligerar el ejemplo utilizaremos Migrate Run, esa versión «lite» de Migration Tools. Emplearemos únicamente los comandos básicos, sin opciones ni parámetros complementarios, con el identificador del archivo de migración como argumento.\nSegundo caso: migrar desde archivos CSV En esta ejecución quiero probar algo interesante. Como ejecutaremos la segunda migración como configuración, me parece divertido recorrer el camino inverso. Propongo no instalar ni activar el módulo personalizado del CSV: lo dejaremos únicamente como ubicación del archivo de datos.\nVamos a llevar la migración a otro lugar y ejecutarla desde allí. Sorpresa. Visita la ruta /admin/config/development/configuration/single/import de tu instalación Drupal. ¡Nos vemos allí!\n4. Migraciones Primer caso: migrar datos embebidos Consultar las migraciones disponibles drush migrate:status drush ms Output from console: ----------------- -------- ------- ---------- ------------- --------------------- Migration ID Status Total Imported Unprocessed Last Imported ----------------- -------- ------- ---------- ------------- --------------------- basic_migration_one Idle 2 0 2 ----------------- -------- ------- ---------- ------------- --------------------- Ejecutar las migraciones drush migrate:import basic_migration_one drush mi basic_migration_one Output from console: ----------------- -------- ------- ---------- ------------- ------------------- Migration ID Status Total Imported Unprocessed Last Imported ----------------- -------- ------- ---------- ------------- ------------------- basic_migration_one Idle 2 2 (100%) 0 2020-03-17 23:19:36 ----------------- -------- ------- ---------- ------------- ------------------- Al visitar /admin/content verás los dos nodos nuevos:\n Revertir las migraciones: deshacer la importación drush migrate:rollback basic_migration_one drush mr basic_migration_one Output from console: [notice] Rolled back 2 items - done with 'basic_migration_one'  Segundo caso: migrar desde archivos CSV En la ruta /admin/config/development/configuration/single/import importaremos el archivo de definición de nuestra migración personalizada.\nCargar la configuración de la migración Ve a Importar → Elemento individual, selecciona «Migration» como tipo de configuración y pega el contenido del archivo original de la migración:\n Pulsa «Importar» y se creará el nuevo objeto en el sistema de configuración.\n¿Y ahora?\nEjecutar la migración Con el archivo de migración bajo la gestión de configuración, puedes ejecutar el proceso con las mismas herramientas del caso anterior. Ahora tenemos una nueva migración disponible desde la consola. Compruébalo con drush migrate:status:\n Puedes ejecutar la migración con drush migrate-import article_csv_import. Se crearán los nuevos nodos. ¿La limitación? No se migrarán las etiquetas ni las imágenes: una etiqueta es una referencia a entidad y una imagen no es simplemente un enlace, sino un archivo. Ambos tipos necesitan otros plugins; lo veremos en futuras entregas.\nDrush cex / Drush cim Al estar la migración en el sistema de configuración, podemos editarla, importarla y exportarla con las herramientas básicas de Drush. Por ejemplo, probando drush cex:\n Como puedes ver, el sistema de configuración ha puesto directamente el nuevo archivo bajo la gestión de Migrate Plus y ha realizado algunas operaciones: renombrarlo con el prefijo migrate_plus.migration o añadir un archivo para el grupo, que sirve para agrupar procesos de migración.\n¿Recuerdas el nombre del archivo? Es el mismo que utilizábamos en /config/install: migrate_plus.migration.article_csv_import.yml. Hemos realizado el mismo proceso, pero desde otra dirección. ¿Te impresiona? ¿No? ¿Al menos te parece interesante?\nRecuerda también que puedes utilizar drush cim con este archivo de configuración para cargar la migración en otra instalación Drupal, siempre que tenga acceso al CSV como fuente de datos, claro.\nAsí hemos migrado unos 102 nodos nuevos utilizando dos enfoques y metodologías diferentes. No está mal.\n5. Conceptos clave Plugins de migración Esto es importante, así que toca repetir la misma canción: hay que conocer el formato de los plugins y el diverso mundo de los plugins de migración existentes.\nCada plugin responde a un tipo de dato, un formato o una fuente concreta. Conviene conocer bien los principales e investigar los que puedas necesitar, porque las migraciones los utilizan intensivamente. Por eso, por ejemplo, no hemos podido migrar los términos de taxonomía ni las imágenes del segundo caso directamente desde el CSV.\nVamos a revisar los plugins que intervienen en estas dos migraciones a partir de sus archivos de definición.\nMigración básica con datos embebidos source: plugin: embedded_data data_rows: ... process: title: creative_title body: engaging_content destination: plugin: 'entity:node' default_bundle: page Para extraer los datos utilizamos el plugin Embedded Data, una clase PHP disponible en /web/core/modules/migrate/src/Plugin/migrate/source/EmbeddedDataSource.php. En su bloque de documentación puedes ver algunas claves de configuración que puedes utilizar en la definición:\n * * Available configuration keys * - data_rows: The source data array. * - ids: The unique ID field of the data. * Precisamente data_rows e ids son las claves que utilizamos: el array de datos de origen y su identificador único. Puedes leer más sobre la clase EmbeddedDataSource en la documentación de la API de Drupal.org.\nAhora fíjate en el bloque process y busca… ¿dónde está el plugin de procesamiento? Esto es interesante. Normalmente cada correspondencia de campos necesita un plugin de proceso. Con un poco de «azúcar sintáctico», Migrate API permite simplificar la definición: cuando un campo no necesita un tratamiento específico, el mismo plugin puede encargarse de obtener su valor. Además, ese plugin predeterminado puede quedar implícito. Si no lo declaramos, Drupal aplica el plugin de proceso por defecto.\nEse plugin implícito es la clase Get, la solución básica para procesar campos. La encontrarás en /web/core/modules/migrate/src/Plugin/migrate/process/Get.php. Más información sobre Get.php en la API de Drupal.org. En otras palabras, escribir esto:\nprocess: title: page_title Equivale a escribir esto otro:\nprocess: title: plugin: get source: page_title Así la vida resulta un poco más sencilla, ¿verdad? Recuerda: si no declaras un plugin de procesamiento para un campo, Drupal aplicará Get por defecto.\nPor último, en el destino utilizamos el plugin de entidad con el parámetro node para crear nodos del tipo page. Esto recurre al plugin de destino Entity.php, una clase abstracta ubicada en web/core/modules/migrate/src/Plugin/migrate/destination/Entity.php, y a su plugin derivado. Puedes leer más sobre los plugins derivados en Drupal, el plugin de destino Entity.php y la clase derivadora de migraciones.\nMigración con un CSV como fuente de datos Creo que en este caso revisar los plugins resulta más sencillo e intuitivo:\nsource: plugin: csv ... process: ... type: plugin: default_value default_value: article destination: plugin: 'entity:node' Para el origen utilizamos el plugin CSV del módulo contribuido migrate_source_csv. En el procesamiento usamos Get por defecto y Default Value para el tipo de contenido. En el destino repetimos el plugin del ejemplo anterior: nuevas entidades.\nMigraciones como código o como configuración Como has visto, hemos tratado cada proceso de una manera distinta. El primero, con datos embebidos, forma parte del código, sin más particularidades. El segundo se trata como un elemento de configuración del sistema: al situarlo en config/install, la instalación puede crear un objeto de configuración.\nEn ambos casos escribimos una definición YAML y colocamos el archivo en una ubicación u otra. Pero hay más diferencias. Resumamos las claves:\n Las migraciones «como código» están disponibles de serie. El módulo Migrate Plus permite tratar la definición como un objeto de configuración. Según el enfoque, cambian la ubicación de los archivos y el flujo de trabajo. Como código, necesitas acceso al sistema de archivos para modificar la definición y gestionarla como cualquier otro archivo de código. Es un enfoque orientado a desarrolladores. Como configuración, puedes modificar la definición desde la interfaz de sincronización de Drupal, en /admin/config/development/configuration, y utilizar las herramientas de exportación e importación drush cex y drush cim. La migración se sincroniza y su definición se guarda en la base de datos. Esto permite trabajar con las definiciones desde la interfaz de usuario. Toda una sorpresa. Como objeto de configuración, la migración crea un registro en el sistema de configuración que puede permanecer cuando se desinstale el módulo de migración. Para eliminarlo con el módulo, añade tu módulo personalizado como dependencia forzada en el YAML. Así se eliminará de la configuración activa al desinstalarlo:  dependencies: enforced: module: - my_own_migration_custom_module  Otra diferencia del enfoque como configuración es el uso de un UUID: un identificador global del sistema de gestión de configuración de Drupal. Añade un UUID propio y único al principio del archivo. Su formato consta de 32 dígitos hexadecimales repartidos en cinco grupos con el patrón 8-4-4-4-12. ¡Genera el tuyo! El original utiliza como ejemplo ilustrativo uuid: cacafuti-1a23-2b45-3c67-4d567890a1b2; contiene caracteres no hexadecimales, por lo que debes sustituirlo por un UUID válido.   6. Recursos Descarga, prueba y experimenta con los recursos utilizados en el artículo. Los recursos relacionados se mantienen ahora en mis snippets de GitLab.\n Archivo de migración básica. Archivo de migración CSV. CSV de origen con datos aleatorios. Código de los dos módulos de migración, básico y CSV, referenciado desde mi perfil de GitLab. Este será un repositorio central para los módulos de la serie, con los dos ejemplos:  Primera migración básica con datos embebidos. Segunda migración básica con un CSV como fuente de datos.   En paralelo a estos artículos publico snippets en GitLab sobre migraciones, con un formato más breve. Desde el primero puedes acceder al resto de la serie: Drupal Migrations Tips (I): Creating a new basic migration structure.  7. :wq! Canción recomendada   ","permalink":"https://www.therussianlullaby.dev/es/blog/drupal-migrations-two-examples/","tags":["Migraciones en Drupal","Migrate API","Módulos contribuidos","Plugins de Drupal","ETL"],"title":"Migraciones en Drupal (II): ejemplos"},{"categories":["Migraciones"],"contents":"Estoy trabajando en unas notas para un borrador que debería convertirse en un libro sobre procesos de migración construidos con Drupal y su API Migrate. La publicación está prevista para junio de 2020, y recopilar, probar y dar forma al contenido se ha convertido en un trabajo considerable.\nTodavía quedan unos meses para el lanzamiento. Así que, para conservar un poco de salud mental y dar un sentido parcial a estas tareas, he pensado publicar aquí algunos artículos breves a partir de esas notas de trabajo.\nDe esa manera puedo compartir algo útil junto a las notas complementarias y, si la COVID-19 me alcanza antes de que salga el libro, al menos habré compartido algo antes. Supongo.\nBueno, ¿de qué quiero hablar en este artículo? Me gustaría elaborar una lista de módulos de Drupal relacionados con los procesos de migración: módulos contribuidos que añaden funcionalidades a una migración. Este artículo es solo una recopilación ligera de recursos básicos. Lo juro.\n Este artículo se publicó originalmente en una versión anterior de mi cuaderno técnico personal y ahora forma parte de The Russian Lullaby.\nFotografía de Unsplash, del usuario Nils Nedel, @nilsnedel\n Índice de contenidos\n 1- Introducción\n2- Recursos básicos: módulos del núcleo\n3- Otros recursos básicos: módulos contribuidos\n4- Recursos adicionales: módulos contribuidos para plugins\n5- Ejecutar migraciones: módulos relacionados con Drush\n6- Autores que deberías conocer\n7- :wq!\n1- Introducción No es fácil hablar de migraciones en general y, por supuesto, tampoco lo es en el contexto de Drupal. Para ejecutarlas necesitas conocer bien la tecnología, los modelos de datos de origen y destino, tener cierta experiencia con procesos ETL y manejarte con los plugins de Drupal. Las migraciones hacen un uso intensivo de estos plugins.\nEn cualquier caso, como el tema es amplio y ahora tengo poco tiempo, he planteado este artículo como un catálogo resumido, de consulta rápida, de herramientas y recursos básicos para trabajar con migraciones.\n2- Recursos básicos: módulos del núcleo   Migrate: la base de las migraciones. El módulo migrate proporciona la API fundamental para migrar en Drupal.\n  Migrate Drupal: migrate_drupal se centra en las actualizaciones desde Drupal 6 o Drupal 7. Permite leer entidades de configuración en Drupal 8.\n  Migrate Drupal Multilingual: migrate_drupal_multilingual. ¿Un módulo experimental del núcleo? Véase esta referencia.\n  Migrate Drupal UI: migrate_drupal_ui ofrece una interfaz para realizar la actualización.\n  3- Otros recursos básicos: módulos contribuidos   Migrate Plus: migrate_plus es un módulo contribuido esencial que amplía las funcionalidades y capacidades de Migrate con numerosos plugins y extensiones.\n  Migrate Tools: migrate_tools es otro recurso imprescindible: proporciona numerosos comandos Drush para ejecutar y gestionar migraciones.\n  Migrate Status: migrate_status. Este pequeño módulo permite obtener información sobre un proceso de migración. ¿Necesitas saber si una migración está ejecutándose? Ofrece un servicio al que puedes llamar para comprobarlo.\n  Migrate Files: migrate_files. Una colección de plugins de proceso tan interesante que te entrarán ganas de mover archivos e imágenes con ella.\n  Migrate Commerce: commerce_migrate. Un marco de propósito general que amplía el módulo Migrate del núcleo para trasladar datos en un escenario de Drupal Commerce.\n  4- Recursos adicionales: módulos contribuidos para plugins En las migraciones de Drupal utilizamos distintos recursos para ejecutar el plan ETL. Uno de los fundamentales, como adelantaba en la introducción, son los plugins de Drupal, que requieren cierto conocimiento y práctica.\nEn una migración, los plugins ayudan a leer la información en la fase E, extracción desde el origen (plugins de origen); realizar la T, transformación o procesamiento (plugins de proceso); y guardar los datos en la L, carga en el destino (plugins de destino). Al combinar estas tres partes solemos obtener un proceso de migración funcional.\nMuchos módulos del núcleo, como el de usuarios, ya incluyen sus propios plugins para facilitar las migraciones. Veamos algunos plugins distribuidos en módulos contribuidos.\nPlugins de origen: Migrate Source Plugins   Migrate Source CSV: migrate_source_csv. Módulo contribuido para migrar datos a Drupal 8 desde un sencillo archivo CSV de los de toda la vida.\n  Migrate Source SQL: custom_sql_migrate_source_plugin. Como particularidad entre los plugins de base de datos, permite incluir consultas SQL directamente en el archivo .yml que describe la migración. Las consultas se ejecutan contra la base de datos de origen.\n  Migrate Source YAML: migrate_source_yaml. Una herramienta sencilla para migrar contenido desde archivos YAML.\n  Plugins de proceso: Migrate Process Plugins   Migrate Process Geofield: geofield. El módulo contribuido Geofield incluye un plugin de proceso personalizado para migraciones. Consulta su plugin de proceso.\n  Migrate Process XML: migrate_process_xml. Proporciona plugins de proceso para xpath y xvalue.\n  Migrate HTML to Paragraphs: migrate_html_to_paragraphs. Ayuda a transformar el HTML del origen en un elemento Paragraph, gestionado mediante un plugin de destino.\n  Plugins de destino: Migrate Destination Plugins \u0026amp; Examples ¿Qué clase de entidades de Drupal se crearán durante la migración? ¿Entidades de contenido? ¿Entidades de configuración? Echa un vistazo.\n  Migrate Destination CSV: d8migrate. Un pequeño módulo personalizado creado por @jonathanfranks.\n  Migrate Destination Config: clase Config.php. Ofrece un plugin para migrar configuración.\n  Migrate Destination Block: clase EntityBlock.php. Como ejemplo de los recursos que puede ofrecer cada componente en una migración, al trasladar entidades de bloque —que son entidades de configuración— conviene consultar las clases PHP de origen, proceso y destino incluidas en su propio módulo.\n   5- Ejecutar migraciones: módulos relacionados con Drush   Migrate Scheduler: migrate_scheduler. Ofrece integración con la API de cron de Drupal para ejecutar migraciones según una programación predefinida.\n  Migrate Run: migrate_run. Comandos Drush para ejecutar migraciones de forma ligera. Es más sencillo que Migrate Tools, pero no ofrece soporte para grupos de migraciones. Tampoco depende de Migrate Plus. Va un poco por libre.\n  Migrate Devel: migrate_devel. Proporciona opciones Drush para mostrar información de depuración durante la ejecución. También incorpora el plugin de proceso debug. Aquí puedes leer un artículo sobre él. Por cierto, en el contexto original del artículo, la compatibilidad con Drush 9 seguía sin resolverse, aunque parecía haber un parche disponible.\n  6- Autores que deberías conocer  Mauricio Dinarte: desarrollador, consultor, formador y propietario de su propio negocio, Agaric. Escribió lo que probablemente sea una lectura obligatoria para cualquiera que quiera aprender a migrar con Drupal: 31 Days of Drupal Migrations, una colección de 31 artículos publicados en Understand Drupal, con los aspectos más importantes, ejemplos, ejercicios y descripciones para entender el mundo interno de las migraciones.  Un material formativo imprescindible. Además, el sitio de su empresa reúne muchos artículos muy buenos sobre el tema bajo la etiqueta migrate.\nAlgunos ejemplos de Mauricio Dinarte:\n Introducción a las migraciones de Paragraphs en Drupal. Utilizar grupos de migración para compartir configuración entre migraciones de Drupal. ¿Cuál es la diferencia entre etiquetas y grupos de migración en Drupal?.  Su perfil en Drupal.org: dinarcon.\n  Tess Flynn: oí hablar de ella leyendo artículos de Mauricio Dinarte. Así conocí a esta desarrolladora experta, ponente y divulgadora de la comunidad Drupal. En su sitio, deninet.com, encontré contenido variado y, sobre todo, una serie muy interesante sobre migraciones bajo la etiqueta drupal-migration.  Por el camino descubrí también que tiene varios módulos contribuidos relacionados con las migraciones y los plugins de proceso.\nAlgunos ejemplos de Tess Flynn:\n Migrate Process URL: proporciona un plugin de proceso para migrar campos de enlace. Migrate Process Vardump: ayuda a depurar migraciones. Otros plugins de proceso: Migrate Process Array, Migrate Process Skip y Migrate Process Trim.  Su perfil en Drupal.org: socketwench.\n  Danny Sipos: autor de una reconocida «biblia» de Drupal, Drupal 8 Module Development, por entonces en su segunda edición, y del sitio webomelette.com. Ponente, formador y asistente habitual a distintos encuentros internacionales de Drupal, también ha escrito artículos interesantes sobre migraciones que deberías leer.  Algunos ejemplos de Danny Sipos:\n Tu primera migración en Drupal 8. Migraciones dinámicas mediante «plantillas» en Drupal 8. Generar rápidamente las cabeceras del plugin de origen CSV mediante Drush.  Su perfil en Drupal.org: upchuk.\n 7- :wq! Canción recomendada   ","permalink":"https://www.therussianlullaby.dev/es/blog/drupal-migrations-one-basic-resources/","tags":["Drupal Migrations","Migrate API","Contrib Modules","Drupal Plugins","ETL"],"title":"Migraciones en Drupal (I): recursos básicos"}]