【问题标题】:PHP: headers already sent when using fwrite but not when using fputcsvPHP:使用 fwrite 时已发送标头,但使用 fputcsv 时未发送
【发布时间】:2013-04-10 19:10:51
【问题描述】:

我知道这个错误背后的理论,但它现在又让我发疯了。我在我的应用程序中使用Tonic。使用这个库,您可以将所有流量重定向到您的 dispatch.php 脚本,然后该脚本执行适当的 Resource 并且 Resource 返回一个由 dispatch.php 显示(输出)的 Response

Response的输出方法如下:

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

所以 AFAIK 这告诉我们,您不能在 Resource 的 php 输出中写入任何内容。

我现在有一个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),因此希望避免这种情况。)

【问题讨论】:

  • 你不应该fwrite$response 而不是$output 吗?
  • 已知此类错误的答案stackoverflow.com/a/8028987
  • 我知道这一点,正如我在问题中所说的那样,实际上可以防止这种 cmets。

标签: php file-io http-headers


【解决方案1】:

$record 未设置,生成级别为NOTICE 的错误。如果您有error_reportingtrue,PHP 会在发送标头之前将此错误放在输出中。

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

【讨论】:

  • 静止。如果您禁用 error_reporting 并查看日志会发生什么情况?
  • 如果我生成一个字符串而不是写入文件并输出它可以工作。我的“文件创建逻辑”是正确的,并且所有变量都已设置。问题在于如何正确返回生成的内容,特别是考虑到 ti 的大小可能为几 MB。
  • 静止。您的代码将内容放在标题之前。找出那是什么内容,你就有答案了。
  • 如果您阅读我的问题,我知道这一点。简单地说,使用 fwrite() -> 错误写入 php://output。使用 fputcsv -> 有效。为什么? (AFAIK fputcsv() 在内部使用 fwrite())
  • 我明白这一点。但是会有一条消息告诉你原因。尝试获取该消息。您可以做的另一件事是使用 Wireshark 嗅探脚本输出的实际内容。
【解决方案2】:

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

先评论一下 fwrite 和 fputcsv:

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

解决方案:

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

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

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

RewriteCond %{REQUEST_FILENAME} !-f

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

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

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

在上面我们重定向到的下载脚本中,sn-p 就可以了(省略验证的东西):

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

浏览器将显示生成文件的下载对话框。

您还需要创建 cron 作业来删除生成的文件。

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

这将删除目录/path/to/generatedFiles 中所有超过 60 分钟的文件。

我使用 ubuntu 服务器,所以你可以将它添加到文件中

/etc/cron.daily/standard

或在该目录中生成一个新文件,或在/etc/cron.hourly 中生成一个包含该命令的新文件。

注意:

我在输入 csv 文件的 sha1 哈希后命名生成的文件,因此名称是唯一的,如果有人在短时间内多次重复相同的请求,您可以再次返回已经生成的文件。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-07-26
    • 1970-01-01
    • 2011-04-17
    • 2016-06-17
    • 1970-01-01
    相关资源
    最近更新 更多