QR Code contains TinyURL of this article.Stream CSV Files without an Intermediate Write to the Server

Whilst working on a performance review project recently, I came across a series of routines that were responsible for creating CSV downloads of data sets from the client’s Oracle database. Each one of these exporters created a CSV file on the server then presented a link to the end user which they would then have to right-click to download the associated file.

This felt broken to me for the following reasons:

  1. The files created on the server were never reused, which was really wasteful;
  2. There was no “clean-up” routine on the file store. These read-once CSV files, once created, remained on the server indefinitely;
  3. Why force the end user to have to think about how to get the download? Once the user clicks the “Download Data Set” link, why not just send the CSV file straight to browser?

I decided to refactor the routines to stream the files directly to the browser, without the intermediate step of creating a physical file on the server. I handled this with the following PHP code:

<?php
function export_csv($data) {
  // No point in creating the export file on the file-system. We'll stream
  // it straight to the browser. Much nicer.

  // Open the output stream
  $fh = fopen('php://output', 'w');

  // Start output buffering (to capture stream contents)
  ob_start();

  // CSV Header
  $header = array('Field 1', 'Field 2', 'Field 3', 'Field 4', 'Field 5', 'Etc...');
  fputcsv($fh, $header);

  // CSV Data
  foreach ($data as $k => $v) {
    $line = array($data['field_1'], $data['field_2'], $data['field_3'], $data['field_4'], $data['field_5'], $data['field_etc']);
    fputcsv($fh, $line);
  }

  // Get the contents of the output buffer
  $string = ob_get_clean();

  // Set the filename of the download
  $filename = 'my_csv_' . date('Ymd') .'-' . date('His');

  // Output CSV-specific headers
  header('Pragma: public');
  header('Expires: 0');
  header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
  header('Cache-Control: private', false);
  header('Content-Type: application/octet-stream');
  header('Content-Disposition: attachment; filename="' . $filename . '.csv";');
  header('Content-Transfer-Encoding: binary');

  // Stream the CSV data
  exit($string);
}
?>

I think this is more elegant and less wasteful than the process it replaced.