PHP - 认识输出控制

169 阅读3分钟

开发环境

  • PHP:7.2
  • Nginx 1.25.1
  • Linux:3.10

简介

使用 输出控制 能够获得更多的输出控制权。

例如:

ob_start();

echo "world !"; // 这个输出将会输入到缓冲区当中,不会输出到客户端。

$string = ob_get_clean(); // 获取缓冲区的内容,并且清除缓冲区的内容 和 关闭缓冲区。

echo "hello ";

echo $string; // 这里就是上面输出的 "world !"

运行结果:

hello world !

开启方式

如何开启这个输出控制缓冲区呢?通常有两种开启方式:全局开启和会话开启。

全局开启

在 PHP 7.2 是处于默认开启状态,在程序开启时为程序自动开启一个输出控制缓冲区,并程序中的运行结束时,将缓冲区的内容输出出来。

php.ini 文件中设置以下配置项:

// 表示默认开启输出控制缓冲区,缓冲区的大小为 4096。
output_buffering = 4096

如果想关闭默认开启的状态:

output_buffering = off

会话开启

在代码中使用 ob_start() 函数开启即可。

ob_start();

// ... 业务代码 ...

示例

接下来通过下面的几个示例来加深对输出控制的认识。

生成静态文件

// 开启输出控制缓冲区
ob_start();

// ... 输出的静态文件内容 ...

// 获取写入缓冲区的内容 - 就是输出的文件内容。
$page = ob_get_clean();

// ... 以下是保存静态文件的操作 ...
$file = getcwd() . DIRECTORY_SEPARATOR . "public" . DIRECTORY_SEPARATOR . "index.html";
@chmod($file, 0755);
$fd = fopen($file, "w");
fwrite($fd, $page);
fclose($fd);

值得注意的是,ob_startchunk_size 参数要是设置大于 0 时 ,并且,输出的内容大小 大于等于 缓冲区的大小,那么缓冲区的内容将被冲刷出来,而不会被 ob_get_clean() 赋值给 $page。 所以在处理这种内容大小不确认的场景时, chunk_size 最好保持默认值 0

错误的示例

// 设置缓冲区大小为 100 字节
ob_start(null, 100);

echo str_repeat('a', 50);
echo str_repeat('a', 50);

// 获取写入缓冲区的内容 - 就是输出的文件内容。
$page = ob_get_clean();

// 将获取到的内容包裹在 {} 中。
echo "{" . $page . '}';

运行结果为:

aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa{}

可以看得出 $page 并没有获取到输出的内容,因为缓冲区满了所以直接输出来。

如果希望在程序出现异常时,不将输出的内容显示出来。

对输出控制缓冲区进行小小的封装:

class OutputBuffer {
	// 控制是否将输出内容显示的参数
	protected $output = false;

	// 控制输出缓冲区的回调函数
	protected function outputBufferCallback(string $buffer) {
		if ($this->output) return $buffer;
		return '';
	}

	// 开启控制输出缓冲区
	public function obStart(): bool {
		$this->output = false;
		return ob_start([$this, 'outputBufferCallback']);
	}

	// 换取缓冲区的内容 并清除和关闭缓冲区
	public function obGetClean(): string {
		$this->output = true;
		$result = ob_get_clean();
		$this->output = false;
		return $result;
	}
}

业务逻辑处理:

$OutputBuffer = new OutputBuffer();
// 开启输出控制缓冲区
$OutputBuffer->obStart();

// ... 业务处理 ...

// 获取缓冲区的内容
// 要是业务处理过程中出现异常,那么就不会走到这一步。
// 那时 output 属性将保持为 false ,程序结束自动触发刷新缓冲区的操作,执行回调函数(outputBufferCallback) 的逻辑,将原本输出的内容替换为空字符串。
echo $OutputBuffer->obGetClean();

这样也就可以在程序出现异常时,不将输出的内容显示在客户端中。

输出控制缓冲区甚至可以嵌套

ob_start(); // 开启输出控制缓冲区 1
echo "a";   

	ob_start();                //开启输出控制缓冲区 2
	echo "b"; 
	$s2 = ob_get_contents();   // 获取缓冲区 2 的内容
	ob_end_flush();            // 将缓冲区 2 的内容输出来 并且 关闭缓冲区 2 -> 这些输出的内容被上层缓冲区(缓冲区 1)接收。

echo "c";                // 继续往缓冲区 1 写入内容
$s1 = ob_get_contents(); // 获取缓冲区 1 的内容
ob_end_flush();          // 将缓冲区 1 的内容输出来 并且 关闭缓冲区 1

echo PHP_EOL;
echo "缓冲区 1:" . $s1 . PHP_EOL;
echo "缓冲区 2:" . $s2 . PHP_EOL;

输出结果:

abc
缓冲区 1:abc
缓冲区 2b

实时向浏览器输出内容

Nginx

因为从PHP到浏览器显示会经过的缓冲区有这些:PHP输出控制缓冲区 -> Fastcgi缓冲区 -> WebServer缓冲区(Nginx|Apache) -> 浏览器。

如果只是单单使用 flush() 函数,是没有办法在 PHP 脚本未运行结束前,就将部分已输出的内容发送到浏览器显示的。

flush() 的作用是刷新 WebServer 的缓冲区,使 WebServer 的缓冲区的内容发送到浏览器。

但是 PHP 的输出内容并不会直接发送给 WebServer ,而是会先发送到 fastcgi 的缓冲区。在程序运行结束后,由 fastcgi 将缓冲区的内容转发给 WebServer。最后 WebServer 再将缓冲区的内容发送给客户端。

当然这也是比较理想的情况,如果其中某个缓冲区满了或者溢出了,会脚本运行结束前,自动将缓冲区的内容发送给上层缓冲区的。

所以为了我们能够顺利测试效果,我们需要将 Nginxfastcgi 的缓冲区关闭。

全局关闭
# nginx.conf
...

http {
...
// 关闭 fastcgi 缓冲区
fastcgi_buffering off;
...
}
单个服务关闭
# conf.d/*.conf
...

location ~ \.php$ {
	// 关闭 fastcgi 缓冲区
	fastcgi_buffering off;
	...
}

重新加载 Nginx:nginx -s reload

现在我们开始代码测试了。

// 先关闭PHP默认开启的输出控制缓冲区
ob_end_clean();

for(var $i = 0; $i < 10; $i++) {
	// 因为已经将 "输出控制缓冲区" 和 "fastcgi缓冲区" 关闭了。
	// 所以这条输出的内容会直接发送到 Nginx(或者Apache) 的缓冲区。
	echo "第{$i}条消息:" . date("Y-m-d H:i:s") . "<br />";
	// 这样我们使用flush()来刷新Nginx缓冲区,就能将输出的内容发送给客户端了。
	flush();
	sleep(1);
}

现在我们就可以用浏览器来访问脚本了。

参考

更多详情请查看以下这些连接。