15

Drupal 8 introduced hook_post_update_NAME() which has some benefits over hook_update_n, for updating modules.

Each hook_post_update_NAME() should only run once, but sometimes I want to rerun it, like when I'm debugging the update hook during development. With hook_update_n, you can reset the schema version in the database.

How do you rerun hook_post_update_NAME()?

8 Answers 8

16

"post_update" hooks that have been run are stored in the database, in the key_value table, post_update collection, but the data is serialized and awkward to update directly.

I used some of the details from @kiamlaluno's answer to create a drush script which you can use to reset a single hook. Here's a basic version (longer version is here):

#!/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");
}

And here's an example of what it looks like when you run it from the command line:

./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
4
  • 4
    have you thought about contributing this back to drush, github.com/drush-ops/drush?
    – powpow12
    Commented Jan 29, 2018 at 17:30
  • 1
    This is a pretty sweet feature, but its a bit too niche for core Drush. Perhaps someone make a commandfile for it. Commented Oct 9, 2018 at 19:37
  • That's a very nice solution. I wish we had baked this into our Drupal setup. Commented Sep 18, 2020 at 18:51
  • 1
    This works well, I had to update the functions for Drush 10x. AND a gotcha is that when selecting '1' in the prompt it will return 0 which evaluates to false, so you need to use $choice !== FALSE Commented Feb 17, 2022 at 20:30
7

Here is an example you can use from the command line with drush php-eval:

drush php:eval '$update_hook_name = "<my_hook_post_update_name>";
$key_value = \Drupal::keyValue("post_update");
$existing_updates = $key_value->get("existing_updates");
$index = array_search($update_hook_name, $existing_updates); 
unset($existing_updates[$index]);
$key_value->set("existing_updates", $existing_updates);'

When you re-run drush updatedb you will see your post_update_hook waiting to be run.

3
  • This worked well for me, just to mention that in drush 9, it is drush php:eval 'command'
    – powpow12
    Commented Jan 29, 2018 at 17:31
  • Very useful, if on read-only environment. Many thanks ;)
    – Mirsoft
    Commented Apr 6, 2020 at 19:05
  • @powpow12 drush eval still works in both Drush 8 and 9
    – mbomb007
    Commented Sep 9, 2020 at 19:19
2

Here's the solution I worked up for rolling back updates. In my case, all the updates I wanted to remove and run again all start with 'MY_MODULE_post_update', so I'm filtering them out based on that string being present; adjust your filter condition in this command as appropriate:

drush ev '$updates = \Drupal::keyValue("post_update")->get("existing_updates"); \Drupal::keyValue("post_update")->set("existing_updates", array_diff_key($updates, array_filter($updates, function($v){return FALSE !== strstr($v,"MY_MODULE_post_update");})));'
1

UpdateRegistry::getPendingUpdateFunctions() contains the following code. See what the comment says.

  // First figure out which hook_{$this->updateType}_NAME got executed
  // already.
  $existing_update_functions = $this->keyValue->get('existing_updates', []);

UpdateRegistry::$updateType is set to 'post_update'.
$this->keyValue is set from UpdateRegistryFactory::create() with the value of $this->container->get('keyvalue')->get('post_update').

The equivalent procedural code to get that keyvalue collection is the following one.

$key_value = \Drupal::keyValue('post_update');

Set existing_updates to an empty array, and Drupal will think none of the post update callbacks have been called.

$key_value = \Drupal::keyValue('post_update');
$key_value->set('existing_updates', []);

Remove the callback name from the existing_updates key of that keyvalue, and Drupal will think that post update callback has not yet been invoked.

1

This is an updated script to use for Drupal 9.x Drush 10.x - it corrects the edge case of selecting the '1' as a choice (which returns 0) and allows the user to filter choices by a PREFIX.

https://gist.github.com/wayne-weibel/01e7f00b32dbcb96eb2785817693483f

<?php

use Drush\Commands\DrushCommands;

/**
 * @file
 * Reset module hook_post_update_NAME
 *
 * A tool for developing and debugging post_update hooks.
 *
 * This script will ONLY reset the hook to be run again ...
 *
 * If data is being changed, backup the database and restore as needed
 *
 * Usage:
 * drush php:script reset_hook_post_update.php [-- PREFIX]
 *
 * @see https://www.drush.org/latest/commands/php_script/
 *
 * https://gist.github.com/bryanbraun/852646078ef6b33d2dc2ecacc96c9865
 * https://www.drush.org/latest/api/Drush/Commands/DrushCommands.html
 */


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

$indexes = array_flip($update_list);
$choices = array_reverse($update_list);
if ($extra) {
    $prefix = array_shift($extra);
    $choices = array_filter(
        $choices,
        fn($c) => str_starts_with($c, $prefix)
    );
}

$io = DrushCommands::io();
$reset = $io->choice("Which post_update hook do you want to reset?", $choices, 0);
if ($reset !== FALSE) {
    $remove = $choices[$reset];
    $index = $indexes[$remove];
    if ($remove === $update_list[$index]) {
        unset($update_list[$index]);
        $key_value->set("existing_updates", $update_list);
        $io->success("$remove has been reset.");
    }
    else {
        // Really should not even get here!
        $io->error("Unable to match selection!");
    }
}
// Cancelled message is handled in `choice`.
1

If it is a one-time thing and you're okay with a quick fix rather than a nice fix that runs with drush updb, then you can do this to save time:

drush eval "require 'modules/custom/FOO/FOO.post_update.php'; FOO_post_update_NAME();"

Here, replace FOO with your module's name and replace NAME based on the name of your post-update hook.

Don't forget to test it on your local dev env and run it on all the environments that require this post-update.

0

There is a quick and dirty way that is fine for local development.

Temporarily append a suffix to the post-update function:

function mymodule_post_update_ABC_001(): {
  [..]
}

Here, _001 is a temporary suffix that you can count up during development, whereas ABC is the actual name of the update that you want to keep.

This method will pollute the key-value table, but this is ok, if you regularly destroy your local database with a new db import from production.

Of course you want to remove the temporary suffix when you commit to version control.

-1

Call it from within hook_update_n() and then do what you were doing before.

1
  • 3
    That doesn't seem like a good idea, since the entire purpose for the hook_post_update mechanism is to have a fully functional Drupal available after all updates have run. It was introduced because there are no guarantees about the state of Drupal during updates.
    – Eelke Blok
    Commented Aug 8, 2018 at 9:59

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.