Sorry, you need to enable JavaScript to visit this website.
Skip to main content
Welcome to our website! Explore our services and portfolio.

Creating a Batch Service for Long-running Processes in Drupal

Submitted by admin on

Create a process (update a Node) that uses the batch API, and can be called either from a form, or from a drush command.


 

The Batch API is essential in Drupal for performing heavy tasks (like updating thousands of nodes or importing a huge CSV) without hitting PHP execution timeouts. It breaks a large task into smaller "chunks" and updates the browser with a progress bar.

Here is the most modern and clean way to implement it using a Form and a dedicated Batch Service.


1. Create the Batch Definition (in a Form)

The batch is typically triggered inside a submitForm() method. We define the metadata and the specific operations to be performed.

src/Form/MyBatchForm.php

PHP

namespace Drupal\my_module\Form;

use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;

class MyBatchForm extends FormBase {
  public function getFormId() { return 'my_batch_form'; }

  public function buildForm(array $form, FormStateInterface $form_state) {
    $form['submit'] = [
      '#type' => 'submit',
      '#value' => $this->t('Start Heavy Processing'),
    ];
    return $form;
  }

  public function submitForm(array &$form, FormStateInterface $form_state) {
    // 1. Define the operations (the functions to call)
    // We'll process 100 items by splitting them into chunks of 10.
    $items = range(1, 100); 
    $chunks = array_chunk($items, 10);
    $operations = [];

    foreach ($chunks as $chunk) {
      $operations[] = [
        '\Drupal\my_module\BatchService::processChunk', // The callback
        [$chunk], // The arguments passed to the callback
      ];
    }

    // 2. Setup the Batch array
    $batch = [
      'title' => $this->t('Processing Items...'),
      'init_message' => $this->t('Starting batch.'),
      'progress_message' => $this->t('Completed @current out of @total.'),
      'error_message' => $this->t('Batch has encountered an error.'),
      'operations' => $operations,
      'finished' => '\Drupal\my_module\BatchService::finished',
    ];

    // 3. Set the batch
    batch_set($batch);
  }
}

2. Create the Processing Service

It is best practice to put the actual logic in a separate class. These methods must be static because the Batch API calls them outside of the standard object context.

src/BatchService.php

PHP

namespace Drupal\my_module;

class BatchService {

  /**
   * Processes a chunk of data.
   */
  public static function processChunk($chunk, &$context) {
    // Initialize results on first run
    if (!isset($context['results']['processed'])) {
      $context['results']['processed'] = 0;
    }

    foreach ($chunk as $id) {
      // PERFORM YOUR HEAVY LOGIC HERE (e.g., $node->save())
      usleep(100000); // Simulating work

      $context['results']['processed']++;
    }

    // Optional: Update the progress message dynamically
    $context['message'] = t('Currently processing item "@id"', ['@id' => end($chunk)]);
  }

  /**
   * Final callback after all operations are done.
   */
  public static function finished($success, $results, $operations) {
    if ($success) {
      $message = t('@count items processed successfully.', ['@count' => $results['processed']]);
      \Drupal::messenger()->addStatus($message);
    }
    else {
      \Drupal::messenger()->addError(t('Finished with an error.'));
    }
  }
}

3. The Lifecycle of a Batch

Understanding how the data flows helps in debugging complex batches:

  1. Initiation: batch_set() registers the tasks in the database and prepares a redirect to /batch.

  2. The Loop: The browser hits /batch?id=.... Drupal runs one operation from the list, returns a JSON response to the browser, and the browser immediately requests the next one.

  3. Sandbox: If an operation is too big for even one request, you can use $context['sandbox'] to track internal progress and set $context['finished'] = 0.5 to tell Drupal to call the same operation again.

  4. Finished: Once all operations return finished = 1 (or return nothing), the final callback is triggered.


4. Pro-Tip: The "Progressive" Sandbox

If you have one single operation that needs to process 10,000 items (rather than pre-chunking), use the sandbox:

PHP

public static function processEverything(&$context) {
  if (empty($context['sandbox'])) {
    $context['sandbox']['progress'] = 0;
    $context['sandbox']['max'] = 10000;
  }

  $limit = 50; // Process 50 per request
  for ($i = 0; $i < $limit; $i++) {
    // Process one item...
    $context['sandbox']['progress']++;
  }

  // Tell Drupal how far along we are (0.0 to 1.0)
  $context['finished'] = $context['sandbox']['progress'] / $context['sandbox']['max'];
}
Next we 

Next we will see how to trigger this same batch from a custom Drush command. 


To trigger a Drupal batch process from the command line, you use a Drush Command. The key difference from a web-based batch is that you call drush_backend_batch_process() instead of letting the Form API handle the redirect.

This allows Drush to spawn its own sub-processes to work through the chunks, providing a progress bar directly in your terminal.


1. Create the Drush Command Class

For Drush 12/13, place your command file in src/Drush/Commands/.

src/Drush/Commands/MyModuleCommands.php

PHP