I needed to generate Australia Post manifests for shipping e-commerce store orders. A PHP controller action written using the CodeIgniter framework creates required CSV files. Ideally this uses an algorithm to optimize a packing plan based on weight, cubic-size, and packing materials available. You know, if you’re feeling ambitious enough to solve the Knapsack Problem. In my case, users instead tick boxes to indicate parcel placement for each item. Not a terrible proposition with less than a dozen items.

With the packing plan decided, CSV files generate for the format accepted by the AusPost API. Private method _download_csv_zip() accepts two parameters:

  • $zip_filename_stub, a filename describing the shipped order/service. e.g., ‘order_39493’
  • $csv_files, associate array of files and their CSV lines. e.g., array('order_39493_auspost.csv' => '"first column","345","third column"')

Custom ZIP creation in PHP

(Error handling below is generalized to simplify this post – throwing an Exception in PHP isn’t so bad.)

private function download($order_id)
{
    // retrieve order

    // decide on how parcel items are packed

    // generate CSV files based on packing plan

    $this->_download_csv_zip($zip_filename_stub, $csv_files);
}

/**
 * Create a zip archive for in-memory CSV files.
 *
 * @param string $zip_filename_stub Filename without an extension.
 * @param array $csv_files Keys are the filenames, values are strings
 *                         containing multiple comma-delimited CSV lines.
 *
 * @return void exit() always called, even in error state.
 */
private function _download_csv_zip($zip_filename_stub, array $csv_files)
{
    $zip_filename = preg_replace('/[^a-zA-Z0-9]/', '', zip_filename_stub) .
        '.zip';

    $temp_filename = tempnam(sys_get_temp_dir(), $zip_filename);

    $zip = new ZipArchive();

    // If zip file creation failed when web server's temp
    // directory cannot be written to, log the error and
    // forward user back to order view with a
    // hands-thrown-in-air message.
    if (TRUE !== ($zip_open_code = $zip->open($temp_filename, ZipArchive::CREATE))) {
        error_log('CSV ZIP file creation failed for ' .
            $zip_filename .
            '. Error code: ' .
            $zip_open_code);

        $this->session->set_userdata('error',
            'Unable to generate ZIP for multiple .csv files generated.');
        header('Location: /orders/');

        exit();
    }

    foreach ($csv_files as $filename => $content) {
        $zip->addFromString($filename, $content);
    }

    if (TRUE !== $zip->close()) {
        error_log('CSV ZIP file creation failed for ' .
            $zip_filename
            '. Check write permissions of temp directories.');

        $this->session->set_userdata('error',
            'Unable to generate ZIP for multiple .csv files generated.);
        header('Location: /orders/'); // forward back to previous page w/ error msg

        exit();
    }

    header('Content-Type: application/zip');
    header('Content-disposition: attachment; filename=' . $zip_filename);
    header('Content-Length: ' . filesize($temp_filename));

    readfile($temp_filename);

    // Delete temporary .zip file created
    unlink($temp_filename);

    exit();
}

Instead use the CodeIgniter library

Immediately after completing this quick implementation I thought, “what functionality does CodeIgniter have for ZIP archives?” Yup, ZIP library. Condense that block above with four lines below.

private function _download_csv_zip($zip_filename_stub, array $csv_files)
{
    $zip_filename = preg_replace('/[^a-zA-Z0-9]/', '', zip_filename_stub) .
        '.zip';

    $this->load->library('zip');

    foreach ($csv_files as $filename => $content) {
        $this->zip->add_data($filename, $content);
    }

    $this->zip->download($zip_filename);
}