<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Migrate API on The Russian Lullaby</title><link>https://www.therussianlullaby.dev/es/tags/migrate-api/</link><description>Recent content in Migrate API on The Russian Lullaby</description><generator>Hugo -- gohugo.io</generator><language>es-ES</language><lastBuildDate>Wed, 14 Oct 2026 00:00:00 +0200</lastBuildDate><atom:link href="https://www.therussianlullaby.dev/es/tags/migrate-api/index.xml" rel="self" type="application/rss+xml"/><item><title>Migraciones en Drupal (VI): la experiencia Rick and Migraty, combinando datos de distintas fuentes</title><link>https://www.therussianlullaby.dev/es/blog/drupal-migrations-six-rick-and-migraty-experience-merging-data-from-different-sources/</link><pubDate>Wed, 14 Oct 2026 00:00:00 +0200</pubDate><guid>https://www.therussianlullaby.dev/es/blog/drupal-migrations-six-rick-and-migraty-experience-merging-data-from-different-sources/</guid><enclosure url="https://www.therussianlullaby.dev/images/post/davidjguru_drupal_migrations_populating_content_from_json_api_main.jpg" type="image/jpeg"/><media:content url="https://www.therussianlullaby.dev/images/post/davidjguru_drupal_migrations_populating_content_from_json_api_main.jpg" medium="image" type="image/jpeg"/><media:thumbnail url="https://www.therussianlullaby.dev/images/post/davidjguru_drupal_migrations_populating_content_from_json_api_main.jpg"/><description>Migraciones en Drupal (VI): combina datos de Rick and Morty, TVmaze y Fandom con la API Migrate, entidades personalizadas y Views.</description><content:encoded><![CDATA[<p>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. <strong>Porque siempre acaba dando guerra</strong>.</p>
<p>Esta sexta entrega vuelve al lado práctico de la migración de datos: <strong>poblar Drupal con datos remotos y estructurados obtenidos de varias API</strong>. 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.</p>
<p><strong>¿Qué demuestra esto sobre Drupal?</strong> 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, <strong>la experiencia Rick and Migraty ofrece un ejemplo práctico con la API Migrate, entidades de contenido personalizadas y Views</strong>. El repositorio enlazado y la configuración de DDEV permiten ejecutar el proyecto en local y examinar cómo encajan esas piezas.</p>
<p>Para conseguirlo, nos conectaremos a API externas, abiertas y de acceso gratuito. Recursos como <a href="https://rickandmortyapi.com/"><strong>la API de Rick and Morty</strong></a>, <a href="https://www.tvmaze.com/api"><strong>la API de TVmaze</strong></a> y <a href="https://rickandmorty.fandom.com/api.php"><strong>la wiki de Rick and Morty en Fandom</strong></a> nos proporcionarán los datos necesarios para poblar nuestras entidades de Drupal con personajes, localizaciones, episodios, puntuaciones y descripciones.</p>
<p><strong>La 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</strong>, para construir perfiles más ricos de los personajes y lugares de nuestro universo animado favorito.</p>
<p><strong>Nota:</strong> A fecha de 14 de octubre de 2026, la versión original de este artículo <strong>supera los 52.000 caracteres y ronda las 10.000 palabras, con un tiempo de lectura estimado de entre 50 y 60 minutos</strong>.</p>
<hr>
<p><strong>Fotografía de Unsplash, del usuario <a href="https://unsplash.com/es/@juliankraus">Julian Kraus, @juliankraus</a></strong></p>
<p><strong>Índice de contenidos</strong></p>
<span class="post-toc" aria-hidden="true"></span>

<!-- raw HTML omitted -->
<p><a href="#0-tldr-solo-quiero-jugar-con-drupal">0. TL;DR (<strong>solo quiero jugar con Drupal</strong>)</a><br>
<a href="#1-de-d%C3%B3nde-sali%C3%B3-la-idea-or%C3%ADgenes">1. De dónde salió la idea (<strong>orígenes</strong>)</a><br>
<a href="#2-qu%C3%A9-buscamos-objetivos">2. Qué buscamos (<strong>objetivos</strong>)</a><br>
<a href="#3-analizar-los-datos-comprender">3. Analizar los datos (<strong>comprender</strong>)</a><br>
<a href="#4-algunas-restricciones-iniciales-espera">4. Algunas restricciones iniciales (<strong>espera</strong>)</a><br>
<a href="#5-contenedores-de-destino-en-drupal-entidades">5. Contenedores de destino en Drupal (<strong>entidades</strong>)</a><br>
<a href="#6-dise%C3%B1ar-la-migraci%C3%B3n-cocinar">6. Diseñar la migración (<strong>cocinar</strong>)</a><br>
<a href="#7-definir-plugins-de-migraci%C3%B3n-soluciones">7. Definir plugins de migración (<strong>soluciones</strong>)</a><br>
<a href="#8-m%C3%A1s-informaci%C3%B3n-de-las-fuentes-enriquecimiento">8. Más información de las fuentes (<strong>enriquecimiento</strong>)</a><br>
<a href="#9-de-los-datos-al-contenido-navegar">9. De los datos al contenido (<strong>navegar</strong>)</a><br>
<a href="#10-la-migraci%C3%B3n-como-operaci%C3%B3n-ejecutar">10. La migración como operación (<strong>ejecutar</strong>)</a><br>
<a href="#11-perder-la-cabeza-depurar">11. Perder la cabeza (<strong>depurar</strong>)</a><br>
<a href="#12-decisiones-que-merece-la-pena-destacar-decidir">12. Decisiones que merece la pena destacar (<strong>decidir</strong>)</a><br>
<a href="#13-qu%C3%A9-he-aprendido-reflexionar">13. Qué he aprendido (<strong>reflexionar</strong>)</a><br>
<a href="#14-pr%C3%B3ximos-pasos-planificar">14. Próximos pasos (<strong>planificar</strong>)</a><br>
<a href="#15-wq">15. :wq!</a></p>
<!-- raw HTML omitted -->
<h2 id="0-tldr-solo-quiero-jugar-con-drupal">0. TL;DR (solo quiero jugar con Drupal)</h2>
<p>Vale, vale, no te juzgo. <strong>Asegúrate de cumplir los requisitos previos del proyecto:</strong></p>
<ol>
<li>Tener Git instalado en tu equipo (<strong>git v2.43.0</strong>).</li>
<li>Tener DDEV instalado (<strong>ddev v1.25.4</strong>, <strong>Docker v29.9.0</strong>).</li>
<li>Disponer de una terminal de Linux.</li>
</ol>
<p>Una vez comprobado todo, sigue estos pasos:</p>
<div class="highlight"><pre style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">git clone https://gitlab.com/davidjguru/rick-and-migraty.git
</code></pre></div><div class="highlight"><pre style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#0086b3">cd</span> rick-and-migraty/
</code></pre></div><div class="highlight"><pre style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">ddev start
</code></pre></div><div class="highlight"><pre style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">ddev composer install
</code></pre></div><p>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:</p>
<div class="highlight"><pre style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">ddev drush site:install --existing-config <span style="color:#d14">\
</span><span style="color:#d14"></span>  --account-name<span style="color:#000;font-weight:bold">=</span>admin <span style="color:#d14">\
</span><span style="color:#d14"></span>  --account-pass<span style="color:#000;font-weight:bold">=</span>admin <span style="color:#d14">\
</span><span style="color:#d14"></span>  -y
</code></pre></div><div class="highlight"><pre style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">ddev launch
</code></pre></div><p>Así se completará la configuración del sitio y se abrirá el proyecto en tu navegador, en <strong><code>https://rick-and-migraty.ddev.site</code></strong>:</p>
<p>
<span class="post-figure">
  <img src="/images/post/davidjguru_drupal_11_migrations_rick_and_migraty_zero_home_web.jpg" alt="Portada de Rick and Migraty en el entorno local." loading="lazy" decoding="async">
  <span class="post-figure__caption">Vista principal de Rick and Migraty en el navegador</span>
</span>

</p>
<p>Lo que realmente importa en la portada son <strong>las acciones que permite realizar el proyecto</strong>:</p>
<p>
<span class="post-figure">
  <img src="/images/post/davidjguru_drupal_11_migrations_rick_and_migraty_zero_2_actions.jpg" alt="Menú principal de acciones del sitio." loading="lazy" decoding="async">
  <span class="post-figure__caption">Menú de acciones: aprender, explorar y borrar cosas</span>
</span>

</p>
<p><strong>¡Recuerda!</strong> Las credenciales de instalación son <code>user: admin, password: admin</code>. También puedes acceder a la documentación del proyecto en la URL local <strong><code>https://docs.rick-and-migraty.ddev.site</code></strong>. <strong>¡Disfrútalo!</strong></p>
<p><strong>¿Te gusta lo que ves?</strong> Puedes apoyarme compartiendo este artículo, dando a conocer el repositorio (<a href="https://gitlab.com/davidjguru/rick-and-migraty"><strong>https://gitlab.com/davidjguru/rick-and-migraty</strong></a>) o añadiéndole una estrella en GitLab:</p>
<p>
<span class="post-figure">
  <img src="/images/post/davidjguru_drupal_11_migrations_rick_and_migraty_zero_3_repo_star.jpg" alt="¡Un poco de cariño para el autor!" loading="lazy" decoding="async">
  <span class="post-figure__caption">¡Apoya el proyecto con una estrella! ⭐</span>
</span>

</p>
<h2 id="1-de-dónde-salió-la-idea-orígenes">1. De dónde salió la idea (orígenes)</h2>
<p>Durante una temporada estuve trabajando en procesos de migración en Drupal basados en la API Migrate. <strong>Algunas de aquellas experiencias profesionales terminaron convirtiéndose en la serie de artículos sobre migraciones en Drupal publicada aquí</strong>.</p>
<p>Por esas mismas fechas llegó a mi vida la serie de animación de Adult Swim <a href="https://www.adultswim.com/rick-and-morty">Rick and Morty</a> (2013–actualidad). Durante un tiempo <strong>fue una de mis series favoritas, a pesar de sus altibajos</strong>, 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).</p>
<p>Quería hacer algo con todo aquello: <strong>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</strong>. 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í <a href="https://rickandmortyapi.com/">The Rick and Morty API</a>, que me entusiasmó bastante.</p>
<p>Y <strong>así nació The Rick and Migraty Experience</strong>.</p>
<p>
<span class="post-figure">
  <img src="/images/post/davidjguru_drupal_11_migrations_rick-and-migraty-one-1200x675.jpg" alt="Imagen principal de The Rick and Migraty Experience." loading="lazy" decoding="async">
  <span class="post-figure__caption">La nueva experiencia Rick and Migraty: ¡Wubba Lubba Dub-Dub!</span>
</span>

</p>
<p>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. <strong>Montones de consultas, preparadas para recibir montones de respuestas JSON</strong>.</p>
<p><a href="/es/blog/drupal-migrations-three-migrating-from-google-spreadsheet/">El tercer artículo de esta serie ya utilizaba una fuente con formato JSON</a>, 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 <strong>quería centrarme en un escenario más propio del trabajo con API</strong>.</p>
<p><strong>La API de Rick and Morty es un buen campo de pruebas para las migraciones en Drupal</strong>: 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.</p>
<p>En este caso, <strong>el objetivo es diseñar el proceso de migración antes de escribir todo el código</strong>. 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.</p>
<h2 id="2-qué-buscamos-objetivos">2. Qué buscamos (objetivos)</h2>
<p>La idea principal, el objetivo central, es <strong>construir una plataforma unificada para explorar y seguir tanto a los personajes que pueblan los episodios como a los propios episodios</strong>. Un lugar donde consultar perfiles de personajes como este:</p>
<p>
<span class="post-figure">
  <img src="/images/post/davidjguru_drupal_11_migrations_rick_and_migraty_two_character_sketch.jpg" alt="Esquema de un perfil de personaje en Rick and Migraty." loading="lazy" decoding="async">
  <span class="post-figure__caption">Esquema de baja fidelidad de un perfil de personaje</span>
</span>

</p>
<p>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. <strong>Todo envuelto en una apariencia coherente, sencilla y atractiva</strong>. Se trata de convertir los bocetos originales en algo más interesante:</p>
<p>
<span class="post-figure">
  <img src="/images/post/davidjguru_drupal_11_migrations_rick_and_migraty_three_ux_evolution.jpg" alt="Evolución de la experiencia de usuario del perfil." loading="lazy" decoding="async">
  <span class="post-figure__caption">Evolución del perfil: del esquema a la maqueta y al diseño</span>
</span>

</p>
<p>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.</p>
<p>Como 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.</p>
<p><strong>Para conseguirlo, diseñaremos un proceso de migración capaz de combinar datos de varias fuentes cuando sea necesario</strong>. Pero antes de escribir archivos YAML debemos comprender bien la naturaleza del problema —o, mejor dicho, del desafío tecnológico— que pretendemos resolver.</p>
<h2 id="3-analizar-los-datos-comprender">3. Analizar los datos (comprender)</h2>
<p>Lo primero es pensar cómo modelar lo que buscamos: <strong>qué información queremos obtener</strong>, qué novedad queremos introducir para ofrecer algo distintivo, como comentábamos antes, y cómo vamos a hacerlo.</p>
<p>La primera pregunta es: <strong>¿qué consideraríamos información valiosa?</strong> Aquí van algunas ideas:</p>
<ul>
<li>Disponer de una <strong>sinopsis de cada episodio</strong> que contextualice lo que sucede.</li>
<li>Obtener una <strong>puntuación o valoración de cada episodio</strong>.</li>
<li>Tener una lista de los <strong>personajes que aparecen en cada episodio</strong>.</li>
<li>Contar con <strong>información de contexto sobre los personajes</strong>, ya sea biográfica o relacionada con su papel en la historia.</li>
<li>Consultar la <strong>lista de episodios en los que aparece un personaje concreto</strong>.</li>
</ul>
<p><strong>Aquí comienza una breve fase de selección de fuentes de datos</strong>. Por razones obvias —forma parte de la motivación original del proyecto— queremos que la fuente principal sea <a href="https://rickandmortyapi.com/">The Rick and Morty API</a>. Sin embargo, frente a los requisitos anteriores, <strong>esta API presenta algunas limitaciones</strong>:</p>
<ul>
<li>Los episodios no incluyen sinopsis ni descripción.</li>
<li>Los episodios no incluyen puntuación ni valoración.</li>
<li>Los personajes no incluyen descripción.</li>
</ul>
<p><strong>Esta API será la fuente canónica del dominio principal</strong>. 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 (<a href="https://rickandmortyapi.com/documentation#get-a-single-episode"><strong>https://rickandmortyapi.com/documentation#get-a-single-episode</strong></a>), aún falta información para completar el modelo:</p>
<p>
<span class="post-figure">
  <img src="/images/post/davidjguru_drupal_11_migrations_rick_and_migraty_five_get_episode_from_API.jpg" alt="Modelo de datos de un episodio en la API de Rick and Morty." loading="lazy" decoding="async">
  <span class="post-figure__caption">Ejemplo de datos obtenidos de un episodio</span>
</span>

</p>
<p>Por tanto, <strong>necesitamos fuentes adicionales</strong> que aporten lo que falta y enriquezcan los datos existentes: descripciones de episodios y personajes, y puntuaciones de episodios.</p>
<p>Para obtener las biografías y las sinopsis podemos utilizar la API de la wiki de <a href="https://www.fandom.com/">Fandom</a>, que ofrece esa información.</p>
<p><strong>¿Y las puntuaciones de los episodios?</strong> Fandom puede ofrecer un campo llamado <em>ratings</em>, 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.</p>
<p>Así que <strong>necesitamos otra fuente para obtener esas puntuaciones</strong>. Utilizaremos la API de <a href="https://www.tvmaze.com/">TVmaze</a>, una base de datos y plataforma comunitaria de seguimiento de series de televisión y series web.</p>
<p>Esto no consiste solo en «<strong>leer JSON y crear contenido en Drupal</strong>». 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.</p>
<p><strong>En resumen…</strong> En este punto ya trabajamos con tres API al modelar el contenido y la migración:</p>
<table>
<thead>
<tr>
<th><strong>API</strong></th>
<th><strong>Papel en la migración</strong></th>
</tr>
</thead>
<tbody>
<tr>
<td><code>https://rickandmortyapi.com/api</code></td>
<td>Fuente canónica de personajes, localizaciones y episodios. Su punto de acceso raíz expone las tres colecciones principales: <code>character</code>, <code>location</code> y <code>episode</code>.</td>
</tr>
<tr>
<td><code>https://rickandmorty.fandom.com/api.php</code></td>
<td>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.</td>
</tr>
<tr>
<td><code>https://www.tvmaze.com/api</code></td>
<td>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.</td>
</tr>
</tbody>
</table>
<p>Esta separación importa. Cuando varias API participan en el mismo modelo de destino, <strong>cada fuente necesita una responsabilidad clara</strong>. De lo contrario, la migración se convierte en una discusión silenciosa entre sistemas remotos y a Drupal le toca recoger el desastre.</p>
<p>Los tres recursos principales de la API de Rick and Morty <strong>deberían corresponderse de forma natural con entidades de contenido de Drupal</strong>:</p>
<ol>
<li><strong>Personajes:</strong> 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 <strong>lo primero que la mayoría querrá consultar</strong>.</li>
<li><strong>Localizaciones:</strong> proporcionan la <strong>estructura espacial del multiverso</strong>: 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.</li>
<li><strong>Episodios:</strong> aportan la <strong>estructura narrativa</strong>. 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.</li>
</ol>
<p>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, <strong>lo interesante es el grafo de relaciones</strong>:</p>
<p>
<span class="post-figure">
  <img src="/images/post/davidjguru_drupal_11_migrations_rick_and_migraty_four_relationship_graph.jpg" alt="Diagrama de relaciones entre los conceptos principales." loading="lazy" decoding="async">
  <span class="post-figure__caption">Relaciones entre conceptos y entidades del proceso de migración</span>
</span>

</p>
<p><strong>Como muestra la imagen:</strong></p>
<ul>
<li>Los personajes tienen un origen y una última localización conocida.</li>
<li>Los personajes aparecen en episodios.</li>
<li>Los episodios contienen muchos personajes.</li>
<li>Las localizaciones contienen residentes.</li>
<li>Los residentes son personajes.</li>
</ul>
<p>Y sí: <strong>esto implica relaciones circulares</strong>.</p>
<h2 id="4-algunas-restricciones-iniciales-espera">4. Algunas restricciones iniciales (espera)</h2>
<p>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.</p>
<p><strong>La primera restricción es el orden de migración.</strong> Las localizaciones y los episodios deben existir antes de que los personajes puedan referenciarlos. Los personajes deben existir antes de rellenar <code>characters</code> en los episodios y <code>residents</code> en las localizaciones. El orden no es un detalle técnico que dejamos para el final: forma parte del modelo de contenido.</p>
<p><strong>La segunda restricción es la autoridad de las fuentes.</strong> 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 <code>api_id</code> y <code>api_url</code>. 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.</p>
<p><strong>La tercera restricción es la repetibilidad.</strong> Durante el desarrollo ejecutaremos muchas importaciones, reversiones y reinicios; limitaremos páginas, almacenaremos respuestas en caché y, a veces, utilizaremos <em>fixtures</em>. 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?</p>
<p>Bienvenidos a las migraciones. <strong>Los datos son un desastre, pero al menos vienen en JSON.</strong></p>
<h2 id="5-contenedores-de-destino-en-drupal-entidades">5. Contenedores de destino en Drupal (entidades)</h2>
<p>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. <strong>Parece más fiel a su naturaleza modelarlos como entidades de contenido personalizadas.</strong></p>
<p>En el proyecto habrá un módulo personalizado dedicado a la API de entidades: <code>rick_and_migraty_api</code>.</p>
<p>Su función no es obtener datos remotos. <strong>Su función es proporcionar el modelo permanente de Drupal que rellenará la capa de migración</strong> y que después el sitio expondrá mediante rutas, Views, plantillas y navegación. Definirá tres entidades de contenido personalizadas:</p>
<ol>
<li><strong>rick_and_migraty_character</strong></li>
<li><strong>rick_and_migraty_location</strong></li>
<li><strong>rick_and_migraty_episode</strong></li>
</ol>
<p>Las tres extienden la misma clase base abstracta:</p>
<div class="highlight"><pre style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-text" data-lang="text">Drupal\rick_and_migraty_api\Entity\RickAndMigratyEntityBase
</code></pre></div><p>Esta clase centraliza los campos necesarios para cualquier recurso importado: <code>api_id</code>, <code>name</code>, <code>api_url</code>, <code>api_created</code>, <code>uid</code>, <code>created</code>, <code>changed</code> y <code>path</code>. <strong>El fundamental es <code>api_id</code>: es obligatorio, tiene una restricción de unicidad y representa el identificador numérico estable de la API de Rick and Morty</strong>. En la práctica, es la identidad externa que utilizamos para actualizaciones, búsquedas de relaciones y depuración.</p>
<p>El campo <code>name</code> actúa también como etiqueta de la entidad mediante <code>entity_keys</code>, mientras que <code>api_url</code> y <code>api_created</code> permiten rastrear la entidad hasta el recurso remoto original. Los campos locales <code>created</code> y <code>changed</code> indican cuándo creó o actualizó Drupal su copia. <strong>Existe un campo <code>uid</code> porque son entidades de contenido reales y Drupal sigue necesitando un propietario para comprobar el acceso y gestionar los formularios de edición</strong>.</p>
<p>La entidad base conserva además los campos de enriquecimiento para una pasada posterior de Fandom: <code>fandom_description</code>, <code>fandom_page_id</code>, <code>fandom_url</code> y <code>fandom_source</code>. Es deliberado: <strong>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</strong>. El modelo hace visible esa diferencia, en lugar de fingir que todos los campos importados tienen la misma autoridad.</p>
<p>Por último, cada entidad incluye un campo base <code>path</code>. <strong>Así Pathauto tiene dónde guardar alias legibles</strong>, mientras las rutas canónicas internas permanecen estables.</p>
<p><strong>Los tres tipos de entidad deben compartir también la misma estructura administrativa</strong>: 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:</p>
<div class="highlight"><pre style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-text" data-lang="text">administer rick and migraty api entities
</code></pre></div><p>Un detalle pequeño pero importante: <strong>la visualización es pública, mientras las operaciones administrativas permanecen protegidas</strong>. 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.</p>
<h3 id="localizaciones">Localizaciones</h3>
<p>Campos de <code>rick_and_migraty_location</code>:</p>
<table>
<thead>
<tr>
<th><strong>Campo</strong></th>
<th><strong>Tipo</strong></th>
<th><strong>Notas</strong></th>
</tr>
</thead>
<tbody>
<tr>
<td><code>location_type</code></td>
<td><code>string</code></td>
<td>Asigna la propiedad <code>type</code> de la API sin utilizar el ambiguo nombre de campo <code>type</code>.</td>
</tr>
<tr>
<td><code>dimension</code></td>
<td><code>string</code></td>
<td>Se almacena como texto porque las dimensiones no son un recurso normalizado independiente de la API.</td>
</tr>
<tr>
<td><code>residents</code></td>
<td>referencia a entidad, múltiple</td>
<td>Referencia a <code>rick_and_migraty_character</code>. Se rellena cuando ya existen los personajes.</td>
</tr>
</tbody>
</table>
<p>Las localizaciones se importan pronto porque los personajes las referencian mediante <code>origin</code> y <code>location.url</code>. La relación inversa <code>residents</code> puede esperar a otra pasada. Así evitamos pedir a la migración que referencie personajes todavía inexistentes.</p>
<h3 id="episodios">Episodios</h3>
<p>Campos de <code>rick_and_migraty_episode</code>:</p>
<table>
<thead>
<tr>
<th><strong>Campo</strong></th>
<th><strong>Tipo</strong></th>
<th><strong>Notas</strong></th>
</tr>
</thead>
<tbody>
<tr>
<td><code>air_date_raw</code></td>
<td><code>string</code></td>
<td>Conserva el valor legible original, como <code>December 2, 2013</code>.</td>
</tr>
<tr>
<td><code>air_date</code></td>
<td><code>datetime</code>, fecha</td>
<td>Fecha normalizada opcional para ordenar y filtrar cuando la conversión resulta fiable.</td>
</tr>
<tr>
<td><code>episode_code</code></td>
<td><code>string</code></td>
<td>Guarda códigos como <code>S01E01</code>.</td>
</tr>
<tr>
<td><code>season</code></td>
<td><code>integer</code></td>
<td>Se obtiene de <code>episode_code</code>.</td>
</tr>
<tr>
<td><code>episode_number</code></td>
<td><code>integer</code></td>
<td>Se obtiene de <code>episode_code</code>.</td>
</tr>
<tr>
<td><code>tvmaze_id</code></td>
<td><code>integer</code></td>
<td>Identificador del episodio en TVmaze para rastrear la puntuación.</td>
</tr>
<tr>
<td><code>tvmaze_url</code></td>
<td><code>uri</code></td>
<td>URL canónica del episodio en TVmaze.</td>
</tr>
<tr>
<td><code>rating_average</code></td>
<td><code>decimal</code></td>
<td>Puntuación media importada de TVmaze, con un decimal.</td>
</tr>
<tr>
<td><code>rating_source</code></td>
<td><code>string</code></td>
<td>Etiqueta del proveedor de la puntuación. El valor esperado es <code>TVmaze</code>.</td>
</tr>
<tr>
<td><code>characters</code></td>
<td>referencia a entidad, múltiple</td>
<td>Referencia a <code>rick_and_migraty_character</code>. Se rellena después de crear los personajes.</td>
</tr>
</tbody>
</table>
<p>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.</p>
<p>Conservamos deliberadamente <code>episode_code</code> como texto de origen y lo descomponemos en <code>season</code> y <code>episode_number</code>. Estos enteros derivados resultan más útiles para filtrar, ordenar, emparejar episodios con TVmaze y construir la navegación.</p>
<h3 id="personajes">Personajes</h3>
<p>Campos de <code>rick_and_migraty_character</code>:</p>
<table>
<thead>
<tr>
<th><strong>Campo</strong></th>
<th><strong>Tipo</strong></th>
<th><strong>Notas</strong></th>
</tr>
</thead>
<tbody>
<tr>
<td><code>status</code></td>
<td><code>list_string</code></td>
<td>Valores permitidos: <code>Alive</code>, <code>Dead</code>, <code>unknown</code>.</td>
</tr>
<tr>
<td><code>species</code></td>
<td><code>string</code></td>
<td>Texto libre porque la API ofrece un conjunto abierto de valores.</td>
</tr>
<tr>
<td><code>character_type</code></td>
<td><code>string</code></td>
<td>Asigna la propiedad <code>type</code> sin utilizar el ambiguo nombre de campo <code>type</code>.</td>
</tr>
<tr>
<td><code>gender</code></td>
<td><code>list_string</code></td>
<td>Valores permitidos: <code>Female</code>, <code>Male</code>, <code>Genderless</code>, <code>unknown</code>.</td>
</tr>
<tr>
<td><code>origin</code></td>
<td>referencia a entidad</td>
<td>Referencia a <code>rick_and_migraty_location</code>, resuelta desde <code>origin.url</code> cuando está disponible.</td>
</tr>
<tr>
<td><code>last_location</code></td>
<td>referencia a entidad</td>
<td>Referencia a <code>rick_and_migraty_location</code>, resuelta desde <code>location.url</code> cuando está disponible.</td>
</tr>
<tr>
<td><code>image_url</code></td>
<td><code>uri</code></td>
<td>Conserva la URL remota del avatar para mantener la trazabilidad.</td>
</tr>
<tr>
<td><code>avatar</code></td>
<td><code>image</code></td>
<td>Imagen local descargada del avatar, guardada en <code>public://rick-and-migraty/characters</code>.</td>
</tr>
<tr>
<td><code>episodes</code></td>
<td>referencia a entidad, múltiple</td>
<td>Referencia a <code>rick_and_migraty_episode</code>.</td>
</tr>
</tbody>
</table>
<p>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 <code>origin</code>, <code>last_location</code> y <code>episodes</code>.</p>
<p>La URL remota <code>image_url</code> y el campo local <code>avatar</code> 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.</p>
<p><strong>El módulo mantiene así un grafo que refleja la API de origen</strong>, sin obligarnos a resolver todo en una única pasada:</p>
<ul>
<li><code>rick_and_migraty_character.origin</code> apunta a <code>rick_and_migraty_location</code>.</li>
<li><code>rick_and_migraty_character.last_location</code> apunta a <code>rick_and_migraty_location</code>.</li>
<li><code>rick_and_migraty_character.episodes</code> apunta a <code>rick_and_migraty_episode</code>.</li>
<li><code>rick_and_migraty_episode.characters</code> vuelve a apuntar a <code>rick_and_migraty_character</code>.</li>
<li><code>rick_and_migraty_location.residents</code> vuelve a apuntar a <code>rick_and_migraty_character</code>.</li>
</ul>
<p>Al ser entidades de contenido personalizadas, <strong>participan de forma natural en Views, rutas de entidad, plantillas y permisos</strong>, sin hacerse pasar por nodos editoriales normales. Puedes ver las entidades implementadas <a href="https://gitlab.com/davidjguru/rick-and-migraty/-/tree/main/web/modules/custom/rick_and_migraty_api/src/Entity"><strong>en el repositorio</strong></a>.</p>
<p><strong>El proyecto define además patrones legibles de Pathauto</strong>:</p>
<table>
<thead>
<tr>
<th><strong>Tipo de entidad</strong></th>
<th><strong>Patrón del alias</strong></th>
</tr>
</thead>
<tbody>
<tr>
<td>Personaje</td>
<td><code>/rick-and-migraty/characters/[rick_and_migraty_character:name]</code></td>
</tr>
<tr>
<td>Localización</td>
<td><code>/rick-and-migraty/locations/[rick_and_migraty_location:name]</code></td>
</tr>
<tr>
<td>Episodio</td>
<td><code>/rick-and-migraty/episodes/[rick_and_migraty_episode:name]</code></td>
</tr>
</tbody>
</table>
<p>Estos alias son deliberadamente plurales y fáciles de leer. Por debajo, las rutas canónicas siguen siendo rutas estables de Drupal, como <code>/rick-and-migraty/character/{rick_and_migraty_character}</code>. <strong>Esta separación mantiene predecible la implementación y agradables las URL públicas</strong>.</p>
<h2 id="6-diseñar-la-migración-cocinar">6. Diseñar la migración (cocinar)</h2>
<p>Una vez resuelto —o casi— el modelo de destino, <strong>el siguiente paso no es escribir archivos YAML a toda velocidad</strong>. 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.</p>
<p><strong>También debemos asumir que aparecerán dificultades y retos nuevos por el camino</strong>. Habrá que iterar, experimentar y encontrar soluciones mediante ensayo y error. Paciencia.</p>
<p>Para empezar a cocinar, recordemos que la API de Rick and Morty es una API REST que devuelve JSON. <strong>No es JSON:API en el sentido estricto</strong> 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.</p>
<p>La API utiliza peticiones <code>GET</code> sobre HTTPS y devuelve respuestas JSON. <strong>Los puntos de acceso de los listados están paginados</strong> e incluyen un objeto <code>info</code> con los metadatos de paginación:</p>
<div class="highlight"><pre style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-json" data-lang="json">{
  <span style="color:#000080">&#34;info&#34;</span>: {
    <span style="color:#000080">&#34;count&#34;</span>: <span style="color:#099">826</span>,
    <span style="color:#000080">&#34;pages&#34;</span>: <span style="color:#099">42</span>,
    <span style="color:#000080">&#34;next&#34;</span>: <span style="color:#d14">&#34;https://rickandmortyapi.com/api/character/?page=2&#34;</span>,
    <span style="color:#000080">&#34;prev&#34;</span>: <span style="color:#000;font-weight:bold">null</span>
  },
  <span style="color:#000080">&#34;results&#34;</span>: []
}
</code></pre></div><p>La fuente proporciona <strong>tres colecciones principales</strong>:</p>
<table>
<thead>
<tr>
<th><strong>Recurso</strong></th>
<th><strong>Punto de acceso</strong></th>
<th><strong>Cantidad aproximada</strong></th>
</tr>
</thead>
<tbody>
<tr>
<td>Personajes</td>
<td><code>/api/character</code></td>
<td>826</td>
</tr>
<tr>
<td>Localizaciones</td>
<td><code>/api/location</code></td>
<td>126</td>
</tr>
<tr>
<td>Episodios</td>
<td><code>/api/episode</code></td>
<td>51</td>
</tr>
</tbody>
</table>
<p>Las cifras exactas importan menos que <strong>la estructura de los datos de origen</strong>:</p>
<ul>
<li>Los resultados de los listados están en <code>results</code>.</li>
<li>La paginación está en <code>info.next</code>.</li>
<li>Las relaciones se representan mediante URL.</li>
<li>Cada objeto tiene un identificador numérico estable.</li>
<li>Desde nuestro punto de vista, la fuente es de solo lectura.</li>
</ul>
<p>Un registro de personaje permite ver <strong>el principal problema de la migración</strong>:</p>
<div class="highlight"><pre style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-json" data-lang="json">{
  <span style="color:#000080">&#34;id&#34;</span>: <span style="color:#099">1</span>,
  <span style="color:#000080">&#34;name&#34;</span>: <span style="color:#d14">&#34;Rick Sanchez&#34;</span>,
  <span style="color:#000080">&#34;status&#34;</span>: <span style="color:#d14">&#34;Alive&#34;</span>,
  <span style="color:#000080">&#34;species&#34;</span>: <span style="color:#d14">&#34;Human&#34;</span>,
  <span style="color:#000080">&#34;type&#34;</span>: <span style="color:#d14">&#34;&#34;</span>,
  <span style="color:#000080">&#34;gender&#34;</span>: <span style="color:#d14">&#34;Male&#34;</span>,
  <span style="color:#000080">&#34;origin&#34;</span>: {
    <span style="color:#000080">&#34;name&#34;</span>: <span style="color:#d14">&#34;Earth (C-137)&#34;</span>,
    <span style="color:#000080">&#34;url&#34;</span>: <span style="color:#d14">&#34;https://rickandmortyapi.com/api/location/1&#34;</span>
  },
  <span style="color:#000080">&#34;location&#34;</span>: {
    <span style="color:#000080">&#34;name&#34;</span>: <span style="color:#d14">&#34;Citadel of Ricks&#34;</span>,
    <span style="color:#000080">&#34;url&#34;</span>: <span style="color:#d14">&#34;https://rickandmortyapi.com/api/location/3&#34;</span>
  },
  <span style="color:#000080">&#34;image&#34;</span>: <span style="color:#d14">&#34;https://rickandmortyapi.com/api/character/avatar/1.jpeg&#34;</span>,
  <span style="color:#000080">&#34;episode&#34;</span>: [
    <span style="color:#d14">&#34;https://rickandmortyapi.com/api/episode/1&#34;</span>,
    <span style="color:#d14">&#34;https://rickandmortyapi.com/api/episode/2&#34;</span>
  ],
  <span style="color:#000080">&#34;url&#34;</span>: <span style="color:#d14">&#34;https://rickandmortyapi.com/api/character/1&#34;</span>,
  <span style="color:#000080">&#34;created&#34;</span>: <span style="color:#d14">&#34;2017-11-04T18:48:46.250Z&#34;</span>
}
</code></pre></div><p>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, <strong>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</strong>.</p>
<p>La primera decisión es <strong>el orden de importación</strong>. Si intentamos resolver todo el grafo en una única importación heroica, nos estamos ofreciendo voluntarios para sufrir.</p>
<p><strong>El proceso que implementaremos estará dividido en fases:</strong></p>
<ol>
<li>Importar las localizaciones.</li>
<li>Importar los episodios, ya enriquecidos con las puntuaciones de TVmaze cuando estén disponibles.</li>
<li>Descargar los archivos de los avatares.</li>
<li>Importar los personajes, resolviendo localizaciones, episodios y archivos de avatar.</li>
<li>Actualizar los episodios para resolver su campo <code>characters</code>.</li>
<li>Actualizar las localizaciones para resolver su campo <code>residents</code>.</li>
</ol>
<p>Esta secuencia es <strong>la respuesta práctica al grafo circular</strong>. Algunos campos de relación ya figuran en el YAML de la primera migración, pero <code>migration_lookup</code> utiliza <code>no_stub: true</code>, de modo que las referencias no resueltas no crean entidades provisionales. <strong>Las referencias inversas pendientes se completan después con <code>--update</code></strong>, cuando ya existen las entidades locales de los personajes.</p>
<p>Ahora que la secuencia está clara, por fin <strong>toca implementar los pasos</strong> que acabamos de describir. Sigamos.</p>
<h3 id="primero-las-localizaciones">Primero, las localizaciones</h3>
<p>En esta migración, <strong>una localización es un dato fundamental sobre un personaje: nos dice de dónde procede</strong> y dónde estaba establecido, al menos al principio, porque los personajes importantes suelen moverse entre dimensiones, planetas y demás.</p>
<p><strong>Las localizaciones son el primer contenedor estable</strong>, ya que los personajes las necesitan para <code>origin</code> y <code>last_location</code>.</p>
<ul>
<li>Punto de acceso: <code>/api/location</code>.</li>
</ul>
<p>Cargaremos:</p>
<ul>
<li><code>api_id</code></li>
<li><code>name</code></li>
<li><code>location_type</code></li>
<li><code>dimension</code></li>
<li><code>api_url</code></li>
<li><code>api_created</code></li>
</ul>
<p>Esta es la definición completa de la migración de localizaciones:</p>







<div class="code-snippet">
  <div class="highlight"><div style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4">
<table style="border-spacing:0;padding:0;margin:0;border:0;width:auto;overflow:auto;display:block;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">11
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">12
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">13
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">14
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">15
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">16
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">17
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">18
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">19
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">20
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">21
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">22
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">23
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">24
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">25
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">26
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">27
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">28
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">29
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">30
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">31
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">32
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">33
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">34
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">35
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">36
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">37
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">38
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">39
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">40
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">41
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">42
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">43
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">44
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">45
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">46
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">47
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">48
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">49
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#000;font-weight:bold">id</span>:<span style="color:#bbb"> </span>rick_and_migraty_location<span style="color:#bbb">
</span><span style="color:#bbb"></span><span style="color:#000;font-weight:bold">label</span>:<span style="color:#bbb"> </span><span style="color:#d14">&#39;Rick and Migraty locations&#39;</span><span style="color:#bbb">
</span><span style="color:#bbb"></span><span style="color:#000;font-weight:bold">migration_group</span>:<span style="color:#bbb"> </span>rick_and_migraty<span style="color:#bbb">
</span><span style="color:#bbb">
</span><span style="color:#bbb"></span><span style="color:#000;font-weight:bold">source</span>:<span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#998;font-style:italic"># Custom source plugin that follows the Rick and Morty API info.next pager.</span><span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#000;font-weight:bold">plugin</span>:<span style="color:#bbb"> </span>rick_and_morty_api<span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#000;font-weight:bold">resource</span>:<span style="color:#bbb"> </span>location<span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#998;font-style:italic"># Avoid status/listing commands walking the public API just to count rows.</span><span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#000;font-weight:bold">skip_count</span>:<span style="color:#bbb"> </span><span style="color:#000;font-weight:bold">true</span><span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#998;font-style:italic"># Cache each API page by URL so repeated imports and retries are gentle.</span><span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#000;font-weight:bold">cache</span>:<span style="color:#bbb"> </span><span style="color:#000;font-weight:bold">true</span><span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#000;font-weight:bold">cache_ttl</span>:<span style="color:#bbb"> </span><span style="color:#099">86400</span><span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#998;font-style:italic"># Keep this at 0 by default; raise it locally if the public API throttles.</span><span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#000;font-weight:bold">request_delay_ms</span>:<span style="color:#bbb"> </span><span style="color:#099">0</span><span style="color:#bbb">
</span><span style="color:#bbb">
</span><span style="color:#bbb"></span><span style="color:#000;font-weight:bold">process</span>:<span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#998;font-style:italic"># Store the external ID as a first-class field. The migrate map still owns</span><span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#998;font-style:italic"># the Drupal entity ID mapping.</span><span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#000;font-weight:bold">api_id</span>:<span style="color:#bbb"> </span>id<span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#000;font-weight:bold">name</span>:<span style="color:#bbb"> </span>name<span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#000;font-weight:bold">location_type</span>:<span style="color:#bbb"> </span>type<span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#000;font-weight:bold">dimension</span>:<span style="color:#bbb"> </span>dimension<span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#000;font-weight:bold">residents</span>:<span style="color:#bbb">
</span><span style="color:#bbb">    </span><span style="color:#d14">-
</span><span style="color:#d14">      # Location rows contain an array of resident character URLs.</span><span style="color:#bbb">
</span><span style="color:#bbb">      </span><span style="color:#000;font-weight:bold">plugin</span>:<span style="color:#bbb"> </span>rick_and_migraty_url_id<span style="color:#bbb">
</span><span style="color:#bbb">      </span><span style="color:#000;font-weight:bold">source</span>:<span style="color:#bbb"> </span>residents<span style="color:#bbb">
</span><span style="color:#bbb">    </span><span style="color:#d14">-
</span><span style="color:#d14">      plugin: migration_lookup</span><span style="color:#bbb">
</span><span style="color:#bbb">      </span><span style="color:#000;font-weight:bold">migration</span>:<span style="color:#bbb"> </span>rick_and_migraty_character<span style="color:#bbb">
</span><span style="color:#bbb">      </span><span style="color:#000;font-weight:bold">no_stub</span>:<span style="color:#bbb"> </span><span style="color:#000;font-weight:bold">true</span><span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#000;font-weight:bold">api_url</span>:<span style="color:#bbb"> </span>url<span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#000;font-weight:bold">api_created</span>:<span style="color:#bbb">
</span><span style="color:#bbb">    </span><span style="color:#000;font-weight:bold">plugin</span>:<span style="color:#bbb"> </span>format_date<span style="color:#bbb">
</span><span style="color:#bbb">    </span><span style="color:#000;font-weight:bold">source</span>:<span style="color:#bbb"> </span>created<span style="color:#bbb">
</span><span style="color:#bbb">    </span><span style="color:#000;font-weight:bold">from_format</span>:<span style="color:#bbb"> </span><span style="color:#d14">&#39;Y-m-d\TH:i:s.v\Z&#39;</span><span style="color:#bbb">
</span><span style="color:#bbb">    </span><span style="color:#000;font-weight:bold">to_format</span>:<span style="color:#bbb"> </span><span style="color:#d14">&#39;Y-m-d\TH:i:s&#39;</span><span style="color:#bbb">
</span><span style="color:#bbb">    </span><span style="color:#000;font-weight:bold">from_timezone</span>:<span style="color:#bbb"> </span>UTC<span style="color:#bbb">
</span><span style="color:#bbb">    </span><span style="color:#000;font-weight:bold">to_timezone</span>:<span style="color:#bbb"> </span>UTC<span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#000;font-weight:bold">uid</span>:<span style="color:#bbb">
</span><span style="color:#bbb">    </span><span style="color:#000;font-weight:bold">plugin</span>:<span style="color:#bbb"> </span>default_value<span style="color:#bbb">
</span><span style="color:#bbb">    </span><span style="color:#000;font-weight:bold">default_value</span>:<span style="color:#bbb"> </span><span style="color:#099">1</span><span style="color:#bbb">
</span><span style="color:#bbb">
</span><span style="color:#bbb"></span><span style="color:#000;font-weight:bold">destination</span>:<span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#000;font-weight:bold">plugin</span>:<span style="color:#bbb"> </span><span style="color:#d14">&#39;entity:rick_and_migraty_location&#39;</span><span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#000;font-weight:bold">validate</span>:<span style="color:#bbb"> </span><span style="color:#000;font-weight:bold">true</span><span style="color:#bbb">
</span><span style="color:#bbb">
</span><span style="color:#bbb"></span><span style="color:#000;font-weight:bold">migration_dependencies</span>:<span style="color:#bbb"> </span>{}<span style="color:#bbb">
</span></code></pre></td></tr></table>
</div>
</div>
  
  <p class="code-snippet__source">
    Source: <a href="https://gitlab.com/davidjguru/rick-and-migraty/-/blob/main/web/modules/custom/rick_and_migraty_migrate/migrations/rick_and_migraty_location.yml" rel="noopener noreferrer" target="_blank">Archivo fuente en GitLab</a>
  </p>
  
</div>


<p>El primer bloque es <strong>la definición del origen</strong>. En lugar de enumerar URL concretas, la migración pide al plugin personalizado <code>rick_and_morty_api</code> el recurso <code>location</code>. Ese pequeño ajuste, <code>resource: location</code>, basta para que construya el primer punto de acceso, siga la paginación, almacene las respuestas en caché y exponga cada localización de <code>results</code> como una fila de migración.</p>
<p>Las asignaciones directas de campos son <strong>deliberadamente aburridas, y eso es bueno</strong>. <code>api_id</code> guarda el identificador numérico externo; <code>name</code>, la etiqueta visible; <code>location_type</code> recibe el valor <code>type</code> de la API; y <code>dimension</code> conserva el espacio ficticio al que pertenece la localización. Por su parte, <code>api_url</code> y <code>api_created</code> permiten rastrear el registro hasta la respuesta original.</p>
<p><strong>Hay una transformación que merece atención</strong>: <code>api_created</code> utiliza <code>format_date</code>. La fuente proporciona una marca temporal como <code>2017-11-10T12:42:04.162Z</code>; Drupal la guarda en UTC con el formato más limpio <code>Y-m-d\TH:i:s</code>. No es una transformación espectacular, pero estas pequeñas normalizaciones facilitan consultar los datos después.</p>
<p>La asignación de <code>residents</code> resulta más interesante porque <strong>es un campo de relación</strong>. Las filas contienen un array de URL de personajes, no identificadores locales de Drupal. Por eso el proceso utiliza primero <code>rick_and_migraty_url_id</code> para extraer los identificadores numéricos y después <code>migration_lookup</code> contra <code>rick_and_migraty_character</code>.</p>
<p>En la primera pasada, <strong>esas búsquedas todavía no pueden resolverse porque no existen los personajes</strong>. No es un fallo del YAML, sino parte del diseño. <code>no_stub: true</code> 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 <code>--update</code> para rellenar <code>residents</code>.</p>
<p>Por último, <strong>el destino es explícito</strong>: <code>entity:rick_and_migraty_location</code>. No creamos nodos, términos de taxonomía ni registros genéricos. Rellenamos la entidad personalizada definida por <code>rick_and_migraty_api</code>, con la validación activada para que las filas incorrectas fallen de forma visible durante la importación y no creen silenciosamente contenido incompleto.</p>
<h3 id="después-los-episodios">Después, los episodios</h3>
<p><strong>Los episodios son el segundo contenedor estable del grafo</strong>. 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.</p>
<ul>
<li>Punto de acceso: <code>/api/episode</code>.</li>
</ul>
<p>La migración guarda <strong>tanto los datos originales del episodio como los valores normalizados que necesitaremos después</strong>.</p>
<p>Cargaremos:</p>
<ul>
<li><code>api_id</code></li>
<li><code>name</code></li>
<li><code>air_date_raw</code></li>
<li><code>air_date</code></li>
<li><code>episode_code</code></li>
<li><code>season</code></li>
<li><code>episode_number</code></li>
<li><code>tvmaze_id</code></li>
<li><code>tvmaze_url</code></li>
<li><code>rating_average</code></li>
<li><code>rating_source</code></li>
<li><code>api_url</code></li>
<li><code>api_created</code></li>
</ul>
<p>Esta es la definición completa:</p>







<div class="code-snippet">
  <div class="highlight"><div style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4">
<table style="border-spacing:0;padding:0;margin:0;border:0;width:auto;overflow:auto;display:block;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">11
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">12
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">13
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">14
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">15
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">16
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">17
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">18
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">19
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">20
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">21
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">22
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">23
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">24
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">25
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">26
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">27
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">28
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">29
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">30
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">31
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">32
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">33
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">34
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">35
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">36
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">37
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">38
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">39
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">40
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">41
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">42
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">43
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">44
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">45
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">46
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">47
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">48
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">49
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">50
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">51
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">52
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">53
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">54
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">55
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">56
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">57
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">58
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">59
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">60
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">61
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">62
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">63
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">64
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">65
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">66
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#000;font-weight:bold">id</span>:<span style="color:#bbb"> </span>rick_and_migraty_episode<span style="color:#bbb">
</span><span style="color:#bbb"></span><span style="color:#000;font-weight:bold">label</span>:<span style="color:#bbb"> </span><span style="color:#d14">&#39;Rick and Migraty episodes&#39;</span><span style="color:#bbb">
</span><span style="color:#bbb"></span><span style="color:#000;font-weight:bold">migration_group</span>:<span style="color:#bbb"> </span>rick_and_migraty<span style="color:#bbb">
</span><span style="color:#bbb">
</span><span style="color:#bbb"></span><span style="color:#000;font-weight:bold">source</span>:<span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#998;font-style:italic"># Episodes are imported before character relationships are backfilled.</span><span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#000;font-weight:bold">plugin</span>:<span style="color:#bbb"> </span>rick_and_morty_api<span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#000;font-weight:bold">resource</span>:<span style="color:#bbb"> </span>episode<span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#998;font-style:italic"># Avoid status/listing commands walking the public API just to count rows.</span><span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#000;font-weight:bold">skip_count</span>:<span style="color:#bbb"> </span><span style="color:#000;font-weight:bold">true</span><span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#998;font-style:italic"># Cache each API page by URL so repeated imports and retries are gentle.</span><span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#000;font-weight:bold">cache</span>:<span style="color:#bbb"> </span><span style="color:#000;font-weight:bold">true</span><span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#000;font-weight:bold">cache_ttl</span>:<span style="color:#bbb"> </span><span style="color:#099">86400</span><span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#998;font-style:italic"># Keep this at 0 by default; raise it locally if the public API throttles.</span><span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#000;font-weight:bold">request_delay_ms</span>:<span style="color:#bbb"> </span><span style="color:#099">0</span><span style="color:#bbb">
</span><span style="color:#bbb">
</span><span style="color:#bbb"></span><span style="color:#000;font-weight:bold">process</span>:<span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#000;font-weight:bold">api_id</span>:<span style="color:#bbb"> </span>id<span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#000;font-weight:bold">name</span>:<span style="color:#bbb"> </span>name<span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#000;font-weight:bold">air_date_raw</span>:<span style="color:#bbb"> </span>air_date<span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#000;font-weight:bold">air_date</span>:<span style="color:#bbb">
</span><span style="color:#bbb">    </span><span style="color:#000;font-weight:bold">plugin</span>:<span style="color:#bbb"> </span>format_date<span style="color:#bbb">
</span><span style="color:#bbb">    </span><span style="color:#000;font-weight:bold">source</span>:<span style="color:#bbb"> </span>air_date<span style="color:#bbb">
</span><span style="color:#bbb">    </span><span style="color:#000;font-weight:bold">from_format</span>:<span style="color:#bbb"> </span><span style="color:#d14">&#39;F j, Y&#39;</span><span style="color:#bbb">
</span><span style="color:#bbb">    </span><span style="color:#000;font-weight:bold">to_format</span>:<span style="color:#bbb"> </span><span style="color:#d14">&#39;Y-m-d&#39;</span><span style="color:#bbb">
</span><span style="color:#bbb">    </span><span style="color:#000;font-weight:bold">from_timezone</span>:<span style="color:#bbb"> </span>UTC<span style="color:#bbb">
</span><span style="color:#bbb">    </span><span style="color:#000;font-weight:bold">to_timezone</span>:<span style="color:#bbb"> </span>UTC<span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#000;font-weight:bold">episode_code</span>:<span style="color:#bbb"> </span>episode<span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#000;font-weight:bold">season</span>:<span style="color:#bbb">
</span><span style="color:#bbb">    </span><span style="color:#000;font-weight:bold">plugin</span>:<span style="color:#bbb"> </span>rick_and_migraty_episode_part<span style="color:#bbb">
</span><span style="color:#bbb">    </span><span style="color:#000;font-weight:bold">source</span>:<span style="color:#bbb"> </span>episode<span style="color:#bbb">
</span><span style="color:#bbb">    </span><span style="color:#000;font-weight:bold">part</span>:<span style="color:#bbb"> </span>season<span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#000;font-weight:bold">episode_number</span>:<span style="color:#bbb">
</span><span style="color:#bbb">    </span><span style="color:#000;font-weight:bold">plugin</span>:<span style="color:#bbb"> </span>rick_and_migraty_episode_part<span style="color:#bbb">
</span><span style="color:#bbb">    </span><span style="color:#000;font-weight:bold">source</span>:<span style="color:#bbb"> </span>episode<span style="color:#bbb">
</span><span style="color:#bbb">    </span><span style="color:#000;font-weight:bold">part</span>:<span style="color:#bbb"> </span>episode<span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#000;font-weight:bold">tvmaze_id</span>:<span style="color:#bbb"> </span>tvmaze_id<span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#000;font-weight:bold">tvmaze_url</span>:<span style="color:#bbb"> </span>tvmaze_url<span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#000;font-weight:bold">rating_average</span>:<span style="color:#bbb"> </span>rating_average<span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#000;font-weight:bold">rating_source</span>:<span style="color:#bbb"> </span>rating_source<span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#000;font-weight:bold">characters</span>:<span style="color:#bbb">
</span><span style="color:#bbb">    </span><span style="color:#d14">-
</span><span style="color:#d14">      # Episode rows contain an array of character URLs.</span><span style="color:#bbb">
</span><span style="color:#bbb">      </span><span style="color:#000;font-weight:bold">plugin</span>:<span style="color:#bbb"> </span>rick_and_migraty_url_id<span style="color:#bbb">
</span><span style="color:#bbb">      </span><span style="color:#000;font-weight:bold">source</span>:<span style="color:#bbb"> </span>characters<span style="color:#bbb">
</span><span style="color:#bbb">    </span><span style="color:#d14">-
</span><span style="color:#d14">      plugin: migration_lookup</span><span style="color:#bbb">
</span><span style="color:#bbb">      </span><span style="color:#000;font-weight:bold">migration</span>:<span style="color:#bbb"> </span>rick_and_migraty_character<span style="color:#bbb">
</span><span style="color:#bbb">      </span><span style="color:#000;font-weight:bold">no_stub</span>:<span style="color:#bbb"> </span><span style="color:#000;font-weight:bold">true</span><span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#000;font-weight:bold">api_url</span>:<span style="color:#bbb"> </span>url<span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#000;font-weight:bold">api_created</span>:<span style="color:#bbb">
</span><span style="color:#bbb">    </span><span style="color:#000;font-weight:bold">plugin</span>:<span style="color:#bbb"> </span>format_date<span style="color:#bbb">
</span><span style="color:#bbb">    </span><span style="color:#000;font-weight:bold">source</span>:<span style="color:#bbb"> </span>created<span style="color:#bbb">
</span><span style="color:#bbb">    </span><span style="color:#000;font-weight:bold">from_format</span>:<span style="color:#bbb"> </span><span style="color:#d14">&#39;Y-m-d\TH:i:s.v\Z&#39;</span><span style="color:#bbb">
</span><span style="color:#bbb">    </span><span style="color:#000;font-weight:bold">to_format</span>:<span style="color:#bbb"> </span><span style="color:#d14">&#39;Y-m-d\TH:i:s&#39;</span><span style="color:#bbb">
</span><span style="color:#bbb">    </span><span style="color:#000;font-weight:bold">from_timezone</span>:<span style="color:#bbb"> </span>UTC<span style="color:#bbb">
</span><span style="color:#bbb">    </span><span style="color:#000;font-weight:bold">to_timezone</span>:<span style="color:#bbb"> </span>UTC<span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#000;font-weight:bold">uid</span>:<span style="color:#bbb">
</span><span style="color:#bbb">    </span><span style="color:#000;font-weight:bold">plugin</span>:<span style="color:#bbb"> </span>default_value<span style="color:#bbb">
</span><span style="color:#bbb">    </span><span style="color:#000;font-weight:bold">default_value</span>:<span style="color:#bbb"> </span><span style="color:#099">1</span><span style="color:#bbb">
</span><span style="color:#bbb">
</span><span style="color:#bbb"></span><span style="color:#000;font-weight:bold">destination</span>:<span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#000;font-weight:bold">plugin</span>:<span style="color:#bbb"> </span><span style="color:#d14">&#39;entity:rick_and_migraty_episode&#39;</span><span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#000;font-weight:bold">validate</span>:<span style="color:#bbb"> </span><span style="color:#000;font-weight:bold">true</span><span style="color:#bbb">
</span><span style="color:#bbb">
</span><span style="color:#bbb"></span><span style="color:#000;font-weight:bold">migration_dependencies</span>:<span style="color:#bbb"> </span>{}<span style="color:#bbb">
</span></code></pre></td></tr></table>
</div>
</div>
  
  <p class="code-snippet__source">
    Source: <a href="https://gitlab.com/davidjguru/rick-and-migraty/-/blob/main/web/modules/custom/rick_and_migraty_migrate/migrations/rick_and_migraty_episode.yml" rel="noopener noreferrer" target="_blank">Archivo fuente en GitLab</a>
  </p>
  
</div>


<p>Lo primero que observamos es que <strong>el plugin de origen sigue siendo <code>rick_and_morty_api</code>, pero <code>resource</code> cambia a <code>episode</code></strong>. 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.</p>
<p>La sección de proceso realiza <strong>tres tipos de trabajo</strong>. Algunos campos son copias directas de la fila de origen: <code>api_id</code>, <code>name</code>, <code>episode_code</code> y <code>api_url</code>. Otros se interpretan o normalizan: <code>air_date</code> convierte una fecha legible como <code>December 2, 2013</code> a <code>Y-m-d</code>; <code>season</code> y <code>episode_number</code> utilizan el plugin de proceso personalizado <code>rick_and_migraty_episode_part</code> para descomponer valores como <code>S01E01</code>.</p>
<p><strong>Los campos de TVmaze ya están presentes en la fila de origen</strong> cuando se ejecuta el proceso. Con <code>resource: episode</code>, el plugin puede cargar los episodios de la serie <code>216</code> de TVmaze, construir una tabla de consulta por temporada y número de episodio y añadir <code>tvmaze_id</code>, <code>tvmaze_url</code>, <code>rating_average</code> y <code>rating_source</code> a la fila. Es un límite importante: TVmaze enriquece las filas canónicas, pero no crea episodios.</p>
<p>Por último, <strong><code>characters</code> aparece en el YAML aunque los personajes aún no existan durante la primera pasada</strong>. La pareja <code>rick_and_migraty_url_id</code> + <code>migration_lookup</code> repite <strong>el mismo patrón de relaciones</strong> utilizado en las localizaciones. Con <code>no_stub: true</code>, 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 <code>--update</code>.</p>
<h3 id="los-avatares-antes-que-los-personajes">Los avatares antes que los personajes</h3>
<p><strong>Los avatares se gestionan deliberadamente en una migración de archivos separada</strong>. 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.</p>
<p>La migración lee las filas de <code>/api/character</code>, toma <strong>la URL remota <code>image</code></strong> y descarga el archivo en el sistema público de archivos de Drupal, bajo:</p>
<div class="highlight"><pre style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-text" data-lang="text">public://rick-and-migraty/characters/
</code></pre></div><p>Esta es la migración de los archivos de avatar:</p>







<div class="code-snippet">
  <div class="highlight"><div style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4">
<table style="border-spacing:0;padding:0;margin:0;border:0;width:auto;overflow:auto;display:block;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">11
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">12
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">13
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">14
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">15
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">16
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">17
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">18
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">19
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">20
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">21
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">22
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">23
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">24
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">25
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">26
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">27
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">28
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">29
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">30
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">31
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">32
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">33
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">34
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">35
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">36
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">37
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">38
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">39
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">40
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">41
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">42
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">43
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#000;font-weight:bold">id</span>:<span style="color:#bbb"> </span>rick_and_migraty_character_avatar<span style="color:#bbb">
</span><span style="color:#bbb"></span><span style="color:#000;font-weight:bold">label</span>:<span style="color:#bbb"> </span><span style="color:#d14">&#39;Rick and Migraty character avatars&#39;</span><span style="color:#bbb">
</span><span style="color:#bbb"></span><span style="color:#000;font-weight:bold">migration_group</span>:<span style="color:#bbb"> </span>rick_and_migraty<span style="color:#bbb">
</span><span style="color:#bbb">
</span><span style="color:#bbb"></span><span style="color:#000;font-weight:bold">source</span>:<span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#000;font-weight:bold">plugin</span>:<span style="color:#bbb"> </span>rick_and_morty_api<span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#000;font-weight:bold">resource</span>:<span style="color:#bbb"> </span>character<span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#000;font-weight:bold">skip_count</span>:<span style="color:#bbb"> </span><span style="color:#000;font-weight:bold">true</span><span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#000;font-weight:bold">cache</span>:<span style="color:#bbb"> </span><span style="color:#000;font-weight:bold">true</span><span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#000;font-weight:bold">cache_ttl</span>:<span style="color:#bbb"> </span><span style="color:#099">86400</span><span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#000;font-weight:bold">request_delay_ms</span>:<span style="color:#bbb"> </span><span style="color:#099">0</span><span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#000;font-weight:bold">constants</span>:<span style="color:#bbb">
</span><span style="color:#bbb">    </span><span style="color:#000;font-weight:bold">avatar_directory</span>:<span style="color:#bbb"> </span><span style="color:#d14">&#39;public://rick-and-migraty/characters/&#39;</span><span style="color:#bbb">
</span><span style="color:#bbb">    </span><span style="color:#000;font-weight:bold">avatar_extension</span>:<span style="color:#bbb"> </span><span style="color:#d14">&#39;.jpeg&#39;</span><span style="color:#bbb">
</span><span style="color:#bbb">
</span><span style="color:#bbb"></span><span style="color:#000;font-weight:bold">process</span>:<span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#000;font-weight:bold">filename</span>:<span style="color:#bbb">
</span><span style="color:#bbb">    </span><span style="color:#000;font-weight:bold">plugin</span>:<span style="color:#bbb"> </span>concat<span style="color:#bbb">
</span><span style="color:#bbb">    </span><span style="color:#000;font-weight:bold">source</span>:<span style="color:#bbb">
</span><span style="color:#bbb">      </span>- id<span style="color:#bbb">
</span><span style="color:#bbb">      </span>- constants/avatar_extension<span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#000;font-weight:bold">destination_uri</span>:<span style="color:#bbb">
</span><span style="color:#bbb">    </span><span style="color:#000;font-weight:bold">plugin</span>:<span style="color:#bbb"> </span>concat<span style="color:#bbb">
</span><span style="color:#bbb">    </span><span style="color:#000;font-weight:bold">source</span>:<span style="color:#bbb">
</span><span style="color:#bbb">      </span>- constants/avatar_directory<span style="color:#bbb">
</span><span style="color:#bbb">      </span>- id<span style="color:#bbb">
</span><span style="color:#bbb">      </span>- constants/avatar_extension<span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#000;font-weight:bold">uri</span>:<span style="color:#bbb">
</span><span style="color:#bbb">    </span><span style="color:#000;font-weight:bold">plugin</span>:<span style="color:#bbb"> </span>download<span style="color:#bbb">
</span><span style="color:#bbb">    </span><span style="color:#000;font-weight:bold">source</span>:<span style="color:#bbb">
</span><span style="color:#bbb">      </span>- image<span style="color:#bbb">
</span><span style="color:#bbb">      </span>- <span style="color:#d14">&#39;@destination_uri&#39;</span><span style="color:#bbb">
</span><span style="color:#bbb">    </span><span style="color:#000;font-weight:bold">file_exists</span>:<span style="color:#bbb"> </span>replace<span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#000;font-weight:bold">uid</span>:<span style="color:#bbb">
</span><span style="color:#bbb">    </span><span style="color:#000;font-weight:bold">plugin</span>:<span style="color:#bbb"> </span>default_value<span style="color:#bbb">
</span><span style="color:#bbb">    </span><span style="color:#000;font-weight:bold">default_value</span>:<span style="color:#bbb"> </span><span style="color:#099">1</span><span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#000;font-weight:bold">status</span>:<span style="color:#bbb">
</span><span style="color:#bbb">    </span><span style="color:#000;font-weight:bold">plugin</span>:<span style="color:#bbb"> </span>default_value<span style="color:#bbb">
</span><span style="color:#bbb">    </span><span style="color:#000;font-weight:bold">default_value</span>:<span style="color:#bbb"> </span><span style="color:#099">1</span><span style="color:#bbb">
</span><span style="color:#bbb">
</span><span style="color:#bbb"></span><span style="color:#000;font-weight:bold">destination</span>:<span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#000;font-weight:bold">plugin</span>:<span style="color:#bbb"> </span><span style="color:#d14">&#39;entity:file&#39;</span><span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#000;font-weight:bold">validate</span>:<span style="color:#bbb"> </span><span style="color:#000;font-weight:bold">true</span><span style="color:#bbb">
</span></code></pre></td></tr></table>
</div>
</div>
  
  <p class="code-snippet__source">
    Source: <a href="https://gitlab.com/davidjguru/rick-and-migraty/-/blob/main/web/modules/custom/rick_and_migraty_migrate/migrations/rick_and_migraty_character_avatar.yml" rel="noopener noreferrer" target="_blank">Archivo fuente en GitLab</a>
  </p>
  
</div>


<p>El origen vuelve a ser <code>rick_and_morty_api</code>, ahora con <code>resource: character</code>, porque la URL del avatar está en los datos del personaje. El bloque <code>constants</code> define dos <strong>valores que no proceden de la API</strong>: el directorio de destino y la extensión del archivo. Mantenerlos en <code>constants</code> facilita leer la sección de proceso.</p>
<p>Los campos <code>filename</code> y <code>destination_uri</code> <strong>se construyen con <code>concat</code></strong>. Un personaje con identificador de origen <code>1</code> se convierte en <code>1.jpeg</code>, y la URI final será <code>public://rick-and-migraty/characters/1.jpeg</code>. Ese nombre determinista ayuda a repetir la operación: la misma fila apunta siempre a la misma ruta local.</p>
<p><strong>La descarga se realiza en el proceso del campo <code>uri</code></strong>. El plugin <code>download</code> recibe dos valores: la URL remota y la URI de destino calculada. Con <code>file_exists: replace</code>, las importaciones repetidas pueden actualizar el archivo en vez de fallar porque un intento anterior ya lo creó.</p>
<p>El destino es <strong><code>entity:file</code>, no una de nuestras entidades de contenido de Rick and Migraty</strong>. Esta migración solo crea entidades de archivo reutilizables. Después, la migración de personajes resolverá <code>avatar/target_id</code> mediante <code>migration_lookup</code>.</p>
<h3 id="los-personajes-después-de-las-localizaciones-los-episodios-y-los-avatares">Los personajes después de las localizaciones, los episodios y los avatares</h3>
<p><strong>Los personajes son el centro de la experiencia de navegación</strong>, pero llegan después de las localizaciones, los episodios y los archivos de avatar. El orden no es arbitrario: necesitan localizaciones para <code>origin</code> y <code>last_location</code>, episodios para <code>episodes</code> y una entidad de archivo para <code>avatar</code>.</p>
<p>El punto de acceso es <code>/api/character</code>, y la migración rellena:</p>
<ul>
<li>Campos básicos como <code>api_id</code>, <code>name</code>, <code>status</code>, <code>species</code>, <code>character_type</code> y <code>gender</code>.</li>
<li>Referencias a localizaciones mediante <code>origin</code> y <code>last_location</code>.</li>
<li>La URL remota <code>image_url</code>.</li>
<li>El campo de imagen local <code>avatar</code>.</li>
<li>Referencias a episodios mediante <code>episodes</code>.</li>
<li>Campos de trazabilidad de la API y de propiedad de la entidad.</li>
</ul>
<p>Esta es la migración implementada:</p>







<div class="code-snippet">
  <div class="highlight"><div style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4">
<table style="border-spacing:0;padding:0;margin:0;border:0;width:auto;overflow:auto;display:block;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">11
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">12
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">13
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">14
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">15
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">16
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">17
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">18
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">19
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">20
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">21
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">22
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">23
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">24
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">25
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">26
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">27
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">28
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">29
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">30
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">31
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">32
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">33
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">34
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">35
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">36
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">37
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">38
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">39
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">40
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">41
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">42
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">43
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">44
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">45
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">46
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">47
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">48
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">49
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">50
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">51
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">52
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">53
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">54
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">55
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">56
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">57
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">58
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">59
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">60
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">61
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">62
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">63
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">64
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">65
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">66
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">67
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">68
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">69
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">70
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">71
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">72
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">73
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">74
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">75
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">76
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">77
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">78
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#000;font-weight:bold">id</span>:<span style="color:#bbb"> </span>rick_and_migraty_character<span style="color:#bbb">
</span><span style="color:#bbb"></span><span style="color:#000;font-weight:bold">label</span>:<span style="color:#bbb"> </span><span style="color:#d14">&#39;Rick and Migraty characters&#39;</span><span style="color:#bbb">
</span><span style="color:#bbb"></span><span style="color:#000;font-weight:bold">migration_group</span>:<span style="color:#bbb"> </span>rick_and_migraty<span style="color:#bbb">
</span><span style="color:#bbb">
</span><span style="color:#bbb"></span><span style="color:#000;font-weight:bold">source</span>:<span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#998;font-style:italic"># Characters depend on locations and episodes for entity references.</span><span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#000;font-weight:bold">plugin</span>:<span style="color:#bbb"> </span>rick_and_morty_api<span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#000;font-weight:bold">resource</span>:<span style="color:#bbb"> </span>character<span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#998;font-style:italic"># Avoid status/listing commands walking the public API just to count rows.</span><span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#000;font-weight:bold">skip_count</span>:<span style="color:#bbb"> </span><span style="color:#000;font-weight:bold">true</span><span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#998;font-style:italic"># Cache each API page by URL so repeated imports and retries are gentle.</span><span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#000;font-weight:bold">cache</span>:<span style="color:#bbb"> </span><span style="color:#000;font-weight:bold">true</span><span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#000;font-weight:bold">cache_ttl</span>:<span style="color:#bbb"> </span><span style="color:#099">86400</span><span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#998;font-style:italic"># Keep this at 0 by default; raise it locally if the public API throttles.</span><span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#000;font-weight:bold">request_delay_ms</span>:<span style="color:#bbb"> </span><span style="color:#099">0</span><span style="color:#bbb">
</span><span style="color:#bbb">
</span><span style="color:#bbb"></span><span style="color:#000;font-weight:bold">process</span>:<span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#000;font-weight:bold">api_id</span>:<span style="color:#bbb"> </span>id<span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#000;font-weight:bold">name</span>:<span style="color:#bbb"> </span>name<span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#000;font-weight:bold">status</span>:<span style="color:#bbb"> </span>status<span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#000;font-weight:bold">species</span>:<span style="color:#bbb"> </span>species<span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#000;font-weight:bold">character_type</span>:<span style="color:#bbb"> </span>type<span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#000;font-weight:bold">gender</span>:<span style="color:#bbb"> </span>gender<span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#000;font-weight:bold">origin</span>:<span style="color:#bbb">
</span><span style="color:#bbb">    </span><span style="color:#d14">-
</span><span style="color:#d14">      # The API nests the origin URL under origin.url.</span><span style="color:#bbb">
</span><span style="color:#bbb">      </span><span style="color:#000;font-weight:bold">plugin</span>:<span style="color:#bbb"> </span>rick_and_migraty_url_id<span style="color:#bbb">
</span><span style="color:#bbb">      </span><span style="color:#000;font-weight:bold">source</span>:<span style="color:#bbb"> </span>origin/url<span style="color:#bbb">
</span><span style="color:#bbb">    </span><span style="color:#d14">-
</span><span style="color:#d14">      plugin: migration_lookup</span><span style="color:#bbb">
</span><span style="color:#bbb">      </span><span style="color:#000;font-weight:bold">migration</span>:<span style="color:#bbb"> </span>rick_and_migraty_location<span style="color:#bbb">
</span><span style="color:#bbb">      </span><span style="color:#000;font-weight:bold">no_stub</span>:<span style="color:#bbb"> </span><span style="color:#000;font-weight:bold">true</span><span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#000;font-weight:bold">last_location</span>:<span style="color:#bbb">
</span><span style="color:#bbb">    </span><span style="color:#d14">-
</span><span style="color:#d14">      # The API nests the last known location URL under location.url.</span><span style="color:#bbb">
</span><span style="color:#bbb">      </span><span style="color:#000;font-weight:bold">plugin</span>:<span style="color:#bbb"> </span>rick_and_migraty_url_id<span style="color:#bbb">
</span><span style="color:#bbb">      </span><span style="color:#000;font-weight:bold">source</span>:<span style="color:#bbb"> </span>location/url<span style="color:#bbb">
</span><span style="color:#bbb">    </span><span style="color:#d14">-
</span><span style="color:#d14">      plugin: migration_lookup</span><span style="color:#bbb">
</span><span style="color:#bbb">      </span><span style="color:#000;font-weight:bold">migration</span>:<span style="color:#bbb"> </span>rick_and_migraty_location<span style="color:#bbb">
</span><span style="color:#bbb">      </span><span style="color:#000;font-weight:bold">no_stub</span>:<span style="color:#bbb"> </span><span style="color:#000;font-weight:bold">true</span><span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#000;font-weight:bold">image_url</span>:<span style="color:#bbb"> </span>image<span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#000;font-weight:bold">avatar/target_id</span>:<span style="color:#bbb">
</span><span style="color:#bbb">    </span><span style="color:#000;font-weight:bold">plugin</span>:<span style="color:#bbb"> </span>migration_lookup<span style="color:#bbb">
</span><span style="color:#bbb">    </span><span style="color:#000;font-weight:bold">migration</span>:<span style="color:#bbb"> </span>rick_and_migraty_character_avatar<span style="color:#bbb">
</span><span style="color:#bbb">    </span><span style="color:#000;font-weight:bold">source</span>:<span style="color:#bbb"> </span>id<span style="color:#bbb">
</span><span style="color:#bbb">    </span><span style="color:#000;font-weight:bold">no_stub</span>:<span style="color:#bbb"> </span><span style="color:#000;font-weight:bold">true</span><span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#000;font-weight:bold">avatar/alt</span>:<span style="color:#bbb"> </span>name<span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#000;font-weight:bold">episodes</span>:<span style="color:#bbb">
</span><span style="color:#bbb">    </span><span style="color:#d14">-
</span><span style="color:#d14">      # Character rows contain an array of episode URLs.</span><span style="color:#bbb">
</span><span style="color:#bbb">      </span><span style="color:#000;font-weight:bold">plugin</span>:<span style="color:#bbb"> </span>rick_and_migraty_url_id<span style="color:#bbb">
</span><span style="color:#bbb">      </span><span style="color:#000;font-weight:bold">source</span>:<span style="color:#bbb"> </span>episode<span style="color:#bbb">
</span><span style="color:#bbb">    </span><span style="color:#d14">-
</span><span style="color:#d14">      plugin: migration_lookup</span><span style="color:#bbb">
</span><span style="color:#bbb">      </span><span style="color:#000;font-weight:bold">migration</span>:<span style="color:#bbb"> </span>rick_and_migraty_episode<span style="color:#bbb">
</span><span style="color:#bbb">      </span><span style="color:#000;font-weight:bold">no_stub</span>:<span style="color:#bbb"> </span><span style="color:#000;font-weight:bold">true</span><span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#000;font-weight:bold">api_url</span>:<span style="color:#bbb"> </span>url<span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#000;font-weight:bold">api_created</span>:<span style="color:#bbb">
</span><span style="color:#bbb">    </span><span style="color:#000;font-weight:bold">plugin</span>:<span style="color:#bbb"> </span>format_date<span style="color:#bbb">
</span><span style="color:#bbb">    </span><span style="color:#000;font-weight:bold">source</span>:<span style="color:#bbb"> </span>created<span style="color:#bbb">
</span><span style="color:#bbb">    </span><span style="color:#000;font-weight:bold">from_format</span>:<span style="color:#bbb"> </span><span style="color:#d14">&#39;Y-m-d\TH:i:s.v\Z&#39;</span><span style="color:#bbb">
</span><span style="color:#bbb">    </span><span style="color:#000;font-weight:bold">to_format</span>:<span style="color:#bbb"> </span><span style="color:#d14">&#39;Y-m-d\TH:i:s&#39;</span><span style="color:#bbb">
</span><span style="color:#bbb">    </span><span style="color:#000;font-weight:bold">from_timezone</span>:<span style="color:#bbb"> </span>UTC<span style="color:#bbb">
</span><span style="color:#bbb">    </span><span style="color:#000;font-weight:bold">to_timezone</span>:<span style="color:#bbb"> </span>UTC<span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#000;font-weight:bold">uid</span>:<span style="color:#bbb">
</span><span style="color:#bbb">    </span><span style="color:#000;font-weight:bold">plugin</span>:<span style="color:#bbb"> </span>default_value<span style="color:#bbb">
</span><span style="color:#bbb">    </span><span style="color:#000;font-weight:bold">default_value</span>:<span style="color:#bbb"> </span><span style="color:#099">1</span><span style="color:#bbb">
</span><span style="color:#bbb">
</span><span style="color:#bbb"></span><span style="color:#000;font-weight:bold">destination</span>:<span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#000;font-weight:bold">plugin</span>:<span style="color:#bbb"> </span><span style="color:#d14">&#39;entity:rick_and_migraty_character&#39;</span><span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#000;font-weight:bold">validate</span>:<span style="color:#bbb"> </span><span style="color:#000;font-weight:bold">true</span><span style="color:#bbb">
</span><span style="color:#bbb">
</span><span style="color:#bbb"></span><span style="color:#000;font-weight:bold">migration_dependencies</span>:<span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#000;font-weight:bold">required</span>:<span style="color:#bbb">
</span><span style="color:#bbb">    </span>- rick_and_migraty_character_avatar<span style="color:#bbb">
</span><span style="color:#bbb">    </span>- rick_and_migraty_location<span style="color:#bbb">
</span><span style="color:#bbb">    </span>- rick_and_migraty_episode<span style="color:#bbb">
</span></code></pre></td></tr></table>
</div>
</div>
  
  <p class="code-snippet__source">
    Source: <a href="https://gitlab.com/davidjguru/rick-and-migraty/-/blob/main/web/modules/custom/rick_and_migraty_migrate/migrations/rick_and_migraty_character.yml" rel="noopener noreferrer" target="_blank">Archivo fuente en GitLab</a>
  </p>
  
</div>


<p>El primer grupo de asignaciones es <strong>directo</strong>: <code>api_id</code>, <code>name</code>, <code>status</code>, <code>species</code>, <code>character_type</code> y <code>gender</code> se copian de los datos del personaje. Observa que el campo <code>type</code> de la API pasa a llamarse <code>character_type</code> en Drupal. Así evitamos confundirlo con el concepto habitual de «tipo» en Drupal y conservamos su valor original.</p>
<p>Las asignaciones de <code>origin</code> y <code>last_location</code> muestran <strong>el proceso de resolución de relaciones en su forma más útil</strong>. La API anida las URL bajo <code>origin/url</code> y <code>location/url</code>. El plugin <code>rick_and_migraty_url_id</code> extrae su identificador numérico y <code>migration_lookup</code> lo resuelve mediante la tabla de correspondencias de <code>rick_and_migraty_location</code>.</p>
<p>El avatar es algo distinto porque <strong>apunta a la migración de archivos, no a una entidad de contenido personalizada</strong>. <code>avatar/target_id</code> busca la entidad creada por <code>rick_and_migraty_character_avatar</code> utilizando el mismo identificador de origen del personaje. <code>avatar/alt</code> se obtiene de su nombre para proporcionar un texto alternativo útil.</p>
<p>El campo <code>episodes</code> utiliza <strong>el mismo patrón de extracción del identificador de una URL</strong>, pero sobre un array de URL de episodios. Como estos ya se importaron, las búsquedas pueden resolverse en la primera pasada de personajes.</p>
<p>El bloque final de dependencias <strong>documenta explícitamente el orden de ejecución</strong>: los personajes requieren archivos de avatar, localizaciones y episodios. <strong>Así Migrate dispone de información suficiente para entender por qué esta migración ocupa una posición posterior en el proceso básico.</strong></p>
<h3 id="relaciones-inversas-después-del-grafo-básico">Relaciones inversas después del grafo básico</h3>
<p>Llegados a este punto, <strong>ya existen las relaciones de ida</strong>:</p>
<ul>
<li>Los personajes apuntan a su origen y a su última localización conocida.</li>
<li>Apuntan a los episodios en los que aparecen.</li>
<li>Apuntan a su archivo local de avatar.</li>
</ul>
<p>Pero <strong>quedan dos relaciones inversas útiles por completar</strong>:</p>
<ul>
<li><code>rick_and_migraty_episode.characters</code></li>
<li><code>rick_and_migraty_location.residents</code></li>
</ul>
<p>Estas asignaciones ya existen en los YAML de episodios y localizaciones. <strong>La razón por la que no se resuelven por completo en la primera pasada es temporal</strong>: ambos se importan antes que los personajes. Sus filas ya contienen las URL de los personajes, pero las entidades locales todavía no existen.</p>
<p>Por eso ambas asignaciones utilizan <strong>la misma pareja de plugins con precaución</strong>:</p>
<div class="highlight"><pre style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#d14">-
</span><span style="color:#d14">  plugin: rick_and_migraty_url_id</span><span style="color:#bbb">
</span><span style="color:#bbb"></span><span style="color:#d14">-
</span><span style="color:#d14">  plugin: migration_lookup</span><span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#000;font-weight:bold">migration</span>:<span style="color:#bbb"> </span>rick_and_migraty_character<span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#000;font-weight:bold">no_stub</span>:<span style="color:#bbb"> </span><span style="color:#000;font-weight:bold">true</span><span style="color:#bbb">
</span></code></pre></div><p>El plugin personalizado <strong>extrae los identificadores de personajes de las URL</strong>. <code>migration_lookup</code> consulta la tabla de correspondencias de Migrate para averiguar dónde terminaron esos personajes en Drupal. <code>no_stub: true</code> evita crear personajes provisionales incompletos cuando la búsqueda todavía no puede resolverse.</p>
<p>Una vez ejecutada la migración de personajes, se realizan <strong>pasadas de actualización</strong>:</p>
<div class="highlight"><pre style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">ddev drush migrate:import rick_and_migraty_episode --update
ddev drush migrate:import rick_and_migraty_location --update
</code></pre></div><p>Es entonces cuando <strong>el grafo se vuelve bidireccional en Drupal</strong>, 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.</p>
<h3 id="estrategia-de-origen-no-fijar-la-paginación-a-mano">Estrategia de origen: no fijar la paginación a mano</h3>
<p>Todas las definiciones anteriores comparten un supuesto discreto: <strong>ninguna quiere saber cuántas páginas tiene hoy la API remota</strong>. El YAML debe indicar qué recurso necesita, no mantener manualmente una lista de URL.</p>
<p>En Drupal, esa responsabilidad vive en <strong>un módulo de migración dedicado</strong>:</p>
<div class="highlight"><pre style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-text" data-lang="text">rick_and_migraty_migrate
</code></pre></div><p>Este módulo <strong>contiene el plugin de origen, los YAML, los plugins de proceso y las utilidades Drush del proyecto</strong>. 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.</p>
<p>La API de Rick and Morty devuelve <strong>respuestas paginadas</strong>. Cada respuesta incluye <code>info.next</code> y un array <code>results</code>. Un origen genérico <code>url</code> 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 <code>results</code> como una fila.</p>
<p>Una posibilidad sería utilizar <strong>un origen URL genérico y enumerar explícitamente todas las páginas</strong>:</p>
<div class="highlight"><pre style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#000;font-weight:bold">source</span>:<span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#000;font-weight:bold">plugin</span>:<span style="color:#bbb"> </span>url<span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#000;font-weight:bold">data_fetcher_plugin</span>:<span style="color:#bbb"> </span>http<span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#000;font-weight:bold">data_parser_plugin</span>:<span style="color:#bbb"> </span>json<span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#000;font-weight:bold">urls</span>:<span style="color:#bbb">
</span><span style="color:#bbb">    </span>- <span style="color:#d14">&#39;https://rickandmortyapi.com/api/location?page=1&#39;</span><span style="color:#bbb">
</span><span style="color:#bbb">    </span>- <span style="color:#d14">&#39;https://rickandmortyapi.com/api/location?page=2&#39;</span><span style="color:#bbb">
</span><span style="color:#bbb">    </span>- <span style="color:#d14">&#39;https://rickandmortyapi.com/api/location?page=3&#39;</span><span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#000;font-weight:bold">item_selector</span>:<span style="color:#bbb"> </span>results<span style="color:#bbb">
</span></code></pre></div><p>Puede funcionar en una pequeña demostración, pero <strong>es frágil</strong>. 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.</p>
<p>El proyecto adopta una opción mejor: <strong>un plugin de origen personalizado de Migrate</strong>, llamado <code>rick_and_morty_api</code>.</p>
<p>Cada migración <strong>elige el recurso con un pequeño ajuste</strong>:</p>
<div class="highlight"><pre style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#000;font-weight:bold">source</span>:<span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#000;font-weight:bold">plugin</span>:<span style="color:#bbb"> </span>rick_and_morty_api<span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#000;font-weight:bold">resource</span>:<span style="color:#bbb"> </span>location<span style="color:#bbb">
</span></code></pre></div><p>El mismo plugin puede leer <strong><code>character</code>, <code>location</code> o <code>episode</code></strong>. Internamente construye la primera URL a partir del recurso, obtiene la respuesta, reúne las filas de <code>results</code>, sigue <code>info.next</code> y se detiene cuando no hay más páginas o cuando un límite local dice «suficiente por esta vez».</p>
<p>En la práctica, <strong>el plugin realiza cuatro tareas por los archivos YAML</strong>:</p>
<ol>
<li>Comienza por el primer punto de acceso.</li>
<li>Lee <code>info.next</code>.</li>
<li>Entrega cada elemento de <code>results</code>.</li>
<li>Se detiene cuando <code>next</code> es <code>null</code>.</li>
</ol>
<p>Así las definiciones son más pequeñas y <strong>la paginación se convierte en código reutilizable</strong>. El YAML se centra en el contrato de la migración: recurso de origen, asignaciones de proceso, plugin de destino y dependencias.</p>
<p>El plugin incluye también <strong>controles para el desarrollo local</strong>. Importan porque las migraciones no son solo código: son operaciones que ejecutaremos una y otra vez mientras desarrollamos, depuramos y revertimos:</p>
<table>
<thead>
<tr>
<th><strong>Ajuste</strong></th>
<th><strong>Valor predeterminado</strong></th>
<th><strong>Para qué sirve</strong></th>
</tr>
</thead>
<tbody>
<tr>
<td><code>base_url</code></td>
<td><code>https://rickandmortyapi.com/api</code></td>
<td>Permite apuntar a otra URL base compatible.</td>
</tr>
<tr>
<td><code>skip_count</code></td>
<td><code>true</code></td>
<td>Evita recorrer toda la API solo para calcular los totales.</td>
</tr>
<tr>
<td><code>cache</code></td>
<td><code>true</code></td>
<td>Reutiliza respuestas remotas en ejecuciones locales repetidas.</td>
</tr>
<tr>
<td><code>cache_ttl</code></td>
<td><code>86400</code></td>
<td>Conserva las respuestas durante un día.</td>
</tr>
<tr>
<td><code>request_delay_ms</code></td>
<td><code>0</code></td>
<td>Introduce una pausa entre peticiones sin caché cuando aparecen limitaciones.</td>
</tr>
<tr>
<td><code>max_pages</code></td>
<td>sin definir</td>
<td>Limita las pruebas básicas a unas pocas páginas.</td>
</tr>
<tr>
<td><code>fixture_base_path</code></td>
<td>sin definir</td>
<td>Utiliza archivos JSON locales deterministas en lugar de HTTP remoto.</td>
</tr>
<tr>
<td><code>enrich_tvmaze_ratings</code></td>
<td><code>true</code></td>
<td>Integra las puntuaciones de TVmaze en las filas de episodios.</td>
</tr>
<tr>
<td><code>tvmaze_base_url</code></td>
<td><code>https://api.tvmaze.com</code></td>
<td>Permite redirigir las llamadas a TVmaze en entornos controlados.</td>
</tr>
<tr>
<td><code>tvmaze_show_id</code></td>
<td><code>216</code></td>
<td>Identificador fijo de Rick and Morty en TVmaze.</td>
</tr>
</tbody>
</table>
<p>Para las pruebas locales, <strong>podemos sobrescribir varios ajustes mediante variables de entorno</strong>, sin editar los YAML versionados:</p>
<div class="highlight"><pre style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">ddev <span style="color:#0086b3">exec</span> env <span style="color:#008080">RICK_AND_MIGRATY_API_MAX_PAGES</span><span style="color:#000;font-weight:bold">=</span><span style="color:#099">1</span> <span style="color:#d14">\
</span><span style="color:#d14"></span>  <span style="color:#008080">RICK_AND_MIGRATY_API_REQUEST_DELAY_MS</span><span style="color:#000;font-weight:bold">=</span><span style="color:#099">250</span> <span style="color:#d14">\
</span><span style="color:#d14"></span>  drush migrate:import rick_and_migraty_location
</code></pre></div><p>La implementación permite <strong>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</strong>. 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.</p>
<p>Aquí es también donde <strong>encaja arquitectónicamente el enriquecimiento de TVmaze</strong>. El YAML de episodios asigna <code>tvmaze_id</code>, <code>tvmaze_url</code>, <code>rating_average</code> y <code>rating_source</code>, 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.</p>
<p>Es <strong>una decisión arquitectónica pequeña pero importante</strong>. 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.</p>
<p>Llegados aquí, el diseño ya no consiste simplemente en «importar JSON a Drupal». Es una secuencia:</p>
<ol>
<li>Importar los contenedores canónicos.</li>
<li>Derivar campos normalizados.</li>
<li>Resolver referencias a partir de URL.</li>
<li>Descargar avatares con una migración de archivos ejecutable por lotes.</li>
<li>Enriquecer determinados registros desde fuentes secundarias.</li>
<li>Permitir almacenar en caché, limitar y repetir las peticiones remotas.</li>
<li>Completar las relaciones inversas mediante actualizaciones cuando ya exista el grafo.</li>
</ol>
<p><strong>Esta es la receta</strong>. Ahora podemos examinar los plugins que convierten esas decisiones en código reutilizable. Las definiciones implementadas están <a href="https://gitlab.com/davidjguru/rick-and-migraty/-/tree/main/web/modules/custom/rick_and_migraty_migrate/migrations"><strong>en el repositorio</strong></a>.</p>
<h2 id="7-definir-plugins-de-migración-soluciones">7. Definir plugins de migración (soluciones)</h2>
<p>La sección anterior describe el orden de la migración. Esta trata sobre <strong>las pequeñas piezas de código personalizado que hacen legible ese orden</strong>.</p>
<p>Llega 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. <strong>YAML es excelente para declarar cómo una fila se convierte en una entidad</strong>, pero las reglas específicas del proyecto necesitan nombres, límites y pruebas. En Rick and Migraty, los plugins son precisamente eso: <strong>soluciones con nombre para problemas que se repiten</strong>.</p>
<p>Aquí 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 «<strong>¿YAML o PHP?</strong>», sino «<strong>¿qué responsabilidad corresponde a cada lugar?</strong>».</p>
<p>El proyecto utiliza cuatro plugins personalizados:</p>
<table>
<thead>
<tr>
<th><strong>Plugin</strong></th>
<th><strong>Tipo</strong></th>
<th><strong>Qué problema evita</strong></th>
</tr>
</thead>
<tbody>
<tr>
<td><code>rick_and_morty_api</code></td>
<td>Origen</td>
<td>Fijar a mano las URL paginadas, repetir la lógica de caché y obligar a cada migración a conocer <code>info.next</code>.</td>
</tr>
<tr>
<td><code>rick_and_migraty_fandom</code></td>
<td>Origen</td>
<td>Tratar Fandom como un segundo conjunto canónico en vez de como una capa de enriquecimiento sobre entidades existentes.</td>
</tr>
<tr>
<td><code>rick_and_migraty_url_id</code></td>
<td>Proceso</td>
<td>Repetir el análisis de URL cada vez que una relación de la API debe convertirse en referencia a entidad.</td>
</tr>
<tr>
<td><code>rick_and_migraty_episode_part</code></td>
<td>Proceso</td>
<td>Mantener <code>S03E07</code> eternamente como cadena cuando necesitamos números de temporada y episodio.</td>
</tr>
</tbody>
</table>
<p>Ninguno es especialmente grande. <strong>Esa es parte de la idea</strong>: resolver problemas pequeños y repetidos, y <strong>mantener las definiciones centradas en el modelo de contenido, no en los detalles de bajo nivel</strong>.</p>
<p>Aquí conviene hacer autocrítica: <strong>los plugins personalizados no deberían ser la respuesta por defecto</strong>. El núcleo y los módulos contribuidos ya ofrecen muchas herramientas. Utilizamos plugins estándar como <code>format_date</code>, <code>concat</code>, <code>download</code>, <code>default_value</code> y <code>migration_lookup</code>; además, <code>migrate_plus</code> aporta el origen genérico <code>url</code>, analizadores JSON y plugins de proceso como <code>preg_match</code>.</p>
<p><strong>¿Por qué no usar <code>migrate_plus:url</code> para todo?</strong> En una prueba de concepto pequeña podríamos hacerlo. Pero la fuente canónica de <strong>Rick and Morty</strong> no es solo <strong>«un archivo JSON en una URL»</strong>. Tiene paginación mediante <code>info.next</code>, caché, pausas entre peticiones, soporte para archivos locales, límites de páginas para pruebas y enriquecimiento de episodios con TVmaze. <strong>Si todo ese comportamiento vive en YAML, cada migración acaba describiendo políticas operativas además de asignaciones de datos.</strong></p>
<p>La fuente de <strong>Fandom</strong> 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. <strong>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</strong>.</p>
<p>El compromiso es este: <strong>los plugins de origen son personalizados porque definen contratos con las fuentes</strong>, no porque nos apeteciera escribir PHP. Los pequeños plugins de proceso merecen un examen más crítico. <code>rick_and_migraty_url_id</code> sigue teniendo sentido porque maneja tanto URL individuales como arrays en un paso legible. Sin embargo, <code>rick_and_migraty_episode_part</code> podría sustituirse por <code>preg_match</code> de <code>migrate_plus</code>:</p>
<div class="highlight"><pre style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#000;font-weight:bold">season</span>:<span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#000;font-weight:bold">plugin</span>:<span style="color:#bbb"> </span>preg_match<span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#000;font-weight:bold">source</span>:<span style="color:#bbb"> </span>episode<span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#000;font-weight:bold">pattern</span>:<span style="color:#bbb"> </span><span style="color:#d14">&#39;/^S(\d+)E(\d+)$/&#39;</span><span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#000;font-weight:bold">group_index</span>:<span style="color:#bbb"> </span><span style="color:#099">1</span><span style="color:#bbb">
</span><span style="color:#bbb">
</span><span style="color:#bbb"></span><span style="color:#000;font-weight:bold">episode_number</span>:<span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#000;font-weight:bold">plugin</span>:<span style="color:#bbb"> </span>preg_match<span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#000;font-weight:bold">source</span>:<span style="color:#bbb"> </span>episode<span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#000;font-weight:bold">pattern</span>:<span style="color:#bbb"> </span><span style="color:#d14">&#39;/^S(\d+)E(\d+)$/&#39;</span><span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#000;font-weight:bold">group_index</span>:<span style="color:#bbb"> </span><span style="color:#099">2</span><span style="color:#bbb">
</span></code></pre></div><p>Esa 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: <strong>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</strong>.</p>
<h3 id="el-plugin-de-la-fuente-canónica">El plugin de la fuente canónica</h3>
<p><code>rick_and_morty_api</code> se ocupa de <strong>la fuente principal de datos</strong>. Recibe un nombre de recurso, comienza en su punto de acceso, sigue la paginación mediante <code>info.next</code> y entrega cada elemento de <code>results</code> como una fila de Migrate.</p>
<p>Por tanto, el YAML puede decir:</p>
<div class="highlight"><pre style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#000;font-weight:bold">source</span>:<span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#000;font-weight:bold">plugin</span>:<span style="color:#bbb"> </span>rick_and_morty_api<span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#000;font-weight:bold">resource</span>:<span style="color:#bbb"> </span>episode<span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#000;font-weight:bold">skip_count</span>:<span style="color:#bbb"> </span><span style="color:#000;font-weight:bold">true</span><span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#000;font-weight:bold">cache</span>:<span style="color:#bbb"> </span><span style="color:#000;font-weight:bold">true</span><span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#000;font-weight:bold">cache_ttl</span>:<span style="color:#bbb"> </span><span style="color:#099">86400</span><span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#000;font-weight:bold">request_delay_ms</span>:<span style="color:#bbb"> </span><span style="color:#099">0</span><span style="color:#bbb">
</span></code></pre></div><p>Es mucho más fácil de mantener que enumerar todas las páginas a mano. Y, sobre todo, ofrece <strong>un único lugar para gestionar las cuestiones operativas</strong>:</p>
<table>
<thead>
<tr>
<th><strong>Ajuste</strong></th>
<th><strong>Para qué sirve</strong></th>
</tr>
</thead>
<tbody>
<tr>
<td><code>skip_count</code></td>
<td>Evita recorrer toda la API pública solo porque <code>migrate:status</code> quiere calcular totales.</td>
</tr>
<tr>
<td><code>cache</code> y <code>cache_ttl</code></td>
<td>Hacen las importaciones locales repetidas más rápidas y menos exigentes con la fuente.</td>
</tr>
<tr>
<td><code>request_delay_ms</code></td>
<td>Permite ralentizar las ejecuciones cuando la API empieza a limitar peticiones.</td>
</tr>
<tr>
<td><code>max_pages</code></td>
<td>Permite realizar pruebas básicas sin importar todos los datos.</td>
</tr>
<tr>
<td><code>fixture_base_path</code></td>
<td>Permite leer instantáneas JSON locales en vez de hacer peticiones HTTP.</td>
</tr>
<tr>
<td><code>tvmaze_base_url</code></td>
<td>Permite redirigir las llamadas a TVmaze en entornos controlados.</td>
</tr>
<tr>
<td><code>tvmaze_show_id</code></td>
<td>Vincula el enriquecimiento a una serie de TVmaze fija y explícita.</td>
</tr>
</tbody>
</table>
<p>Es uno de esos detalles modestos que cambian el día a día. <strong>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</strong>.</p>
<p>El plugin completo está en <a href="https://gitlab.com/davidjguru/rick-and-migraty/-/blob/main/web/modules/custom/rick_and_migraty_migrate/src/Plugin/migrate/source/RickAndMortyApi.php"><code>RickAndMortyApi.php</code></a>. Para este artículo importa menos cada línea de la clase que el contrato que ofrece al YAML: <strong>elige un recurso, recibe filas y mantén centralizada la relación con el sistema remoto</strong>.</p>
<h3 id="el-problema-del-lenguaje-de-las-url">El problema del lenguaje de las URL</h3>
<p>La API de Rick and Morty expresa <strong>las relaciones como URL</strong>:</p>
<div class="highlight"><pre style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-text" data-lang="text">https://rickandmortyapi.com/api/location/20
https://rickandmortyapi.com/api/episode/1
https://rickandmortyapi.com/api/character/42
</code></pre></div><p>Las referencias a entidades de Drupal no necesitan esas URL. <strong>Necesitan identificadores locales de entidades de destino</strong>. El puente es el plugin de proceso <code>rick_and_migraty_url_id</code>.</p>
<p>Esta es su implementación:</p>







<div class="code-snippet">
  <div class="highlight"><div style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4">
<table style="border-spacing:0;padding:0;margin:0;border:0;width:auto;overflow:auto;display:block;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">11
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">12
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">13
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">14
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">15
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">16
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">17
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">18
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">19
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">20
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">21
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">22
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">23
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">24
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">25
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">26
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">27
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">28
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">29
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">30
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">31
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">32
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">33
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">34
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">35
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">36
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">37
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">38
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">39
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">40
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">41
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">42
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">43
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">44
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">45
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">46
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">47
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">48
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">49
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">50
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">51
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">52
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">53
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">54
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">55
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">56
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">57
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">58
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">59
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">60
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">61
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">62
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">63
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">64
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">65
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">66
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">67
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">68
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">69
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">70
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">71
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">72
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">73
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">74
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">75
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">76
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">77
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">78
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">79
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-php" data-lang="php"><span style="color:#999;font-weight:bold;font-style:italic">&lt;?php</span>

<span style="color:#000;font-weight:bold">declare</span>(strict_types<span style="color:#000;font-weight:bold">=</span><span style="color:#099">1</span>);

<span style="color:#000;font-weight:bold">namespace</span> Drupal\rick_and_migraty_migrate\Plugin\migrate\process;

<span style="color:#000;font-weight:bold">use</span> Drupal\migrate\Attribute\MigrateProcess;
<span style="color:#000;font-weight:bold">use</span> Drupal\migrate\MigrateExecutableInterface;
<span style="color:#000;font-weight:bold">use</span> Drupal\migrate\ProcessPluginBase;
<span style="color:#000;font-weight:bold">use</span> Drupal\migrate\Row;

<span style="color:#d14">/**
</span><span style="color:#d14"> * Extracts numeric Rick and Morty API IDs from resource URLs.
</span><span style="color:#d14"> *
</span><span style="color:#d14"> * The API represents many relationships as URLs, for example
</span><span style="color:#d14"> * `https://rickandmortyapi.com/api/location/20`. Migrate lookup plugins need
</span><span style="color:#d14"> * the stable source ID (`20`), not the full URL, so this plugin converts either
</span><span style="color:#d14"> * a single URL or an array of URLs into integer IDs.
</span><span style="color:#d14"> *
</span><span style="color:#d14"> * Example:
</span><span style="color:#d14"> *
</span><span style="color:#d14"> * @code
</span><span style="color:#d14"> * process:
</span><span style="color:#d14"> *   origin:
</span><span style="color:#d14"> *     plugin: migration_lookup
</span><span style="color:#d14"> *     migration: rick_and_migraty_location
</span><span style="color:#d14"> *     source:
</span><span style="color:#d14"> *       plugin: rick_and_migraty_url_id
</span><span style="color:#d14"> *       source: origin/url
</span><span style="color:#d14"> * @endcode
</span><span style="color:#d14"> */</span>
<span style="color:#998;font-style:italic">#[MigrateProcess(&#39;rick_and_migraty_url_id&#39;)]
</span><span style="color:#998;font-style:italic"></span><span style="color:#000;font-weight:bold">final</span> <span style="color:#000;font-weight:bold">class</span> <span style="color:#458;font-weight:bold">RickAndMigratyUrlId</span> <span style="color:#000;font-weight:bold">extends</span> ProcessPluginBase {

  <span style="color:#d14">/**
</span><span style="color:#d14">   * {@inheritdoc}
</span><span style="color:#d14">   */</span>
  <span style="color:#000;font-weight:bold">public</span> <span style="color:#000;font-weight:bold">function</span> <span style="color:#900;font-weight:bold">transform</span>(<span style="color:#008080">$value</span>, MigrateExecutableInterface <span style="color:#008080">$migrate_executable</span>, Row <span style="color:#008080">$row</span>, <span style="color:#008080">$destination_property</span>)<span style="color:#000;font-weight:bold">:</span> mixed {
    <span style="color:#000;font-weight:bold">if</span> (is_array(<span style="color:#008080">$value</span>)) {
      <span style="color:#998;font-style:italic">// Relationship arrays such as episode.characters should stay arrays, but
</span><span style="color:#998;font-style:italic"></span>      <span style="color:#998;font-style:italic">// empty or malformed URLs are filtered out before lookup.
</span><span style="color:#998;font-style:italic"></span>      <span style="color:#000;font-weight:bold">return</span> array_values(array_filter(array_map(
        <span style="color:#000;font-weight:bold">static</span> fn (mixed <span style="color:#008080">$item</span>)<span style="color:#000;font-weight:bold">:</span> <span style="color:#000;font-weight:bold">?</span>int <span style="color:#000;font-weight:bold">=&gt;</span> self<span style="color:#000;font-weight:bold">::</span><span style="color:#008080">extractId</span>(<span style="color:#008080">$item</span>),
        <span style="color:#008080">$value</span>,
      )));
    }

    <span style="color:#000;font-weight:bold">return</span> self<span style="color:#000;font-weight:bold">::</span><span style="color:#008080">extractId</span>(<span style="color:#008080">$value</span>);
  }

  <span style="color:#d14">/**
</span><span style="color:#d14">   * Extracts the trailing numeric segment from an API URL.
</span><span style="color:#d14">   *
</span><span style="color:#d14">   * @param mixed $value
</span><span style="color:#d14">   *   A URL-like scalar value from the source row.
</span><span style="color:#d14">   *
</span><span style="color:#d14">   * @return int|null
</span><span style="color:#d14">   *   The numeric API ID, or NULL when the value cannot identify a resource.
</span><span style="color:#d14">   */</span>
  <span style="color:#000;font-weight:bold">private</span> <span style="color:#000;font-weight:bold">static</span> <span style="color:#000;font-weight:bold">function</span> <span style="color:#900;font-weight:bold">extractId</span>(mixed <span style="color:#008080">$value</span>)<span style="color:#000;font-weight:bold">:</span> <span style="color:#000;font-weight:bold">?</span>int {
    <span style="color:#000;font-weight:bold">if</span> (<span style="color:#000;font-weight:bold">!</span>is_scalar(<span style="color:#008080">$value</span>)) {
      <span style="color:#000;font-weight:bold">return</span> <span style="color:#000;font-weight:bold">NULL</span>;
    }

    <span style="color:#008080">$value</span> <span style="color:#000;font-weight:bold">=</span> trim((string) <span style="color:#008080">$value</span>);
    <span style="color:#000;font-weight:bold">if</span> (<span style="color:#008080">$value</span> <span style="color:#000;font-weight:bold">===</span> <span style="color:#d14">&#39;&#39;</span>) {
      <span style="color:#000;font-weight:bold">return</span> <span style="color:#000;font-weight:bold">NULL</span>;
    }

    <span style="color:#998;font-style:italic">// The API uses resource URLs ending in the numeric ID. Matching only the
</span><span style="color:#998;font-style:italic"></span>    <span style="color:#998;font-style:italic">// trailing segment keeps the plugin independent from the configured host.
</span><span style="color:#998;font-style:italic"></span>    <span style="color:#000;font-weight:bold">if</span> (preg_match(<span style="color:#d14">&#39;/\/(\d+)$/&#39;</span>, <span style="color:#008080">$value</span>, <span style="color:#008080">$matches</span>) <span style="color:#000;font-weight:bold">!==</span> <span style="color:#099">1</span>) {
      <span style="color:#000;font-weight:bold">return</span> <span style="color:#000;font-weight:bold">NULL</span>;
    }

    <span style="color:#000;font-weight:bold">return</span> (int) <span style="color:#008080">$matches</span>[<span style="color:#099">1</span>];
  }

}
</code></pre></td></tr></table>
</div>
</div>
  
  <p class="code-snippet__source">
    Source: <a href="https://gitlab.com/davidjguru/rick-and-migraty/-/blob/main/web/modules/custom/rick_and_migraty_migrate/src/Plugin/migrate/process/RickAndMigratyUrlId.php" rel="noopener noreferrer" target="_blank">Archivo fuente en GitLab</a>
  </p>
  
</div>


<p>Hace una sola cosa: <strong>extraer el segmento numérico final de una URL de la API de Rick and Morty</strong>. 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.</p>
<p>Para una relación individual, el proceso es fácil de leer:</p>
<div class="highlight"><pre style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#000;font-weight:bold">origin</span>:<span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#d14">-
</span><span style="color:#d14">    plugin: rick_and_migraty_url_id</span><span style="color:#bbb">
</span><span style="color:#bbb">    </span><span style="color:#000;font-weight:bold">source</span>:<span style="color:#bbb"> </span>origin/url<span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#d14">-
</span><span style="color:#d14">    plugin: migration_lookup</span><span style="color:#bbb">
</span><span style="color:#bbb">    </span><span style="color:#000;font-weight:bold">migration</span>:<span style="color:#bbb"> </span>rick_and_migraty_location<span style="color:#bbb">
</span><span style="color:#bbb">    </span><span style="color:#000;font-weight:bold">no_stub</span>:<span style="color:#bbb"> </span><span style="color:#000;font-weight:bold">true</span><span style="color:#bbb">
</span></code></pre></div><p>El primer plugin extrae el identificador de origen. El segundo pregunta a <strong>la tabla de correspondencias de la API Migrate</strong> qué entidad local se creó a partir de ese identificador. El mismo patrón funciona con arrays como <code>episode.characters</code>, <code>location.residents</code> y <code>character.episode</code>.</p>
<p>Es un buen ejemplo de <strong>un plugin de proceso que hace lo justo</strong>. No sabe de personajes, localizaciones, episodios ni campos. Solo traduce el lenguaje de relaciones de la API a identificadores que Drupal puede buscar.</p>
<h3 id="convertir-los-códigos-de-episodio-en-estructura">Convertir los códigos de episodio en estructura</h3>
<p>Los códigos de episodio requieren otra <strong>transformación pequeña pero importante</strong>. La API proporciona valores como:</p>
<div class="highlight"><pre style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-text" data-lang="text">S03E07
</code></pre></div><p>Son útiles para las personas, pero <strong>insuficientes para ordenar, filtrar o cruzar los datos con TVmaze</strong>. El modelo guarda tanto el código original como sus partes estructuradas:</p>
<div class="highlight"><pre style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#000;font-weight:bold">season</span>:<span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#000;font-weight:bold">plugin</span>:<span style="color:#bbb"> </span>rick_and_migraty_episode_part<span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#000;font-weight:bold">source</span>:<span style="color:#bbb"> </span>episode<span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#000;font-weight:bold">part</span>:<span style="color:#bbb"> </span>season<span style="color:#bbb">
</span><span style="color:#bbb"></span><span style="color:#000;font-weight:bold">episode_number</span>:<span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#000;font-weight:bold">plugin</span>:<span style="color:#bbb"> </span>rick_and_migraty_episode_part<span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#000;font-weight:bold">source</span>:<span style="color:#bbb"> </span>episode<span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#000;font-weight:bold">part</span>:<span style="color:#bbb"> </span>episode<span style="color:#bbb">
</span></code></pre></div><p>El plugin implementado es deliberadamente estricto:</p>







<div class="code-snippet">
  <div class="highlight"><div style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4">
<table style="border-spacing:0;padding:0;margin:0;border:0;width:auto;overflow:auto;display:block;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">11
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">12
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">13
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">14
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">15
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">16
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">17
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">18
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">19
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">20
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">21
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">22
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">23
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">24
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">25
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">26
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">27
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">28
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">29
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">30
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">31
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">32
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">33
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">34
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">35
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">36
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">37
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">38
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">39
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">40
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">41
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">42
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">43
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">44
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-php" data-lang="php"><span style="color:#999;font-weight:bold;font-style:italic">&lt;?php</span>

<span style="color:#000;font-weight:bold">declare</span>(strict_types<span style="color:#000;font-weight:bold">=</span><span style="color:#099">1</span>);

<span style="color:#000;font-weight:bold">namespace</span> Drupal\rick_and_migraty_migrate\Plugin\migrate\process;

<span style="color:#000;font-weight:bold">use</span> Drupal\migrate\Attribute\MigrateProcess;
<span style="color:#000;font-weight:bold">use</span> Drupal\migrate\MigrateExecutableInterface;
<span style="color:#000;font-weight:bold">use</span> Drupal\migrate\MigrateException;
<span style="color:#000;font-weight:bold">use</span> Drupal\migrate\ProcessPluginBase;
<span style="color:#000;font-weight:bold">use</span> Drupal\migrate\Row;

<span style="color:#d14">/**
</span><span style="color:#d14"> * Extracts season or episode numbers from an API episode code.
</span><span style="color:#d14"> *
</span><span style="color:#d14"> * The source API stores episode position as a compact string like `S03E07`.
</span><span style="color:#d14"> * Drupal Views and filters are easier to build when season and episode number
</span><span style="color:#d14"> * are available as separate integer fields.
</span><span style="color:#d14"> *
</span><span style="color:#d14"> * Configuration keys:
</span><span style="color:#d14"> * - part: Required. Either `season` or `episode`.
</span><span style="color:#d14"> */</span>
<span style="color:#998;font-style:italic">#[MigrateProcess(&#39;rick_and_migraty_episode_part&#39;)]
</span><span style="color:#998;font-style:italic"></span><span style="color:#000;font-weight:bold">final</span> <span style="color:#000;font-weight:bold">class</span> <span style="color:#458;font-weight:bold">RickAndMigratyEpisodePart</span> <span style="color:#000;font-weight:bold">extends</span> ProcessPluginBase {

  <span style="color:#d14">/**
</span><span style="color:#d14">   * {@inheritdoc}
</span><span style="color:#d14">   */</span>
  <span style="color:#000;font-weight:bold">public</span> <span style="color:#000;font-weight:bold">function</span> <span style="color:#900;font-weight:bold">transform</span>(<span style="color:#008080">$value</span>, MigrateExecutableInterface <span style="color:#008080">$migrate_executable</span>, Row <span style="color:#008080">$row</span>, <span style="color:#008080">$destination_property</span>)<span style="color:#000;font-weight:bold">:</span> <span style="color:#000;font-weight:bold">?</span>int {
    <span style="color:#008080">$part</span> <span style="color:#000;font-weight:bold">=</span> <span style="color:#008080">$this</span><span style="color:#000;font-weight:bold">-&gt;</span><span style="color:#008080">configuration</span>[<span style="color:#d14">&#39;part&#39;</span>] <span style="color:#000;font-weight:bold">??</span> <span style="color:#000;font-weight:bold">NULL</span>;
    <span style="color:#000;font-weight:bold">if</span> (<span style="color:#000;font-weight:bold">!</span>in_array(<span style="color:#008080">$part</span>, [<span style="color:#d14">&#39;season&#39;</span>, <span style="color:#d14">&#39;episode&#39;</span>], <span style="color:#000;font-weight:bold">TRUE</span>)) {
      <span style="color:#000;font-weight:bold">throw</span> <span style="color:#000;font-weight:bold">new</span> MigrateException(<span style="color:#d14">&#39;The rick_and_migraty_episode_part plugin requires part: season or part: episode.&#39;</span>);
    }

    <span style="color:#000;font-weight:bold">if</span> (<span style="color:#000;font-weight:bold">!</span>is_scalar(<span style="color:#008080">$value</span>) <span style="color:#000;font-weight:bold">||</span> preg_match(<span style="color:#d14">&#39;/^S(\d+)E(\d+)$/&#39;</span>, (string) <span style="color:#008080">$value</span>, <span style="color:#008080">$matches</span>) <span style="color:#000;font-weight:bold">!==</span> <span style="color:#099">1</span>) {
      <span style="color:#000;font-weight:bold">return</span> <span style="color:#000;font-weight:bold">NULL</span>;
    }

    <span style="color:#998;font-style:italic">// Match groups are stable because the expression is anchored to the whole
</span><span style="color:#998;font-style:italic"></span>    <span style="color:#998;font-style:italic">// API code: group 1 is season, group 2 is episode number.
</span><span style="color:#998;font-style:italic"></span>    <span style="color:#000;font-weight:bold">return</span> (int) (<span style="color:#008080">$part</span> <span style="color:#000;font-weight:bold">===</span> <span style="color:#d14">&#39;season&#39;</span> <span style="color:#000;font-weight:bold">?</span> <span style="color:#008080">$matches</span>[<span style="color:#099">1</span>] <span style="color:#000;font-weight:bold">:</span> <span style="color:#008080">$matches</span>[<span style="color:#099">2</span>]);
  }

}
</code></pre></td></tr></table>
</div>
</div>
  
  <p class="code-snippet__source">
    Source: <a href="https://gitlab.com/davidjguru/rick-and-migraty/-/blob/main/web/modules/custom/rick_and_migraty_migrate/src/Plugin/migrate/process/RickAndMigratyEpisodePart.php" rel="noopener noreferrer" target="_blank">Archivo fuente en GitLab</a>
  </p>
  
</div>


<p>Exige <code>part: season</code> o <code>part: episode</code> y acepta valores con la forma <code>S03E07</code>. Si el valor no encaja, devuelve <code>NULL</code> en vez de inventarse un número. Importa porque <strong>los valores incorrectos de origen no deberían convertirse silenciosamente en una estructura falsa</strong>.</p>
<p>El resultado es sencillo:</p>
<div class="highlight"><pre style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-text" data-lang="text">episode_code = S03E07
season = 3
episode_number = 7
</code></pre></div><p>Este tipo de transformación <strong>facilita el resto del proyecto</strong>. 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.</p>
<p>Puedes consultar los plugins personalizados de origen y proceso <a href="https://gitlab.com/davidjguru/rick-and-migraty/-/tree/main/web/modules/custom/rick_and_migraty_migrate/src/Plugin/migrate"><strong>en el repositorio</strong></a>.</p>
<h2 id="8-más-información-de-las-fuentes-enriquecimiento">8. Más información de las fuentes (enriquecimiento)</h2>
<p>El proyecto utiliza tres fuentes externas, pero <strong>no todas tienen la misma autoridad</strong>.</p>
<p><strong>La API de Rick and Morty es la fuente canónica del conjunto básico</strong>: 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.</p>
<p>Esa distinción mantiene la migración bajo control. Nos ofrece una regla que podemos repetir durante toda la implementación: <strong>las fuentes secundarias enriquecen registros existentes; no crean el conjunto canónico</strong>.</p>
<h3 id="tvmaze-enriquece-las-filas-de-episodios">TVmaze enriquece las filas de episodios</h3>
<p>La <strong>API de Rick and Morty</strong> proporciona <strong>la identidad del episodio, su código, la fecha de emisión y la lista de personajes que aparecen</strong>. No ofrece puntuaciones. TVmaze cubre esa carencia.</p>
<p>La decisión importante es <strong>evitar las búsquedas aproximadas durante las importaciones normales</strong>. La serie ya está seleccionada y su identificador de TVmaze es explícito:</p>
<table>
<thead>
<tr>
<th><strong>Campo</strong></th>
<th><strong>Valor</strong></th>
</tr>
</thead>
<tbody>
<tr>
<td>Identificador de la serie en TVmaze</td>
<td><code>216</code></td>
</tr>
<tr>
<td>Nombre de la serie</td>
<td><code>Rick and Morty</code></td>
</tr>
<tr>
<td>URL de la serie</td>
<td><code>https://www.tvmaze.com/shows/216/rick-and-morty</code></td>
</tr>
</tbody>
</table>
<p>El plugin puede cargar:</p>
<div class="highlight"><pre style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-text" data-lang="text">https://api.tvmaze.com/shows/216/episodes
</code></pre></div><p>Y <strong>construir una tabla de consulta por temporada y número de episodio</strong>. Así, cada fila de Rick and Morty llega al proceso enriquecida con:</p>
<div class="highlight"><pre style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#000;font-weight:bold">process</span>:<span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#000;font-weight:bold">tvmaze_id</span>:<span style="color:#bbb"> </span>tvmaze_id<span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#000;font-weight:bold">tvmaze_url</span>:<span style="color:#bbb"> </span>tvmaze_url<span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#000;font-weight:bold">rating_average</span>:<span style="color:#bbb"> </span>rating_average<span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#000;font-weight:bold">rating_source</span>:<span style="color:#bbb"> </span>rating_source<span style="color:#bbb">
</span></code></pre></div><p>Fíjate en la clave de unión: <strong>temporada y número de episodio, no el título</strong>. 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.</p>
<p>La asociación queda así:</p>
<div class="highlight"><pre style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-text" data-lang="text">Rick and Morty API: S01E01
season: 1
episode_number: 1
TVmaze show: 216
TVmaze episode: season 1, number 1
</code></pre></div><p>Obtenemos puntuaciones mientras <strong>la API de Rick and Morty sigue siendo responsable de la identidad del episodio</strong>.</p>
<h3 id="fandom-enriquece-entidades-existentes">Fandom enriquece entidades existentes</h3>
<p>Fandom es otra clase de fuente. <strong>Es rica, útil y editorial</strong>, 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 <code>Pilot</code> no es precisamente una expresión única en Internet.</p>
<p>Por eso <strong>las migraciones de Fandom se ejecutan después de crear las entidades básicas</strong>:</p>
<div class="highlight"><pre style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-text" data-lang="text">rick_and_migraty_character_fandom
rick_and_migraty_location_fandom
rick_and_migraty_episode_fandom
</code></pre></div><p>Actualizan entidades existentes de Drupal. <strong>No crean registros canónicos nuevos</strong>.</p>
<p>El plugin de origen <code>rick_and_migraty_fandom</code> <strong>parte de las entidades de Drupal</strong>, 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.</p>
<p>La migración de enriquecimiento de personajes es un ejemplo compacto:</p>







<div class="code-snippet">
  <div class="highlight"><div style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4">
<table style="border-spacing:0;padding:0;margin:0;border:0;width:auto;overflow:auto;display:block;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">11
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">12
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">13
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">14
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">15
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">16
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">17
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">18
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">19
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">20
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">21
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">22
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">23
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">24
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">25
</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">26
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#000;font-weight:bold">id</span>:<span style="color:#bbb"> </span>rick_and_migraty_character_fandom<span style="color:#bbb">
</span><span style="color:#bbb"></span><span style="color:#000;font-weight:bold">label</span>:<span style="color:#bbb"> </span><span style="color:#d14">&#39;Rick and Migraty character Fandom enrichment&#39;</span><span style="color:#bbb">
</span><span style="color:#bbb"></span><span style="color:#000;font-weight:bold">migration_group</span>:<span style="color:#bbb"> </span>rick_and_migraty<span style="color:#bbb">
</span><span style="color:#bbb">
</span><span style="color:#bbb"></span><span style="color:#000;font-weight:bold">source</span>:<span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#000;font-weight:bold">plugin</span>:<span style="color:#bbb"> </span>rick_and_migraty_fandom<span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#000;font-weight:bold">entity_type</span>:<span style="color:#bbb"> </span>rick_and_migraty_character<span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#000;font-weight:bold">skip_count</span>:<span style="color:#bbb"> </span><span style="color:#000;font-weight:bold">true</span><span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#000;font-weight:bold">page_id_map</span>:<span style="color:#bbb">
</span><span style="color:#bbb">    </span><span style="color:#000;font-weight:bold">1</span>:<span style="color:#bbb"> </span><span style="color:#099">2073</span><span style="color:#bbb">
</span><span style="color:#bbb">    </span><span style="color:#000;font-weight:bold">2</span>:<span style="color:#bbb"> </span><span style="color:#099">2074</span><span style="color:#bbb">
</span><span style="color:#bbb">
</span><span style="color:#bbb"></span><span style="color:#000;font-weight:bold">process</span>:<span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#000;font-weight:bold">id</span>:<span style="color:#bbb"> </span>entity_id<span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#000;font-weight:bold">fandom_description</span>:<span style="color:#bbb"> </span>fandom_description<span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#000;font-weight:bold">fandom_page_id</span>:<span style="color:#bbb"> </span>fandom_page_id<span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#000;font-weight:bold">fandom_url</span>:<span style="color:#bbb"> </span>fandom_url<span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#000;font-weight:bold">fandom_source</span>:<span style="color:#bbb"> </span>fandom_source<span style="color:#bbb">
</span><span style="color:#bbb">
</span><span style="color:#bbb"></span><span style="color:#000;font-weight:bold">destination</span>:<span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#000;font-weight:bold">plugin</span>:<span style="color:#bbb"> </span><span style="color:#d14">&#39;entity:rick_and_migraty_character&#39;</span><span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#000;font-weight:bold">validate</span>:<span style="color:#bbb"> </span><span style="color:#000;font-weight:bold">true</span><span style="color:#bbb">
</span><span style="color:#bbb">
</span><span style="color:#bbb"></span><span style="color:#000;font-weight:bold">migration_dependencies</span>:<span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#000;font-weight:bold">required</span>:<span style="color:#bbb">
</span><span style="color:#bbb">    </span>- rick_and_migraty_character<span style="color:#bbb">
</span></code></pre></td></tr></table>
</div>
</div>
  
  <p class="code-snippet__source">
    Source: <a href="https://gitlab.com/davidjguru/rick-and-migraty/-/blob/main/web/modules/custom/rick_and_migraty_migrate/migrations/rick_and_migraty_character_fandom.yml" rel="noopener noreferrer" target="_blank">Archivo fuente en GitLab</a>
  </p>
  
</div>


<p>El plugin está en <a href="https://gitlab.com/davidjguru/rick-and-migraty/-/blob/main/web/modules/custom/rick_and_migraty_migrate/src/Plugin/migrate/source/RickAndMigratyFandom.php"><code>RickAndMigratyFandom.php</code></a>. Utiliza dos servicios de apoyo: <a href="https://gitlab.com/davidjguru/rick-and-migraty/-/blob/main/web/modules/custom/rick_and_migraty_migrate/src/Fandom/FandomClient.php"><code>FandomClient.php</code></a>, para las peticiones a MediaWiki con caché, y <a href="https://gitlab.com/davidjguru/rick-and-migraty/-/blob/main/web/modules/custom/rick_and_migraty_migrate/src/Fandom/FandomDescriptionExtractor.php"><code>FandomDescriptionExtractor.php</code></a>, para convertir el HTML interpretado de la wiki en párrafos introductorios legibles.</p>
<p>El destino sigue siendo el mismo tipo de entidad:</p>
<div class="highlight"><pre style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#000;font-weight:bold">destination</span>:<span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#000;font-weight:bold">plugin</span>:<span style="color:#bbb"> </span><span style="color:#d14">&#39;entity:rick_and_migraty_character&#39;</span><span style="color:#bbb">
</span></code></pre></div><p>Y <strong>las asignaciones actualizan los campos de enriquecimiento</strong>:</p>
<div class="highlight"><pre style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#000;font-weight:bold">process</span>:<span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#000;font-weight:bold">id</span>:<span style="color:#bbb"> </span>entity_id<span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#000;font-weight:bold">fandom_description</span>:<span style="color:#bbb"> </span>fandom_description<span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#000;font-weight:bold">fandom_page_id</span>:<span style="color:#bbb"> </span>fandom_page_id<span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#000;font-weight:bold">fandom_url</span>:<span style="color:#bbb"> </span>fandom_url<span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#000;font-weight:bold">fandom_source</span>:<span style="color:#bbb"> </span>fandom_source<span style="color:#bbb">
</span></code></pre></div><p>La regla es deliberadamente conservadora: <strong>si la coincidencia es ambigua, se omite y se registra</strong>. Es más fácil corregir una descripción ausente que una equivocada que parece fiable.</p>
<p>El YAML hace visible ese contrato. Las entradas explícitas de <code>page_id_map</code> permiten fijar casos difíciles conocidos. <code>aliases</code> 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 <code>id: entity_id</code>, por lo que el enriquecimiento llega al registro ya importado.</p>
<h2 id="9-de-los-datos-al-contenido-navegar">9. De los datos al contenido (navegar)</h2>
<p>Una migración no termina cuando hay filas en las tablas. <strong>Se vuelve útil cuando las personas pueden recorrer, consultar y entender los datos importados</strong>.</p>
<p>Por eso importa el modelo de entidades. Una vez que <strong>personajes, localizaciones y episodios existen como entidades reales de Drupal</strong>, el sitio puede exponerlos como páginas, rutas, Views y navegación basada en relaciones.</p>
<p>
<span class="post-figure">
  <img src="/images/post/davidjguru_drupal_11_migrations_rick_and_migraty_seven_main_list_item.jpg" alt="Lista completa de elementos." loading="lazy" decoding="async">
  <span class="post-figure__caption">Listado completo con filtros y cuadro de búsqueda</span>
</span>

</p>
<p>En una página de personaje, <strong>las preguntas útiles son inmediatas</strong>:</p>
<ul>
<li>¿Quién es este personaje?</li>
<li>¿Cuál es su estado, especie y género?</li>
<li>¿De dónde procede?</li>
<li>¿Dónde fue visto por última vez?</li>
<li>¿En qué episodios aparece?</li>
<li>¿Qué contexto adicional aporta Fandom?</li>
</ul>
<p>
<span class="post-figure">
  <img src="/images/post/davidjguru_drupal_11_migrations_rick_and_migraty_eight_alien_rick_profile.jpg" alt="Perfil de un personaje." loading="lazy" decoding="async">
  <span class="post-figure__caption">Ejemplo de personaje: Alien Rick</span>
</span>

</p>
<p>Para una localización, <strong>las preguntas cambian</strong>:</p>
<ul>
<li>¿Qué clase de lugar es?</li>
<li>¿A qué dimensión pertenece?</li>
<li>¿Qué personajes residen allí?</li>
</ul>
<p>
<span class="post-figure">
  <img src="/images/post/davidjguru_drupal_11_migrations_rick_and_migraty_nine_birdperson_location.jpg" alt="Perfil de una localización." loading="lazy" decoding="async">
  <span class="post-figure__caption">Ejemplo de localización: los recuerdos de Birdperson</span>
</span>

</p>
<p>Y para un episodio:</p>
<ul>
<li>¿A qué temporada pertenece y qué número tiene?</li>
<li>¿Cuándo se emitió?</li>
<li>¿Qué puntuación ofrece TVmaze?</li>
<li>¿Qué personajes aparecen?</li>
</ul>
<p>
<span class="post-figure">
  <img src="/images/post/davidjguru_drupal_11_migrations_rick_and_migraty_ten_anatomy_park_episode.jpg" alt="Página de un episodio." loading="lazy" decoding="async">
  <span class="post-figure__caption">Página del episodio Anatomy Park</span>
</span>

</p>
<p>Los mismos datos pueden convertirse en vistas: <strong>personajes por dimensión, episodios por temporada, localizaciones con muchos residentes o un pequeño explorador de personajes por estado</strong>. Aquí la migración deja de ser un ejercicio de backend y se convierte en un producto de contenido.</p>
<p>Las plantillas del tema muestran esa idea:</p>
<ul>
<li><a href="https://gitlab.com/davidjguru/rick-and-migraty/-/blob/main/web/themes/custom/rick_and_migraty_theme/templates/content/rick-and-migraty-character.html.twig"><code>rick-and-migraty-character.html.twig</code></a> muestra las insignias del perfil, el avatar, la descripción de Fandom, los enlaces a episodios y los metadatos de origen.</li>
<li><a href="https://gitlab.com/davidjguru/rick-and-migraty/-/blob/main/web/themes/custom/rick_and_migraty_theme/templates/content/rick-and-migraty-location.html.twig"><code>rick-and-migraty-location.html.twig</code></a> muestra los metadatos de la localización, sus residentes y el contexto de Fandom.</li>
<li><a href="https://gitlab.com/davidjguru/rick-and-migraty/-/blob/main/web/themes/custom/rick_and_migraty_theme/templates/content/rick-and-migraty-episode.html.twig"><code>rick-and-migraty-episode.html.twig</code></a> muestra temporada y episodio, las insignias de puntuación de TVmaze y las apariciones de personajes.</li>
</ul>
<p>La migración no se limita a rellenar campos: <strong>alimenta una experiencia de navegación</strong>. Los campos elegidos se convierten en variables que el tema puede utilizar: <code>avatar_url</code>, <code>episode_links</code>, <code>resident_cards</code>, <code>appearance_cards</code>, <code>fandom.description</code>, <code>profile.api_url</code>, etc.</p>
<p>El proyecto también incluye una ruta de exploración:</p>
<div class="highlight"><pre style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#000;font-weight:bold">rick_and_migraty_migrate.browse</span>:<span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#000;font-weight:bold">path</span>:<span style="color:#bbb"> </span><span style="color:#d14">&#39;/browse-all-content&#39;</span><span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#000;font-weight:bold">defaults</span>:<span style="color:#bbb">
</span><span style="color:#bbb">    </span><span style="color:#000;font-weight:bold">_controller</span>:<span style="color:#bbb"> </span><span style="color:#d14">&#39;\Drupal\rick_and_migraty_migrate\Controller\BrowseContentController::browse&#39;</span><span style="color:#bbb">
</span><span style="color:#bbb">    </span><span style="color:#000;font-weight:bold">_title</span>:<span style="color:#bbb"> </span><span style="color:#d14">&#39;Browse all content&#39;</span><span style="color:#bbb">
</span><span style="color:#bbb">  </span><span style="color:#000;font-weight:bold">requirements</span>:<span style="color:#bbb">
</span><span style="color:#bbb">    </span><span style="color:#000;font-weight:bold">_access</span>:<span style="color:#bbb"> </span><span style="color:#d14">&#39;TRUE&#39;</span><span style="color:#bbb">
</span></code></pre></div><p>Su controlador, <a href="https://gitlab.com/davidjguru/rick-and-migraty/-/blob/main/web/modules/custom/rick_and_migraty_migrate/src/Controller/BrowseContentController.php"><code>BrowseContentController.php</code></a>, hace dos cosas interesantes para este artículo. Primero, ofrece <strong>una matriz de entidades importadas con búsqueda</strong> mediante <code>/browse-all-content</code>. 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.</p>
<p>Esto explica también por qué reducir todo a campos de texto habría sido un mal atajo. <strong>Lo interesante del conjunto de datos es el grafo</strong>. 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.</p>
<h2 id="10-la-migración-como-operación-ejecutar">10. La migración como operación (ejecutar)</h2>
<p>La implementación es una mitad de la migración. <strong>La operación es la otra mitad</strong>.</p>
<p>Esto 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: <strong>puntos de entrada repetibles</strong>.</p>
<p>El proyecto admite varias formas de ejecución, cada una con su finalidad:</p>
<table>
<thead>
<tr>
<th><strong>Punto de entrada</strong></th>
<th><strong>Uso más adecuado</strong></th>
</tr>
</thead>
<tbody>
<tr>
<td><code>scripts/rick-and-migraty-migrate.sh</code></td>
<td>Ejecuciones locales repetibles, especialmente con archivos de prueba.</td>
</tr>
<tr>
<td><code>ddev drush ramt:migrate:*</code></td>
<td>Operaciones de desarrollo: reinicio, importación y lotes de avatares.</td>
</tr>
<tr>
<td>Ventana modal del navegador</td>
<td>Panel visual para ejecutar y observar la migración paso a paso.</td>
</tr>
</tbody>
</table>
<p>El script está en <a href="https://gitlab.com/davidjguru/rick-and-migraty/-/blob/main/scripts/rick-and-migraty-migrate.sh"><code>scripts/rick-and-migraty-migrate.sh</code></a>. 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.</p>
<p>Para una ejecución local con archivos de prueba existentes:</p>
<div class="highlight"><pre style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">scripts/rick-and-migraty-migrate.sh --fixtures
</code></pre></div><p>Si primero hay que generarlos:</p>
<div class="highlight"><pre style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">scripts/rick-and-migraty-migrate.sh --build-fixtures
</code></pre></div><p>Estos archivos viven en:</p>
<div class="highlight"><pre style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-text" data-lang="text">.ddev/.tmp-fixtures/rick-and-migraty-api
</code></pre></div><p>Permiten <strong>leer las filas JSON canónicas de Rick and Morty desde archivos locales estables</strong>, en lugar de hacerlo desde la API pública. Así separamos dos problemas que de otro modo se mezclan:</p>
<ol>
<li>¿Está fallando el código de la migración?</li>
<li>¿O el servicio remoto está lento, limita peticiones, no está disponible o ha cambiado?</li>
</ol>
<p>Para comprobar la estructura más deprisa, <strong>podemos omitir las fases costosas</strong>:</p>
<div class="highlight"><pre style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">scripts/rick-and-migraty-migrate.sh --fixtures --no-avatars --no-fandom
</code></pre></div><p>El ciclo normal con Drush es intencionadamente breve:</p>
<div class="highlight"><pre style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">ddev drush ramt:migrate:reset
ddev drush ramt:migrate:import
</code></pre></div><p>Estos comandos se implementan en <a href="https://gitlab.com/davidjguru/rick-and-migraty/-/blob/main/web/modules/custom/rick_and_migraty_migrate/src/Drush/Commands/RickAndMigratyMigrateCommands.php"><code>RickAndMigratyMigrateCommands.php</code></a>. 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.</p>
<p>El comando de reinicio es importante. <strong>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 <code>migrate:status</code> sigue considerando importadas las filas.</strong></p>
<p>La limpieza se delega en <a href="https://gitlab.com/davidjguru/rick-and-migraty/-/blob/main/web/modules/custom/rick_and_migraty_migrate/src/MigrationResetter.php"><code>MigrationResetter.php</code></a>, que hace explícita la operación: eliminar entidades personalizadas, entidades de archivo y archivos físicos de avatares, borrar alias <code>/rick-and-migraty/%</code>, vaciar tablas <code>migrate_map_*</code> y <code>migrate_message_*</code> e invalidar cachés.</p>
<p>Las descargas de avatares se gestionan <strong>como una operación propia</strong>:</p>
<div class="highlight"><pre style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">ddev drush ramt:migrate:avatars --batch-size<span style="color:#000;font-weight:bold">=</span><span style="color:#099">10</span> --sleep<span style="color:#000;font-weight:bold">=</span><span style="color:#099">12</span> --retries<span style="color:#000;font-weight:bold">=</span><span style="color:#099">3</span>
</code></pre></div><p>La forma del comando refleja la naturaleza del trabajo. <strong>Descargar cientos de archivos remotos no equivale a asignar campos JSON: requiere lotes, reintentos y pausas</strong>. La utilidad importa avatares mediante rangos <code>--idlist</code>, añade una pausa local entre peticiones y reintenta los lotes fallidos antes de rendirse.</p>
<p>La separación ofrece una vía de recuperación práctica: <strong>si las descargas fallan a mitad del proceso, podemos repetir la fase de archivos sin dar por roto todo el modelo de migración</strong>.</p>
<h3 id="los-límites-de-peticiones-forman-parte-del-diseño">Los límites de peticiones forman parte del diseño</h3>
<p>Durante el desarrollo apareció un recordatorio muy concreto de que las API remotas no son infinitas: <strong>respuestas <code>429 Too Many Requests</code></strong>. Es fácil considerarlas una molestia, pero en una migración aportan información útil. <strong>Nos dicen que pedimos demasiado, demasiado deprisa o demasiadas veces a un servicio que no controlamos</strong>.</p>
<p>La respuesta no fue ocultar el error, sino <strong>convertir el límite de peticiones en una restricción de diseño</strong>:</p>
<ul>
<li>Las respuestas se almacenan en caché por URL para no pedir las mismas páginas en cada ejecución.</li>
<li>Los archivos de prueba pueden sustituir las llamadas remotas cuando comprobamos la lógica de asignación.</li>
<li><code>RICK_AND_MIGRATY_API_MAX_PAGES</code> limita las pruebas básicas a un subconjunto pequeño.</li>
<li><code>request_delay_ms</code> ralentiza las peticiones que no tienen respuesta en caché.</li>
<li>Las descargas de avatares se ejecutan por lotes con <code>--sleep</code> y <code>--retries</code>.</li>
<li>Las importaciones de avatares usan rangos <code>--idlist</code>, de modo que podemos repetir un lote fallido sin repetir toda la migración.</li>
</ul>
<p>Por eso la fase de avatares tiene su propia secuencia y no queda escondida dentro de la importación de personajes. <strong>Las descargas remotas son la parte más agresiva del proceso</strong>: cientos de peticiones HTTP, transferencias binarias, entidades de archivo locales, escrituras en disco y reintentos. <strong>Separarlas hace la migración más respetuosa y mucho más fácil de recuperar</strong>.</p>
<p>Dicho de otro modo: <strong>el <code>429</code> no fue solo un código de error; fue una revisión de diseño enviada por la API remota</strong>.</p>
<h3 id="el-navegador-es-un-panel-de-control">El navegador es un panel de control</h3>
<p>El flujo del navegador resulta útil, pero <strong>no debería pretender ser el motor de migración</strong>.</p>
<p>La primera versión intentó realizar demasiado trabajo en una petición larga. <strong>Eso es frágil</strong>, porque PHP-FPM, nginx, los proxies y los navegadores tienen sus propias opiniones sobre las peticiones HTTP prolongadas, y ninguna es sentimental. 😉</p>
<p>El flujo actual utiliza <strong>pasos POST más pequeños</strong>:</p>
<div class="highlight"><pre style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-text" data-lang="text">/rick-and-migraty/run-migration-step/{step}
</code></pre></div><p>La ruta se declara en <a href="https://gitlab.com/davidjguru/rick-and-migraty/-/blob/main/web/modules/custom/rick_and_migraty_migrate/rick_and_migraty_migrate.routing.yml"><code>rick_and_migraty_migrate.routing.yml</code></a> y la gestiona <a href="https://gitlab.com/davidjguru/rick-and-migraty/-/blob/main/web/modules/custom/rick_and_migraty_migrate/src/Controller/BrowseContentController.php"><code>BrowseContentController.php</code></a>.</p>
<p>Esta es la ventana modal:</p>
<p>
<span class="post-figure">
  <img src="/images/post/davidjguru_drupal_11_migrations_rick_and_migraty_six_modal_window.jpg" alt="Ventana modal con el progreso de la migración." loading="lazy" decoding="async">
  <span class="post-figure__caption">Progreso y trazas de la migración en la ventana modal</span>
</span>

</p>
<p>Muestra <strong>el progreso, las trazas recientes y un resumen final</strong>. La misma traza se escribe en watchdog mediante el canal <code>rick_and_migraty_migrate</code>, de modo que un síntoma observado en el navegador puede investigarse con Drush:</p>
<div class="highlight"><pre style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">ddev drush watchdog:show --type<span style="color:#000;font-weight:bold">=</span>rick_and_migraty_migrate --count<span style="color:#000;font-weight:bold">=</span><span style="color:#099">50</span>
</code></pre></div><p>El trabajo de avatares se divide internamente en rangos como <code>avatars-1</code>, <code>avatars-2</code>, etc. El usuario ve una sola fase, «<strong>Procesando avatares</strong>», pero el backend recibe <strong>unidades de trabajo más pequeñas cuyos fallos, registros y recuperación son más claros</strong>.</p>
<p>La distinción importa: el navegador no sustituye a Drush. <strong>Es un panel guiado sobre los mismos pasos operativos</strong>, útil para observar el proceso y hacer visible la experiencia de migración.</p>
<h2 id="11-perder-la-cabeza-depurar">11. Perder la cabeza (depurar)</h2>
<p>Cuando falla una migración, resulta tentador mirar fijamente el YAML hasta que confiese. A veces funciona. A menudo <strong>solo conseguimos poner nervioso al YAML</strong>. 😬</p>
<p>Una rutina mejor es <strong>depurar de fuera hacia dentro</strong>.</p>
<ul>
<li><strong>Primero</strong>, inspecciona el origen:</li>
</ul>
<div class="highlight"><pre style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">curl -s <span style="color:#d14">&#39;https://rickandmortyapi.com/api/character&#39;</span> | jq <span style="color:#d14">&#39;.info&#39;</span>
curl -s <span style="color:#d14">&#39;https://rickandmortyapi.com/api/character&#39;</span> | jq <span style="color:#d14">&#39;.results[0]&#39;</span>
curl -s <span style="color:#d14">&#39;https://rickandmortyapi.com/api/location&#39;</span> | jq <span style="color:#d14">&#39;.results[0].residents&#39;</span>
curl -s <span style="color:#d14">&#39;https://rickandmortyapi.com/api/episode&#39;</span> | jq <span style="color:#d14">&#39;.results[0].characters&#39;</span>
curl -s <span style="color:#d14">&#39;https://api.tvmaze.com/shows/216/episodes&#39;</span> | jq <span style="color:#d14">&#39;.[0] | {id, name, season, number, rating}&#39;</span>
</code></pre></div><p>Todavía no pretendemos depurar Drupal. Antes hay que responder una pregunta más sencilla: <strong>¿la fuente sigue teniendo la estructura que espera la migración?</strong> Si ha cambiado o un punto de acceso está lento o caído, el YAML no es el primer sospechoso.</p>
<ul>
<li><strong>Después</strong>, inspecciona <strong>el estado de la migración</strong>:</li>
</ul>
<div class="highlight"><pre style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">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
</code></pre></div><p><code>migrate:status</code> 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: <strong>el estado de la API Migrate no equivale al contenido visible</strong>.</p>
<p>Si faltan relaciones inversas tras una ejecución parcial, repite directamente las pasadas de actualización:</p>
<div class="highlight"><pre style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">ddev drush migrate:import rick_and_migraty_episode --update
ddev drush migrate:import rick_and_migraty_location --update
</code></pre></div><p>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 <strong>completen relaciones cuyos personajes de destino ahora sí pueden resolverse</strong>.</p>
<p>Si el problema está en el enriquecimiento de <strong>Fandom</strong>, <strong>depura esa fase sin repetir las importaciones básicas</strong>:</p>
<div class="highlight"><pre style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">ddev 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
</code></pre></div><p>Y repite únicamente las migraciones de enriquecimiento:</p>
<div class="highlight"><pre style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">ddev drush migrate:import rick_and_migraty_character_fandom --update --feedback<span style="color:#000;font-weight:bold">=</span><span style="color:#099">50</span>
ddev drush migrate:import rick_and_migraty_location_fandom --update --feedback<span style="color:#000;font-weight:bold">=</span><span style="color:#099">25</span>
ddev drush migrate:import rick_and_migraty_episode_fandom --update --feedback<span style="color:#000;font-weight:bold">=</span><span style="color:#099">25</span>
</code></pre></div><p>Esta es otra razón para ejecutar el enriquecimiento por separado. Un problema de análisis de <strong>Fandom</strong> no debería obligarnos a importar de nuevo todo el contenido canónico. <strong>El grafo básico y el enriquecimiento editorial son ámbitos de depuración distintos</strong>.</p>
<p>Cuando una fila se resiste, <strong>la lista de comprobación resulta familiar</strong>:</p>
<ul>
<li>Revisa la indentación del YAML.</li>
<li>Inspecciona los selectores del origen.</li>
<li>Inspecciona las tablas <code>migrate_map_*</code>.</li>
<li>Inspecciona las tablas <code>migrate_message_*</code>.</li>
<li>Confirma que las dependencias se ejecutaron en el orden esperado.</li>
<li>Utiliza Xdebug en DDEV cuando sospeches de un plugin de proceso personalizado.</li>
</ul>
<p>Para este proyecto añadiría unas comprobaciones:</p>
<ul>
<li>Confirma <code>RICK_AND_MIGRATY_API_FIXTURE_BASE_PATH</code> si utilizas archivos de prueba.</li>
<li>Confirma que <code>RICK_AND_MIGRATY_API_MAX_PAGES</code> no limita inesperadamente la ejecución.</li>
<li>Inspecciona las entradas de watchdog de <code>rick_and_migraty_migrate</code>.</li>
<li>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.</li>
</ul>
<p>En los lugares aburridos suelen estar <strong>las pistas útiles</strong>: 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.</p>
<h2 id="12-decisiones-que-merece-la-pena-destacar-decidir">12. Decisiones que merece la pena destacar (decidir)</h2>
<p>Hay varias decisiones que conviene documentar porque <strong>determinan la implementación más que cualquier YAML individual</strong>. Si alguien retoma el proyecto más adelante, me gustaría que las conociera antes de modificar las migraciones.</p>
<ul>
<li>
<p>En primer lugar, <strong>las relaciones circulares son previsibles</strong>. 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.</p>
</li>
<li>
<p>En segundo lugar, <strong>la paginación forma parte del contrato del origen</strong>. 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.</p>
</li>
<li>
<p>En tercer lugar, <strong>los identificadores externos no son opcionales</strong>. El <code>id</code> de origen se guarda como <code>api_id</code> 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.</p>
</li>
<li>
<p>En cuarto lugar, <strong>las imágenes merecen su propia fase</strong>. Los avatares se descargan mediante una migración separada y pueden procesarse por lotes. La entidad conserva <code>image_url</code>, pero gestionar archivos locales es una operación distinta de asignar campos y debe tratarse como tal.</p>
</li>
<li>
<p>En quinto lugar, <strong>debemos tratar con consideración a las API públicas</strong>. La caché, los archivos de prueba, los límites de páginas y las pausas no son adornos. Los errores <code>429 Too Many Requests</code> lo dejaron claro: la consideración no es solo una cuestión de educación; hace posible repetir la migración.</p>
</li>
<li>
<p>Por último, <strong>las fuentes de enriquecimiento no son fuentes canónicas</strong>. TVmaze aporta puntuaciones. Fandom aporta descripciones. Ninguna puede redefinir el conjunto básico de Rick and Morty.</p>
</li>
</ul>
<p>La lista no es solo documentación: <strong>también protege el diseño</strong>. Cuando el proyecto crezca, cada nueva migración debería contrastarse con estas decisiones:</p>
<ul>
<li><strong>¿Conserva la autoridad de cada fuente?</strong></li>
<li><strong>¿Mantiene explícitas las relaciones?</strong></li>
<li><strong>¿Sigue siendo repetible?</strong></li>
<li><strong>¿Evita que los servicios remotos se conviertan en un peaje constante para desarrollar?</strong></li>
</ul>
<h2 id="13-qué-he-aprendido-reflexionar">13. Qué he aprendido (reflexionar)</h2>
<p>Si tuviera que extraer algunos principios reutilizables de la experiencia <strong>Rick and Migraty</strong>, serían estos.</p>
<p><strong>Elabora un perfil de la fuente antes de escribir demasiado YAML.</strong> 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.</p>
<p><strong>Conserva el grafo cuando el valor está en el grafo.</strong> 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.</p>
<p><strong>Mantén acotadas las transformaciones.</strong> 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.</p>
<p><strong>Ordena las importaciones deliberadamente.</strong> 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.</p>
<p><strong>Considera las operaciones parte del diseño.</strong> 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.</p>
<p>Y quizá lo más importante: <strong>no permitas que las fuentes secundarias difuminen el contrato</strong>. 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.</p>
<p><strong>Eso es lo que reutilizaría en otro proyecto aunque su temática fuera completamente distinta.</strong> Cambiarían los nombres, las API y los campos, pero se mantendría el patrón:</p>
<ol>
<li><strong>Elegir la fuente canónica.</strong></li>
<li><strong>Modelar el grafo.</strong></li>
<li><strong>Aislar los enriquecimientos.</strong></li>
<li><strong>Hacer que repetir la ejecución sea aburrido.</strong></li>
</ol>
<h2 id="14-próximos-pasos-planificar">14. Próximos pasos (planificar)</h2>
<p>Este proyecto ha reunido paginación, relaciones, enriquecimiento, avatares, archivos de prueba y ejecución repetible en una migración funcional de Drupal. <strong>Los próximos pasos parten de esa implementación</strong>:</p>
<ul>
<li><strong>Mantener el artículo y la documentación de MkDocs alineados con el código</strong> a medida que evolucione el proyecto, actualizando ejemplos y capturas cuando cambie el comportamiento.</li>
<li><strong>Explorar la migración de avatares en otro artículo</strong>, porque la gestión de archivos remotos, los lotes y la recuperación merecen un examen más detallado.</li>
<li><strong>Revisar la cobertura del conjunto canónico</strong> y decidir cómo tratar episodios y personajes ausentes, conservando identificadores externos estables y responsabilidades explícitas de las fuentes.</li>
</ul>
<p>También hay otro posible artículo a la vista: <strong>el panel de control del navegador</strong>. 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.</p>
<p>Por ahora, <strong>lo importante es que el diseño sea visible</strong>. 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. <strong>Es suficiente para esta sexta parada de la serie</strong>.</p>
<p>Intentar 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: <strong>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</strong>.</p>
<p><strong>Nota sobre la documentación:</strong> La estructura y la intención de este artículo siguen los principios de <a href="https://diataxis.fr/">Diátaxis</a>, distinguiendo tutoriales, guías prácticas, explicaciones y material de referencia para situar las instrucciones y el contexto conceptual donde corresponden.</p>
<p><strong>Nota sobre el idioma:</strong> 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.</p>
<h2 id="15-wq">15. :wq!</h2>
<h5 id="canción-recomendada-fabrizio-de-andré---ballata-dellamore-cieco">Canción recomendada: Fabrizio De André - Ballata dell&rsquo;amore cieco</h5>


<div class="embed-responsive embed-responsive-16by9">
  <iframe
    class="embed-responsive-item"
    src="https://www.youtube-nocookie.com/embed/aX8KPb5yFzI"
    title="YouTube video"
    loading="lazy"
    allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
    allowfullscreen></iframe>
</div>

]]></content:encoded></item><item><title>Migraciones en Drupal (V): depurar migraciones II</title><link>https://www.therussianlullaby.dev/es/blog/drupal-migrations-five-debugging-migrations-ii/</link><pubDate>Mon, 27 Jul 2020 00:00:00 +0000</pubDate><guid>https://www.therussianlullaby.dev/es/blog/drupal-migrations-five-debugging-migrations-ii/</guid><enclosure url="https://www.therussianlullaby.dev/images/post/davidjguru_drupal_migrations_debugging_xdebug_main.jpg" type="image/jpeg"/><media:content url="https://www.therussianlullaby.dev/images/post/davidjguru_drupal_migrations_debugging_xdebug_main.jpg" medium="image" type="image/jpeg"/><media:thumbnail url="https://www.therussianlullaby.dev/images/post/davidjguru_drupal_migrations_debugging_xdebug_main.jpg"/><description>Migraciones en Drupal (V): depurar migraciones II</description><content:encoded><![CDATA[<p>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.</p>
<hr>
<p><strong>Foto de Unsplash, de <a href="https://unsplash.com/@dreamquest12610">Neenu Vimalkumar, @dreamquest12610</a></strong></p>
<hr>
<p><strong>Índice</strong></p>
<span class="post-toc" aria-hidden="true"></span>

<!-- raw HTML omitted -->
<ul>
<li><a href="#1-introducci%C3%B3n-a-xdebug">1. Introducción a Xdebug</a></li>
<li><a href="#2-xdebug-en-ddev">2. Xdebug en DDEV</a></li>
<li><a href="#3-configurar-el-ide-paso-a-paso">3. Configurar el IDE paso a paso</a></li>
<li><a href="#4-errores-al-ejecutar-drush-en-contenedores">4. Errores al ejecutar Drush en contenedores</a></li>
<li><a href="#5-wq">5. :wq!</a></li>
</ul>
<!-- raw HTML omitted -->
<h2 id="1-introducción-a-xdebug">1. Introducción a Xdebug</h2>
<p>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 <strong>añadiremos la tercera pieza: depurar una migración desde el código que interviene en ella</strong>, siguiendo las clases PHP que hacen funcionar todo el mecanismo.</p>
<p><a href="https://xdebug.org/">Xdebug</a> 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 <a href="https://ddev.readthedocs.io/en/stable/">DDEV</a>, una herramienta basada en contenedores Docker que ya incluye Xdebug preparado para utilizarlo.</p>
<p>DDEV 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 <a href="https://github.com/drud/awesome-ddev">awesome-ddev</a>.</p>
<h2 id="2-xdebug-en-ddev">2. Xdebug en DDEV</h2>
<p>Si partimos de un entorno DDEV, podemos activar Xdebug de dos maneras.</p>
<p>La primera es editar <code>config.yaml</code>, en <code>your_project/.ddev/config.yaml</code>, y activar Xdebug. En el archivo mostrado en la captura, la opción aparece en la línea 9:</p>
<p>
<img src="/images/post/davidjguru_drupal_migrations_ddev_enabling.png" alt="Activar Xdebug en la configuración de DDEV" loading="lazy" decoding="async">

</p>
<p>La segunda es utilizar los comandos de DDEV desde fuera del contenedor:</p>
<pre><code>ddev exec enable_xdebug
ddev exec disable_xdebug
</code></pre><p>O también:</p>
<pre><code>ddev xdebug
ddev xdebug on
ddev xdebug off
</code></pre><p>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.</p>
<p>En 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:</p>
<ul>
<li><a href="https://ddev.readthedocs.io/en/stable/users/step-debugging/#step-debugging-with-ddev-and-xdebug">Depuración paso a paso con DDEV y Xdebug</a>.</li>
<li><a href="https://glamanate.com/blog/xdebug-over-command-line-ddev">Xdebug desde la línea de comandos con DDEV</a>.</li>
</ul>
<p>El equipo de PhpStorm ya explicó la técnica básica en <a href="https://blog.jetbrains.com/phpstorm/2012/03/new-in-4-0-easier-debugging-of-remote-php-command-line-scripts/">este artículo sobre depuración remota de scripts PHP de consola</a>. Utilizaremos PhpStorm para el ejemplo.</p>
<h2 id="3-configurar-el-ide-paso-a-paso">3. Configurar el IDE paso a paso</h2>
<p>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.</p>
<p>Sigamos estos pasos:</p>
<p>
<img src="/images/post/davidjguru_drupal_migrations_debugging_xdebug_server.png" alt="Configuración del servidor Xdebug en PhpStorm" loading="lazy" decoding="async">

</p>
<h3 id="1-servidor-del-proyecto">1. Servidor del proyecto</h3>
<p>Abre File → Settings → Languages &amp; 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.</p>
<h3 id="2-configuración-inicial">2. Configuración inicial</h3>
<p>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 <code>/var/www/html/</code>, mientras que los archivos del equipo pueden estar en una ruta como <code>/home/user/folder/projects/my_project</code>. Configuraremos la correspondencia entre ambas rutas.</p>
<h3 id="3-escuchar-conexiones-de-depuración">3. Escuchar conexiones de depuración</h3>
<p>Activa el botón de escucha de conexiones de depuración del IDE.</p>
<h3 id="4-cargar-una-variable-de-entorno-en-el-contenedor">4. Cargar una variable de entorno en el contenedor</h3>
<p>Tenemos dos opciones. Podemos definirla para cada sesión entrando en el contenedor con <code>ddev ssh</code> y ejecutando allí:</p>
<p><code>export PHP_IDE_CONFIG=&quot;serverName=your.project.name.ddev.site&quot;</code></p>
<p>También podemos establecerla de forma persistente mediante un archivo complementario <code>docker-compose.env.yaml</code> en <code>.ddev/</code>, junto a los demás archivos de configuración de Docker Compose:</p>
<pre><code>version: '3.6'
services:
 web:
   environment:
     - PHP_IDE_CONFIG=serverName=your.project.name.ddev.site
</code></pre><p>Al detener y arrancar la red de contenedores con <code>ddev stop</code> y <code>ddev start</code>, este archivo se combinará con los demás y la variable se cargará automáticamente.</p>
<h3 id="5-puntos-de-interrupción">5. Puntos de interrupción</h3>
<p>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: <a href="https://api.drupal.org/api/drupal/core%21modules%21migrate%21src%21Plugin%21migrate%21process%21Get.php/class/Get">la clase Get.php</a>, situada en <code>/core/modules/migrate/src/Plugin/migrate/process/Get.php</code>.</p>
<p>Es un buen lugar para detener algunos ejemplos de migración. Podemos añadir un punto de interrupción en <code>transform()</code>, que en la versión de la captura aparece en la línea 106:</p>
<p>
<img src="/images/post/davidjguru_drupal_migrations_debugging_xdebug_breakpoints.png" alt="Puntos de interrupción con Xdebug" loading="lazy" decoding="async">

</p>
<h2 id="4-errores-al-ejecutar-drush-en-contenedores">4. Errores al ejecutar Drush en contenedores</h2>
<p>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 <code>/usr/local/bin/drush</code>, en lugar del ejecutable del proyecto.</p>
<p>
<img src="/images/post/davidjguru_drupal_migrations_debugging_xdebug_drush_mapping.png" alt="Error de correspondencia de rutas de Drush en el contenedor" loading="lazy" decoding="async">

</p>
<p>Tenemos dos opciones.</p>
<p>Podemos ejecutar Drush directamente desde el directorio <code>vendor</code> del proyecto, que ya está incluido en el mapeo de rutas del servidor remoto. El original muestra estas formas:</p>
<ul>
<li>Desde fuera del contenedor: <code>ddev exec /vendor/bin/drush migrate-status</code>.</li>
<li>Desde dentro, tras <code>ddev ssh</code>: <code>/vendor/bin/drush migrate-status</code>.</li>
</ul>
<p>La otra opción la recomienda <a href="https://twitter.com/randyfay">Randy Fay, @randyfay</a>, uno de los creadores de DDEV, en <a href="https://github.com/drud/ddev/issues/2341#issuecomment-650618158">este comentario de una incidencia de GitHub</a>.</p>
<p>Consiste en aprovechar el comando de ejemplo del archivo <code>drush.example</code>, en <code>/your-name-project/.ddev/commands/web/</code>, y sustituir la llamada a Drush por la ruta del ejecutable del proyecto: pasar de <code>drush $@</code> a <code>/var/www/html/vendor/bin/drush $@</code>.</p>
<p>Así podemos depurar las migraciones con Xdebug en DDEV. Encontrarás más información en:</p>
<ul>
<li><a href="https://ddev.readthedocs.io/en/latest/users/step-debugging/#phpstorm-debugging-setup">Configuración de la depuración con PhpStorm</a>.</li>
<li><a href="https://github.com/wodby/docker4drupal/issues/127">Incidencia 127 de docker4drupal</a>.</li>
<li><a href="https://github.com/drud/ddev/issues/2341">Incidencia 2341 de DDEV</a>.</li>
</ul>
<p><strong>¡Ahora puedes ejecutar tu migración cómodamente!</strong> Lanza los comandos Drush de la migración, comprueba que se alcanzan los puntos de interrupción y empieza a seguir su ejecución.</p>
<h2 id="5-wq">5. :wq!</h2>
<h5 id="canción-recomendada">Canción recomendada</h5>


<div class="embed-responsive embed-responsive-16by9">
  <iframe
    class="embed-responsive-item"
    src="https://www.youtube-nocookie.com/embed/hwmRQ0PBtXU"
    title="YouTube video"
    loading="lazy"
    allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
    allowfullscreen></iframe>
</div>

]]></content:encoded></item><item><title>Migraciones en Drupal (IV): depurar migraciones I</title><link>https://www.therussianlullaby.dev/es/blog/drupal-migrations-four-debugging-migrations-i/</link><pubDate>Mon, 29 Jun 2020 00:00:00 +0000</pubDate><guid>https://www.therussianlullaby.dev/es/blog/drupal-migrations-four-debugging-migrations-i/</guid><enclosure url="https://www.therussianlullaby.dev/images/post/davidjguru_drupal_migrations_debugging_migrations_main.jpg" type="image/jpeg"/><media:content url="https://www.therussianlullaby.dev/images/post/davidjguru_drupal_migrations_debugging_migrations_main.jpg" medium="image" type="image/jpeg"/><media:thumbnail url="https://www.therussianlullaby.dev/images/post/davidjguru_drupal_migrations_debugging_migrations_main.jpg"/><description>Migraciones en Drupal (IV): depurar migraciones I</description><content:encoded><![CDATA[<p>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.</p>
<hr>
<p><strong>Foto de Unsplash, de <a href="https://unsplash.com/@epan5">Krzysztof Niewolny, @epan5</a></strong></p>
<hr>
<p><strong>Índice</strong></p>
<span class="post-toc" aria-hidden="true"></span>

<!-- raw HTML omitted -->
<ul>
<li><a href="#1-introducci%C3%B3n">1. Introducción</a></li>
<li><a href="#2-depuraci%C3%B3n-b%C3%A1sica-revisa-tus-archivos">2. Depuración básica: revisa tus archivos</a></li>
<li><a href="#3-depuraci%C3%B3n-intermedia-con-migrate-devel">3. Depuración intermedia con Migrate Devel</a></li>
<li><a href="#4-wq">4. :wq!</a></li>
</ul>
<!-- raw HTML omitted -->
<h2 id="1-introducción">1. Introducción</h2>
<p>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 <a href="https://gitlab.com/davidjguru/drupal-custom-modules-examples/-/tree/master/Migrations/migration_google_sheet">módulo migration_google_sheet de mi cuenta de GitLab</a>.</p>
<h2 id="2-depuración-básica-revisa-tus-archivos">2. Depuración básica: revisa tus archivos</h2>
<p>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.</p>
<h3 id="21-revisar-el-archivo-de-definición-de-la-migración">2.1. Revisar el archivo de definición de la migración</h3>
<p>Comenzamos con el paso más intuitivo. Hay errores que están a la vista y, por habituales que sean, no siempre resultan evidentes.</p>
<p>Antes de nada, revisaremos dos cuestiones que aparecen con frecuencia:</p>
<p><strong>Espacios en blanco:</strong> un espacio adicional puede causar problemas en el archivo de definición. Recorre sus líneas para localizar espacios sobrantes.</p>
<p><strong>Errores de sangrado:</strong> 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.</p>
<p>Puedes apoyarte en un validador de sintaxis YAML como <a href="http://www.yamllint.com">www.yamllint.com</a>, aunque también tendrás que comprobar el resultado.</p>
<h3 id="22-revisar-los-registros-de-la-base-de-datos">2.2. Revisar los registros de la base de datos</h3>
<p>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 <em>migration_google_sheet_wrong</em>, pasan a ser setenta y cinco. Se han generado dos tablas nuevas:</p>
<div class="highlight"><pre style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-toml" data-lang="toml">cache_migrate
cache_discovery_migration
</code></pre></div><p>Después de ejecutar la migración con identificador <code>taxonomy_google_sheet_wrong</code>, incluida en el módulo personalizado, aparecen otras dos tablas relacionadas con la migración:</p>
<ul>
<li><strong>migrate_map_taxonomy_google_sheet</strong>. 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 <em>lookups</em> 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.</li>
<li><strong>migrate_message_taxonomy_google_sheet</strong>. Guarda los mensajes asociados a la ejecución, también organizados por fila. Cada mensaje tiene su propio identificador y está asociado al <code>id_hash</code> de la fila de origen:</li>
</ul>
<p>
<img src="/images/post/davidjguru_drupal_migrations_debugging_two.png" alt="Columnas de la tabla de mensajes de migración" loading="lazy" decoding="async">

</p>
<p>Podemos obtener esta información con Drush: el contenido de la tabla es lo que aparece al ejecutar:</p>
<div class="highlight"><pre style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-toml" data-lang="toml">drush migrate<span style="color:#a61717;background-color:#e3d2d2">:</span>messages id<span style="color:#a61717;background-color:#e3d2d2">-</span>migration
</code></pre></div><p>Es una forma útil de empezar a recopilar información sobre los errores de la migración.</p>
<h3 id="24-recargar-los-objetos-de-configuración">2.4. Recargar los objetos de configuración</h3>
<p>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 <code>config/install</code>.</p>
<p>Cada 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?</p>
<p>Primero 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:</p>
<div class="highlight"><pre style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-toml" data-lang="toml">dependencies<span style="color:#a61717;background-color:#e3d2d2">:</span>
   enforced<span style="color:#a61717;background-color:#e3d2d2">:</span>
      module<span style="color:#a61717;background-color:#e3d2d2">:</span>
          <span style="color:#a61717;background-color:#e3d2d2">-</span> migration_google_sheet
</code></pre></div><p>Podemos realizar importaciones específicas con Drush o Drupal Console, aprovechando las opciones de importación individual.</p>
<p><strong>Con Drupal Console:</strong></p>
<div class="highlight"><pre style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-toml" data-lang="toml">drupal config<span style="color:#a61717;background-color:#e3d2d2">:</span>import<span style="color:#a61717;background-color:#e3d2d2">:</span>single <span style="color:#a61717;background-color:#e3d2d2">--</span>directory=<span style="color:#d14">&#34;/modules/custom/migration_google_sheet/config/install&#34;</span> <span style="color:#a61717;background-color:#e3d2d2">--</span>file=<span style="color:#d14">&#34;migrate_plus.migration.taxonomy_google_sheet.yml&#34;</span>

</code></pre></div><p><strong>Con Drush:</strong></p>
<div class="highlight"><pre style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-toml" data-lang="toml">drush cim <span style="color:#a61717;background-color:#e3d2d2">--</span>partial <span style="color:#a61717;background-color:#e3d2d2">--</span>source=<span style="color:#a61717;background-color:#e3d2d2">/</span>folder<span style="color:#a61717;background-color:#e3d2d2">/</span>
</code></pre></div><p>También podemos eliminar objetos de la configuración activa con ambas herramientas:</p>
<div class="highlight"><pre style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-toml" data-lang="toml">drush config<span style="color:#a61717;background-color:#e3d2d2">-</span>delete <span style="color:#d14">&#34;migrate_plus.migration.taxonomy_google_sheet&#34;</span>
drupal config<span style="color:#a61717;background-color:#e3d2d2">:</span>delete active <span style="color:#d14">&#34;migrate_plus.migration.taxonomy_google_sheet&#34;</span>
</code></pre></div><p>Si preferimos la interfaz de Drupal, el módulo contribuido <a href="https://www.drupal.org/project/config_delete">Config Delete</a> 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:</p>
<pre><code>composer require drupal/config_delete
drush en config_delete -y
</code></pre><p>
<img src="/images/post/davidjguru_drupal_migrations_debugging_eight.png" alt="Acciones del módulo Config Delete" loading="lazy" decoding="async">

</p>
<p>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 <a href="https://www.drupal.org/project/config_update">Configuration Update Manager</a>.</p>
<h2 id="3-depuración-intermedia-con-migrate-devel">3. Depuración intermedia con Migrate Devel</h2>
<p>Hemos revisado los datos detenidamente y, aun así, la migración de términos desde Google Spreadsheet no parece funcionar.</p>
<p>Necesitamos 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.</p>
<h3 id="31-migrate-devel">3.1. Migrate Devel</h3>
<p><a href="https://www.drupal.org/project/migrate_devel">Migrate Devel</a> es un módulo contribuido que añade funcionalidades mediante opciones nuevas de Drush. Funciona con <em>migrate_tools</em> y <em>migrate_run</em>.</p>
<h5 id="actualización-del-03072020-versión-8x-20-alpha2">Actualización del 03/07/2020: versión 8.x-2.0-alpha2</h5>
<p>Justo cuando publiqué este artículo, <a href="https://twitter.com/MartianWebDev">Andrew Macpherson</a>, 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.</p>
<p>Para instalarla, la descargamos con Composer y la activamos con Drush: <a href="https://www.drupal.org/project/migrate_devel/releases/8.x-2.0-alpha2">Migrate Devel 8.x-2.0-alpha2</a>.</p>
<pre><code>composer 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
</code></pre><h5 id="para-versiones-anteriores-a-8x-20-alpha2">Para versiones anteriores a 8.x-2.0-alpha2</h5>
<p>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.</p>
<p>En la rama 8.x-1.x había una etiqueta publicada dos semanas antes: <a href="https://git.drupalcode.org/project/migrate_devel/-/tree/8.x-1.4">migrate_devel/tree/8.x-1.4</a>.</p>
<p>Para utilizar esas versiones con Drush 9 o superior, el parche de <a href="https://www.drupal.org/node/2938677">esta incidencia</a> resultaba prácticamente imprescindible. Aunque tampoco parecía definitivo, permitía ejecutar de forma controlada algunas funciones del módulo. Veremos sus aportaciones.</p>
<p>Podemos descargar el parche con wget y aplicarlo con git apply:</p>
<pre><code>cd /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
</code></pre><p>Otra posibilidad es añadirlo a la sección de parches de <code>composer.json</code>, si tenemos activada la extensión <a href="https://github.com/cweagans/composer-patches">composer-patches</a>.</p>
<p>La instalamos con:</p>
<pre><code>composer require cweagans/composer-patches
</code></pre><p>Y añadimos el parche a la sección <code>extra</code> de <code>composer.json</code>:</p>
<p>
<img src="/images/post/davidjguru_drupal_migrations_debugging_three.png" alt="Incorporación del parche a Composer" loading="lazy" decoding="async">

</p>
<h5 id="cómo-funciona">Cómo funciona</h5>
<p>Al ejecutar una migración con las opciones de Migrate Devel obtenemos valores por consola que podemos inspeccionar. Por ejemplo, con <strong>&ndash;migrate-debug</strong>:</p>
<p>
<img src="/images/post/davidjguru_drupal_migrations_debugging_four.png" alt="Primera parte de la salida de Migrate Devel" loading="lazy" decoding="async">



<img src="/images/post/davidjguru_drupal_migrations_debugging_five.png" alt="Segunda parte de la salida de Migrate Devel" loading="lazy" decoding="async">

</p>
<p>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:</p>
<p>
<img src="/images/post/davidjguru_drupal_migrations_debugging_six.png" alt="Salida de Migrate Devel y registro en la base de datos" loading="lazy" decoding="async">

</p>
<p>Vemos que al valor <code>1</code> de origen, en el primer array, se le asignó el identificador <code>117</code> 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.</p>
<p><strong>¿Y los suscriptores de eventos?</strong> Migrate Devel crea una clase que implementa <code>EventSubscriberInterface</code> y escucha eventos de Migrate API, disponibles en el módulo migrate del core:</p>
<pre><code>Called from +56
/var/www/html/web/modules/contrib/migrate_devel/src/EventSubscriber/MigrationEventSubscriber.php
</code></pre><p>La llamada procede de la clase que escucha estos eventos. En <a href="https://git.drupalcode.org/project/drupal/-/tree/9.0.x/core/modules/migrate/src/Event">modules/migrate/src/Event</a> se definen muchos, pero Migrate Devel presta atención especialmente a dos:</p>
<ol>
<li><a href="https://api.drupal.org/api/drupal/core%21modules%21migrate%21src%21Event%21MigratePostRowSaveEvent.php/class/MigratePostRowSaveEvent/8.2.x">MigratePostRowSaveEvent.php</a>.</li>
<li><a href="https://api.drupal.org/api/drupal/core%21modules%21migrate%21src%21Event%21MigratePreRowSaveEvent.php/8.6.x">MigratePreRowSaveEvent.php</a>.</li>
</ol>
<p>Estos corresponden a las dos opciones Drush del módulo, que originalmente acababan llamando a <code>dump()</code> 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:</p>
<pre><code>  /**
   * 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-&gt;getRow();

    $using_drush = function_exists('drush_get_option');
    if ($using_drush &amp;&amp; drush_get_option('migrate-debug-pre')) {
      // Start with capital letter for variables since this is actually a label.
      $Source = $row-&gt;getSource();
      $Destination = $row-&gt;getDestination();

      // We use kint directly here since we want to support variable naming.
      kint_require();
      \Kint::dump($Source, $Destination);
    }
  }
</code></pre><p>Ahora tenemos:</p>
<pre><code>  /**
   * 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-&gt;getRow();

    if (in_array('migrate-debug-pre', \Drush\Drush::config()-&gt;get('runtime.options'))) {
      // Start with capital letter for variables since this is actually a label.
      $Source = $row-&gt;getSource();
      $Destination = $row-&gt;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
      );
    }
  }
</code></pre><p>Puedes consultar los cambios en <a href="https://git.drupalcode.org/project/migrate_devel/-/blob/8.x-2.0-alpha2/src/EventSubscriber/MigrationEventSubscriber.php">MigrationEventSubscriber.php de 8.x-2.0-alpha2</a>.</p>
<p>También encontrarás más información sobre eventos y suscriptores en el artículo de The Russian Lullaby <a href="https://www.therussianlullaby.dev/blog/building-symfony-events-for-drupal/">Building Symfony events for Drupal</a>.</p>
<h3 id="32-el-plugin-de-proceso-debug">3.2. El plugin de proceso Debug</h3>
<p>Migrate Devel también incorpora un plugin de proceso llamado <code>debug</code>, definido en <code>Debug.php</code>. Lo encontrarás en <code>/web/modules/contrib/migrate_devel/src/Plugin/migrate/process/Debug.php</code>. Su documentación explica su responsabilidad:</p>
<div class="highlight"><pre style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-toml" data-lang="toml"><span style="color:#a61717;background-color:#e3d2d2">/**</span>
 <span style="color:#a61717;background-color:#e3d2d2">*</span> Debug the process pipeline.
 <span style="color:#a61717;background-color:#e3d2d2">*</span>
 <span style="color:#a61717;background-color:#e3d2d2">*</span> Prints the input value, assuming that you are running the migration from the
 <span style="color:#a61717;background-color:#e3d2d2">*</span> command line, and sends it to the next step in the pipeline unaltered.
 <span style="color:#a61717;background-color:#e3d2d2">*</span>
 <span style="color:#a61717;background-color:#e3d2d2">*</span> Available configuration keys<span style="color:#a61717;background-color:#e3d2d2">:</span>
 <span style="color:#a61717;background-color:#e3d2d2">*</span> <span style="color:#a61717;background-color:#e3d2d2">-</span> label<span style="color:#a61717;background-color:#e3d2d2">:</span> <span style="color:#a61717;background-color:#e3d2d2">(</span>optional<span style="color:#a61717;background-color:#e3d2d2">)</span> a string to print before the debug output. Include any
 <span style="color:#a61717;background-color:#e3d2d2">*</span>   trailing punctuation or space characters.
 <span style="color:#a61717;background-color:#e3d2d2">*</span> <span style="color:#a61717;background-color:#e3d2d2">-</span> multiple<span style="color:#a61717;background-color:#e3d2d2">:</span> <span style="color:#a61717;background-color:#e3d2d2">(</span>optional<span style="color:#a61717;background-color:#e3d2d2">)</span> set to TRUE to ask the next step in the process
 <span style="color:#a61717;background-color:#e3d2d2">*</span>   pipeline to process array values individually, like the multiple_values
 <span style="color:#a61717;background-color:#e3d2d2">*</span>   plugin from the Migrate Plus module.
</code></pre></div><p>El método <code>transform()</code>, heredado de la clase abstracta <code>ProcessPluginBase</code>, no transforma el valor: utiliza <code>print_r</code> de PHP para mostrar por consola tanto valores escalares como arrays.</p>
<p>Podemos 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 <code>process</code> de la migración de términos para revisar los valores que se están migrando.</p>
<p>Primero cambiaremos la estructura. Como sabemos por las entregas anteriores, esta forma:</p>
<div class="highlight"><pre style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-toml" data-lang="toml">process<span style="color:#a61717;background-color:#e3d2d2">:</span>
 name<span style="color:#a61717;background-color:#e3d2d2">:</span> name
 description<span style="color:#a61717;background-color:#e3d2d2">:</span> description
 path<span style="color:#a61717;background-color:#e3d2d2">:</span> url
 status<span style="color:#a61717;background-color:#e3d2d2">:</span> published
</code></pre></div><p>Utiliza implícitamente el plugin Get.php y equivale a esta otra:</p>
<div class="highlight"><pre style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-toml" data-lang="toml">process<span style="color:#a61717;background-color:#e3d2d2">:</span>
 name<span style="color:#a61717;background-color:#e3d2d2">:</span>
   plugin<span style="color:#a61717;background-color:#e3d2d2">:</span> get
   source<span style="color:#a61717;background-color:#e3d2d2">:</span> name
 description<span style="color:#a61717;background-color:#e3d2d2">:</span>
   plugin<span style="color:#a61717;background-color:#e3d2d2">:</span> get
   source<span style="color:#a61717;background-color:#e3d2d2">:</span> description
 path<span style="color:#a61717;background-color:#e3d2d2">:</span>
   plugin<span style="color:#a61717;background-color:#e3d2d2">:</span> get
   source<span style="color:#a61717;background-color:#e3d2d2">:</span> url
 status<span style="color:#a61717;background-color:#e3d2d2">:</span>
   plugin<span style="color:#a61717;background-color:#e3d2d2">:</span> get
   source<span style="color:#a61717;background-color:#e3d2d2">:</span> published
</code></pre></div><p>Añadimos Debug a la cadena con una etiqueta informativa para cada campo:</p>
<div class="highlight"><pre style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-toml" data-lang="toml">process<span style="color:#a61717;background-color:#e3d2d2">:</span>
 name<span style="color:#a61717;background-color:#e3d2d2">:</span>
   plugin<span style="color:#a61717;background-color:#e3d2d2">:</span> debug
   label<span style="color:#a61717;background-color:#e3d2d2">:</span> <span style="color:#d14">&#39;Processing name field value: &#39;</span>
   plugin<span style="color:#a61717;background-color:#e3d2d2">:</span> get
   source<span style="color:#a61717;background-color:#e3d2d2">:</span> name
 description<span style="color:#a61717;background-color:#e3d2d2">:</span>
   plugin<span style="color:#a61717;background-color:#e3d2d2">:</span> debug
   label<span style="color:#a61717;background-color:#e3d2d2">:</span> <span style="color:#d14">&#39;Processing description field value: &#39;</span>
   plugin<span style="color:#a61717;background-color:#e3d2d2">:</span> get
   source<span style="color:#a61717;background-color:#e3d2d2">:</span> description
 path<span style="color:#a61717;background-color:#e3d2d2">:</span>
   plugin<span style="color:#a61717;background-color:#e3d2d2">:</span> debug
   label<span style="color:#a61717;background-color:#e3d2d2">:</span> <span style="color:#d14">&#39;Processing path field value: &#39;</span>
   plugin<span style="color:#a61717;background-color:#e3d2d2">:</span> get
   source<span style="color:#a61717;background-color:#e3d2d2">:</span> url
 status<span style="color:#a61717;background-color:#e3d2d2">:</span>
   plugin<span style="color:#a61717;background-color:#e3d2d2">:</span> debug
   label<span style="color:#a61717;background-color:#e3d2d2">:</span> <span style="color:#d14">&#39;Processing status field value:  &#39;</span>
   plugin<span style="color:#a61717;background-color:#e3d2d2">:</span> get
   source<span style="color:#a61717;background-color:#e3d2d2">:</span> published
</code></pre></div><p>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:</p>
<pre><code>drush pmu migration_google_sheet &amp;&amp; drush en migration_google_sheet -y
</code></pre><p>Al ejecutarla de nuevo aparecerá información sobre los valores:</p>
<p>
<img src="/images/post/davidjguru_drupal_migrations_debugging_seven.png" alt="Información de los valores mostrada por Migrate Devel" loading="lazy" decoding="async">

</p>
<p>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:</p>
<pre><code> drush migrate-import --migrate-debug taxonomy_google_sheet --limit=&quot;1 items&quot;
</code></pre><p>Esto combina la salida posterior al almacenamiento —a diferencia de <code>--migrate-debug-pre</code>— con la información del plugin, los valores mostrados por la herramienta de volcado y el identificador final de la única entidad procesada.</p>
<p>Aquí 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.</p>
<p>Puedes revisar <a href="https://git.drupalcode.org/project/migrate_devel/-/blob/8.x-2.x/src/Plugin/migrate/process/Debug.php">el código de Debug.php</a> y <a href="https://www.drupal.org/node/3021648">la incidencia de Drupal.org en la que surgió la propuesta de este plugin</a>.</p>
<p>Con esta aproximación empezamos la subserie de depuración de migraciones. ¡Pronto, más experiencias!</p>
<h2 id="4-wq">4. :wq!</h2>
<h5 id="canción-recomendada">Canción recomendada</h5>


<div class="embed-responsive embed-responsive-16by9">
  <iframe
    class="embed-responsive-item"
    src="https://www.youtube-nocookie.com/embed/Z2tvlp7RnlM"
    title="YouTube video"
    loading="lazy"
    allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
    allowfullscreen></iframe>
</div>

]]></content:encoded></item><item><title>Migraciones en Drupal (III): migrar desde Google Spreadsheet</title><link>https://www.therussianlullaby.dev/es/blog/drupal-migrations-three-migrating-from-google-spreadsheet/</link><pubDate>Wed, 27 May 2020 00:00:00 +0000</pubDate><guid>https://www.therussianlullaby.dev/es/blog/drupal-migrations-three-migrating-from-google-spreadsheet/</guid><enclosure url="https://www.therussianlullaby.dev/images/post/davidjguru_drupal_migrations_debugging_main.jpg" type="image/jpeg"/><media:content url="https://www.therussianlullaby.dev/images/post/davidjguru_drupal_migrations_debugging_main.jpg" medium="image" type="image/jpeg"/><media:thumbnail url="https://www.therussianlullaby.dev/images/post/davidjguru_drupal_migrations_debugging_main.jpg"/><description>Migraciones en Drupal (III): migrar desde Google Spreadsheet</description><content:encoded><![CDATA[<p>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.</p>
<hr>
<p><strong>Foto de Unsplash, de <a href="https://unsplash.com/@zhenhappy">Pan Xiaozhen, @zhenhappy</a></strong></p>
<hr>
<p><strong>Índice</strong></p>
<span class="post-toc" aria-hidden="true"></span>

<!-- raw HTML omitted -->
<ul>
<li><a href="#1-introducci%C3%B3n-recordemos-los-procesos-etl">1. Introducción: recordemos los procesos ETL</a></li>
<li><a href="#2-exponer-datos-mediante-google-spreadsheet">2. Exponer datos mediante Google Spreadsheet</a></li>
<li><a href="#3-particularidades-de-la-transformaci%C3%B3n-a-json">3. Particularidades de la transformación a JSON</a></li>
<li><a href="#4-caracter%C3%ADsticas-de-la-migraci%C3%B3n">4. Características de la migración</a></li>
<li><a href="#5-m%C3%B3dulo-personalizado-para-la-migraci%C3%B3n">5. Módulo personalizado para la migración</a></li>
<li><a href="#6-wq">6. :wq!</a></li>
</ul>
<!-- raw HTML omitted -->
<h2 id="1-introducción-recordemos-los-procesos-etl">1. Introducción: recordemos los procesos ETL</h2>
<p>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.</p>
<p>En 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:</p>
<p>
<img src="/images/post/davidjguru_drupal_migrations_ETL_scheme.png" alt="Esquema ETL y visión general de Drupal Migrate API" loading="lazy" decoding="async">

</p>
<p>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:</p>
<p>
<img src="/images/post/davidjguru_drupal_migration_taxonomy_term_fields.png" alt="Campos básicos de un término de taxonomía" loading="lazy" decoding="async">

</p>
<p>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 <a href="/es/blog/drupal-migrations-two-examples/#migraciones-como-c%C3%B3digo-o-como-configuraci%C3%B3n">en el artículo anterior de la serie</a>.</p>
<h2 id="2-exponer-datos-mediante-google-spreadsheet">2. Exponer datos mediante Google Spreadsheet</h2>
<p>Para extraer los datos necesitamos una hoja cuyos valores estén expuestos. He creado <a href="https://docs.google.com/spreadsheets/d/1bKGbPbgeuXaBfcKetaDqoDimmYcerQY_hT1rqzw4TbM/edit?usp=sharing">esta hoja de cálculo de Google</a>, con columnas que corresponden a los campos de un término de taxonomía y están preparadas para la migración.</p>
<p>Para procesar la fuente utilizaremos el <a href="https://www.drupal.org/project/migrate_google_sheets">módulo contribuido Migrate Google Sheets</a>. Descárgalo con Composer y actívalo:</p>
<pre><code>composer require drupal/migrate_plus #If applicable
composer require drupal/migrate_google_sheets
drush en migrate_plus migrate_google_sheet -y
</code></pre><p>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.</p>
<ul>
<li>Primero extraemos el <strong>workbook-id</strong> de la dirección de la hoja: <code>docs.google.com/spreadsheets/d/1bKGbPbgeuXaBfcKetaDqoDimmYcerQY_hT1rqzw4TbM/</code>. En este caso, <strong>workbook-id: <code>1bKGbPbgeuXaBfcKetaDqoDimmYcerQY_hT1rqzw4TbM</code></strong>.</li>
<li>También necesitamos el <strong>worksheet-index</strong>, el índice de la pestaña que contiene los datos. Aquí es <strong>1</strong>.</li>
<li>Construimos la URL JSON con el patrón <strong><code>spreadsheets.google.com/feeds/list/[workbook-id]/[worksheet-index]/public/values?alt=json</code></strong>. Para nuestro ejemplo: <a href="http://spreadsheets.google.com/feeds/list/1bKGbPbgeuXaBfcKetaDqoDimmYcerQY_hT1rqzw4TbM/1/public/values?alt=json">URL de los valores en JSON</a>.</li>
</ul>
<p><strong>¡Eso es todo!</strong> La hoja queda expuesta como un archivo JSON y podemos obtener los valores mediante los plugins de origen elegidos para Migrate API.</p>
<p>
<img src="/images/post/davidjguru_drupal_migrations_Google_Spreadsheet_source.png" alt="Datos de Google Spreadsheet en formato JSON" loading="lazy" decoding="async">

</p>
<h2 id="3-particularidades-de-la-transformación-a-json">3. Particularidades de la transformación a JSON</h2>
<p>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:</p>
<pre><code>      {
        &quot;gsx$id&quot;: {
          &quot;$t&quot;: &quot;1&quot;
        },
        &quot;gsx$name&quot;: {
          &quot;$t&quot;: &quot;Term 1&quot;
        },
        &quot;gsx$description&quot;: {
          &quot;$t&quot;: &quot;Descrip 1&quot;
        },
        &quot;gsx$url&quot;: {
          &quot;$t&quot;: &quot;/t1&quot;
        }
        &quot;gsx$published&quot;: {
          &quot;$t&quot;: &quot;1&quot;
        }
      }
</code></pre><p>Algunos elementos han cambiado respecto al formato original. Fíjate en la comparación:</p>
<p>
<img src="/images/post/davidjguru_drupal_migration_google_spreadsheet_json.png" alt="Comparación de los formatos de datos" loading="lazy" decoding="async">

</p>
<p>La transformación a JSON introduce cambios respecto a la hoja. Por ejemplo, Google modifica los identificadores de los campos añadiendo prefijos:</p>
<ul>
<li><code>gsx$</code> para los nombres de campo.</li>
<li><code>$t</code> para los valores almacenados en los campos.</li>
</ul>
<p>Las cabeceras cambian así:</p>
<ol>
<li><code>Id</code> pasa a ser <code>gsx$id</code>.</li>
<li><code>Name</code> pasa a ser <code>gsx$name</code>.</li>
<li><code>Description</code> pasa a ser <code>gsx$description</code>.</li>
<li><code>Url</code> pasa a ser <code>gsx$url</code>.</li>
<li><code>Published</code> pasa a ser <code>gsx$published</code>.</li>
</ol>
<p>Como estos cambios son estables, el plugin se encarga de procesarlos:</p>
<pre><code>// Class GoogleSheets.php
// Module migrate_google_sheets
// @see https://git.drupalcode.org/project/migrate_google_sheets/

// Actual values are stored in gsx$&lt;field&gt;['$t'].
$this-&gt;currentItem[$field_name] = $current['gsx$' . $selector]['$t'];
</code></pre><p>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 <code>feed/entry</code>, que el módulo también resuelve:</p>
<pre><code>// For Google Sheets, the actual row data lives under feed-&gt;entry.
if (isset($array['feed']) &amp;&amp; isset($array['feed']['entry'])) {
 $array = $array['feed']['entry'];
}
</code></pre><h2 id="4-características-de-la-migración">4. Características de la migración</h2>
<p>La primera observación es que el plugin de Google Spreadsheet extiende el analizador JSON, como vemos en la clase:</p>
<pre><code>use 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 = &quot;google_sheets&quot;,
 *   title = @Translation(&quot;Google Sheets&quot;)
 * )
 */
class GoogleSheets extends JSON implements ContainerFactoryPluginInterface {
...
}
</code></pre><p>Su bloque de anotaciones lo identifica como un <strong>Data Parser</strong>, así que necesitamos más piezas: un plugin de origen y un <strong>Data Fetcher</strong>. El plugin de Google Spreadsheet será la tercera pieza del proceso.</p>
<p>¿Qué plugins de origen tenemos disponibles en nuestra instalación Drupal? Vamos a consultarlos:</p>
<pre><code>drupal debug:plugin migrate.source
</code></pre><p>La consola mostrará:</p>
<pre><code>drupal@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$
</code></pre><p>Podemos utilizar la clase Url.php como plugin de origen, ya que el archivo se expone mediante una URL. En <a href="https://git.drupalcode.org/project/migrate_plus/-/blob/8.x-5.x/src/Plugin/migrate/source/Url.php">la clase Url.php</a> vemos que necesita un analizador de datos; en nuestro caso será la clase de Google Spreadsheet:</p>
<pre><code>  /**
   * The data parser plugin.
   *
   * @var \Drupal\migrate_plus\DataParserPluginInterface
   */
  protected $dataParserPlugin;
</code></pre><p>Al buscar un componente que recupere los datos encontramos <a href="https://git.drupalcode.org/project/migrate_plus/-/blob/8.x-5.x/src/Plugin/migrate_plus/data_fetcher/Http.php">la clase Http.php</a>, preparada para trabajar con el plugin de origen URL mediante HTTP:</p>
<pre><code>/**
 * 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 = &quot;http&quot;,
 *   title = @Translation(&quot;HTTP&quot;)
 * )
 */
class Http extends DataFetcherPluginBase implements ContainerFactoryPluginInterface {
</code></pre><p>Por otra parte, el constructor de Url.php recibe las direcciones de origen. Podemos proporcionar una URL o un conjunto de ellas:</p>
<pre><code>  /**
   * {@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-&gt;sourceUrls = $configuration['urls'];
  }
</code></pre><p>Al reunir las piezas, debemos darles una estructura según su función y posición. Nuestro bloque de origen quedará así:</p>
<pre><code>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'
</code></pre><h2 id="5-módulo-personalizado-para-la-migración">5. Módulo personalizado para la migración</h2>
<p>Crearemos un módulo llamado <code>migration_google_sheet</code> con esta estructura:</p>
<pre><code>/project/web/modules/custom/
                     \__migration_google_sheet/
                         \__migration_google_sheet.info.yml
                           \__config/
                                \__install/
                                     \__migrate_plus.migration.migration.taxonomy_google_sheet.yml
</code></pre><p>Archivo de definición de la migración: <code>migrate_plus.migration.taxonomy_google_sheet.yml</code>.</p>
<pre><code>langcode: 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
</code></pre><p>Ejecutamos la migración desde la consola:</p>
<pre><code>drush 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'
</code></pre><p>¡Hecho! Hemos migrado treinta términos de taxonomía desde una hoja de Google:</p>
<p>
<img src="/images/post/davidjguru_drupal_migration_migrating_taxonomy_terms.png" alt="Términos de taxonomía recién migrados" loading="lazy" decoding="async">

</p>
<p>Puedes descargar o clonar el módulo personalizado <a href="https://gitlab.com/davidjguru/drupal-custom-modules-examples/-/tree/master/Migrations">desde mi repositorio de GitLab</a>.</p>
<h2 id="6-wq">6. :wq!</h2>
<h5 id="canción-recomendada">Canción recomendada</h5>


<div class="embed-responsive embed-responsive-16by9">
  <iframe
    class="embed-responsive-item"
    src="https://www.youtube-nocookie.com/embed/je0lFe0MHjU"
    title="YouTube video"
    loading="lazy"
    allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
    allowfullscreen></iframe>
</div>

]]></content:encoded></item><item><title>Migraciones en Drupal (II): ejemplos</title><link>https://www.therussianlullaby.dev/es/blog/drupal-migrations-two-examples/</link><pubDate>Tue, 17 Mar 2020 00:00:00 +0000</pubDate><guid>https://www.therussianlullaby.dev/es/blog/drupal-migrations-two-examples/</guid><enclosure url="https://www.therussianlullaby.dev/images/post/davidjguru_drupal_thinking_about_drupal_migrations_examples_main.jpg" type="image/jpeg"/><media:content url="https://www.therussianlullaby.dev/images/post/davidjguru_drupal_thinking_about_drupal_migrations_examples_main.jpg" medium="image" type="image/jpeg"/><media:thumbnail url="https://www.therussianlullaby.dev/images/post/davidjguru_drupal_thinking_about_drupal_migrations_examples_main.jpg"/><description>Migraciones en Drupal (II): ejemplos con datos embebidos y archivos CSV, plugins y gestión como código o configuración.</description><content:encoded><![CDATA[<p>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.</p>
<p>En 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.</p>
<p>Hay 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.</p>
<hr>
<p><strong>Foto de Unsplash, de <a href="https://unsplash.com/@emileseguin">Émile Séguin, @emileseguin</a></strong></p>
<p><strong>Índice</strong></p>
<span class="post-toc" aria-hidden="true"></span>

<!-- raw HTML omitted -->
<ul>
<li><a href="#1-introducci%C3%B3n">1. Introducción</a></li>
<li><a href="#2-preparativos">2. Preparativos</a></li>
<li><a href="#3-enfoques">3. Enfoques</a></li>
<li><a href="#4-migraciones">4. Migraciones</a></li>
<li><a href="#5-conceptos-clave">5. Conceptos clave</a></li>
<li><a href="#6-recursos">6. Recursos</a></li>
<li><a href="#7-wq">7. :wq!</a></li>
</ul>
<!-- raw HTML omitted -->
<h2 id="1-introducción">1. Introducción</h2>
<p><a href="https://www.drupal.org/docs/8/api/migrate-api">La API de migraciones de Drupal</a> 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.</p>
<p>Para este artículo me había propuesto modelar dos procesos de migración diferentes siguiendo una idea que podría resumirse como <strong>«primum vivere, deinde philosophari»</strong>: primero experimentar y después teorizar. Por eso he decidido organizarlo de una manera particular:</p>
<ul>
<li>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.</li>
<li>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.</li>
</ul>
<p>En los siguientes pasos trabajaremos con dos experiencias concretas:</p>
<ol>
<li><strong>Migrar datos desde un formato embebido</strong>, quizá el ejemplo más sencillo de una migración en Drupal.</li>
<li><strong>Migrar datos desde un archivo CSV</strong>, un caso algo más complejo que el anterior.</li>
</ol>
<p>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.</p>
<h2 id="2-preparativos">2. Preparativos</h2>
<h3 id="primer-caso-migrar-datos-embebidos">Primer caso: migrar datos embebidos</h3>
<p>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.</p>
<p>Entre las opciones disponibles elegiremos <a href="https://www.drupal.org/project/migrate_run">migrate_run</a>, 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.</p>
<p>La 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.</p>
<p>Conviene 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, <a href="https://www.drupal.org/project/migrate_run/releases/8.x-1.0-alpha4">como la Alpha 4, que todavía estaba preparada para Drush 8</a>.</p>
<p><strong>Con Composer y Drush:</strong></p>
<pre><code>composer require drupal/migrate_run
drush pmu migrate_tools # If you need
drush en migrate migrate_run -y
drush cr
</code></pre><p><strong>Con Drupal Console:</strong></p>
<pre><code>composer require drupal/migrate_run
drupal mou migrate_tools # If you need
drupal moi migrate migrate_run
</code></pre><p>En la ruta <code>/admin/modules</code> verás lo siguiente:</p>
<p>
<img src="/images/post/davidjguru_drupal_migrations_examples_modules.png" alt="Activación de los módulos Migrate y Migrate Run" loading="lazy" decoding="async">

</p>
<h4 id="construcción-de-los-recursos">Construcción de los recursos</h4>
<p>Crearemos un módulo personalizado para nuestra primera migración:</p>
<pre><code>cd project/web/modules/custom
mkdir migration_basic_module
</code></pre><p>Después, el archivo <code>migration_basic_module.info.yml</code>, con este contenido:</p>
<div class="highlight"><pre style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-toml" data-lang="toml">name<span style="color:#a61717;background-color:#e3d2d2">:</span> <span style="color:#d14">&#39;Migration Basic Module&#39;</span>
type<span style="color:#a61717;background-color:#e3d2d2">:</span> module
description<span style="color:#a61717;background-color:#e3d2d2">:</span> <span style="color:#d14">&#39;Just a basic example of basic migration process.&#39;</span>
package<span style="color:#a61717;background-color:#e3d2d2">:</span> <span style="color:#d14">&#39;Migrations Examples 2000&#39;</span>
core<span style="color:#a61717;background-color:#e3d2d2">:</span> <span style="color:#099">8</span>.x
dependencies<span style="color:#a61717;background-color:#e3d2d2">:</span>
  <span style="color:#a61717;background-color:#e3d2d2">-</span> drupal<span style="color:#a61717;background-color:#e3d2d2">:</span>migrate
</code></pre></div><p>Crea el archivo de definición de la migración en la ruta <code>/migration_basic_module/migrations/basic_migration_one.yml</code>.</p>
<p>Nuestro archivo declarativo <code>basic_migration_one.yml</code> 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:</p>
<ol>
<li>Un título: una cadena de texto.</li>
<li>Un cuerpo: un texto basado en el generador ChiquitoIpsum*, <a href="http://www.chiquitoipsum.com">http://www.chiquitoipsum.com</a>.</li>
</ol>
<p>*<a href="https://en.wikipedia.org/wiki/Chiquito_de_la_Calzada">Chiquito de la Calzada</a> fue una figura popular en España, un humorista legendario.</p>
<p><strong>basic_migration_one.yml</strong></p>
<div class="highlight"><pre style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-toml" data-lang="toml">id<span style="color:#a61717;background-color:#e3d2d2">:</span> basic_migration_one
label<span style="color:#a61717;background-color:#e3d2d2">:</span> <span style="color:#d14">&#39;Custom Basic Migration 2000&#39;</span>
source<span style="color:#a61717;background-color:#e3d2d2">:</span>
  plugin<span style="color:#a61717;background-color:#e3d2d2">:</span> embedded_data
  data_rows<span style="color:#a61717;background-color:#e3d2d2">:</span>
    <span style="color:#a61717;background-color:#e3d2d2">-</span>
      unique_id<span style="color:#a61717;background-color:#e3d2d2">:</span> <span style="color:#099">1</span>
      page_title<span style="color:#a61717;background-color:#e3d2d2">:</span> <span style="color:#d14">&#39;Title for migrated node - One&#39;</span>
      page_content<span style="color:#a61717;background-color:#e3d2d2">:</span> <span style="color:#d14">&#39;Lorem fistrum mamaar se calle ustée tiene musho pelo.&#39;</span>
    <span style="color:#a61717;background-color:#e3d2d2">-</span>
      unique_id<span style="color:#a61717;background-color:#e3d2d2">:</span> <span style="color:#099">2</span>
      page_title<span style="color:#a61717;background-color:#e3d2d2">:</span> <span style="color:#d14">&#39;Title for migrated node - Two&#39;</span>
      page_content<span style="color:#a61717;background-color:#e3d2d2">:</span> <span style="color:#d14">&#39;Se calle ustée caballo blanco caballo negroorl.&#39;</span>
  ids<span style="color:#a61717;background-color:#e3d2d2">:</span>
    unique_id<span style="color:#a61717;background-color:#e3d2d2">:</span>
      type<span style="color:#a61717;background-color:#e3d2d2">:</span> integer
process<span style="color:#a61717;background-color:#e3d2d2">:</span>
  title<span style="color:#a61717;background-color:#e3d2d2">:</span> article_title
  body<span style="color:#a61717;background-color:#e3d2d2">:</span> article_content
destination<span style="color:#a61717;background-color:#e3d2d2">:</span>
  plugin<span style="color:#a61717;background-color:#e3d2d2">:</span> <span style="color:#d14">&#39;entity:node&#39;</span>
  default_bundle<span style="color:#a61717;background-color:#e3d2d2">:</span> page
</code></pre></div><p>Esta será la estructura del nuevo módulo personalizado del ejemplo de migración básica:</p>
<pre><code>/project/web/modules/custom/
                     \__migration_basic_module/
                         \__migration_basic_module.info.yml
                             \__migrations/
                                 \__basic_migration_one.yml
</code></pre><p><strong>Activación de todos los módulos necesarios con Drush:</strong></p>
<pre><code>drush pm:enable -y migrate migrate_run migration_basic_module
drush cr
</code></pre><p><strong>O con Drupal Console:</strong></p>
<pre><code>drupal moi migrate migrate_run migration_basic_module
</code></pre><h3 id="segundo-caso-migrar-desde-archivos-csv">Segundo caso: migrar desde archivos CSV</h3>
<p>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, <a href="https://www.drupal.org/project/migrate_source_csv">migrate_source_csv</a>. En su versión 3.x, este módulo utiliza league/csv para procesar los archivos CSV. Vamos allá, con Composer y Drush:</p>
<pre><code>composer 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
</code></pre><p>Ahora, en la ruta <code>/admin/modules/</code>:</p>
<p>
<img src="/images/post/davidjguru_drupal_migrations_csv_source_modules.png" alt="Activación de Migrate, Migrate Plus y Migrate Tools" loading="lazy" decoding="async">

</p>
<h4 id="construcción-de-los-recursos-1">Construcción de los recursos</h4>
<p>Crearemos otro módulo personalizado para la segunda migración:</p>
<pre><code>cd project/web/modules/custom
mkdir migration_csv_module
</code></pre><p>Con un nuevo archivo <code>migration_csv_module.info.yml</code>:</p>
<div class="highlight"><pre style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-toml" data-lang="toml">name<span style="color:#a61717;background-color:#e3d2d2">:</span> <span style="color:#d14">&#39;Migration CSV Module&#39;</span>
type<span style="color:#a61717;background-color:#e3d2d2">:</span> module
description<span style="color:#a61717;background-color:#e3d2d2">:</span> <span style="color:#d14">&#39;Just a basic example of basic migration process with a CSV source.&#39;</span>
package<span style="color:#a61717;background-color:#e3d2d2">:</span> <span style="color:#d14">&#39;Migrations Examples 2000&#39;</span>
core<span style="color:#a61717;background-color:#e3d2d2">:</span> <span style="color:#099">8</span>.x
dependencies<span style="color:#a61717;background-color:#e3d2d2">:</span>
  <span style="color:#a61717;background-color:#e3d2d2">-</span> drupal<span style="color:#a61717;background-color:#e3d2d2">:</span>migrate
  <span style="color:#a61717;background-color:#e3d2d2">-</span> drupal<span style="color:#a61717;background-color:#e3d2d2">:</span>migrate_tools
  <span style="color:#a61717;background-color:#e3d2d2">-</span> drupal<span style="color:#a61717;background-color:#e3d2d2">:</span>migrate_plus
</code></pre></div><p>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 <code>/migration_csv_module/config/install/</code>.</p>
<p>La estructura tendrá este aspecto:</p>
<pre><code>/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
</code></pre><p>Necesitamos un CSV con los datos originales que vamos a migrar. Podemos generarlo fácilmente con herramientas web como <a href="https://www.mockaroo.com">Mockaroo, un generador de datos aleatorios bastante bueno</a>. He creado un CSV con los campos <code>id</code>, <code>title</code>, <code>body</code>, <code>tags</code> e <code>image</code>. Los recursos relacionados se mantienen ahora en <a href="https://gitlab.com/users/davidjguru/snippets">mis snippets de GitLab</a>.</p>
<p>Este 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 <code>/csv</code>:</p>
<p>
<img src="/images/post/davidjguru_drupal_csv_migrate_module.png" alt="Estructura del módulo de migración CSV" loading="lazy" decoding="async">

</p>
<p>Y ahora, nuestro archivo <code>migrate_plus.migration.article_csv_import.yml</code>. En apartados posteriores explicaremos su construcción y sus secciones:</p>
<div class="highlight"><pre style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-toml" data-lang="toml">uuid<span style="color:#a61717;background-color:#e3d2d2">:</span> <span style="color:#099">1</span>bcec3e7<span style="color:#099">-0</span>a49<span style="color:#099">-4473-87</span>a2<span style="color:#099">-6</span>dca09b91aba
langcode<span style="color:#a61717;background-color:#e3d2d2">:</span> en
status<span style="color:#a61717;background-color:#e3d2d2">:</span> <span style="color:#000;font-weight:bold">true</span>
dependencies<span style="color:#a61717;background-color:#e3d2d2">:</span> <span style="color:#a61717;background-color:#e3d2d2">{</span>  <span style="color:#a61717;background-color:#e3d2d2">}</span>
id<span style="color:#a61717;background-color:#e3d2d2">:</span> article_csv_import
label<span style="color:#a61717;background-color:#e3d2d2">:</span> <span style="color:#d14">&#39;Migrating articles&#39;</span>
source<span style="color:#a61717;background-color:#e3d2d2">:</span>
  plugin<span style="color:#a61717;background-color:#e3d2d2">:</span> csv
  path<span style="color:#a61717;background-color:#e3d2d2">:</span> modules<span style="color:#a61717;background-color:#e3d2d2">/</span>custom<span style="color:#a61717;background-color:#e3d2d2">/</span>migration_csv_module<span style="color:#a61717;background-color:#e3d2d2">/</span>csv<span style="color:#a61717;background-color:#e3d2d2">/</span>migration_csv_articles.csv
  delimiter<span style="color:#a61717;background-color:#e3d2d2">:</span> <span style="color:#d14">&#39;,&#39;</span>
  enclosure<span style="color:#a61717;background-color:#e3d2d2">:</span> <span style="color:#d14">&#39;&#34;&#39;</span>
  header_offset<span style="color:#a61717;background-color:#e3d2d2">:</span> <span style="color:#099">0</span>
  ids<span style="color:#a61717;background-color:#e3d2d2">:</span>
    <span style="color:#a61717;background-color:#e3d2d2">-</span> id
  fields<span style="color:#a61717;background-color:#e3d2d2">:</span>
    <span style="color:#a61717;background-color:#e3d2d2">-</span>
      name<span style="color:#a61717;background-color:#e3d2d2">:</span> id
      label<span style="color:#a61717;background-color:#e3d2d2">:</span> <span style="color:#d14">&#39;Unique Id&#39;</span>
    <span style="color:#a61717;background-color:#e3d2d2">-</span>
      name<span style="color:#a61717;background-color:#e3d2d2">:</span> title
      label<span style="color:#a61717;background-color:#e3d2d2">:</span> Title
    <span style="color:#a61717;background-color:#e3d2d2">-</span>
      name<span style="color:#a61717;background-color:#e3d2d2">:</span> body
      label<span style="color:#a61717;background-color:#e3d2d2">:</span> <span style="color:#d14">&#39;Post Body&#39;</span>
    <span style="color:#a61717;background-color:#e3d2d2">-</span>
      name<span style="color:#a61717;background-color:#e3d2d2">:</span> tags
      label<span style="color:#a61717;background-color:#e3d2d2">:</span> <span style="color:#d14">&#39;Taxonomy Tag&#39;</span>
    <span style="color:#a61717;background-color:#e3d2d2">-</span>
      name<span style="color:#a61717;background-color:#e3d2d2">:</span> image
      label<span style="color:#a61717;background-color:#e3d2d2">:</span> <span style="color:#d14">&#39;Image Field&#39;</span>
process<span style="color:#a61717;background-color:#e3d2d2">:</span>
  title<span style="color:#a61717;background-color:#e3d2d2">:</span> title
  body<span style="color:#a61717;background-color:#e3d2d2">:</span> body
  tags<span style="color:#a61717;background-color:#e3d2d2">:</span> field_tags
  image<span style="color:#a61717;background-color:#e3d2d2">:</span> field_image
  type<span style="color:#a61717;background-color:#e3d2d2">:</span>
    plugin<span style="color:#a61717;background-color:#e3d2d2">:</span> default_value
    default_value<span style="color:#a61717;background-color:#e3d2d2">:</span> article
destination<span style="color:#a61717;background-color:#e3d2d2">:</span>
  plugin<span style="color:#a61717;background-color:#e3d2d2">:</span> <span style="color:#d14">&#39;entity:node&#39;</span>
</code></pre></div><p>Ya tenemos todos los recursos necesarios para crear la nueva migración. Veamos cómo vamos a abordar el proceso.</p>
<h2 id="3-enfoques">3. Enfoques</h2>
<p>Describiremos los diferentes enfoques que aplicaremos a nuestros ejemplos para entenderlos mejor.</p>
<h3 id="primer-caso-migrar-datos-embebidos-1">Primer caso: migrar datos embebidos</h3>
<p>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.</p>
<p>Ademá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.</p>
<p>Para 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.</p>
<h3 id="segundo-caso-migrar-desde-archivos-csv-1">Segundo caso: migrar desde archivos CSV</h3>
<p>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.</p>
<p>Vamos a llevar la migración a otro lugar y ejecutarla desde allí. Sorpresa. Visita la ruta <code>/admin/config/development/configuration/single/import</code> de tu instalación Drupal. ¡Nos vemos allí!</p>
<h2 id="4-migraciones">4. Migraciones</h2>
<h3 id="primer-caso-migrar-datos-embebidos-2">Primer caso: migrar datos embebidos</h3>
<h4 id="consultar-las-migraciones-disponibles">Consultar las migraciones disponibles</h4>
<pre><code>drush migrate:status
drush ms

Output from console:
----------------- -------- ------- ---------- ------------- ---------------------
  Migration ID      Status   Total   Imported   Unprocessed   Last Imported
----------------- -------- ------- ---------- ------------- ---------------------
basic_migration_one   Idle     2       0          2
----------------- -------- ------- ---------- ------------- ---------------------
</code></pre><h4 id="ejecutar-las-migraciones">Ejecutar las migraciones</h4>
<pre><code>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
----------------- -------- ------- ---------- ------------- -------------------
</code></pre><p>Al visitar <code>/admin/content</code> verás los dos nodos nuevos:</p>
<p>
<img src="/images/post/davidjguru_drupal_migrations_basic_migration_embedded.png" alt="Migración básica de datos embebidos en Drupal" loading="lazy" decoding="async">

</p>
<h4 id="revertir-las-migraciones-deshacer-la-importación">Revertir las migraciones: deshacer la importación</h4>
<pre><code>drush migrate:rollback basic_migration_one
drush mr basic_migration_one

Output from console:

[notice] Rolled back 2 items - done with 'basic_migration_one'
</code></pre><p>
<img src="/images/post/davidjguru_drupal_migrations_basic_migration_command.png" alt="Comandos de la migración básica de Drupal" loading="lazy" decoding="async">

</p>
<h3 id="segundo-caso-migrar-desde-archivos-csv-2">Segundo caso: migrar desde archivos CSV</h3>
<p>En la ruta <code>/admin/config/development/configuration/single/import</code> importaremos el archivo de definición de nuestra migración personalizada.</p>
<h4 id="cargar-la-configuración-de-la-migración">Cargar la configuración de la migración</h4>
<p>Ve a Importar → Elemento individual, selecciona «Migration» como tipo de configuración y pega el contenido del archivo original de la migración:</p>
<p>
<img src="/images/post/davidjguru_drupal_migration_load_file_by_config.png" alt="Carga de la definición de migración mediante configuración" loading="lazy" decoding="async">

</p>
<p>Pulsa «Importar» y se creará el nuevo objeto en el sistema de configuración.</p>
<p><strong>¿Y ahora?</strong></p>
<h4 id="ejecutar-la-migración">Ejecutar la migración</h4>
<p>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 <code>drush migrate:status</code>:</p>
<p>
<img src="/images/post/davidjguru_drupal_migration_ask_for_migrations.png" alt="Estado de las migraciones con Drush" loading="lazy" decoding="async">

</p>
<p>Puedes ejecutar la migración con <code>drush migrate-import article_csv_import</code>. 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.</p>
<h4 id="drush-cex--drush-cim">Drush cex / Drush cim</h4>
<p>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 <code>drush cex</code>:</p>
<p>
<img src="/images/post/davidjguru_drupal_migration_drush_cex.png" alt="Exportación de configuración con Drush cex" loading="lazy" decoding="async">

</p>
<p>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 <code>migrate_plus.migration</code> o añadir un archivo para el grupo, que sirve para agrupar procesos de migración.</p>
<p>¿Recuerdas el nombre del archivo? Es el mismo que utilizábamos en <code>/config/install</code>: <code>migrate_plus.migration.article_csv_import.yml</code>. Hemos realizado el mismo proceso, pero desde otra dirección. ¿Te impresiona? ¿No? ¿Al menos te parece interesante?</p>
<p>Recuerda también que puedes utilizar <code>drush cim</code> 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.</p>
<p>Así hemos migrado unos 102 nodos nuevos utilizando dos enfoques y metodologías diferentes. No está mal.</p>
<h2 id="5-conceptos-clave">5. Conceptos clave</h2>
<h3 id="plugins-de-migración">Plugins de migración</h3>
<p>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.</p>
<p>Cada 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.</p>
<p>Vamos a revisar los plugins que intervienen en estas dos migraciones a partir de sus archivos de definición.</p>
<h4 id="migración-básica-con-datos-embebidos">Migración básica con datos embebidos</h4>
<pre><code>source:
  plugin: embedded_data
  data_rows:
         ...
process:
  title: creative_title
  body: engaging_content
destination:
  plugin: 'entity:node'
  default_bundle: page
</code></pre><p>Para extraer los datos utilizamos el plugin Embedded Data, una clase PHP disponible en <code>/web/core/modules/migrate/src/Plugin/migrate/source/EmbeddedDataSource.php</code>. En su bloque de documentación puedes ver algunas claves de configuración que puedes utilizar en la definición:</p>
<pre><code> *
 * Available configuration keys
 * - data_rows: The source data array.
 * - ids: The unique ID field of the data.
 *
</code></pre><p>Precisamente <code>data_rows</code> e <code>ids</code> son las claves que utilizamos: el array de datos de origen y su identificador único. <a href="https://api.drupal.org/api/drupal/core%21modules%21migrate%21src%21Plugin%21migrate%21source%21EmbeddedDataSource.php/class/EmbeddedDataSource/8.8.x">Puedes leer más sobre la clase EmbeddedDataSource en la documentación de la API de Drupal.org</a>.</p>
<p>Ahora fíjate en el bloque <code>process</code> y busca… <strong>¿dónde está el plugin de procesamiento?</strong> 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.</p>
<p>Ese plugin implícito es la clase Get, la solución básica para procesar campos. La encontrarás en <code>/web/core/modules/migrate/src/Plugin/migrate/process/Get.php</code>. <a href="https://api.drupal.org/api/drupal/core%21modules%21migrate%21src%21Plugin%21migrate%21process%21Get.php/class/Get">Más información sobre Get.php en la API de Drupal.org</a>. En otras palabras, escribir esto:</p>
<pre><code>process:
  title: page_title
</code></pre><p>Equivale a escribir esto otro:</p>
<pre><code>process:
  title:
    plugin: get
    source: page_title
</code></pre><p>Así la vida resulta un poco más sencilla, ¿verdad? <strong>Recuerda: si no declaras un plugin de procesamiento para un campo, Drupal aplicará Get por defecto.</strong></p>
<p>Por último, en el destino utilizamos el plugin de entidad con el parámetro <code>node</code> para crear nodos del tipo <code>page</code>. Esto recurre al plugin de destino Entity.php, una clase abstracta ubicada en <code>web/core/modules/migrate/src/Plugin/migrate/destination/Entity.php</code>, y a su plugin derivado. Puedes leer más sobre <a href="https://www.drupal.org/docs/8/api/plugin-api/plugin-derivatives">los plugins derivados en Drupal</a>, <a href="https://api.drupal.org/api/drupal/core%21modules%21migrate%21src%21Plugin%21migrate%21destination%21Entity.php/8.8.x">el plugin de destino Entity.php</a> y <a href="https://api.drupal.org/api/drupal/core%21modules%21migrate%21src%21Plugin%21Derivative%21MigrateEntity.php/class/MigrateEntity/8.8.x">la clase derivadora de migraciones</a>.</p>
<h4 id="migración-con-un-csv-como-fuente-de-datos">Migración con un CSV como fuente de datos</h4>
<p>Creo que en este caso revisar los plugins resulta más sencillo e intuitivo:</p>
<pre><code>source:
  plugin: csv
 ...
 process:
 ...
   type:
     plugin: default_value
     default_value: article
 destination:
   plugin: 'entity:node'
</code></pre><p>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.</p>
<h3 id="migraciones-como-código-o-como-configuración">Migraciones como código o como configuración</h3>
<p>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 <code>config/install</code>, la instalación puede crear un objeto de configuración.</p>
<p>En 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:</p>
<ul>
<li>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.</li>
<li>Según el enfoque, cambian la ubicación de los archivos y el flujo de trabajo.</li>
<li>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.</li>
<li>Como configuración, puedes modificar la definición desde la interfaz de sincronización de Drupal, en <code>/admin/config/development/configuration</code>, y utilizar las herramientas de exportación e importación <code>drush cex</code> y <code>drush cim</code>. 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.</li>
<li>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:</li>
</ul>
<div class="highlight"><pre style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-toml" data-lang="toml">  dependencies<span style="color:#a61717;background-color:#e3d2d2">:</span>
    enforced<span style="color:#a61717;background-color:#e3d2d2">:</span>
      module<span style="color:#a61717;background-color:#e3d2d2">:</span>
        <span style="color:#a61717;background-color:#e3d2d2">-</span> my_own_migration_custom_module
</code></pre></div><ul>
<li>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 <code>8-4-4-4-12</code>. ¡Genera el tuyo! El original utiliza como ejemplo ilustrativo <code>uuid: cacafuti-1a23-2b45-3c67-4d567890a1b2</code>; contiene caracteres no hexadecimales, por lo que debes sustituirlo por un UUID válido.</li>
</ul>
<hr>
<h2 id="6-recursos">6. Recursos</h2>
<p>Descarga, prueba y experimenta con los recursos utilizados en el artículo. Los recursos relacionados se mantienen ahora en <a href="https://gitlab.com/users/davidjguru/snippets">mis snippets de GitLab</a>.</p>
<ol>
<li>Archivo de migración básica.</li>
<li>Archivo de migración CSV.</li>
<li>CSV de origen con datos aleatorios.</li>
<li>Código de los dos módulos de migración, básico y CSV, referenciado desde <a href="https://gitlab.com/davidjguru">mi perfil de GitLab</a>. Este será un repositorio central para los módulos de la serie, con los dos ejemplos:
<ul>
<li>Primera migración básica con datos embebidos.</li>
<li>Segunda migración básica con un CSV como fuente de datos.</li>
</ul>
</li>
<li>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: <a href="https://gitlab.com/snippets/1916815">Drupal Migrations Tips (I): Creating a new basic migration structure</a>.</li>
</ol>
<h2 id="7-wq">7. :wq!</h2>
<h5 id="canción-recomendada">Canción recomendada</h5>


<div class="embed-responsive embed-responsive-16by9">
  <iframe
    class="embed-responsive-item"
    src="https://www.youtube-nocookie.com/embed/8hEYwk0bypY"
    title="YouTube video"
    loading="lazy"
    allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
    allowfullscreen></iframe>
</div>

]]></content:encoded></item><item><title>Migraciones en Drupal (I): recursos básicos</title><link>https://www.therussianlullaby.dev/es/blog/drupal-migrations-one-basic-resources/</link><pubDate>Tue, 25 Feb 2020 00:00:00 +0000</pubDate><guid>https://www.therussianlullaby.dev/es/blog/drupal-migrations-one-basic-resources/</guid><enclosure url="https://www.therussianlullaby.dev/images/featured-post/davidjguru_drupal_8_thinking_about_drupal_migrations_main.jpg" type="image/jpeg"/><media:content url="https://www.therussianlullaby.dev/images/featured-post/davidjguru_drupal_8_thinking_about_drupal_migrations_main.jpg" medium="image" type="image/jpeg"/><media:thumbnail url="https://www.therussianlullaby.dev/images/featured-post/davidjguru_drupal_8_thinking_about_drupal_migrations_main.jpg"/><description>Recursos básicos para las migraciones en Drupal: módulos del núcleo, módulos contribuidos, plugins, herramientas Drush y autores recomendados.</description><content:encoded><![CDATA[<p>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.</p>
<p>Todaví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.</p>
<p>De 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.</p>
<p><strong>Bueno, ¿de qué quiero hablar en este artículo?</strong> 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.</p>
<hr>
<p><strong>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.</strong></p>
<p><strong>Fotografía de Unsplash, del usuario <a href="https://unsplash.com/@nilsnedel">Nils Nedel, @nilsnedel</a></strong></p>
<hr>
<p><strong>Índice de contenidos</strong></p>
<span class="post-toc" aria-hidden="true"></span>

<!-- raw HTML omitted -->
<p><a href="#1--introducci%C3%B3n">1- Introducción</a><br>
<a href="#2--recursos-b%C3%A1sicos-m%C3%B3dulos-del-n%C3%BAcleo">2- Recursos básicos: módulos del núcleo</a><br>
<a href="#3--otros-recursos-b%C3%A1sicos-m%C3%B3dulos-contribuidos">3- Otros recursos básicos: módulos contribuidos</a><br>
<a href="#4--recursos-adicionales-m%C3%B3dulos-contribuidos-para-plugins">4- Recursos adicionales: módulos contribuidos para plugins</a><br>
<a href="#5--ejecutar-migraciones-m%C3%B3dulos-relacionados-con-drush">5- Ejecutar migraciones: módulos relacionados con Drush</a><br>
<a href="#6--autores-que-deber%C3%ADas-conocer">6- Autores que deberías conocer</a><br>
<a href="#7--wq">7- :wq!</a></p>
<!-- raw HTML omitted -->
<h2 id="1--introducción">1- Introducción</h2>
<p>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 <a href="https://en.wikipedia.org/wiki/Extract,_transform,_load">procesos ETL</a> y manejarte con los plugins de Drupal. Las migraciones hacen un uso intensivo de estos plugins.</p>
<p>En 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.</p>
<h2 id="2--recursos-básicos-módulos-del-núcleo">2- Recursos básicos: módulos del núcleo</h2>
<ul>
<li>
<p><strong>Migrate:</strong> la base de las migraciones. El módulo <a href="https://git.drupalcode.org/project/drupal/tree/8.7.x/core/modules/migrate">migrate</a> proporciona la API fundamental para migrar en Drupal.</p>
</li>
<li>
<p><strong>Migrate Drupal:</strong> <a href="https://git.drupalcode.org/project/drupal/tree/8.7.x/core/modules/migrate_drupal">migrate_drupal</a> se centra en las actualizaciones desde Drupal 6 o Drupal 7. Permite leer entidades de configuración en Drupal 8.</p>
</li>
<li>
<p><strong>Migrate Drupal Multilingual:</strong> <a href="https://git.drupalcode.org/project/drupal/tree/8.7.x/core/modules/migrate_drupal_multilingual">migrate_drupal_multilingual</a>. ¿Un módulo experimental del núcleo? Véase <a href="https://www.drupal.org/node/2959712">esta referencia</a>.</p>
</li>
<li>
<p><strong>Migrate Drupal UI:</strong> <a href="https://git.drupalcode.org/project/drupal/tree/8.7.x/core/modules/migrate_drupal_ui">migrate_drupal_ui</a> ofrece una interfaz para realizar la actualización.</p>
</li>
</ul>
<h2 id="3--otros-recursos-básicos-módulos-contribuidos">3- Otros recursos básicos: módulos contribuidos</h2>
<ul>
<li>
<p><strong>Migrate Plus:</strong> <a href="https://www.drupal.org/project/migrate_plus">migrate_plus</a> es un módulo contribuido esencial que amplía las funcionalidades y capacidades de Migrate con numerosos plugins y extensiones.</p>
</li>
<li>
<p><strong>Migrate Tools:</strong> <a href="https://www.drupal.org/project/migrate_tools">migrate_tools</a> es otro recurso imprescindible: proporciona numerosos comandos Drush para ejecutar y gestionar migraciones.</p>
</li>
<li>
<p><strong>Migrate Status:</strong> <a href="https://www.drupal.org/project/migrate_status">migrate_status</a>. 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.</p>
</li>
<li>
<p><strong>Migrate Files:</strong> <a href="https://www.drupal.org/project/migrate_file">migrate_files</a>. Una colección de plugins de proceso tan interesante que te entrarán ganas de mover archivos e imágenes con ella.</p>
</li>
<li>
<p><strong>Migrate Commerce:</strong> <a href="https://www.drupal.org/project/commerce_migrate">commerce_migrate</a>. 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.</p>
</li>
</ul>
<h2 id="4--recursos-adicionales-módulos-contribuidos-para-plugins">4- Recursos adicionales: módulos contribuidos para plugins</h2>
<p>En las migraciones de Drupal utilizamos distintos recursos para ejecutar el plan <a href="https://en.wikipedia.org/wiki/Extract,_transform,_load">ETL</a>. Uno de los fundamentales, como adelantaba en la introducción, son los <a href="https://www.drupal.org/docs/8/api/plugin-api">plugins de Drupal</a>, que requieren cierto conocimiento y práctica.</p>
<p>En una migración, los plugins ayudan a leer la información en la fase E, extracción desde el origen (<a href="https://www.drupal.org/docs/8/api/migrate-api/migrate-source-plugins">plugins de origen</a>); realizar la T, transformación o procesamiento (<a href="https://www.drupal.org/docs/8/api/migrate-api/migrate-process-plugins">plugins de proceso</a>); y guardar los datos en la L, carga en el destino (<a href="https://www.drupal.org/docs/8/api/migrate-api/migrate-destination-plugins-examples">plugins de destino</a>). Al combinar estas tres partes solemos obtener un proceso de migración funcional.</p>
<p>Muchos 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.</p>
<h3 id="plugins-de-origen-migrate-source-pluginshttpswwwdrupalorgdocs8apimigrate-apimigrate-source-plugins">Plugins de origen: <a href="https://www.drupal.org/docs/8/api/migrate-api/migrate-source-plugins">Migrate Source Plugins</a></h3>
<ul>
<li>
<p><strong>Migrate Source CSV:</strong> <a href="https://www.drupal.org/project/migrate_source_csv">migrate_source_csv</a>. Módulo contribuido para migrar datos a Drupal 8 desde un sencillo archivo CSV de los de toda la vida.</p>
</li>
<li>
<p><strong>Migrate Source SQL:</strong> <a href="https://www.drupal.org/project/custom_sql_migrate_source_plugin">custom_sql_migrate_source_plugin</a>. Como particularidad entre los plugins de base de datos, permite incluir consultas SQL directamente en el archivo <code>.yml</code> que describe la migración. Las consultas se ejecutan contra la base de datos de origen.</p>
</li>
<li>
<p><strong>Migrate Source YAML:</strong> <a href="https://www.drupal.org/project/migrate_source_yaml">migrate_source_yaml</a>. Una herramienta sencilla para migrar contenido desde archivos YAML.</p>
</li>
</ul>
<h3 id="plugins-de-proceso-migrate-process-pluginshttpswwwdrupalorgdocs8apimigrate-apimigrate-process-plugins">Plugins de proceso: <a href="https://www.drupal.org/docs/8/api/migrate-api/migrate-process-plugins">Migrate Process Plugins</a></h3>
<ul>
<li>
<p><strong>Migrate Process Geofield:</strong> <a href="https://www.drupal.org/project/geofield">geofield</a>. El módulo contribuido Geofield incluye un plugin de proceso personalizado para migraciones. Consulta <a href="https://www.drupal.org/docs/8/api/migrate-api/migrate-process-plugins/contrib-process-plugin-geofield_latlon">su plugin de proceso</a>.</p>
</li>
<li>
<p><strong>Migrate Process XML:</strong> <a href="https://www.drupal.org/project/migrate_process_xml">migrate_process_xml</a>. Proporciona plugins de proceso para <code>xpath</code> y <code>xvalue</code>.</p>
</li>
<li>
<p><strong>Migrate HTML to Paragraphs:</strong> <a href="https://www.drupal.org/project/migrate_html_to_paragraphs">migrate_html_to_paragraphs</a>. Ayuda a transformar el HTML del origen en un elemento Paragraph, gestionado mediante un plugin de destino.</p>
</li>
</ul>
<h3 id="plugins-de-destino-migrate-destination-plugins--exampleshttpswwwdrupalorgdocs8apimigrate-apimigrate-destination-plugins-examples">Plugins de destino: <a href="https://www.drupal.org/docs/8/api/migrate-api/migrate-destination-plugins-examples">Migrate Destination Plugins &amp; Examples</a></h3>
<p>¿Qué clase de entidades de Drupal se crearán durante la migración? ¿Entidades de contenido? ¿Entidades de configuración? Echa un vistazo.</p>
<ul>
<li>
<p><strong>Migrate Destination CSV:</strong> <a href="https://github.com/jonathanfranks/d8migrate/tree/master/web/modules/custom/migrate_destination_csv">d8migrate</a>. Un pequeño módulo personalizado creado por @jonathanfranks.</p>
</li>
<li>
<p><strong>Migrate Destination Config:</strong> <a href="https://api.drupal.org/api/drupal/core%21modules%21migrate%21src%21Plugin%21migrate%21destination%21Config.php/class/Config/8.5.x">clase Config.php</a>. Ofrece un plugin para migrar configuración.</p>
</li>
<li>
<p><strong>Migrate Destination Block:</strong> <a href="https://api.drupal.org/api/drupal/core%21modules%21block%21src%21Plugin%21migrate%21destination%21EntityBlock.php/class/EntityBlock/8.7.x">clase EntityBlock.php</a>. 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.</p>
</li>
</ul>
<p>
<img src="/images/post/davidjguru_drupal_8_migrate_entity_block.png" alt="Migración de entidades de bloque en Drupal 8." loading="lazy" decoding="async">

</p>
<h2 id="5--ejecutar-migraciones-módulos-relacionados-con-drush">5- Ejecutar migraciones: módulos relacionados con Drush</h2>
<ul>
<li>
<p><strong>Migrate Scheduler:</strong> <a href="https://www.drupal.org/project/migrate_scheduler">migrate_scheduler</a>. Ofrece integración con la API de cron de Drupal para ejecutar migraciones según una programación predefinida.</p>
</li>
<li>
<p><strong>Migrate Run:</strong> <a href="https://www.drupal.org/project/migrate_run">migrate_run</a>. 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.</p>
</li>
<li>
<p><strong>Migrate Devel:</strong> <a href="https://www.drupal.org/project/migrate_devel">migrate_devel</a>. Proporciona opciones Drush para mostrar información de depuración durante la ejecución. También incorpora el plugin de proceso <a href="https://git.drupalcode.org/project/migrate_devel/blob/8.x-1.x/src/Plugin/migrate/process/Debug.php">debug</a>. <a href="https://agaric.coop/blog/how-debug-drupal-migrations-part-2">Aquí puedes leer un artículo sobre él</a>. Por cierto, en el contexto original del artículo, <a href="https://www.drupal.org/project/migrate_devel/issues/2938677">la compatibilidad con Drush 9 seguía sin resolverse</a>, aunque <a href="https://www.drupal.org/files/issues/2018-10-08/migrate_devel-drush9-2938677-6.patch">parecía haber un parche disponible</a>.</p>
</li>
</ul>
<h2 id="6--autores-que-deberías-conocer">6- Autores que deberías conocer</h2>
<ul>
<li><strong><a href="https://twitter.com/dinarcon">Mauricio Dinarte</a>:</strong> desarrollador, consultor, formador y propietario de su propio negocio, <a href="https://agaric.coop">Agaric</a>. Escribió lo que probablemente sea una lectura obligatoria para cualquiera que quiera aprender a migrar con Drupal: <em>31 Days of Drupal Migrations</em>, una colección de 31 artículos publicados en <a href="https://understanddrupal.com/migrations">Understand Drupal</a>, con los aspectos más importantes, ejemplos, ejercicios y descripciones para entender el mundo interno de las migraciones.</li>
</ul>
<p>Un material formativo imprescindible. Además, el sitio de su empresa reúne muchos artículos muy buenos sobre el tema bajo la etiqueta <a href="https://agaric.coop/tags/migrate">migrate</a>.</p>
<p><strong>Algunos ejemplos de Mauricio Dinarte:</strong></p>
<ol>
<li><a href="https://agaric.coop/blog/introduction-paragraphs-migrations-drupal">Introducción a las migraciones de Paragraphs en Drupal</a>.</li>
<li><a href="https://agaric.coop/blog/using-migration-groups-share-configuration-among-drupal-migrations">Utilizar grupos de migración para compartir configuración entre migraciones de Drupal</a>.</li>
<li><a href="https://agaric.coop/blog/what-difference-between-migration-tags-and-migration-groups-drupal">¿Cuál es la diferencia entre etiquetas y grupos de migración en Drupal?</a>.</li>
</ol>
<p>Su perfil en Drupal.org: <a href="https://www.drupal.org/u/dinarcon">dinarcon</a>.</p>
<hr>
<ul>
<li><strong><a href="https://twitter.com/socketwench">Tess Flynn</a>:</strong> 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, <a href="https://deninet.com">deninet.com</a>, encontré contenido variado y, sobre todo, una serie muy interesante sobre migraciones bajo la etiqueta <a href="https://deninet.com/tag/drupal-migration">drupal-migration</a>.</li>
</ul>
<p>Por el camino descubrí también que tiene varios módulos contribuidos relacionados con las migraciones y los plugins de proceso.</p>
<p><strong>Algunos ejemplos de Tess Flynn:</strong></p>
<ol>
<li><a href="https://www.drupal.org/project/migrate_process_url">Migrate Process URL</a>: proporciona un plugin de proceso para migrar campos de enlace.</li>
<li><a href="https://www.drupal.org/project/migrate_process_vardump">Migrate Process Vardump</a>: ayuda a depurar migraciones.</li>
<li>Otros plugins de proceso: <a href="https://www.drupal.org/project/migrate_process_array">Migrate Process Array</a>, <a href="https://www.drupal.org/project/migrate_process_skip">Migrate Process Skip</a> y <a href="https://www.drupal.org/project/migrate_process_trim">Migrate Process Trim</a>.</li>
</ol>
<p>Su perfil en Drupal.org: <a href="https://www.drupal.org/u/socketwench">socketwench</a>.</p>
<hr>
<ul>
<li><strong><a href="https://twitter.com/drupalexp">Danny Sipos</a>:</strong> autor de una reconocida «biblia» de Drupal, <a href="https://www.amazon.com/Drupal-Module-Development-modules-version/dp/1789612365/">Drupal 8 Module Development, por entonces en su segunda edición</a>, y del sitio <a href="https://www.webomelette.com/">webomelette.com</a>. Ponente, formador y asistente habitual a distintos encuentros internacionales de Drupal, también ha escrito artículos interesantes sobre migraciones que deberías leer.</li>
</ul>
<p><strong>Algunos ejemplos de Danny Sipos:</strong></p>
<ol>
<li><a href="https://www.sitepoint.com/your-first-drupal-8-migration/">Tu primera migración en Drupal 8</a>.</li>
<li><a href="https://www.webomelette.com/dynamic-migrations-using-templates-drupal-8">Migraciones dinámicas mediante «plantillas» en Drupal 8</a>.</li>
<li><a href="https://www.webomelette.com/quickly-generate-headers-csv-migrate-source-plugin-using-drush">Generar rápidamente las cabeceras del plugin de origen CSV mediante Drush</a>.</li>
</ol>
<p>Su perfil en Drupal.org: <a href="https://www.drupal.org/u/upchuk">upchuk</a>.</p>
<hr>
<h2 id="7--wq">7- :wq!</h2>
<h5 id="canción-recomendada">Canción recomendada</h5>


<div class="embed-responsive embed-responsive-16by9">
  <iframe
    class="embed-responsive-item"
    src="https://www.youtube-nocookie.com/embed/ADwfyxpriAM"
    title="YouTube video"
    loading="lazy"
    allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
    allowfullscreen></iframe>
</div>

]]></content:encoded></item></channel></rss>