简体   繁体   中英

Write to PHP output buffer and then download CSV from buffer

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.

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 .

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.

I did some tweaks to your code.

  • Moved headers before any output, as suggested by PHP's doc ;

Remember that header() must be called before any actual output is sent, either by normal HTML tags, blank lines in a file, or from PHP

  • Removed some headers that didn't make much of a change;
  • Commented another option of writing csv header using the select column's names;
  • Now content length works;
  • There is no need to echo $basic_info as it is already at output buffer and we redirected it inside a file through headers;
  • Removed @ (PHP Error Control Operator) as it may cause overhead, don't have a link to show you right now but you might find it if you search. You should think twice before silencing errors, most times it should be fixed instead of silenced.

header('Content-Type: text/csv');
header('Content-Disposition: attachment; filename=test.csv');  
header('Expires: 0');  
header('Cache-Control: no-cache'); 

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

fputcsv($basic_info, $basic_header);  
$count = 0; // auxiliary variable to write csv header in a different way

while($user_row = $get_users_stmt->fetch(PDO::FETCH_ASSOC)) {  
    // Write select column's names as CSV header
    if ($count == 0) {
        fputcsv($basic_info, array_keys($user_row));  
    }

    fputcsv($basic_info, $user_row);  
    $count++;
}  

// get size of output after last output data sent  
$streamSize = ob_get_length(); 
fclose($basic_info);  

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM