2

我知道这个错误背后的理论,但它现在又让我发疯了。我在我的应用程序中使用Tonic。使用这个库,您可以将所有流量重定向到您的dispatch.php脚本,然后执行适当的ResourceResource返回Responsedispatch.php.

的输出方法Response如下:

/**
 * Output the response
 */
public function output()
{
    foreach ($this->headers as $name => $value) {
        header($name.': '.$value, true, $this->responseCode());
    }
    echo $this->body;
}

所以 AFAIK 这告诉我们你不能在 .php 文件中写任何东西到 php 输出Resource

我现在有一个Resource从输入 csv 动态生成 csv 并将其输出到浏览器(它将 1 列数据转换为不同格式)。

$csv = fopen('php://output', 'w');
// sets header
$response->__set('contentDisposition:', 'attachment; filename="' . $fileName . '"');
while (($line = fgetcsv($filePointer, 0, ",", '"')) !== FALSE) {
    // generate line
    fputcsv($csv, $line);
}
fclose($filePointer);
return $response;

这可以 100% 正常工作。标题没有问题,并且生成了正确的文件并提供下载。这已经令人困惑了,因为我们在设置标头之前写入输出?fputcsv 实际上做了什么?

我有第二个资源做类似的事情,但它输出自定义文件格式(文本文件)。

$output = fopen('php://output', 'w');
// sets header
$response->__set('contentDisposition:', 'attachment; filename="' . $fileName . '"');
while (($line = fgetcsv($filePointer, 0, ",", '"')) !== FALSE) {
    // generate a record (multiple lines) not shown / snipped
    fwrite($output, $record);
}
fclose($filePointer);
return $response;

唯一的区别是它使用 fwrite 而不是 fputcsv 和 bang

 headers already sent by... // line number = fwrite()

这非常令人困惑!恕我直言,在这两种情况下它实际上都应该失败吗?为什么第一个有效?我怎样才能让第二个工作?(我可以生成一个包含文件的巨大字符串并将其放入响应正文中并且它可以工作。但是文件可能相当大(高达 50 mb),因此希望避免这种情况。)

4

2 回答 2

0

$record未设置,生成 level 错误NOTICE。如果必须error_reportingtruePHP 会在发送标头之前将此错误放在输出中。

设置error_reporting为 false 并留意您的日志。

于 2013-04-09T07:49:42.820 回答
0

这是我的解决方案。我暂时不会将其标记为答案,因为也许有人想出了比这更好(更简单)的东西。

首先是关于 fwrite 和 fputcsv 的评论:

fputcsv 有一个完全不同的源,与 fwrite 没有太多共同之处(它在内部不调用 fwrite,它是 C 源代码中的一个单独的函数)。因为我不知道 CI 无法说出为什么他们的行为不同,但他们确实如此。

解决方案:

生成的文件可能是“大”的,具体取决于输入,因此通过字符串连接生成整个文件并将其保存在内存中并不是一个很好的解决方案。

我用谷歌搜索了一下,找到了 apache 的 mod_xsendfile。这通过在 php 中设置一个自定义标头来工作,该标头包含要发送给用户的文件的路径。然后 mod 删除该自定义标头并将文件作为响应发送。

mod_xsendfile 的问题在于它与我也使用的 mod_rewrite 不兼容。您将收到 404 错误。要解决此问题,您需要添加

RewriteCond %{REQUEST_FILENAME} !-f

到 apache 配置中的相应位置(如果请求是针对实际物理存在的文件,则不要重写)。然而,这还不够。您需要在未重写的 php 脚本中设置标头 X-Sendfile,它是一个实际存在的 php 文件。

因此,在最后生成文件的 \Tonic\Resource 类中,我重定向到上述脚本:

$response->__set('location', $url . "?fileName=" . urlencode($fileName));
$response->code = \Tonic\Response::FOUND;
return $response;

在我们重定向到上面代码片段的下载脚本中,只需执行(省略验证内容):

$filePath = trim($_GET['fileName']);
header ('X-Sendfile: ' . $filePath);    
header ('Content-Disposition: attachment; filename="' . $filePath . '"');

and the browser will display a download dialog for the generated file.

You will also need to create cron job to delete the generated files.

/usr/bin/find /path/to/generatedFiles/ -type f -mmin +60 -exec rm {} + 

this will delete all files older than 60 min in directory /path/to/generatedFiles.

I use ubuntu server so you can add it to the file

/etc/cron.daily/standard

or generated a new file in that directory or generated a new file in /etc/cron.hourly containing that command.

Note:

I name the generated files after the sha1 hash of the input csv file so the name is unique and if someone repeats the same requests several times in a short period you can just return the already generated file a second time.

于 2013-04-10T11:10:51.650 回答