Migraciones

Migraciones en Drupal (III): migrar desde Google Spreadsheet

Migraciones en Drupal (III): migrar desde Google Spreadsheet

Los sistemas y subsistemas relacionados con la API de migraciones de Drupal resultan apasionantes. En las entregas anteriores quise trazar un mapa lo más completo posible de recursos, posibilidades y personas de referencia en la primera parte, y explicar algunos mecanismos básicos de las migraciones en la segunda. Sabiendo que todo ello abre la puerta a miles de opciones, posibilidades y técnicas, no quería dejar pasar una tercera entrega sin compartir experiencias migrando datos desde un formato tan habitual como una hoja de cálculo de Google: una de las formas en que solemos recibir los datos.


Foto de Unsplash, de Pan Xiaozhen, @zhenhappy


Índice

1. Introducción: recordemos los procesos ETL

Empecemos planteando un escenario: tenemos que migrar una lista de términos de taxonomía a nuestra instalación Drupal. El origen de los datos importa, porque determina qué plugins utilizaremos para la extracción y puede influir también en el procesamiento y en la carga final en el destino.

En este artículo imaginaremos que hemos «heredado» una tarea de migración que había iniciado otra persona. Consiste en poblar con términos de taxonomía un vocabulario ya existente en Drupal. Para practicar con otros plugins y modelos de migración, utilizaremos una hoja de Google como fuente. No olvidemos el marco en el que trabajamos:

Esquema ETL y visión general de Drupal Migrate API

Nuestro objetivo es rellenar los campos de un término de taxonomía con los datos de una hoja externa de Google. Estos son los campos que debemos completar:

Campos básicos de un término de taxonomía

Lo haremos describiendo un proceso Migrate y ejecutándolo como configuración. ¿Conoces las diferencias entre migraciones como código y como configuración? Encontrarás algunas claves en el artículo anterior de la serie.

2. Exponer datos mediante Google Spreadsheet

Para extraer los datos necesitamos una hoja cuyos valores estén expuestos. He creado esta hoja de cálculo de Google, con columnas que corresponden a los campos de un término de taxonomía y están preparadas para la migración.

Para procesar la fuente utilizaremos el módulo contribuido Migrate Google Sheets. Descárgalo con Composer y actívalo:

composer require drupal/migrate_plus #If applicable
composer require drupal/migrate_google_sheets
drush en migrate_plus migrate_google_sheet -y

Este módulo trata el recurso como un archivo JSON, pero antes tenemos que realizar algunas tareas: exponer la hoja como fuente JSON utilizando las herramientas de Google.

  • Primero extraemos el workbook-id de la dirección de la hoja: docs.google.com/spreadsheets/d/1bKGbPbgeuXaBfcKetaDqoDimmYcerQY_hT1rqzw4TbM/. En este caso, workbook-id: 1bKGbPbgeuXaBfcKetaDqoDimmYcerQY_hT1rqzw4TbM.
  • También necesitamos el worksheet-index, el índice de la pestaña que contiene los datos. Aquí es 1.
  • Construimos la URL JSON con el patrón spreadsheets.google.com/feeds/list/[workbook-id]/[worksheet-index]/public/values?alt=json. Para nuestro ejemplo: URL de los valores en JSON.

¡Eso es todo! La hoja queda expuesta como un archivo JSON y podemos obtener los valores mediante los plugins de origen elegidos para Migrate API.

Datos de Google Spreadsheet en formato JSON

3. Particularidades de la transformación a JSON

Ahora podemos ver la fuente JSON en el navegador. Puede resultar útil una extensión que presente los datos con formato, como hace Firefox de forma predeterminada:

      {
        "gsx$id": {
          "$t": "1"
        },
        "gsx$name": {
          "$t": "Term 1"
        },
        "gsx$description": {
          "$t": "Descrip 1"
        },
        "gsx$url": {
          "$t": "/t1"
        }
        "gsx$published": {
          "$t": "1"
        }
      }

Algunos elementos han cambiado respecto al formato original. Fíjate en la comparación:

Comparación de los formatos de datos

La transformación a JSON introduce cambios respecto a la hoja. Por ejemplo, Google modifica los identificadores de los campos añadiendo prefijos:

  • gsx$ para los nombres de campo.
  • $t para los valores almacenados en los campos.

Las cabeceras cambian así:

  1. Id pasa a ser gsx$id.
  2. Name pasa a ser gsx$name.
  3. Description pasa a ser gsx$description.
  4. Url pasa a ser gsx$url.
  5. Published pasa a ser gsx$published.

Como estos cambios son estables, el plugin se encarga de procesarlos:

// Class GoogleSheets.php
// Module migrate_google_sheets
// @see https://git.drupalcode.org/project/migrate_google_sheets/

// Actual values are stored in gsx$<field>['$t'].
$this->currentItem[$field_name] = $current['gsx$' . $selector]['$t'];

Esto nos permite utilizar los nombres de los datos en su forma «plana»: en minúsculas y sin espacios ni caracteres especiales. Además, las filas se encuentran en la ruta feed/entry, que el módulo también resuelve:

// For Google Sheets, the actual row data lives under feed->entry.
if (isset($array['feed']) && isset($array['feed']['entry'])) {
 $array = $array['feed']['entry'];
}

4. Características de la migración

La primera observación es que el plugin de Google Spreadsheet extiende el analizador JSON, como vemos en la clase:

use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\migrate\MigrateException;
use Drupal\migrate_plus\Plugin\migrate_plus\data_parser\JSON;
use GuzzleHttp\Exception\RequestException;

/**
 * Obtain Google Sheet data for migration.
 *
 * @DataParser(
 *   id = "google_sheets",
 *   title = @Translation("Google Sheets")
 * )
 */
class GoogleSheets extends JSON implements ContainerFactoryPluginInterface {
...
}

Su bloque de anotaciones lo identifica como un Data Parser, así que necesitamos más piezas: un plugin de origen y un Data Fetcher. El plugin de Google Spreadsheet será la tercera pieza del proceso.

¿Qué plugins de origen tenemos disponibles en nuestra instalación Drupal? Vamos a consultarlos:

drupal debug:plugin migrate.source

La consola mostrará:

drupal@migrations-web:/var/www/html$ drupal debug:plugin migrate.source
 --------------- ---------------------------------------------------------
  Plugin ID       Plugin class
 --------------- ---------------------------------------------------------
  embedded_data   Drupal\migrate\Plugin\migrate\source\EmbeddedDataSource
  empty           Drupal\migrate\Plugin\migrate\source\EmptySource
  url             Drupal\migrate_plus\Plugin\migrate\source\Url
 --------------- ---------------------------------------------------------

drupal@migrations-web:/var/www/html$

Podemos utilizar la clase Url.php como plugin de origen, ya que el archivo se expone mediante una URL. En la clase Url.php vemos que necesita un analizador de datos; en nuestro caso será la clase de Google Spreadsheet:

  /**
   * The data parser plugin.
   *
   * @var \Drupal\migrate_plus\DataParserPluginInterface
   */
  protected $dataParserPlugin;

Al buscar un componente que recupere los datos encontramos la clase Http.php, preparada para trabajar con el plugin de origen URL mediante HTTP:

/**
 * Retrieve data over an HTTP connection for migration.
 *
 * Example:
 *
 * @code
 * source:
 *   plugin: url
 *   data_fetcher_plugin: http
 *   headers:
 *     Accept: application/json
 *     User-Agent: Internet Explorer 6
 *     Authorization-Key: secret
 *     Arbitrary-Header: foobarbaz
 * @endcode
 *
 * @DataFetcher(
 *   id = "http",
 *   title = @Translation("HTTP")
 * )
 */
class Http extends DataFetcherPluginBase implements ContainerFactoryPluginInterface {

Por otra parte, el constructor de Url.php recibe las direcciones de origen. Podemos proporcionar una URL o un conjunto de ellas:

  /**
   * {@inheritdoc}
   */
  public function __construct(array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration) {
    if (!is_array($configuration['urls'])) {
      $configuration['urls'] = [$configuration['urls']];
    }
    parent::__construct($configuration, $plugin_id, $plugin_definition, $migration);

    $this->sourceUrls = $configuration['urls'];
  }

Al reunir las piezas, debemos darles una estructura según su función y posición. Nuestro bloque de origen quedará así:

source:
  plugin: url
  data_fetcher_plugin: http
  data_parser_plugin: google_sheets
  urls: 'http://spreadsheets.google.com/feeds/list/1bKGbPbgeuXaBfcKetaDqoDimmYcerQY_hT1rqzw4TbM/1/public/values?alt=json'

5. Módulo personalizado para la migración

Crearemos un módulo llamado migration_google_sheet con esta estructura:

/project/web/modules/custom/
                     \__migration_google_sheet/
                         \__migration_google_sheet.info.yml
                           \__config/
                                \__install/
                                     \__migrate_plus.migration.migration.taxonomy_google_sheet.yml

Archivo de definición de la migración: migrate_plus.migration.taxonomy_google_sheet.yml.

langcode: en
status: true
dependencies:
  enforced:
    module:
      - migration_google_sheet
id: taxonomy_google_sheet
label: 'Migrating Taxonomy'
source:
  plugin: url
  data_fetcher_plugin: http
  data_parser_plugin: google_sheets
  urls: 'http://spreadsheets.google.com/feeds/list/1bKGbPbgeuXaBfcKetaDqoDimmYcerQY_hT1rqzw4TbM/1/public/values?alt=json'
  fields:
    - name: id
      label: 'Id'
      selector: id
    - name: name
      label: 'Name'
      selector: name
    - name: description
      label: 'Description'
      selector: description
    - name: url
      label: 'Url'
      selector: url
    - name: published
      label: 'Published'
      selector: published
  ids:
    id:
      type: integer
process:
  name: name
  description: description
  path: url
  status: published
destination:
  plugin: 'entity:taxonomy_term'
  default_bundle: tags

Ejecutamos la migración desde la consola:

drush en migration_google_sheet -y
drush migrate:import taxonomy_google_sheet

[notice] Processed 30 items (30 created, 0 updated, 0 failed, 0 ignored) - done with 'taxonomy_google_sheet'

¡Hecho! Hemos migrado treinta términos de taxonomía desde una hoja de Google:

Términos de taxonomía recién migrados

Puedes descargar o clonar el módulo personalizado desde mi repositorio de GitLab.

6. :wq!

Canción recomendada

Únete a la newsletter

Recibe artículos sobre Drupal y desarrollo web directamente en tu bandeja de entrada.

No spam. Unsubscribe at any time.