Third & GroveThird & Grove
May 18, 2017 - Ed Hornig

Using the Batch API and hook_update_N in Drupal 8

In this post, we show you how to add a new field to a content type in Drupal 8 and resave all nodes with default values for the field, using the Batch API and hook_update_N.

Let’s say you have existing content on your Drupal 8 site and you want to add a new field to a content type. In our example, we’ll add a boolean field_registered to the Person content type. You can do this easily enough through the GUI, but after you create the field, field_registered will not have any values in the database. This means that if you have a View which filters by field_registered, it will return no results.

To solve this problem, we need to assign a default value to field_registered for all Person nodes.

use Drupal\node\Entity\Node;
 
$nids = \Drupal::entityQuery('node')
  ->condition('type', 'person')
  ->execute();
 
foreach($nids as $nid) {
  $node = Node::load($nid);
  $node->field_registered->value = 0;
  $node->save();
}

 

This code should be executed in hook_update_N(), which should be put in the .install file of an appropriate custom module. This allows us to execute the code through the GUI at /update.php or using the drush command drush updb.

use Drupal\node\Entity\Node;
 
/**
* Implements hook_update_N().
*
* Set default value to new field field_registered on all Person nodes.
*/
function MY_MODULE_update_8201() {
  $nids = \Drupal::entityQuery('node')
    ->condition('type', 'person')
    ->execute();
 
  foreach($nids as $nid) {
    $node = Node::load($nid);
    $node->field_board_member->value = 0;
    $node->save();
  }
}

 

If there are less than 50 Person nodes, this should suffice, but if you already have lots of existing content on the site, loading and saving all these nodes could cause PHP to time out. In this case, we should make use of the $sandbox parameter in hook_update_N($sandbox) to indicate that the Batch API should be used for our update. This is known as a multi-pass update.

To run a multi-pass update, you must set $sandbox[‘#finished’] equal to a number between 0 and 1 within hook_update_N(). This number should indicate the percent of work complete. When $sandbox[‘#finished’] is equal to 1, Drupal knows the batch process is complete. Note that $sandbox is passed by reference (indicated by the & symbol). For example, the code below will loop through 10 times before $sandbox[‘#finished’] == 1 and the process is complete.

/**
* Implements hook_update_N().
*/
function MY_MODULE_update_8001(&$sandbox) {
 
  if (!isset($sandbox['total'])) {
    $sandbox['total'] = 10;
    $sandbox['current'] = 0;
  }
 
  $sandbox['current']++;
 
  //Once $sandbox['#finished'] == 1, the process is complete.
  $sandbox['#finished'] = ($sandbox['current'] / $sandbox['total']);
}

 

Now we can assign default field_registered values to all of our Person nodes in batches. For the first pass through, we’ll set $sandbox[‘total’] to be the total number of Person nodes, and $sandbox[‘current’] to be zero. We’re using Drupal 8’s entity.query service to find all these nodes. When we’re ready to process the first batch of nodes, we can use the range() method to only grab the nodes in our batch, starting at $sandbox[‘current’] and ending at $sandbox[‘current’] + $nodes_per_batch.

use Drupal\node\Entity\Node;
 
/**
* Implements hook_update_N().
*
* Set default value to new field field_registered on all Person nodes.
*/
function MY_MODULE_update_8001(&$sandbox) {
  // Initialize some variables during the first pass through.
  if (!isset($sandbox['total'])) {
    $nids = \Drupal::entityQuery('node')
      ->condition('type', 'person')
      ->execute();
    $sandbox['total'] = count($nids);
    $sandbox['current'] = 0;
  }
 
  $nodes_per_batch = 25;
 
  // Handle one pass through.
  $nids = \Drupal::entityQuery('node')
    ->condition('type', 'person')
    ->range($sandbox['current'], $sandbox['current'] + $nodes_per_batch)
    ->execute();
 
  foreach($nids as $nid) {
    $node = Node::load($nid);
    $node->field_board_member->value = 0;
    $node->save();
    $sandbox['current']++;
  }
 
  drupal_set_message($sandbox['current'] . ' nodes processed.');
 
  if ($sandbox['total'] == 0) {
    $sandbox['#finished'] = 1;
  } else {
    $sandbox['#finished'] = ($sandbox['current'] / $sandbox['total']);
  }
 
}

 

That’s it! We have successfully set new default field values on all nodes of the Person content type, without overloading PHP by processing them all at once. Now our view which filters by our new field will work because all the Person nodes will have values set for the field.