I have two modules, module_alpha and module_beta.
Both modules install a database table with hook_schema()
which they use to track information about published nodes. Data is logged in the table with service methods executed from hook_node_insert()
and hook_node_update()
hooks. This all works.
But, I need to process all existing nodes when the module is installed to backfill the custom tables with information about the already existing published nodes. Each module implements hook_install()
and uses a batch process to process all existing published nodes. However, I am seeing strange inconsistencies. Sometimes the batch process executes, sometimes it does not. A scenario may work for one module, but not the other. All of this even though the code is nearly identical. Here are the scenarios I've tested:
Module install method |
Module Alpha |
Module Beta |
drush en MYMODULE |
Batch executes |
Batch does not execute |
drush cim |
Batch does not execute |
Batch does not execute |
UI, enable module from /admin/modules |
Batch executes |
Batch executes |
UI, sync config from /admin/config/development/configuration |
Batch executes |
Batch executes |
Batch executes indicates that the batch executes completely as expected, with progress messages, and my table is filled with data. Batch does not execute indicates that the batch is not executed at all. No error messages shown or in logs. The weirdest part about this data is that drush en module_alpha
always succeeds and drush en module_beta
always fails.
What could be causing the batch process to work in some cases but not in others? How can I consistently execute this batch process across all 4 of these methods, whether the module is enabled via UI or drush? To be honest, the only method I'll use is drush cim
Code:
In MYMODULE.install:
function MYMODULE_install() {
$batch = new \Drupal\Core\Batch\BatchBuilder();
$batch
->setTitle(t('Bulk processing existing items.'))
// This operation references a different function per module.
// In both cases, the function lives in MYMODULE.module and the code is nearly identical.
->addOperation('_MYMODULE_initialize_items', []);
batch_set($batch->toArray());
}
In MYMODULE.module:
function _MYMODULE_initialize_items(&$context) {
$batch_size = 25;
$entity_type = 'node';
// I target different bundles in module_alpha and module_beta.
$bundle = 'MY_TARGET_BUNDLE';
if (!isset($context['sandbox']['processed'])) {
$context['sandbox']['processed'] = 0;
}
if (empty($context['sandbox']['entity_ids'])) {
$context['sandbox']['entity_ids'] = \Drupal::entityTypeManager()
->getStorage($entity_type)
->getQuery()
->condition('type', $bundle)
->condition('status', 1)
->execute();
if (is_array($context['sandbox']['entity_ids'])) {
$context['sandbox']['total'] = count($context['sandbox']['entity_ids']);
}
}
if (!empty($context['sandbox']['entity_ids'])) {
$current_batch_ids = array_slice($context['sandbox']['entity_ids'], $context['sandbox']['processed'], $batch_size);
$current_batch_entities = \Drupal::entityTypeManager()
->getStorage($entity_type)
->loadMultiple($current_batch_ids);
foreach ($current_batch_entities as $entity) {
// I use a different services and methods in module_alpha and module_beta.
\Drupal::service('MYMODULE.my_service')
->processEntity($entity);
$context['sandbox']['processed']++;
}
}
if (!empty($context['sandbox']['total'])) {
$context['finished'] = $context['sandbox']['processed'] / $context['sandbox']['total'];
$context['message'] = t('Processed @processed of @total items.', [
'@processed' => $context['sandbox']['processed'],
'@total' => $context['sandbox']['total'],
]);
}
else {
$context['finished'] = 1;
}
}
Example service method called in the batch process:
public function processEntity(EntityInterface $entity) {
// $this->db is injected `@database` service, i.e. \Drupal::service('database');
return $this->db
->insert('MYMODULE_mytable')
->fields([
'entity_type' => $entity->getEntityTypeId(),
'entity_bundle' => $entity->bundle(),
'entity_uuid' => $entity->uuid(),
// etc...
])
->execute();
}