如何在PHP中将标准输出重定向到文件?

64

下面的代码几乎可以工作,但不是我真正想要的:

ob_start();
echo 'xxx';
$contents = ob_get_contents();
ob_end_clean();
file_put_contents($file,$contents);

有没有更自然的方法?


1
示例代码将输出重定向到文件,而不是STDOUT。Bas的解决方案似乎只能工作在CLI(和较小程度上的CGI)环境中,这些流可以互换使用。Apache模块PHP则不行。 - user340140
8个回答

134

在 PHP 中,可以直接将 STDOUT 写入文件,这比使用输出缓冲区要简单得多且更加直接。

在脚本的开头执行以下代码:

fclose(STDIN);
fclose(STDOUT);
fclose(STDERR);
$STDIN = fopen('/dev/null', 'r');
$STDOUT = fopen('application.log', 'wb');
$STDERR = fopen('error.log', 'wb');
为什么要在最开始这样做呢?因为此时不应该打开任何文件描述符,因为当关闭标准输入、输出和错误文件描述符时,前三个新的描述符将成为新的标准输入、输出和错误文件描述符。
在我的示例中,我将标准输入重定向到 /dev/null ,将输出和错误 文件描述符重定向到日志文件。这是在 PHP 创建守护进程脚本时的常见做法。
如果要写入application.log文件,则可以使用以下命令:
echo "Hello world\n";

要写入error.log,必须执行以下操作:

fwrite($STDERR, "Something went wrong\n"); 
请注意,当您更改输入、输出和错误描述符时,内置PHP常量STDIN、STDOUT和STDERR将变得无法使用。PHP不会将这些常量更新为新的描述符,并且不允许重新定义这些常量(毕竟它们被称为常量)。

2
该功能的文档可以在php.net找到。 - icirellik
8
这是基本的Unix行为(这也是它不能在Windows上运行的原因)。 Unix将最小的可用文件描述符给予文件。鉴于fcloses关闭0、1和2,它们变得可用于下一个打开的3个文件。此外,Unix的简洁性令人惊叹。 - Eric
3
如果这是用于cron脚本,你可能想要使用模式为afopen函数,以便不会覆盖之前的日志。 - nullability
12
好 grief。这只是纯粹的运气使然。这不是一个解决方案。 - CXJ
2
@Pacerier,没错。变量的命名无所谓,我只是为了可读性而将它们与它们的常量等效物保持相同。真正重要的是关闭默认描述符并打开新描述符的顺序。 - Bas Peters
显示剩余13条评论

23

8
如果您使用的是 PHP 5.3 或更新版本,我建议使用闭包函数而无需将 $ob_file 设置为全局变量:$ob_file_callback = function($buffer) use ($ob_file) { fwrite($ob_file, $buffer); }; ob_start($ob_file_callback); - Pascal Rosin
这对我有用,我认为应该接受这个答案。 - Nam G VU
如果您知道如何重定向stdin,请分享。 - Nam G VU
你提供的opera.com网站链接似乎已经失效(差不多15年过去了)。你有其他的信息来源吗?(不过你的回答已经很清楚了,谢谢) - Marten Koetsier

10

在我的特定情况下,没有一个答案适用于我需要以跨平台的方式重定向输出,以便我可以使用tail-f log.txt或另一个日志查看应用程序来跟踪日志。 我想出了以下解决方案:

$logFp = fopen('log.txt', 'w');

ob_start(function($buffer) use($logFp){
    fwrite($logFp, $buffer);
}, 1); //notice the use of chunk_size == 1

echo "first output\n";
sleep(10)
echo "second output\n";

ob_end_clean();

我没有注意到任何性能问题,但如果您有的话,可以将chunk_size更改为更大的值。

现在只需使用tail -f命令跟踪日志文件:

tail -f log.txt

ob_start(function($buffer) use($logFp){” 这是什么神奇的操作?自从什么时候可以将函数作为参数传递了?这个 'use' 是什么意思?这种 PHP 语法从什么时候开始有效? - Luc

8
不,输出缓冲是最好的选择。虽然只需执行以下操作会更好些:
ob_start();
echo 'xxx';
$contents = ob_get_flush();
file_put_contents($file,$contents);

4
ob_get_flush()函数是否也会将缓冲区内容发送到浏览器?或许使用ob_get_clean()更好。 - Tom Haigh
2
有没有办法同时获取 STDERR? - xer0x

7
使用eio pecl模块非常简单,您还可以捕获PHP内部错误、var_dump、echo等。在这个代码中,您可以找到一些不同情况的示例。
$fdout = fopen('/tmp/stdout.log', 'wb');
$fderr = fopen('/tmp/stderr.log', 'wb');

eio_dup2($fdout, STDOUT);
eio_dup2($fderr, STDERR);
eio_event_loop();

fclose($fdout);
fclose($fderr);

// output examples
echo "message to stdout\n";

$v2dump = array(10, "graphinux");
var_dump($v2dump);

// php internal error/warning
$div0 = 10/0;

// user errors messages
fwrite(STDERR, "user controlled error\n");

调用eio_event_loop是为了确保之前的eio请求已经被处理。如果你需要在日志上追加,在fopen调用时,请使用模式'ab'而不是'wb'。

安装eio模块非常容易(http://php.net/manual/es/eio.installation.php)。我使用版本1.2.6的eio模块测试了这个例子。


2
你可以安装Eio扩展:

pecl install eio

并复制文件描述符。
$temp=fopen('/tmp/my_stdout','a');
$my_data='my something';
$foo=eio_dup2($temp,STDOUT,EIO_PRI_MAX,function($data,$esult,$request){
    var_dump($data,$esult,$request);
    var_dump(eio_get_last_error($request));
},$my_data);
eio_event_loop();
echo "something to stdout\n";
fclose($temp);

这将创建一个新的文件描述符并重定向标准输出流

同样可以对标准错误流进行操作

常量STD[OUT|ERR]仍然可用


1
我知道这个问题已经很老了,但是尝试做这个问题所要求的事情的人可能最终会来到这里...包括你们两个。
如果您在特定环境下运行...
  • 在Linux下运行(可能大多数其他类Unix操作系统,未经测试)
  • 通过CLI运行(在Web服务器上未经测试)
您实际上可以关闭所有文件描述符(是的,所有文件描述符,这意味着最好在执行的最开始就这样做...例如,在调用pcntl_fork()将进程后台化为守护进程之后立即进行(这似乎是这样做的最常见需求))。
fclose( STDIN );  // fd 3
fclose( STDERR);  // fd 2
fclose( STDOUT ); // fd 1

然后重新打开文件描述符,将它们分配给一个不会超出作用域而被垃圾回收的变量。因为Linux会可预测地按正确顺序打开它们。

$kept_in_scope_variable_fd1 = fopen(...); // fd 1
$kept_in_scope_variable_fd2 = fopen(...); // fd 2
$kept_in_scope_variable_fd3 = fopen( '/dev/null', ... ); // fd 3

你可以使用任何文件或设备来完成这个操作。我以 STDIN(fd3)的 /dev/null 为例,因为这可能是此类代码最常见的情况。
完成后,您应该能够像使用函数写入文件一样执行正常的操作,比如 echo、print_r、var_dump 等等。这在尝试后台运行代码时非常有用,因为您不想或不能将其重写为指针输出友好型文件。
对于其他环境和打开其他 FD 的情况可能会有所不同。我的建议是从一个小测试脚本开始,证明它在您的环境中是否有效,然后再进行集成。
祝你好运。

我在阅读时不知何故跳过了Bas Peters的答案。他们的答案与我的相同,只是我的回答提供了更多的上下文信息。请给他们的答案点赞。 - Apokalyptik

0

这里有一个丑陋的解决方案,对我遇到的问题(需要调试)非常有用。

if(file_get_contents("out.txt") != "in progress")
{
    file_put_contents("out.txt","in progress");
    $content = file_get_contents('http://'.$_SERVER['HTTP_HOST'].$_SERVER['REQUEST_URI']);
    file_put_contents("out.txt",$content);
}

主要的缺点是最好不要使用 $_POST 变量。 但你不必把它放在开头。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接