You are here

public static function DataExport::processBatch in Views data export 8

Implements callback_batch_operation() - perform processing on each batch.

Writes rendered data export View rows to an output file that will be returned by callback_batch_finished() (i.e. finishBatch) when we're done.

Parameters

string $view_id: ID of the view.

string $display_id: ID of the view display.

array $args: Views arguments.

array $exposed_input: Exposed input.

int $total_rows: Total rows.

array $query_parameters: Query string parameters.

string $redirect_url: Redirect URL.

mixed $context: Batch context information.

Throws

\Drupal\Core\Entity\EntityStorageException

\PhpOffice\PhpSpreadsheet\Exception

\PhpOffice\PhpSpreadsheet\Reader\Exception

\PhpOffice\PhpSpreadsheet\Writer\Exception

File

src/Plugin/views/display/DataExport.php, line 664

Class

DataExport
Provides a data export display plugin.

Namespace

Drupal\views_data_export\Plugin\views\display

Code

public static function processBatch($view_id, $display_id, array $args, array $exposed_input, $total_rows, array $query_parameters, $redirect_url, &$context) {

  // Add query string back to the URL for processing.
  if ($query_parameters) {
    \Drupal::request()->query
      ->add($query_parameters);
  }

  // Load the View we're working with and set its display ID so we get the
  // content we expect.
  $view = Views::getView($view_id);
  $view
    ->setDisplay($display_id);
  $view
    ->setArguments($args);
  $view
    ->setExposedInput($exposed_input);
  if (isset($context['sandbox']['progress'])) {
    $view
      ->setOffset($context['sandbox']['progress']);
  }
  $export_limit = $view->display_handler
    ->getOption('export_limit');
  $view
    ->preExecute($args);

  // Build the View so the query parameters and offset get applied. so our
  // This is necessary for the total to be calculated accurately and the call
  // to $view->render() to return the items we expect to process in the
  // current batch (i.e. not the same set of N, where N is the number of
  // items per page, over and over).
  $view
    ->build();

  // First time through - create an output file to write to, set our
  // current item to zero and our total number of items we'll be processing.
  if (empty($context['sandbox'])) {

    // Set the redirect URL and the automatic download configuration in the
    // results array so they can be accessed when the batch is finished.
    $context['results'] = [
      'automatic_download' => $view->display_handler->options['automatic_download'],
      'redirect_url' => $redirect_url,
    ];

    // Initialize progress counter, which will keep track of how many items
    // we've processed.
    $context['sandbox']['progress'] = 0;

    // Initialize file we'll write our output results to.
    // This file will be written to with each batch iteration until all
    // batches have been processed.
    // This is a private file because some use cases will want to restrict
    // access to the file. The View display's permissions will govern access
    // to the file.
    $current_user = \Drupal::currentUser();
    $user_ID = $current_user
      ->isAuthenticated() ? $current_user
      ->id() : NULL;
    $timestamp = \Drupal::time()
      ->getRequestTime();
    $filename = \Drupal::token()
      ->replace($view
      ->getDisplay()->options['filename'], [
      'view' => $view,
    ]);
    $extension = reset($view
      ->getDisplay()->options['style']['options']['formats']);

    // Checks if extension is already included in the filename.
    if (!preg_match("/^.*\\.({$extension})\$/i", $filename)) {
      $filename = $filename . "." . $extension;
    }
    $user_dir = $user_ID ? "{$user_ID}-{$timestamp}" : $timestamp;
    $view_dir = $view_id . '_' . $display_id;

    // Determine if the export file should be stored in the public or private
    // file system.
    $store_in_public_file_directory = TRUE;
    $streamWrapperManager = \Drupal::service('stream_wrapper_manager');

    // Check if the private file system is ready to use.
    if ($streamWrapperManager
      ->isValidScheme('private')) {
      $store_in_public_file_directory = $view
        ->getDisplay()
        ->getOption('store_in_public_file_directory');
    }
    if ($store_in_public_file_directory === TRUE) {
      $directory = "public://views_data_export/{$view_dir}/{$user_dir}/";
    }
    else {
      $directory = "private://views_data_export/{$view_dir}/{$user_dir}/";
    }
    try {
      $fileSystem = \Drupal::service('file_system');
      $fileSystem
        ->prepareDirectory($directory, FileSystemInterface::CREATE_DIRECTORY);
      $destination = $directory . $filename;
      $file = file_save_data('', $destination, FileSystemInterface::EXISTS_REPLACE);
      if (!$file) {

        // Failed to create the file, abort the batch.
        unset($context['sandbox']);
        $context['success'] = FALSE;
        throw new StorageException('Could not create a temporary file.');
      }
      $file
        ->setTemporary();
      $file
        ->save();

      // Create sandbox variable from filename that can be referenced
      // throughout the batch processing.
      $context['sandbox']['vde_file'] = $file
        ->getFileUri();

      // Store URI of export file in results array because it can be accessed
      // in our callback_batch_finished (finishBatch) callback. Better to do
      // this than use a SESSION variable. Also, we're not returning any
      // results so the $context['results'] array is unused.
      $context['results']['vde_file'] = $context['sandbox']['vde_file'];
    } catch (StorageException $e) {
      $message = t('Could not write to temporary output file for result export (@file). Check permissions.', [
        '@file' => $context['sandbox']['vde_file'],
      ]);
      \Drupal::logger('views_data_export')
        ->error($message);
    }
  }

  // Render the current batch of rows - these will then be appended to the
  // output file we write to each batch iteration.
  // Make sure that if limit is set the last batch will output the remaining
  // amount of rows and not more.
  $items_this_batch = $view->display_handler
    ->getOption('export_batch_size');
  if ($export_limit && $context['sandbox']['progress'] + $items_this_batch > $export_limit) {
    $items_this_batch = $export_limit - $context['sandbox']['progress'];
  }

  // Set the limit directly on the query.
  $view->query
    ->setLimit((int) $items_this_batch);
  $view
    ->execute($display_id);

  // Check to see if the build failed.
  if (!empty($view->build_info['fail'])) {
    return;
  }
  if (!empty($view->build_info['denied'])) {
    return;
  }

  // We have to render the whole view to get all hooks executes.
  // Only rendering the display handler would result in many empty fields.
  $rendered_rows = $view
    ->render();
  $string = (string) $rendered_rows['#markup'];

  // Workaround for CSV headers, remove the first line.
  if ($context['sandbox']['progress'] != 0 && reset($view
    ->getStyle()->options['formats']) == 'csv') {
    $string = preg_replace('/^[^\\n]+/', '', $string);
  }

  // Workaround for XML.
  $output_format = reset($view
    ->getStyle()->options['formats']);
  if ($output_format == 'xml') {
    $maximum = $export_limit ? $export_limit : $total_rows;

    // Remove xml declaration and response opening tag.
    if ($context['sandbox']['progress'] != 0) {
      $string = str_replace('<?xml version="1.0"?>', '', $string);
      $string = str_replace('<response>', '', $string);
    }

    // Remove response closing tag.
    if ($context['sandbox']['progress'] + $items_this_batch < $maximum) {
      $string = str_replace('</response>', '', $string);
    }
  }

  // Workaround for XLS/XLSX.
  if ($context['sandbox']['progress'] != 0 && ($output_format == 'xls' || $output_format == 'xlsx')) {
    $vdeFileRealPath = \Drupal::service('file_system')
      ->realpath($context['sandbox']['vde_file']);
    $previousExcel = IOFactory::load($vdeFileRealPath);
    file_put_contents($vdeFileRealPath, $string);
    $currentExcel = IOFactory::load($vdeFileRealPath);

    // Append all rows to previous created excel.
    $rowIndex = $previousExcel
      ->getActiveSheet()
      ->getHighestRow();
    foreach ($currentExcel
      ->getActiveSheet()
      ->getRowIterator() as $row) {
      if ($row
        ->getRowIndex() == 1) {

        // Skip header.
        continue;
      }
      $rowIndex++;
      $colIndex = 0;
      foreach ($row
        ->getCellIterator() as $cell) {
        $previousExcel
          ->getActiveSheet()
          ->setCellValueByColumnAndRow(++$colIndex, $rowIndex, $cell
          ->getValue());
      }
    }
    $objWriter = new Xlsx($previousExcel);
    $objWriter
      ->save($vdeFileRealPath);
  }
  elseif (file_put_contents($context['sandbox']['vde_file'], $string, FILE_APPEND) === FALSE) {

    // Write to output file failed - log in logger and in ResponseText on
    // batch execution page user will end up on if write to file fails.
    $message = t('Could not write to temporary output file for result export (@file). Check permissions.', [
      '@file' => $context['sandbox']['vde_file'],
    ]);
    \Drupal::logger('views_data_export')
      ->error($message);
    throw new ServiceUnavailableHttpException(NULL, $message);
  }

  // Update the progress of our batch export operation (i.e. number of
  // items we've processed). Note can exceed the number of total rows we're
  // processing, but that's considered in the if/else to determine when we're
  // finished below.
  $context['sandbox']['progress'] += $items_this_batch;

  // If our progress is less than the total number of items we expect to
  // process, we updated the "finished" variable to show the user how much
  // progress we've made via the progress bar.
  if ($context['sandbox']['progress'] < $total_rows) {
    $context['finished'] = $context['sandbox']['progress'] / $total_rows;
  }
  else {

    // We're finished processing, set progress bar to 100%.
    $context['finished'] = 1;
  }
}