How to create a clean OOP Batch on Drupal 9?
Tags: drupal9, php, drupal, batch
Published: 2022-09-18 10:30:41

While we are waiting for Drupal 9 core implementation of OOP Batches we need a start point for them.

Example is for Drupal 9.2.6+ (PHP 7.4.2+)

Possible path to modules: `web/modules/custom`

* Create module and name it `module_example` and add there `/module_example/module_example.module`.
It's required file so just place it in module folder.

`module_example.module`
```php
<?php
/*
 * Every module needs this file.
*/

```

* Add `/module_example/module_example.info.yml` file.
Also required file, fill in info about module.

`module_example.info.yml`
```php
# Change name, description, package on your needs.
name: Module Example
type: module
description: Module example
package: Module example
core_version_requirement: ^9

```

* Add `/module_example/module_example.routing.yml` file.
Fill in here route for our form from where we will be starting our batch.

`module_example.routing.yml`
```php
# Change here routing name,path,_form,_title for your specific form.
module_example.admin_form.example_form:
  path: '/admin/config/system/module-example'
  defaults:
    _form: '\Drupal\module_example\Form\ExampleForm'
    _title: 'Form example'
  requirements:
    _user_is_logged_in: 'TRUE'
    _permission: 'administer site configuration'
```

* Add `/module_example/module_example.services.yml` file.
We need it for our batch, it will be a service because we haven't any special definition for it.

`module_example.services.yml`
```php
# Change here service name, class and arguments to inject for your needs.
services:
  module_example.batch_example:
    class: Drupal\module_example\Batch\BatchExample
    arguments: ['@messenger', '@extension.list.module']

```

* Create `/module_example/src` folder.
* Create `/module_example/src/Form` folder.
* Create `/module_example/src/Form/ExampleForm.php` file.
We need this form to control our batch from somewhere.
This example uses form.

`ExampleForm.php`
```php
<?php

namespace Drupal\module_example\Form;

use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * Implements an Example form.
 */
class ExampleForm extends FormBase {

  /**
   * Batch injection.
   *
   * @var \Drupal\module_example\Batch\BatchExample
    *   Grab the path from module_example.services.yml file.
   */
  protected $batch;

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container) {
    // Instantiates this form class.
    $instance = parent::create($container);
    // Put here injection of your batch service by name from module_example.services.yml file.
    $instance->batch = $container->get('module_example.batch_example');

    return $instance;
  }

  /**
   * {@inheritdoc}
   */
  public function getFormId() {
    // Any name for your form to indicate it.
    return 'example_of_form';
  }

  /**
   * {@inheritdoc}
   */
  public function buildForm(array $form, FormStateInterface $form_state) {
    // No need for t() in admin part.
    // Triggers submitForm() function.
    $form['submit'] = [
      '#type' => 'submit',
      '#prefix' => '<br>',
      '#value' => 'Start batch',
    ];

    return $form;
  }

  /**
   * {@inheritdoc}
   */
  public function submitForm(array &$form, FormStateInterface $form_state) {
    // As we don't need to change in our batch depending on inputs,
    // just passing empty array or removing param.
    $values = [];
    $this->batch->run($values);
  }

}

```

* Create `/module_example/src/Batch` folder.
* Add `/module_example/src/Batch/BatchExample.php` file.

`BatchExample.php`
```php
<?php

namespace Drupal\module_example\Batch;

use Drupal\Core\Batch\BatchBuilder;
use Drupal\Core\Messenger\MessengerInterface;
use Drupal\Core\Extension\ModuleExtensionList;
use Drupal\Core\DependencyInjection\DependencySerializationTrait;

/**
 * Batch Example.
 */
class BatchExample {

  use DependencySerializationTrait;

  /**
   * The messenger.
   *
   * @var \Drupal\Core\Messenger\MessengerInterface
   */
  protected $messenger;

  /**
   * Module extension list.
   *
   * @var \Drupal\Core\Extension\ModuleExtensionList
   */
  protected $moduleExtensionList;

  /**
   * Constructor.
   *
   * @param \Drupal\Core\Messenger\MessengerInterface $messenger
   *   The messenger.
   * @param \Drupal\Core\Extension\ModuleExtensionList $moduleExtensionList
   *   The module extension list.
   */
  public function __construct(
    MessengerInterface $messenger,
    ModuleExtensionList $moduleExtensionList
  ) {
    $this->messenger = $messenger;
    $this->moduleExtensionList = $moduleExtensionList;
  }

  /**
   * The Starting point for batch.
   *
   * @param $values
   *   If you need to change behavior of batch but not much, send values from external.
   */
  public function run($values) {
    // File needs to be placed /module_example/src/Batch or hardcode the name of module.
    $moduleName = basename(dirname(__DIR__, 2));
    $modulePath = $this->moduleExtensionList->getPath($moduleName);
    $batchBuilder = new BatchBuilder();
    $batchBuilder
      // Change batch name here.
      ->setTitle('Example batch')
      ->setInitMessage('Initializing. <br/><b style="color: #f00;">Navigating away will stop the process.</b>')
      ->setProgressMessage('Completed @current of @total. <br/><b style="color: #f00;">Navigating away will stop the process.</b>')
      ->setErrorMessage('Batch has encountered an error.')
      ->setFile($modulePath . '/src/Batch/' . basename(__FILE__));

    // Dummy data, grab data on this place.
    $items = [
      ['id' => 0, 'data' => 'item'],
      ['id' => 1, 'data' => 'item'],
      ['id' => 2, 'data' => 'item'],
    ];
    if (!empty($items)) {
      foreach ($items as $item) {
        // Adding operations that we will process on each item in a batch.
        $batchBuilder->addOperation([$this, 'process'], [
          $item,
          // Add how many variables you need here.
        ]);
      }
      $batchBuilder->setFinishCallback([$this, 'finish']);
      // Changing it to array that we can set it in functional way (only way on this moment).
      $batch = $batchBuilder->toArray();
      batch_set($batch);
    }
    else {
      $this->messenger->addMessage('No entities exists');
    }
  }

  /**
   * Batch processor.
   */
  public function process($item, &$context) {
    try {
      $id = $item['id'];
      // Display a progress message.
      $context['message'] = "Now processing {$id} entity...";
      // Body of the batch, logic that needs to be presented place here.
      $changed = FALSE;
      // For example we will change item data.
      if (!empty($item['data'])) {
        $item['data'] .= $id; 
        $changed = TRUE;
      }
      if ($changed === TRUE) {
        // Save the changes for your objects here.
      }
      else {
        // Skip this step if something went wrong or changes weren't presented.
        throw new \Exception('Skip if cant handle');
      }
    }
    catch (\Throwable $th) {
      $this->messenger->addError($th->getMessage());
    }
  }

  /**
   * Finish operation.
   */
  public function finish($success, $results, $operations, $elapsed) {
    if ($success) {
      // Change success message here.
      $this->messenger->addMessage('Example batch is finished!');
    }
    else {
      $error_operation = reset($operations);
      $arguments = print_r($error_operation[1], TRUE);
      $message = "An error occurred while processing {$error_operation[0]} with arguments: {$arguments}";
      $this->messenger->addMessage($message, 'error');
    }
  }

}

```

It's only a skeleton for batch without full overview of it's functionality.
No `$context['sandbox']` and else where used here but you can read about it in official documentation here - https://www.drupal.org/docs/7/api/batch-api/overview