Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Write to PHP output buffer and then download CSV from buffer

Tags:

php

csv

I need to write a CSV file to the PHP output buffer and then download that file to the client's computer after it's done writing. (I wanted to just write it on the server and download it which was working, but it turns out I won't have write access on production servers).

I have the following PHP script:

$basic_info = fopen("php://output", 'w');
$basic_header = array(HEADER_ITEMS_IN_HERE);

@fputcsv($basic_info, $basic_header);

while($user_row = $get_users_stmt->fetch(PDO::FETCH_ASSOC)) {
    @fputcsv($basic_info, $user_row);
}

@fclose($basic_info);

header('Content-Description: File Transfer');
header('Content-Type: application/csv');
header('Content-Disposition: attachment; filename=test.csv');
header('Expires: 0');
header('Cache-Control: must-revalidate');
header('Pragma: public');
header('Content-Length: ' . filesize("php://output"));
ob_clean();
flush();
readfile("php://output");

I'm not sure what to do. The CSV file downloads but displays nothing. I assume it has something to do with the ordering of my ob_clean() and flush() commands, but I'm not sure what's the best way to order these things.

Any help is appreciated.

like image 539
Jon Rubins Avatar asked Apr 02 '13 16:04

Jon Rubins


2 Answers

You're doing a little too much. Create the script with the sole purpose of outputting the CSV. Just print it out directly to the screen. Don't worry about headers or buffers or php://output or anything like that yet.

Once you've confirmed that you're printing the data out to the screen appropriately, just add these headers at the beginning:

<?php
header("Content-disposition: attachment; filename=test.csv");
header("Content-Type: text/csv");
?>

... confirm that that downloads the file appropriately. Then you can add the other headers if you like (the headers I included above are those I've used myself without any extra cruft to get this working, the others are basically for efficiency and cache control, some of which may already be handled appropriately by your server, and may or may not be important for your particular application).

If you want, use output buffering with ob_start() and ob_get_clean() to get the output contents into a string which you can then use to fill out Content-Length.

like image 101
Jason Avatar answered Nov 15 '22 00:11

Jason


As mentioned in my comments of Edson's answer, I expected a "headers already sent" warning at the last line of code:

header('Content-Length: '.$streamSize);

since output is written before this header is sent, but his example works ok.

Some investigation leads me to to following conclusions:

At the time you use an output buffer (weither a user one, or the default PHP one), you may send HTTP headers and content the way you want. You know that any protocol require to send headers before body (thus the term "header"), but when you use an ouput buffer layer, PHP will take care of this for you. Any PHP function playing with output headers (header(), setcookie(), session_start()) will in fact use the internal sapi_header_op() function which just fills in the headers buffer. When you then write output, using say printf(), it writes into the output buffer (assuming one). When the output buffer is to be sent, PHP starts sending the headers first, and then the body. PHP takes care of everything for you. If you dont like this behavior, you have no other choice than disabling any output buffer layer.

and

The default size of the PHP buffer under most configurations is 4096 bytes (4KB) which means PHP buffers can hold data up to 4KB. Once this limit is exceeded or PHP code execution is finished, buffered content is automatically sent to whatever back end PHP is being used (CGI, mod_php, FastCGI). Output buffering is always Off in PHP-CLI.

Edson's code works because the output buffer did not automatically get flushed because it doesn't exceed the buffer size (and the script isn't terminated obviously before the last header is sent).

As soon as the data in the output buffer exceeds the buffer size, the warning will be raised. Or in his example, when the data of

$get_users_stmt->fetch(PDO::FETCH_ASSOC)

is too large.

To prevent this, you should manage the output buffering yourself with the ob_start() and ob_end_flush(); like below:

// Turn on output buffering
ob_start();

// Define handle to output stream
$basic_info   = fopen("php://output", 'w');

// Define and write header row to csv output 
$basic_header = array('Header1', 'Header2');  
fputcsv($basic_info, $basic_header);  

$count = 0; // Auxiliary variable to write csv header in a different way

// Get data for remaining rows and write this rows to csv output
while($user_row = $get_users_stmt->fetch(PDO::FETCH_ASSOC)) {  
    if ($count == 0) {
        // Write select column's names as CSV header
        fputcsv($basic_info, array_keys($user_row));  
    } else {
        //Write data row
        fputcsv($basic_info, $user_row);
    }
    $count++;
}  

// Get size of output after last output data sent  
$streamSize = ob_get_length();

//Close the filepointer
fclose($basic_info);  

// Send the raw HTTP headers
header('Content-Type: text/csv');
header('Content-Disposition: attachment; filename=test.csv');  
header('Expires: 0');  
header('Cache-Control: no-cache');
header('Content-Length: '. ob_get_length());

// Flush (send) the output buffer and turn off output buffering
ob_end_flush();

You're still bound to other limits, though.

like image 35
DigiLive Avatar answered Nov 14 '22 23:11

DigiLive