4

Most tutorials about Drupal batch processing suggests that you provide something like the following as a callback for when the batch has finished.

function mymodule_finished($success, $results, $operations) {
  if ($success) {
    drupal_set_message(t('Success'));
  } else {
    $error_operation = reset($operations);
    drupal_set_message(t('At least one error occurred'));
  }
}

This is also how my finished-callback looks like (I know you can report more fancy statistics about the operations, but that is not relevant here. I want to keep this question simple).

I want to loop through a bunch of files in a batch and report at the end if any of them are not readable by the web server. I do not want to abort the batch when a non readable file is found.

My test site is a clean Drupal 7.37 + Devel and a custom module called "mymodule".

This is how operations function for processing a single file looks like:

function mymodule_process_one($file, &$context) {
  $context['results'][] = check_plain($file);
  if (!is_readable ($file)) {
    // I need some code here that indicates that this is an error.
  }
}

I've indicated with a comment were I think there should be some code that indicates that there is some non-fatal error. My question is: What should this code be?

I've made sure that one of files processed are not readable. Using Devel to print out the three variables passed into mymodule_finished, this is what I get:

$success is TRUE.
$results is an array with all the file names.
$operations is an empty array.

Since there is no actual code in the operations-function to mark an unreadable file as an error , my finished-callback always print "Success", even if there are one or more unreadable files in the batch. How can I make it report any errors at the end?

PS: I've searched the site and found examples on how to terminate the batch process if a fatal error occurs in an operation - e.g.: How to terminate a batch when an error occurs and Can a batch process be stopped. I am not interested in that. I want an answer that explains how to process all items and produce an error message in finished callback if one or more operations failed.

Edit: I know that I can accomplish this by using a global db variable as an error flag. However, I am not interested in workarounds. I want to learn how do this correctly using the batch API.

  • Never crawled in this Drupal subdomain, but I think you might: - first variable_set('mymodule_batch_error', false) at the begin of your batch - then in mymodule_process_one() variable_set('mymodule_batch_error', true) if an error occurs - finally test variable_get('mymodule_batch_error') in mymodule_finished() to choose which message has to be displayed – cFreed Jun 8 '15 at 1:26
  • Thanks for replying. Your answer is a workaround. I am looking for how to do this correctly with the batch API. I've edited the question to make this goal clearer. – Free Radical Jun 9 '15 at 5:22
3

See here for more information on this: https://api.drupal.org/api/drupal/includes%21form.inc/group/batch/7

Updated answer:

After the comments I realized what you are trying to do, so I setup the example module and came up with this code to show if one error happened but others ran. You can take a look and hopefully use this to help come up with your final answer.

<?php

/**
 * @file
 * Outlines how a module can use the Batch API.
 */

/**
 * @defgroup batch_example Example: Batch API
 * @ingroup examples
 * @{
 * Outlines how a module can use the Batch API.
 *
 * Batches allow heavy processing to be spread out over several page
 * requests, ensuring that the processing does not get interrupted
 * because of a PHP timeout, while allowing the user to receive feedback
 * on the progress of the ongoing operations. It also can prevent out of memory
 * situations.
 *
 * The @link batch_example.install .install file @endlink also shows how the
 * Batch API can be used to handle long-running hook_update_N() functions.
 *
 * Two harmless batches are defined:
 * - batch 1: Load the node with the lowest nid 100 times.
 * - batch 2: Load all nodes, 20 times and uses a progressive op, loading nodes
 *   by groups of 5.
 * @see batch
 */

/**
 * Implements hook_menu().
 */
function batch_example_menu() {
  $items = array();
  $items['examples/batch_example'] = array(
    'title' => 'Batch example',
    'description' => 'Example of Drupal batch processing',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('batch_example_simple_form'),
    'access callback' => TRUE,
  );

  return $items;
}

/**
 * Form builder function to allow choice of which batch to run.
 */
function batch_example_simple_form() {
  $form['description'] = array(
    '#type' => 'markup',
    '#markup' => t('This example is modified for files and sets error if only one file is not readable.'),
  );
  $form['batch'] = array(
    '#type' => 'select',
    '#title' => 'Choose batch',
    '#options' => array(
      'batch_1' => t('batch 1 - load files'),
    ),
  );
  $form['submit'] = array(
    '#type' => 'submit',
    '#value' => 'Go',
  );

  // If no nodes, prevent submission.
  // Find out if we have a node to work with. Otherwise it won't work.
  $nid = batch_example_lowest_nid();
  if (empty($nid)) {
    drupal_set_message(t("You don't currently have any nodes, and this example requires a node to work with. As a result, this form is disabled."));
    $form['submit']['#disabled'] = TRUE;
  }
  return $form;
}

/**
 * Submit handler.
 *
 * @param array $form
 *   Form API form.
 * @param array $form_state
 *   Form API form.
 */
function batch_example_simple_form_submit($form, &$form_state) {
  $function = 'batch_example_' . $form_state['values']['batch'];

  // Reset counter for debug information.
  $_SESSION['http_request_count'] = 0;

  // Execute the function named batch_example_1 or batch_example_2.
  $batch = $function();
  batch_set($batch);
}


/**
 * Batch 1 definition: Scan file directory /maintenance on test site.
 *
 */
function batch_example_batch_1() {

  $operations = array();


  $files = file_scan_directory('sites/default/files/maintenance', '/.*\.(png|jpg)$/');
  $i = 0;
  foreach ($files as $value) {
    $file_efq = new EntityFieldQuery();
    $file_efq->entityCondition('entity_type', 'file')
      ->propertyCondition('filename', $value->filename);
    $file_efq_result = $file_efq->execute();
    if (count($file_efq_result)) {
      // This will only add the first match from EFQ for this filename
      $fid = array_pop($file_efq_result['file'])->fid;
    }
    if (!empty($fid)){
       $operations[] = array(
        'batch_example_op_1',
        array(
          $fid,
          t('(Operation @operation)', array('@operation' => $i)),
        ),
      );
      $i++;
    }
  }

  $batch = array(
    'operations' => $operations,
    'finished' => 'batch_example_finished',
  );
  return $batch;
}

/**
 * Batch operation for batch 1: load a file.
 *
 * This is the function that is called on each operation in batch 1.
 */
function batch_example_op_1($fid, $operation_details, &$context) {
  // $node = node_load($nid, NULL, TRUE);
     $file = file_load($fid);
  // Store some results for post-processing in the 'finished' callback.
  // The contents of 'results' will be available as $results in the
  // 'finished' function (in this example, batch_example_finished()).
  if (is_readable($file->uri)){
    $context['results'][] = $file->fid . ' : ' . check_plain($file->filename) . ' ' . $operation_details;
  }else{
    $context['results']['error'] = $file->fid . ' : ' . check_plain($file->filename) . ' ' . $operation_details;
  }
  // Optional message displayed under the progressbar.
  $context['message'] = t('Loading file "@uri"', array('@uri' => $file->uri)) . ' ' . $operation_details;

  _batch_example_update_http_requests();
}


/**
 * Batch 'finished' callback used by both batch 1 and batch 2.
 */
function batch_example_finished($success, $results, $operations) {

  if (!$results['error']) {
    // Here we could do something meaningful with the results.
    // We just display the number of nodes we processed...
    drupal_set_message(t('@count results processed in @requests HTTP requests.', array('@count' => count($results), '@requests' => _batch_example_get_http_requests())));
    drupal_set_message(t('The final result was "%final"', array('%final' => end($results))));
  }
  else {
    // An error occurred.
    // $operations contains the operations that remained unprocessed.
    $error_operation = $results['error'];
    drupal_set_message(
      t('An error occurred while processing @operation',
        array(
          '@operation' => print_r($error_operation)
        )
      )
    );
    drupal_set_message(t('The final result was "%final"', array('%final' => end($results))));
  }
}



/**
 * Utility function to increment HTTP requests in a session variable.
 */
function _batch_example_update_http_requests() {
  $_SESSION['http_request_count']++;
}

/**
 * Utility function to count the HTTP requests in a session variable.
 *
 * @return int
 *   Number of requests.
 */
function _batch_example_get_http_requests() {
  return !empty($_SESSION['http_request_count']) ? $_SESSION['http_request_count'] : 0;
}
/**
 * @} End of "defgroup batch_example".
 */
?>

You can see where I scan a public directory on my site, then I go in and set one of those files to not be readable by Drupal and I get my error message. Looking at the pages I included earlier that is exactly what you are supposed to do and the $operations variable would be anything that fails with a Fatal error. Hope that helps. If you need any explanations just let me know, and sorry about leaving examples in there, I will try to clean it up later.

The main part of the code from above that answers OP's question is below with explanation:

  if (is_readable($file->uri)){
    $context['results'][] = $file->fid . ' : ' . check_plain($file->filename) . ' ' . $operation_details;
  }else{
    $context['results']['error'] = $file->fid . ' : ' . check_plain($file->filename) . ' ' . $operation_details;
  }

In the batch documentation it states:

// The 'success' parameter means no fatal PHP errors were detected. All
// other error management should be handled using 'results'.

So you would have to change your finished callback to include the error management also.

// I don't recommend using this, I just used it as a test. You should use $success and your other error management handling together here.
if (!$results['error']) {
    // Here we could do something meaningful with the results.
    // We just display the number of nodes we processed...
    drupal_set_message(t('@count results processed in @requests HTTP requests.', array('@count' => count($results), '@requests' => _batch_example_get_http_requests())));
    drupal_set_message(t('The final result was "%final"', array('%final' => end($results))));
  }
  else {
    // An error occurred.
    // $operations contains the operations that remained unprocessed.
    $error_operation = $results['error'];
    drupal_set_message(
      t('An error occurred while processing @operation',
        array(
          '@operation' => print_r($error_operation)
        )
      )
    );
    drupal_set_message(t('The final result was "%final"', array('%final' => end($results))));
  }

While looking at some more of the doc pages I found a link to a github module that has another setup of error handling view $results array and how they use this. This may help to see what can actually be done here and other ways it can be setup. Here is the page for that module CSV Import. This is the documentation page that was linked to the module code that has the example there, you will notice that $context['results']['failed_rows'][] is what they use in their error handling.

Old parts of answer, left b/c some of the information is relevant or could be to future observers.

This is the way the example modules show to print errors and number of items that completed at the end with the callback function. It says the $operations variable shows the number of operations that did not run.

function batch_example_finished($success, $results, $operations) {
if ($success) {
  // Here we could do something meaningful with the results.
  // We just display the number of nodes we processed...
  drupal_set_message(t('@count results processed in @requests HTTP requests.', array('@count' => count($results), '@requests' => _batch_example_get_http_requests())));
  drupal_set_message(t('The final result was "%final"', array('%final' => end($results))));
}
else {
  // An error occurred.
  // $operations contains the operations that remained unprocessed.
  $error_operation = reset($operations);
  drupal_set_message(t('An error occurred while processing @operation with arguments : @args', array(
   '@operation' => $error_operation[0],
   '@args' =>  print_r($error_operation[0], TRUE),
    )));
  }
}

You can find more about this in the example modules: https://api.drupal.org/api/examples/batch_example%21batch_example.module/function/batch_example_finished/7

If you want to make sure that the success works (may not work with some non-core modules enabled) you can do a couple of different things to throw an error.

Explicitly set an error in the json callbacks

<?php
   function some_batch_operation(&$context) {
     // do stuff
     // if something goes wrong
     $result = array('status' => FALSE, 'data' => $message);
     die(json_encode($result));
   }
?>

Throw an exception.

<?php
   throw new Exception(t('My error message.')); 
 ?>
4

Preamble:
The answer given by burnsjermey did help me find the solution. He deserves kudos for pointing me to the correct documentation and this project on github. Both helped a lot, so I've accepted his answer.

However, his answer contains a lot of stuff that has nothing to do with the question. Also, his code examples have bugs that stops them from working. Therefore, I also want to add a self-answer containing only code snippets that are relevant to the question asked.


Self-answer:
First, as pointed out in the API documentation, non-fatal error management should be handled using 'results'. Hence, when we encounter a non-fatal error, we add an error message to the results array:

function mymodule_process_one($file, &$context) {
  if (!is_readable ($file)) {
    $context['results']['error'][] = t('@file is not readable.', array('@file' => $file));
  }
  else {
    $context['results'][] = t('@file processed.', array('@file' => $file));
  }
}

Then, in the finished-callback, we put code that looks for these errors, and prints them out if there are any.

function mymodule_finished($success, $results, $operations) {
  if ($success) {
    if (isset($results['error'])) {
      drupal_set_message(t('There were !count non-fatal error(s):', array(
       '!count' => count($results['error']),
      )), 'error');
      foreach ($results['error'] as $error) {
         drupal_set_message($error, 'error');
      }
    }
    else {
      drupal_set_message(t('No errors.'));
    }
  } else {
    $error_operation = reset($operations);
    drupal_set_message(t('A fatal error occurred'));
  }
}

That's all folks!


PS:
If you want to learn how to handle fatal errors during batch processing, the best answer is the accepted answer to: Can a batch process be stopped.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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