おぎろぐはてブロ

なんだかんだエンジニアになって10年以上

出力バッファリング

PHP言うても、PHP自体のソースコードレベルの話ばっかりしてるから、カテゴリ名が微妙だ。PHP APIとか?

PHP出力制御関数

PHPでは、出力制御関数というものが用意されています。これは、出力をそのまま出力するのでなくて、一旦バッファに格納し、コールバックで加工したり、変数に入れたりということをするものです。

<?php

ob_start();

echo "Hello\n";

$hello = ob_get_contents();
ob_end_clean();

var_dump($hello);
?>

といった感じで、ob_start()から始めて、ob_end_clean()までのデータは画面に出力される代わりにバッファに格納されます。そして、ob_get_contents()で、変数に代入しています。この他の使い方として、ob_start(コールバック関数名)と、ob_end_flush()の組み合わせで、コールバック関数で加工するなどがあります。

ちょっといいかもしれない例 (PHPスクリプト篇)

よい例が思いつかないですが、知ってると非常に強力なことが、ごくまれにあります。(年に1回くらい?) 自分が最近Extensionで実装したものだと、includeとの組み合わせです。
PHPリファレンス includeの16.10にもincludeしたファイルのバッファリングの例がありますが、includeは、戻り値を返してくれるのを利用してみます。

  • include先でreturnした → returnした値が返却
  • include成功したけど、returnしてない → 1 が返却
  • include失敗 → E_WARNINGで、false が返却

これをふまえて、失敗したら出力を破棄してしまうというコード。

本体

<?php
ob_start();
$ret = include 'hoge.php';
if ($ret) {
  ob_end_flush();
} else {
  ob_end_clean();
}
?>

hoge.php

ほげほげ!
ほげほげ!
<?php
  // さらに読み込むときも
  if(! include 'moge.php') return false;
  
  return false; // 何か失敗したらreturn falseで返る
?>

E_ERRORのエラーが起きてしまうと、通常と同じく実行が終了してしまいますが、PHPコードの割合が低いものをincludeする場合には、便利なことがあるかもしれません。
しかし、if(! include 'moge.php') て動くけど気持ち悪い。。

Cレベルでの実装

と、前置き長くなったですが、この仕組みをPHPソースレベルで利用するのは、意外と簡単です。
使ってる例として、highlight_file()があります。この関数は、第2引数で、trueが指定されてると、戻り値に返却、falseでは、標準出力に出力します。

ext/standard/basic_functions.c

/* {{{ proto bool highlight_file(string file_name [, bool return] )
   Syntax highlight a source file */
PHP_FUNCTION(highlight_file)
{
    zval *filename;
    zend_syntax_highlighter_ini syntax_highlighter_ini;
    zend_bool i = 0;

    if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "z|b", &filename, &i) == FAILURE) {
        RETURN_FALSE;
    }
    convert_to_string(filename);

    if (PG(safe_mode) && (!php_checkuid(Z_STRVAL_P(filename), NULL, CHECKUID_ALLOW_ONLY_FILE))) {
        RETURN_FALSE;
    }

    if (php_check_open_basedir(Z_STRVAL_P(filename) TSRMLS_CC)) {
        RETURN_FALSE;
    }

    if (i) {
        php_start_ob_buffer (NULL, 0, 1 TSRMLS_CC);
    }

    php_get_highlight_struct(&syntax_highlighter_ini);

    if (highlight_file(Z_STRVAL_P(filename), &syntax_highlighter_ini TSRMLS_CC) == FAILURE) {
        RETURN_FALSE;
    }

    if (i) {
        php_ob_get_buffer (return_value TSRMLS_CC);
        php_end_ob_buffer (0, 0 TSRMLS_CC);
    } else {
        RETURN_TRUE;
    }
}
/* }}} */

syntax hilightをする機能本体は別の関数に分離されているので、すっきりしてます。この中で、第2引数は zend_bool i にセットされます。(C言語なので、bool型がなく、そのためzend_bool) そして、 i が真のとき、php_start_ob_buffer (NULL, 0, 1 TSRMLS_CC)が呼ばれています。これが、PHPレベルでのob_start()にあたる関数です。

関数定義はこんなのです。(main/php_output.h)

int php_start_ob_buffer(zval *output_handler, // コールバック関数
                        uint chunk_size,      // chunk_size byteを超える度に、 最初の改行の際にコールバック関数をコール
                        zend_bool erase       // 0のとき、スクリプト終了までバッファは削除されません
                        TSRMLS_DC);

void php_end_ob_buffer(zend_bool send_buffer, // バッファを出力するか破棄するか
                       zend_bool just_flush   // バッファを継続するかどうか
                       TSRMLS_DC);

上のコードで、 i が真のときの挙動は、

php_start_ob_buffer (NULL, 0, 1 TSRMLS_CC);

php_get_highlight_struct(&syntax_highlighter_ini);

if (highlight_file(Z_STRVAL_P(filename), &syntax_highlighter_ini TSRMLS_CC) == FAILURE) {
    RETURN_FALSE;
}

php_ob_get_buffer (return_value TSRMLS_CC);
php_end_ob_buffer (0, 0 TSRMLS_CC);

php_start_ob_bufferを呼び出しといて、あとは最後でreturn_valueにつめるだけです。

php_end_ob_bufferのPHP関数との対応

php_end_ob_bufferは、中身を出力するか破棄するか、と、バッファを継続するか終わるかを引数で指定することができ、ざっくり以下のPHP関数と対応しています。

ob_flush() → php_end_ob_buffer(1, 1 TSRMLS_CC);
ob_clean() → php_end_ob_buffer(0, 1 TSRMLS_CC);
ob_end_flush() → php_end_ob_buffer(1, 0 TSRMLS_CC);
ob_end_clean() → php_end_ob_buffer(0, 0 TSRMLS_CC);
ob_get_flush() → php_ob_get_buffer(return_value TSRMLS_CC) してから php_end_ob_buffer(1, 0 TSRMLS_CC);
ob_get_clean() → php_ob_get_buffer(return_value TSRMLS_CC) してから php_end_ob_buffer(0, 0 TSRMLS_CC);
ob_get_contents() → php_ob_get_buffer(return_value TSRMLS_CC)