Upgrade Your Drupal Skills

We trained 1,000+ Drupal Developers over the last decade.

See Advanced Courses NAH, I know Enough

D7 create zip archive from multiple custom uploaded files

Parent Feed: 

In this article I would like to share another interesting task connected with upload files in D7.

Task is to have custom form that allows to upload multiple files at once and create zip archive from these files on finish.

I really liked the way google mail handles attached files form and tried to implement similar behavior but without writing custom javascript. Thanks to Form API and #ajax property it is very managable. So lets dive into code!

Big thanks to Examples module that helped me with example of dynamically adding form elements.

/**
 * Form builder.
 */
function example_zip_file_form($form, &$form_state) {
  // Init num_files and uploaded_files variables if they are not set yet.
  if (empty($form_state['num_files'])) {
    $form_state['num_files'] = 1;
  }
  if (empty($form_state['uploaded_files'])) {
    $form_state['uploaded_files'] = array();
  }
 
  $form['file_upload_fieldset'] = array(
    '#type' => 'fieldset',
    '#title' => t('Uploaded files'),
    // Set up the wrapper so that AJAX will be able to replace the fieldset.
    '#prefix' => '<div id="uploaded-files-fieldset-wrapper">',
    '#suffix' => '</div>',
  );
 
  for ($i = 0; $i < $form_state['num_files']; $i++) {
    // Show upload form element only if it is new or
    // it is not unset (name equal to FALSE).
    if (
      !isset($form_state['uploaded_files']['files']['name']['file_upload_' . $i]) ||
      (isset($form_state['uploaded_files']['files']['name']['file_upload_' . $i]) && $form_state['uploaded_files']['files']['name']['file_upload_' . $i] !== FALSE)) {
      $form['file_upload_fieldset']['file_upload_' . $i] = array(
        '#type' => 'file',
        '#prefix' => '<div class="clear-block">',
        '#size' => 22,
        '#theme_wrappers' => array(),
      );
      $form['file_upload_fieldset']['file_upload_remove_' . $i] = array(
        '#type' => 'submit',
        '#name' => 'file_upload_remove_' . $i,
        '#value' => t('Remove file'),
        '#submit' => array('example_zip_file_remove'),
        '#ajax' => array(
          'callback' => 'example_zip_file_refresh',
          'wrapper' => 'uploaded-files-fieldset-wrapper',
        ),
        '#suffix' => '</div>',
      );
 
      // If file already uploaded we add its name as prefix.
      if (    isset($form_state['uploaded_files']['files']['name']['file_upload_' . $i])
          && !empty($form_state['uploaded_files']['files']['name']['file_upload_' . $i])) {
        $form['file_upload_fieldset']['file_upload_' . $i]['#type'] = 'markup';
        $form['file_upload_fieldset']['file_upload_' . $i]['#markup'] = t('File: @filename', array('@filename' => $form_state['uploaded_files']['files']['name']['file_upload_' . $i]));
      }
    }
  }
 
  // Add new button.
  $form['add_new'] = array(
    '#type' => 'submit',
    '#value' => t('Add another file'),
    '#submit' => array('example_zip_file_add'),
    '#ajax' => array(
      'callback' => 'example_zip_file_refresh',
      'wrapper' => 'uploaded-files-fieldset-wrapper',
    ),
    '#limit_validation_errors' => array(),
  );
 
  // Submit button.
  $form['submit'] = array(
    '#type' => 'submit',
    '#value' => t('Create zip archive from uploaded files'),
  );
 
  return $form;
}

First of all we keep information about already uploaded files in $form_state['uploaded_files'] variable and number of file form elements to show in $form_state['num_files'] element.

On the form we have two ajaxified buttons "Add another file" and "Remove file". Their submit functions should add / remove new file form and keep information about already uploaded files. And of course mark form to be rebuild.

/**
 * Callback for Remove button.
 *
 * Remove uploaded file from 'uploaded_files' array and rebuild the form.
 */
function example_zip_file_remove($form, &$form_state) {
  $form_state['uploaded_files'] = example_zip_file_array_merge($form_state['uploaded_files'], $_FILES);
  $file_to_remove_name = str_replace('_remove', '', $form_state['clicked_button']['#name']);
  $form_state['uploaded_files']['files']['name'][$file_to_remove_name] = FALSE;
  $form_state['rebuild'] = TRUE;
}
 
/**
 * Add new form file input element and save uploaded files to 'uploaded_files' variable.
 */
function example_zip_file_add($form, &$form_state) {
  $form_state['num_files']++;
  $form_state['uploaded_files'] = example_zip_file_array_merge($form_state['uploaded_files'], $_FILES);
  $form_state['rebuild'] = TRUE;
}

Function example_zip_file_array_merge merges recursively arrays. It was not possible to use array_merge_recursive in this case as it create array element in hierarchy instead of replacing it. To be clear lets see example from official documentation page:

$ar1 = array("color" => array("favorite" => "red"), 5);
$ar2 = array(10, "color" => array("favorite" => "green", "blue"));
$result = array_merge_recursive($ar1, $ar2);
print_r($result);

This leads to result:

Array
(
    [color] => Array
        (
            [favorite] => Array
                (
                    [0] => red
                    [1] => green
                )

            [0] => blue
        )

    [0] => 5
    [1] => 10
)

But we need:

Array
(
    [color] => Array
        (
            [favorite] => green
            [0] => blue
        )

    [0] => 5
    [1] => 10
)

The ajax callback of above mentioned two buttons "example_zip_file_refresh" just returns fieldset with file form elements of rebuilt form.

/**
 * AJAX callback. Retrieve proper element.
 */
function example_zip_file_refresh($form, $form_state) {
  return $form['file_upload_fieldset'];
}

Now lets take a look at form submit handler, where we create Zip archive.

/**
 * Form submit handler.
 */
function example_zip_file_form_submit($form, &$form_state) {
  // Merge uploaded files.
  $form_state['uploaded_files'] = example_zip_file_array_merge($form_state['uploaded_files'], $_FILES);
  $_FILES = $form_state['uploaded_files'];
 
  // Walk through files and save uploaded files.
  $uploaded_files = array();
  foreach ($_FILES['files']['name'] as $file_key => $value) {
    $file = file_save_upload($file_key);
    $uploaded_files[] = $file;
  }
 
  // Create Zip archive form uploaded files.
  $archive_uri = 'temporary://download_' . REQUEST_TIME . '.zip';
  $zip = new ZipArchive;
  if ($zip->open(drupal_realpath($archive_uri), ZipArchive::CREATE) === TRUE) {
    foreach ($uploaded_files as $file) {
      $zip->addFile(drupal_realpath($file->uri), $file->filename);
    }
    $zip->close();
    drupal_set_message(t('Zip archive successfully created. !link', array('!link' => l(file_create_url($archive_uri), file_create_url($archive_uri)))));
  }
  else {
    drupal_set_message(t('Error creating Zip archive.'), 'error');
  }
}

So thats it. Now we can let users create their own Zip archives.

In my task I had to build custom form that is part of multistep form. In ideal situation I would probably go for letting user to submit node creating form with unlimited value filefield to handle all ajax file uploads for me. And then just add hook_node_presave implementation where I would create Zip archive and added this zip file to another filefield.

But this example is more to show how ajax forms work on real life task. Hope you find this interesting and useful.

You are welcome to test module attached to this article.

Author: 
Original Post: 

About Drupal Sun

Drupal Sun is an Evolving Web project. It allows you to:

  • Do full-text search on all the articles in Drupal Planet (thanks to Apache Solr)
  • Facet based on tags, author, or feed
  • Flip through articles quickly (with j/k or arrow keys) to find what you're interested in
  • View the entire article text inline, or in the context of the site where it was created

See the blog post at Evolving Web

Evolving Web