Skip to main content
Image
Code

How to implement Post Updates in Drupal

Post updates are processes that are executed to update the database of a Drupal installation. They are extremely useful when we need to address certain changes in the database or its structure.

Real case

Imagine that there are changes to an external API that your website uses to make POST requests. These new changes made to the API now cause an incompatibility error in the requests, because the API expects that the value of one of the fields will never arrive empty. The first step to solve this is to modify the configuration of said field so that it is required, and that the new content generated always has that field with some value. But what happens with the content already created? Many of these contents have that empty field, and they are what cause the error with the API. This is where we can use the post updates that Drupal provides.

How to implement a post update in the database

We need to program a script that processes all the contents of type "API Data" and assigns the value "0" to the "field_api_target" field that has an empty value.

The first step to implement a post update is to create a file MODULE.post_update.php , where "MODULE" is the name of your custom module.

Next, we are going to use the hook_post_update_NAME() . Here we must change the "hook" string to the name of our custom module and the "NAME" string to the identifier that we want to assign to this update task.

/**
 * Implements hook_post_update_NAME().
 */
function MODULE_post_update_api_data_value(&$sandbox) { }

First of all, we are going to adjust the variable $sandbox, which is the one that has the information of the update process. Through the condition   if (!isset($sandbox['total']))we achieve that it only enters this block in the first iteration of the batch process, and it will be to establish the total number of nodes to process and the current one (0).

if (!isset($sandbox['total'])) {
  // Get API Data content type nid's in first iteration.
    $nids = Drupal::entityQuery('node')
      ->condition('type', 'apidata')
      ->execute();
    $sandbox['total'] = count($nids);
    $sandbox['current'] = 0;
  }

We continue to configure what the batch process will be like. We establish that the nodes that are processed per batch are 50 by 50. Because the function will call itself recursively until the update process is finished , we must update its progress. We do this using an entityQuery indicating the type of content and the range, which will be in batches of 50.

$nodes_per_batch = 50;
$nids = Drupal::entityQuery('node')
  ->condition('type', 'apidata')
  ->range($sandbox['current'], $sandbox['current'] + $nodes_per_batch)
  ->execute();

Now is the time to make the changes we need to the node entity. For our specific example, we must update the field "field_api_target" and assign it the value "0" in those nodes that have no value (empty). To do this we go through $nids defined in the current code block, load the node entity, check if the field value is empty, and if it is we set the value "0". Finally we save the node and sequentially increase the variable $sandbox['current'].

foreach ($nids as $nid) {
  $node = Drupal::entityTypeManager()
    ->getStorage('node')
    ->load($nid);
  // Check if the field is empty.
  if (empty($node->field_api_target->value)) {
    // If is empty, set default value (0).
    $node->field_api_target->value = 0;
  }
  
  $node->save();
  $sandbox['current']++;
}

Finally, we add a condition to check if the batch process has finished, and if not, update the progress percentage.

if ($sandbox['total'] === 0) {
  // Until it is finished, the batch does not stop.
  $sandbox['#finished'] = 1;
}
else {
  // Indicates the percentage of the completed.
  $sandbox['#finished'] = ($sandbox['current'] / $sandbox['total']);
}

The complete code would look like this.

/**
 * Implements hook_post_update_NAME().
 */
function MODULE_post_update_api_data_value(&$sandbox) {

  if (!isset($sandbox['total'])) {
  // Get API Data content type nid's.
    $nids = Drupal::entityQuery('node')
      ->condition('type', 'apidata')
      ->execute();
    $sandbox['total'] = count($nids);
    $sandbox['current'] = 0;
  }

  $nodes_per_batch = 50;
  $nids = Drupal::entityQuery('node')
    ->condition('type', 'apidata')
    ->range($sandbox['current'], $nodes_per_batch)
    ->execute();

  foreach ($nids as $nid) {
    $node = Drupal::entityTypeManager()
      ->getStorage('node')
      ->load($nid);

	// Check if the field is empty.
    if (empty($node->field_api_target->value)) {
      // If is empty, set default value (0).
      $node->field_api_target->value = 0;
    }

    $node->save();
    $sandbox['current']++;
  }

  if ($sandbox['total'] === 0) {
    // Until it is finished, the batch does not stop.
    $sandbox['#finished'] = 1;
  }
  else {
    // Indicates the percentage of the completed.
    $sandbox['#finished'] = ($sandbox['current'] / $sandbox['total']);
  }
}

How to run a post update from Drupal

To execute a post update we must start a database update process. Therefore, it is highly recommended:

  • Make a backup of the database.
  • Put the website under maintenance.

To start a database update, we can do it from drush or the web interface .

Update database from Drush

Carrying out a database update from Drush is safe, efficient and more recommended than through the web interface. This is because from the web interface we run the risk of getting a timeout error that paralyzes the batch process or does not show the update progress correctly.

This article explains how to install Drush .

To launch the database update process, we must execute the following Drush command:

drush updb

We will be asked for confirmation. After confirming, the update process will start.

Update database from web interface

To start the update process in Drupal from the web interface, we must visit the URL your-domain.com/update.php . It is important that we are logged in with a user whose role has permissions to perform system updates. From here we will be shown graphically the pending updates and those that will be carried out.

End a database upgrade process

To conclude, we must review the status of the site, check if the nodes of type "API Data" have the value of the "field_api_target" field set to "0" and not empty. If everything is correct, we can deactivate the website maintenance mode.

The next times we want to implement new post updates, we have to take into account that those that were implemented in the past remain registered in the database, so they cannot be executed again.

We can obtain a complete list of all post updates executed on a Drupal website through the following Drush command:

drush eval '
$key_value = \Drupal::keyValue("post_update");
$update_list = $key_value->get("existing_updates");
print_r($update_list);
'

Rerun hook_post_update_NAME() more than once

hook_post_update_NAME()There may be times when you need to run a to update the database more than once , especially in development environments to test and validate that an implementation works.

In these cases, I'm sorry to tell you that Drupal does not allow you to re-launch a hook_post_update_NAME(). When the hook is executed for the first time, once the process is finished, it is saved in the key_value table of the database so that the same post_update NAME can never be launched again; you would have to create another one.

But don't worry, there is an unofficial solution that allows, through a custom drush command, to delete one or more executed post_updates from the key_value table of the database, so that they can be executed normally again. You can find the script in this post , and I'll show it to you below:

#!/usr/bin/env drush

$key_value = \Drupal::keyValue('post_update');
$update_list = $key_value->get('existing_updates');

$choice = drush_choice($update_list, dt('Which post_update hook do you want to reset?'));

if ($choice) {
  $removed_el = $update_list[$choice];
  unset($update_list[$choice]);
  $key_value->set('existing_updates', $update_list);
  drush_print("$removed_el was reset");
} else {
  drush_print("Reset was cancelled");
}

You can see the full version of the script here .

Then all you have to do is run the drush command from the command line to obtain the following output:

./scripts/reset_hook_post_update_NAME.drush

Which post_update hook do you want to reset?
 [0]   :  Cancel
 [1]   :  system_post_update_add_region_to_entity_displays
 [2]   :  system_post_update_hashes_clear_cache
 [3]   :  system_post_update_recalculate_configuration_entity_dependencies
 [4]   :  system_post_update_timestamp_plugins
 [5]   :  my_module_post_update_example_hook

# The script pauses for user input. 
5 

my_module_post_update_example_hook was reset

This way, we can re-launch it hook_post_update_NAME()as many times as we need.

Conclusion

Post updates are really useful on a daily basis, and if we learn to work with them correctly we will save ourselves a lot of problems during their implementation.

And you, have you ever used a post update? How have you implemented it? Leave your comment and lend a hand to the Drupal Sapiens community ;)

See you next time!

Tags

Join the Drupal Sapiens Community

Save content that interests you, connect with others in the community, contribute to win prizes and much more.

Login or create an account here

Latest posts

Featured

Last news