I'm working on a Drupal 8 custom module to scrape pages from a site for migration. Since we're dealing with a large number of pages, I wanted to set up batch processing, to deal with timeouts.
I looked at two tutorials, this one, and one here. However, when I ran my batch, it didn't work as expected. The progress bar immediately filled up to all 71 items, and there were no imported pages, but a number of errors in the error log.
To rule out confounding factors, I created a simple module for testing the batch api (I didn't find the batch represented in the D8 examples module). It exhibits the same behavior: it immediately processes all items, and there are no successes, just errors. This an the example error:
Warning: call_user_func_array() expects parameter 1 to be a valid callback, class '\Drupal\batch_test\BatchTestForm' not found in _batch_process() (line 252 of C:\wamp64\www\D8\core\includes\batch.inc)
`#0` C:\wamp64\www\D8\core\includes\bootstrap.inc(566): _drupal_error_handler_real(2, 'call_user_func_...', 'C:\\wamp64\\www\\p...', 252, Array)
`#1` [internal function]: _drupal_error_handler(2, 'call_user_func_...', 'C:\\wamp64\\www\\p...', 252, Array)
`#2` C:\wamp64\www\D8\core\includes\batch.inc(252): call_user_func_array('\\Drupal\\batch_t...', NULL)
`#3` C:\wamp64\www\D8\core\includes\batch.inc(95): _batch_process()
`#4` C:\wamp64\www\D8\core\includes\batch.inc(77): _batch_do()
`#5` C:\wamp64\www\D8\core\modules\system\src\Controller\BatchController.php(55): _batch_page(Object(Symfony\Component\HttpFoundation\Request))
`#6` [internal function]: Drupal\system\Controller\BatchController->batchPage(Object(Symfony\Component\HttpFoundation\Request))
`#7` C:\wamp64\www\D8\core\lib\Drupal\Core\EventSubscriber\EarlyRenderingControllerWrapperSubscriber.php(123): call_user_func_array(Array, Array)
`#8` C:\wamp64\www\D8\core\lib\Drupal\Core\Render\Renderer.php(574): Drupal\Core\EventSubscriber\EarlyRenderingControllerWrapperSubscriber->Drupal\Core\EventSubscriber\{closure}()
The following is the form that I'm using. Both of the tutorials indicate that calling _batch_process()
is not necessary when calling batch_set()
in a form submit.
namespace Drupal\batch_test\Form;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Form\FormBase;
class BatchTestForm extends FormBase {
public function getFormId() {
return 'batch_test_admin_form';
}
public function buildForm(array $form, FormStateInterface $form_state) {
$form['submit'] = [
'#type' => 'submit',
'#value' => $this->t('Test Batch'),
'#button_type' => 'primary',
];
return $form;
}
public function submitForm(array &$form, FormStateInterface $form_state) {
$batch = array(
'title' => t('Test Batch'),
'operations' => [],
'finished' => 'Drupal\BatchTest\Form\batchCallback',
'progress_message' => t('Processed @current out of @total.'),
);
for ( $i = 0; $i <= mt_rand(10,100); $i++ ) {
$batch['operations'][] = ['\Drupal\batch_test\BatchTestForm::process',$i];
}
batch_set($batch);
}
public function process($input, &$context) {
// log a random string
\Drupal::logger('batch_test')->notice(base64_encode(random_bytes(10)));
sleep(mt_rand(1,100));
}
public function batchCallback($success, $results, $operations) {
if ($success) {
$message = \Drupal::translation()->formatPlural(
count($results),
'One page scraped.', '@count pages scraped.'
);
}
else {
$message = t('Finished with an error.');
}
drupal_set_message($message);
}
}
I suspect that something is wrong in the specification of the $batch
variable, but I've cross-checked it, and it seems to conform to what the tutorials indicate.
The example in the documentation does not clearly indicate how to pass multiple data points to be called on with the same function.
'operations' => array(
array('my_function_1', array($account->id(), 'story')),
array('my_function_2', array()),
),
Interestingly, one tutorial says to pass the data as a single array.
$batch = array(
'title' => t('Deleting Node...'),
'operations' => array(
array(
'\Drupal\batch_example\DeleteNode::deleteNodeExample',
array($nids)
),
),
'finished' => '\Drupal\batch_example\DeleteNode::deleteNodeExampleFinishedCallback',
);
Another tutorial indicates to pass each data point as a separate element in the operations array.
$operations = [];
foreach ($letters as $letter) {
$operations[] = ['smackmybatch_process_letter', [$letter]];
}
I'm not entirely sure what this one is specifying.
'operations' => array(
array('disc_migrate', array('courses', array('foo' => 'bar'))),
),
So, what is the correct way to pass the data that I want to have processed in the batch API?
$batch['operations'][] = ['\Drupal\batch_test\BatchTestForm::process',$i];
should be$batch['operations'][] = ['\Drupal\batch_test\Form\BatchTestForm::process',$i];
; which would explain the warning your get first.