PHP で書いたプログラムで処理時間がかかる場合に、途中経過を echo で出力し、flush 関数でバッファー内容を逐次ブラウザへ送信するようなプログラムがあります。
ところが flush 関数がなぜか動作しない…なんてことはありませんか?
この記事では、その原因と JavaScript を使った解決策をご紹介いたします。
目次
原因
PHP の Flush 関数が効かない場合、Google 検索をすると…「.htaccess ファイル内で gzip 圧縮を抑止するコードを書き加える」、「Nginx の設定ファイルにて fastcgi_buffering を off にする」などの解決策が書かれています。
これらの解決策は、使用しているサーバーが、ローカル PC であったり、社内管理のサーバーやハウジングされた専用サーバーなどの場合には 解決できることもあります。
ただ「ローカル環境だとうまくいくのに 本番環境では動作しない」とか「サーバーを移管したら 動作しなくなった」など、上記の解決策を施しても 全く flush 関数が効かない!という状況も起こりえます。
サーバーによる高速化?!
原始的な Web サービスは、PHP からの出力を随時ブラウザに送信するので PHP の処理が重くなっても その処理の途中経過を echo や printf で出力し、flush 関数や ob_flush 関数で逐次ブラウザに返すことが可能です。
ところが、NginX などの最新の Web サービスやサーバー高速化を掲げるサーバー会社の独自仕様によって、PHP などの特定の拡張子のファイルから出力されるデータは 一旦 Web サービス内でバッファリングされ、出力が完全に終了した時点で(End Of File)コンテンツをまとめて(gzip 圧縮などをして)一括してブラウザに送信する…などの処理が施されている場合があります。
そのせいで PHP プログラムのバッファーをいくら「flush!ob_flush!」と連呼してみたところで Web サービス内のバッファーに蓄えられるだけで 全くブラウザに送信されない という「のれんに腕押し」「ぬかに釘」状態が発生してしまうようです、、
エックスサーバーの場合
個人的にこの問題に直面したのは エックスサーバー
の実行環境だったのですが、上記を踏まえて エックスサーバー
に問い合わせてみたのですが(色々調べて頂いたのですが)結論としては「(高速化やコンテンツ圧縮に伴う)サーバーの仕様です」(専用サーバーも含める)との事でした。
となると … どう.htaccess や php.ini をいじったところで flush 関数は効きません、、
ならば、プログラムを最小限に書き換えて、PHP の処理経過をブラウザに通知する方法を独自に編みだすしかありません…
flush を使った PHP プログラムの例
flush 関数を使って処理経過を逐次ブラウザに通知し、進捗状況をプログレスバーなどで表示する PHP プログラムの例としては(簡単に書くと)こんなイメージではないかと思います(serverside.php)。
<?php
[PHP 事前処理コード]
?>
<html>
<body>
[進捗状況を表示する html コード]
<script>
[進捗状況を表示する JavaScript コード]
</script>
</body>
</html>
<?php
[PHP 重い処理 ~ ループ開始]
echo "<script>[「??%」を出力する処理]</script>";
flush(); ob_flush();
[PHP 重い処理 ~ ループ終了]
?>
解決の流れ
この問題を解決するためには … まず、html 部分は一旦ブラウザに返さなければならず、そのページ内の処理として(次のページを読み出すのではなく)JavaScript から非同期で PHP の重い処理を呼び出し、かつ、その処理経過を html に通知として返す必要があります。
1. html を切り取って progressbar.php を作成します
“serverside.php” の4行目から11行目までを切り取って “progressbar.php” として作成します。
<html>
<body>
[進捗状況を表示する html コード]
<script>
[進捗状況を表示する JavaScript コード]
</script>
</body>
</html>
2. 呼び出し元のリンクを変更
元々 “serverside.php” を呼び出していたリンクを “progressbar.php?url=serverside.php” としてリンクを変更します。
つまり、元々のリンクをクリックすると “progressbar.php” が呼び出されて 上記の html コードがブラウザに返されて表示されます。
この html 内の JavaScript コードから 非同期で “serverside.php” を呼び出さなければなりません。そのためには XMLHttpRequest オブジェクトを使います。
3. XMLHttpRequest オブジェクト
JavaScript の XMLHttpRequest オブジェクトは、以下の3行の構文で “serverside.php” を非同期で呼び出すことが可能です。
let objXHR = new XMLHttpRequest();
objXHR.open("GET", "serverside.php");
objXHR.send();
これを上記の “progressbar.php” 内で html が読み込まれたと同時に実行するには こんな感じに修正します…
<html>
<body>
[進捗状況を表示する html コード]
<script>
window.onload = function(){
let objXHR = new XMLHttpRequest();
objXHR.open("GET", "serverside.php");
objXHR.send();
};
[進捗状況を表示する JavaScript コード]
</script>
</body>
</html>
4. 進捗状況を連続的に受け取る
次に “serverside.php” から、進捗状況となるデータを連続的に受け取る処理を組み込まなければなりません。
※onprogress イベントを使うと “serverside.php” から書き出した値を連続的に受け取れる気もするのですが…ここでも PHP ファイルからのデータはバッファリングされて 一括送信されてしまいますので「逐次処理」になりません、、
少し工夫して “serverside.php” から逐次 値を受信するために、”serverside.php” から(仮に)”progress.txt” などのテキストファイルを書き出し、クライアント側の JavaScript から連続的に サーバー上に書き出された “progress.txt” ファイルを読み込むことで 進捗状況を反映していくことができるのではないか?という方法です。
つまり、上記の “progressbar.php” の9行目から サーバー上の “progress.txt” を読み込む処理を書き加えると こんな感じになります…
<html>
<body>
[進捗状況を表示する html コード]
<script>
window.onload = function(){
let objXHR = new XMLHttpRequest();
objXHR.open("GET", "serverside.php");
objXHR.send();
objXHR.open("GET", "progress.txt");
objXHR.send();
};
[進捗状況を表示する JavaScript コード]
</script>
</body>
</html>
さらに、”progress.txt” を連続的に読み込むためには setInterval 関数で 例えば 100ms ごとに “progress.txt” を読み込み、”100″ を読み込んだら タイマーを終了するような処理を追加すると…
<html>
<body>
[進捗状況を表示する html コード]
<script>
window.onload = function(){
let objXHR = new XMLHttpRequest();
objXHR.open("GET", "serverside.php");
objXHR.send();
var lngTimerID = setInterval(function(){
objXHR.open("GET", "progress.txt");
objXHR.onreadystatechange = function(){
if (objXHR.readyState == XMLHttpRequest.DONE){
if (objXHR.status === 200){
if (objXHR.responseText == "100"){
clearInterval(lngTimerID);
} else {
[進捗状況を表示する JavaScript コード]
}
}
}
}
objXHR.send();
}, 100);
};
[進捗状況を表示する JavaScript コード]
</script>
</body>
</html>
5. serverside.php 側の書き換え
“serverside.php” は最上部のサンプルコードのようになっていた訳ですが、html 部分は切り出したのでもう必要なくなり、echo で script を出力していた部分を file_put_contents 関数で 進捗状況を “progressbar.txt” に書き出す処理に修正すれば完成です。
こんな感じです…
<?php
[PHP 事前処理コード]
?>
<?php
[PHP 重い処理 ~ ループ開始]
file_put_contents("progressbar.txt", "50");//進捗状況を出力します
[PHP 重い処理 ~ ループ終了]
?>
まとめ
筆者の僕の環境では無事に flush 関数での処理を置き換えることができました。
環境によっては少し手を加えて “progressbar.php” を便利に作り変えることもできると思います。
もし上記のサンプルで疑問点などあれば、こちらの記事にコメントか [プロフィール / お問い合わせ] メニューからお問い合わせ頂けましたら、少しずつでも記事を修正していければと思ってます。
SNS開発18年で2つのSNSを開発・運用中の当社が、あなたのアイデアを形にするお手伝いをします。