<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>JSON APIs on The Russian Lullaby</title><link>https://www.therussianlullaby.dev/es/tags/json-apis/</link><description>Recent content in JSON APIs 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/json-apis/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></channel></rss>