为什么导出数据时下载窗口不会长时间出现在浏览器中

I try export data from database to .csv. When I click export link I don't see save window in browser for a very long time if there is quite a lot amount of data. It can be quite confusing if the script looks like hanging for some time and after quite a long time one can see save window in browser. The code is something like this in controller:

$this->_helper->layout->disableLayout();
        $this->_helper->viewRenderer->setNoRender();
        $fileName = $list->list_name . '.csv';

        $this->getResponse()->setHeader('Content-Type', 'text/csv; charset=utf-8')
                            ->setHeader('Content-Disposition', 'attachment; filename="'. $fileName . '"');      

        $contacts = new Contact();
        $contacts->export($listId);

Export method reads records one by one and prints it something like this:

    $fp = fopen('php://output', 'w');        
    foreach ($mongodbCursor as $subscriber) {
           $row = formRow($subscriber);
       fputcsv($fp, $row);
        }       

I see on some applications that save winow appear almost immediately and when you click save you see progress of downloading.

I tried to replace:

    $this->getResponse()->setHeader('Content-Type', 'text/csv; charset=utf-8')
                        ->setHeader('Content-Disposition', 'attachment; filename="'. $fileName . '"');

with this one:

    header('Content-Type: text/csv; charset=utf-8');
    header('Content-Disposition: attachment; filename="'. $fileName . '"');

It didn't help so far. I wonder if it's possible to send headers before all data are read one by one from database? Thank your for your assistance.

Hmm I'm not familiar with php://output, my application writes my information with fopen,fwrite,fclose to a temporary file afterwards I give it out with similiar header(); options.

            $filesize = filesize("tmp/export.csv");
            header("Content-Type: text/csv");
            header("Content-Disposition: attachment; filename=\"export.csv\"");
            header("Conent-Length: $filesize");
            readfile("tmp/export.csv");
            unlink("tmp/export.csv");
            exit;

This one gives the download window of your browser instantly.

You could try to do this:

  • call the header function instead of $this->getResponse()->setHeader() (the response content might be saved in a buffer and outputed only when it is completed - the time the export finishes)
  • try to echo the content directly instead of writing to php://output (if you set the headers before that, everything you echo will be placed in the generated CSV file)

EDIT

Replace fputcsv with a function like print_row below

function print_row($row) {
        echo '"' . implode('","', $row) . '"' . "
"; 
}

The function gets the first parameter as an array, adds " and , and echoes to content.

// $file = path of file

header('Content-Description: File Transfer');
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename='.basename($file));
header('Content-Transfer-Encoding: binary');
header('Expires: 0');
header('Cache-Control: must-revalidate');
header('Pragma: public');
header('Content-Length: ' . filesize($file));
ob_clean();
flush();
readfile($file);
exit;

Source: http://php.net/manual/en/function.readfile.php

Try using application/force-download instead of text/csv in your Content-Type value.

header("Content-Type: application/force-download; name=\"" . $fileName . "\""); 

This will force an actual download box instead of just showing the content in the browser.

Here is some documentation from MIME Application:

Mime application/force-download, which is typically expressed as mime application/x-force-download, is typically used in conjunction with PHP code to trigger a download box rather than for displaying content in a Web browser.

For example, if you had a file on your website that you want the user to download rather than view in the Web browser, you could enter the appropriate mime application/force-download code in the file’s header content type field. Note that mime application/force-download is not a registered MIME type. In addition, when used in PHP code, the preferred spelling of mime application/force-download contains the “x-” prefix.

I don't know if this a good solution, I didn't explore it much, but it seems it's working. It cotroller after I add some data to buffer after headers, before exporting.

    header('Content-Type: text/csv; charset=utf-8');
    header('Content-Disposition: attachment; filename="'. $fileName . '"');

    echo str_pad('', ini_get('output_buffering'));      
    ob_flush();
    flush();

    Model_Subscriber1::exportList($listId);

To make it works in controller I added in Zend Bootstrap.php:

/**
 * hint to the dispatcher that it should not use output buffering to capture output generated by action controllers.
 *  By default, the dispatcher captures any output and appends it to the response object body content. 
 */
protected function _initFront()
{
    $frontController = Zend_Controller_Front::getInstance();
    $frontController->setParam('disableOutputBuffering', 1);        
}

It look like in this case I get download window in browser quickly and then data are exporting which could take quite a long time. I don't know if this solution is acceptable now. I'd be glad to here your opinion about it. It cause issues with adding empty data to export file. So, I change it controller to.

    header('Content-Type: text/csv; charset=utf-8');
    header('Content-Disposition: attachment; filename="'. $fileName . '"');

    Model_Subscriber1::exportList($listId);

And chang function for export to something like this:

  foreach ($mongodbCursor as $subscriber) {
           $row = formRow($subscriber);
    echo '"' . implode('","', $row) . '"' . "
";
    ob_flush();
    flush();                    

  }       

Your function may want to call flush / ob_flush just before long operation and let the HTTP header send to client before long process.