Introducción a la Batch API de Drupal

gears.jpg
Solucionex
11
Mar 21

Seguramente os hayáis encontrado en alguna situación en la que es necesario realizar alguna ejecución muy costosa en Drupal. Por ejemplo, una lectura e importación de datos directamente desde un fichero CSV: si este fichero contiene muchos datos, o el procesamiento que se realiza con estos es muy complejo, es probable que PHP nos devuelva un error timeout. Si hemos indicado en nuestra configuración de PHP que nunca de un timeout (cosa que no es para nada recomendable), pueden surgir problemas de memoria o de bloqueos del sistema, por lo que no es una solución válida.

En Drupal, podemos solucionar este problema haciendo uso de la Batch API.

 

La Batch API de Drupal

Para Drupal, un proceso Batch se trata de una ejecución que se ha separado en varias ejecuciones más pequeñas. Para el ejemplo anterior, en vez de leer y ejecutar todo el fichero de golpe, podemos separar el procesamiento de cada línea en su propia ejecución, evitando problemas de timeout o consumos excesivos de memoria.

Drupal ya utiliza este tipo de procesos en muchos sitios. Por ejemplo, cuando seleccionamos y eleminamos varios contenidos del portal, aparecerá la típica barra de progreso que nos indicará el estado actual de esta ejecución, e irá avanzando hasta terminar. También lo utilizan módulos contribuídos como Pathauto Sitemap, ya que realizan ejecuciones muy costosas cuando regeneran sus tablas de datos.

 

Drupal Batch

Al separar una ejecución muy larga en varias más pequeñas, podemos evitar que PHP nos devuelva el timeout, y además no se bloqueará el procesado de PHP durante demasiado tiempo.

El uso de esta API tiene otras ventajas, como la representación visual de la ejecución del proceso. Podemos ver la barra de progreso a medida que se van ejecutando las tareas, que siempre está bien para saber que el proceso está funcionando y que no se ha bloqueado. También se pueden modificar los mensajes de estado que van apareciendo. Es especialmente útil para devolver algún feedback visual, sobre todo para usuarios no-expertos.

 

Cómo usar la Batch API

Todas estas ventajas están muy bien, pero pierden utilidad si afectan muy negativamente al tiempo de desarrollo. Por suerte, el uso de Batch en Drupal es muy simple y fácil de implementar.

Vamos a ver un ejemplo de una posible implementación en Drupal 8 o 9 y sus principales características u opciones.

 

Primero, creamos una estructura Batch con una serie de campos, que enviaremos a la función batch_set para inicializar la ejecución.

  public function batchImportCSV($csv) {
    $batch = [
      'title'            => 'Importing CSV...',
      'operations'       => [],
      'init_message'     => 'Starting...',
      'progress_message' => 'Processed @current out of @total.',
      'error_message'    => 'An error occurred during processing',
      'finished'         => '\Drupal\my_custom_module\Batch\MyCustomBatch::importFinished',
    ];

    if ($handle = fopen($csv, 'r')) {
      while ($line = fgetcsv($handle, 0, ';')) {
        $batch['operations'][] = [
          '\Drupal\my_custom_module\Batch\MyCustomBatch::importLine',
          [array_map('base64_encode', $line)],
        ];
      }
      fclose($handle);
    }
    batch_set($batch);
  }

En este ejemplo, tenemos una función batchImportCSV que recibe como parámetro una dirección a un CSV. Esta función podríamos llamarla desde cualquier sitio, como el submit de un formulario, algún hook, servicios...

Lo primero que hacemos es definir la variable $batch. Esta variable tiene los siguientes parámetros:

  • operations: El único parámetro requerido. Debe ser un array en el que cada item es otro array con el callback (función) a la que llamaremos y los parámetros que le pasemos a esta.
  • title: Un string con el título que queremos que aparezca en la pantalla del Batch.
  • init_message: Mensaje que aparece al inicializar la ejecución.
  • error_message: Mensaje que aparece si surge algún error durante el procesamiento del Batch.
  • finished: Callback que se ejecutará al terminar el proceso entero.

Existen más parámetros, pero estos son los esenciales.

Al definir este $batch, el campo más importante es el array de operaciones que se ejecutarán.

En este caso, lo que hacemos es abrir el fichero CSV, y añadir por cada línea una operación que reciba dicha línea como parámetro. Cuando se ejecute el Batch, será llamada la función importLine($line) tantas veces como líneas tuviera el CSV.

El último paso es llamar a la función batch_set(), que recibirá como parámetro el $batch que hemos construido. Tan pronto se llame a esta función, se iniciará el proceso.

 

Con esto hecho, falta crear los callbacks que hemos indicado para el $batch. Estos pueden estar en cualquier sitio, pero una forma limpia de hacerlo es la siguiente:


namespace Drupal\my_custom_module\Batch;

use Drupal\Core\File\FileSystemInterface;

/**
 * Methods for running the CSV import in a batch.
 *
 * @package Drupal\my_custom_module
 */
class MyCustomBatch {

  /**
   * Handle batch completion.
   */
  public static function importFinished($success, $results, $operations) {
    $messenger = \Drupal::messenger();
    $messenger->addMessage('Imported ' . $results['rows_imported'] . ' rows.');

   /* 
      Añadimos aquí cualquier ejecución final que necesitemos.
   */

    return 'The CSV import has completed.';
  }


  /**
   * Process a single line.
   */
  public static function importLine($line, &$context) {
    $context['results']['rows_imported']++;
    $line = array_map('base64_decode', $line);
    $context['message'] = t('Importing row ' . $context['results']['rows_imported']);

    /* 
      Añadimos aquí el procesado que necesitemos hacer.
    */
  }

}

Creamos así los dos callbacks, el que se ejecutará por cada línea y el final. Sólo será necesario añadir la lógica que necesitemos en cada caso y listo, tenemos un proceso Batch 100% funcional.

En la función que utilicemos para cada línea, podemos hacer uso de la variable $context. Esta variable mantiene su valor entre cada llamada, por lo que nos permite, por ejemplo, ir almacenando cuántas líneas hemos ejecutado, o los errores o avisos que vayan surgiendo. En este caso, almacenamos en results la cantidad de líneas importadas. Este results se envía al callback de finalización, por lo que podemos obtener desde ahí cuántas filas se han ejecutado, si ha habido errores, etc, e imprimir por pantalla o al log lo que necesitemos.

 

Conclusión

En resumen, la Batch API de Drupal nos proporciona un mecanismo para poder realizar operaciones muy costosas asegurándonos de que se van a ejecutar sin errores y sin bloquear el sistema. Es fácil de utilizar y tiene varias ventajas extra, como el control de errores o la visualización del progreso, por lo que es muy recomendable saber que existe, y usarla cuando lo consideremos adecuado.