Créer un batch avec Drupal

Quand on a des traitements lourds à effectuer dans Drupal il est judicieux d’utiliser la fonctionnalité de « batch » de Drupal.

En effet prenons un exemple tout simple de mise à jour de contenu en masse avec un message de confirmation à la fin du traitement qui affiche le nombre de contenus mis à jour :

$manager = \Drupal::entityTypeManager();
$nodes = $manager->getStorage('node')
  ->loadByProperties(['type' => 'page']);
$count = 0;

foreach ($nodes as $nid => $node) {
  $node->set('field_custom', 'value');
  $node->save();
  $count++;
}

\Drupal::messenger()->addStatus(t(
  '@count nodes updated.',
  ['@count' => $count]
));

Imaginons qu’on est 500 contenus, ou qu’on est plusieurs champs à mettre à jour, ou qu’on est une API à appelé pour chaque contenu… Le temps de traitement risque d’être plus long que le max_execution_time du serveur et donc de déclencher un timeout. Le traitement peut également consommer tout le CPU ou toute le SWAP du serveur et ainsi faire tomber le serveur.

On pourrait être tenté de « faire du sâle » et ajouter des modifications de variables de configuration PHP dans notre script :

// En mode dégueu mais raisonnable :
ini_set('memory_limit', '512MB');
set_time_limit(600);

// En mode gros bourrin inconscient :
ini_set('memory_limit', '-1');
set_time_limit(0);

Mais cette option n’est pas acceptable en environnement de production et ce code ne passera jamais votre « code quality check » et le lead dev de l’équipe va forcément venir vous mettre une tatane et refuser poliment (ou pas) votre merge request.

Alors que faire ? Un batch pardis !

Voilà comment on défini un batch :

  • title : Le titre de la page en back-office qui s’affiche au lancement du batch
  • operations : La liste des opérations à réaliser durant le batch
  • finished : Callback exécuté à la fin du traitement
// Total of contents to update.
$total = 0;
// Define batch operations.
$operations = [];
// Add operation.
foreach ($nodes as $nid => $node) {
  $operations[] = [
    '\Drupal\my_module\Service\Hello::updateNode',
    [$node],
  ];
  $total++;
}
// Define the batch.
$batch = [
  'title' => $this->t(
    'Updating @total contents',
    ['@total' => $total]
  ),
  'operations' => $operations,
  'finished' => '\Drupal\my_module\Service\Hello::batchFinished',
];
// Run the batch.
batch_set($batch);

Comme vous le constatez dans le tableau « operations » on défini quelle méthode de quelle classe on appelle et les paramètres de la méthode.

Notez bien que l’appel de la méthode est statique, cela signifie qu’à l’intérieur de cette méthode vous ne pourrez pas utiliser « $this« .

Voyons maintenant un exemple de méthode appelée :

/**
 * Update the node.
 *
 * @param \Drupal\node\Entity\Node $node
 *   The node to update.
 * @param array $context
 *   The batch context.
 */
public function updateNode(Node $node, &$context) {
  try {
    $node->set('field_custom', 'hello');
    $node->save();
    $context['results'][] = $node->id();
    $context['message'] = 'Update "' . $node->getTitle() . '"';
  }
  catch (Exception $e) {
    $context['message'] = 'Error when update node ' . $node->id();
    $context['message'] .= $e->getMessage();
  }
}

Vous voyez qu’en plus des arguments envoyer depuis l’opération du batch il y a un argument supplémentaire « $context » qui est envoyé par le batch lui-même. Cet argument vous permet d’interagir avec le batch durant l’opération pour afficher des information à l’utilisateur.

Pour la fonction de call back on peut prendre le parti d’afficher le statut du batch (succès ou échec) ainsi que le nombre de contenu mis à jour.

/**
 * Batch Finished callback.
 *
 * @param bool  $success
 *   Success of the operation.
 * @param array $results
 *   Array of results for post processing.
 * @param array $operations
 *   Array of operations.
 */
public function batchFinished($success, array $results, array $operations) {
  $messenger = \Drupal::messenger();

  if ($success) {
    $messenger->addStatus(t(
      '@count node updated.',
      ['@count' => count($results)]
    ));
  }
  else {
    $error_operation = reset($operations);
    $messenger->addError(t(
      'An error occurred while processing @operation with arguments : @args',
      [
        '@operation' => $error_operation[0],
        '@args' => print_r($error_operation[0], TRUE),
      ]
    ));
  }
}

Publié

dans

,

par

Étiquettes :