Ever deployed an update hook that failed halfway through, fixed the bug, and wondered how to re-run just that one hook without starting from scratch? Or needed to test a specific update hook during development without running all pending updates? There's a targeted approach for this.
What you will learn:
- What update hooks are and when they run
- How to execute a specific update hook using Drush
- Testing update hooks during development
- Handling failed updates and re-running them safely
- Best practices for writing reliable update hooks
Prerequisites:
- Basic understanding of Drupal modules and
.installfiles - Familiarity with Drush commands
- A working Drupal 11 environment (examples use DDEV)
- Understanding of PHP functions
Estimated time: 20 minutes
Understanding Update Hooks
What Are Update Hooks?
Update hooks are special functions in a module's .install file that execute database changes, configuration updates, or other one-time operations when updating your Drupal site. They follow the naming pattern hook_update_N() where N is an integer version number.
Example from web/modules/custom/my_module/my_module.install:
<?php
/**
* @file
* Install, update and uninstall functions for My Module.
*/
declare(strict_types=1);
/**
* Add a new field to store user preferences.
*/
function my_module_update_9001(): void {
$config_factory = \Drupal::configFactory();
$config = $config_factory->getEditable('my_module.settings');
$config->set('enable_preferences', TRUE);
$config->save();
}
/**
* Migrate legacy data to new structure.
*/
function my_module_update_9002(): string {
$database = \Drupal::database();
// Perform migration logic here
$count = $database->update('my_module_data')
->fields(['migrated' => 1])
->condition('migrated', 0)
->execute();
return t('Migrated @count records.', ['@count' => $count]);
}
When Do Update Hooks Run?
Update hooks execute in these scenarios:
- Via Drush: Running
drush updb -y(update database) - Via UI: Visiting
/update.phpin your browser - During Deployment: Automated in CI/CD pipelines
Drupal tracks which updates have run in the key_value table and only executes hooks with higher version numbers than the last recorded update.
Running a Specific Update Hook
The Command
To run a specific update hook directly, use this Drush command:
drush php-eval "module_load_install('MYMODULE'); MYMODULE_update_NUMBER();"
Replace:
MYMODULEwith your module's machine nameNUMBERwith the update hook number
Real-World Examples
Example 1: Testing a Configuration Update
You have this update hook:
/**
* Enable the new analytics feature.
*/
function my_module_update_9003(): void {
$config = \Drupal::configFactory()->getEditable('my_module.settings');
$config->set('analytics_enabled', TRUE);
$config->save();
}
Run it directly:
# Via DDEV
ddev drush php-eval "module_load_install('my_module'); my_module_update_9003();"
# Direct Drush (production)
vendor/bin/drush php-eval "module_load_install('my_module'); my_module_update_9003();"
Example 2: Re-running a Failed Update with Return Value
If your update hook returns a message:
/**
* Rebuild node access records.
*/
function my_module_update_9004(): string {
node_access_rebuild();
return t('Node access records have been rebuilt.');
}
Capture the output:
ddev drush php-eval "module_load_install('my_module'); echo my_module_update_9004();"
Output:
Node access records have been rebuilt.
Example 3: Update Hook with Database Changes
/**
* Add status column to custom table.
*/
function my_module_update_9005(): void {
$schema = \Drupal::database()->schema();
if (!$schema->fieldExists('my_module_data', 'status')) {
$schema->addField('my_module_data', 'status', [
'type' => 'varchar',
'length' => 20,
'not null' => TRUE,
'default' => 'active',
'description' => 'Record status',
]);
}
}
Run and verify:
# Run the update
ddev drush php-eval "module_load_install('my_module'); my_module_update_9005();"
# Verify the field was added
ddev drush sqlq "DESCRIBE my_module_data;"
Common Use Cases
Scenario 1: Failed Update During Deployment
Problem: Update hook my_module_update_9007() failed in production due to a bug. You've fixed the code and need to re-run just that hook.
Solution:
- Fix the bug in your update hook code
- Deploy the fixed code
- Run the specific update:
ssh user@production.server
cd /var/www/html/site
vendor/bin/drush php-eval "module_load_install('my_module'); my_module_update_9007();"
- Mark it as complete in Drupal's tracking (if successful):
vendor/bin/drush php-eval "\Drupal::keyValue('system.schema')->set('my_module', 9007);"
Warning: Only mark as complete if you're certain the update succeeded. Drupal won't run it again automatically otherwise.
Scenario 2: Testing During Development
Problem: You're writing a complex update hook and want to test it multiple times without incrementing the version number.
Workflow:
- Write your update hook (e.g.,
my_module_update_9010()) - Test it:
ddev drush php-eval "module_load_install('my_module'); my_module_update_9010();"
- Check results, modify code, repeat step 2
- Once working, run officially:
ddev drush updb -y
Scenario 3: Debugging Update Hook Logic
Problem: Update hook isn't behaving as expected. You need to step through it.
Solution: Add debugging output:
/**
* Complex data migration.
*/
function my_module_update_9008(): string {
$database = \Drupal::database();
// Debug: Show what we're about to process
$count = $database->select('old_table', 'o')
->countQuery()
->execute()
->fetchField();
\Drupal::logger('my_module')->notice('Processing @count records', ['@count' => $count]);
// Actual migration logic
// ...
return t('Processed @count records.', ['@count' => $count]);
}
Run with output:
ddev drush php-eval "module_load_install('my_module'); echo my_module_update_9008();"
# Check logs
ddev drush watchdog:show --type=my_module
Important Considerations
Update Hook Numbering
Drupal uses a specific numbering scheme:
- Drupal 8/9: Format
80XXor90XX(e.g.,8001,9001) - Drupal 10/11: Format
100XXor110XX(e.g.,10001,11001) - Sequential: Each module maintains its own sequence
Best practice: Start Drupal 11 modules at 11001 and increment sequentially.
Idempotency
Update hooks should be idempotent - safe to run multiple times without causing problems.
Bad (not idempotent):
function my_module_update_9009(): void {
$schema = \Drupal::database()->schema();
// This will fail if run twice!
$schema->addField('my_table', 'new_field', [...]);
}
Good (idempotent):
function my_module_update_9009(): void {
$schema = \Drupal::database()->schema();
// Check first
if (!$schema->fieldExists('my_table', 'new_field')) {
$schema->addField('my_table', 'new_field', [...]);
}
}
Dependency Handling
Update hooks can depend on other updates running first. Declare dependencies using hook_update_dependencies().
/**
* Implements hook_update_dependencies().
*/
function my_module_update_dependencies(): array {
// Indicate that my_module_update_9011() must run AFTER my_module_update_9005()
// and other_module_update_9003().
$dependencies['my_module'][9011] = [
'my_module' => 9005,
'other_module' => 9003,
];
return $dependencies;
}
Example update function with dependencies:
/**
* Migrate user data to new table structure.
*
* Depends on update 9005 creating the new table.
*/
function my_module_update_9011(): void {
// This runs only after dependencies complete
$database = \Drupal::database();
// ... migration logic ...
}
Note: When running updates manually with
php-eval, dependencies are not checked automatically. Ensure prerequisite updates have run first.
Best Practices
Do
- Test in a safe environment first - Never run untested updates directly in production
- Make hooks idempotent - Always check if changes already exist before applying them
- Add descriptive docblocks - Explain what the update does and why
- Return status messages - Use return values to report what happened
- Use transactions for database changes - Wrap complex operations in transactions
- Log important operations - Use
\Drupal::logger()for debugging
Avoid
- Assuming state - Don't assume content or configuration exists
- Skipping error handling - Always wrap risky operations in try-catch blocks
- Hardcoding values - Use configuration where possible
- Running destructive operations without confirmation - Especially for data deletion
- Forgetting to test rollback scenarios - Have a backup plan
Example: Well-Written Update Hook
<?php
declare(strict_types=1);
/**
* Migrate article content from legacy field to new paragraph field.
*
* Converts old 'field_legacy_content' to 'field_content_paragraphs'.
* This is a one-time migration for the content restructuring project.
*
* Dependencies: Requires paragraphs module update 11001 to run first.
* See my_module_update_dependencies() for dependency declaration.
*/
function my_module_update_11002(): string {
$entity_type_manager = \Drupal::entityTypeManager();
$logger = \Drupal::logger('my_module');
try {
$storage = $entity_type_manager->getStorage('node');
$query = $storage->getQuery()
->condition('type', 'article')
->exists('field_legacy_content')
->accessCheck(FALSE);
$nids = $query->execute();
if (empty($nids)) {
return t('No articles found requiring migration.');
}
$count = 0;
foreach ($storage->loadMultiple($nids) as $node) {
// Check if already migrated (idempotency check)
if (!$node->get('field_content_paragraphs')->isEmpty()) {
continue;
}
// Perform migration
$legacy_value = $node->get('field_legacy_content')->value;
// Create paragraph entity
// ... migration logic ...
$node->save();
$count++;
}
$logger->notice('Migrated @count articles', ['@count' => $count]);
return t('Successfully migrated @count articles.', ['@count' => $count]);
}
catch (\Exception $e) {
$logger->error('Migration failed: @message', ['@message' => $e->getMessage()]);
throw new \Exception('Article migration failed: ' . $e->getMessage());
}
}
Debugging Failed Updates
Check Update Status
See which updates have run and which are pending:
# Show pending updates
ddev drush updatedb:status
# Show installed schema versions
ddev drush php-eval "print_r(\Drupal::keyValue('system.schema')->getAll());"
Reset Update Status
If you need to mark an update as not yet run (to allow it to run again via drush updb):
# Set module to earlier version
ddev drush php-eval "\Drupal::keyValue('system.schema')->set('my_module', 9005);"
# Now update 9006 and later will run again
ddev drush updb -y
Danger: This can cause issues if the update already made changes. Only do this if you understand the implications.
View Update Registry
# See all registered update hooks
ddev drush php-eval "module_load_install('my_module'); print_r(get_defined_functions()['user']);" | grep update
Common Errors
Error: "Module X is not installed"
# Verify module is enabled
ddev drush pml | grep my_module
# Enable if needed
ddev drush en my_module -y
Error: "Call to undefined function"
Problem: Drupal hasn't loaded the .install file.
Solution:
# Explicitly load it
ddev drush php-eval "module_load_install('my_module'); my_module_update_9001();"
Error: "Schema version already exists"
Problem: Drupal thinks the update already ran.
Solution: Check if it actually did:
ddev drush php-eval "echo \Drupal::keyValue('system.schema')->get('my_module');"
If you need to run it again, either reset the schema version or add a new update hook with a higher number.
Alternative: Using Drush's Update Runner
While php-eval is useful for testing, Drush provides a safer way to run updates that respects dependencies:
# Run all pending updates
ddev drush updb -y
This approach:
- Checks dependencies
- Updates the schema registry automatically
- Provides proper error handling
- Shows progress output
Note: Unlike
php-eval, there is no built-in way to run just a single specific update via Drush. You can only run all pending updates together.
Quick Reference
Essential Commands
| Command | Purpose |
|---|---|
drush php-eval "module_load_install('MODULE'); MODULE_update_N();" |
Run specific update hook |
drush updb -y |
Run all pending updates |
drush updatedb:status |
Show pending updates |
drush php-eval "\Drupal::keyValue('system.schema')->get('MODULE');" |
Check current schema version |
drush php-eval "\Drupal::keyValue('system.schema')->set('MODULE', N);" |
Set schema version |
Next Steps
Continue learning about Drupal updates and deployment:
- Post Update Hooks - Using
hook_post_update_NAME()for configuration updates - Deploy Hooks - Drupal 10+
hook_deploy_NAME()for deployment tasks - Batch API - Processing large datasets in update hooks
- Configuration Management - Coordinating config updates with code changes
Further Reading:
Summary
You've learned how to:
- ✅ Execute specific update hooks using
drush php-eval - ✅ Test updates during development without affecting the registry
- ✅ Handle failed updates and re-run them safely
- ✅ Write idempotent, well-structured update hooks
- ✅ Debug common update hook problems
Key takeaway: While drush updb is the standard way to run updates, directly invoking specific hooks with php-eval is invaluable for testing, debugging, and handling edge cases during development and deployment.