PHPを例外安全にするためのイディオム

  • 22
    いいね
  • 5
    コメント
この記事は最終更新日から1年以上が経過しています。

(例外安全ネタでもう少し長い記事が書きたいんだけど、思いつきだけまとめておく)

PHPにはアトミックでない関数が結構ある。本来1つの処理だったものが複数に分かれているような関数。終了する方の関数を呼ばずにいると、変な状態のままになってしまう。

  • fopen/fclose
  • flock(LOCK_EX)/flock(LOCK_UN)
  • PDO::beginTransaction / PDO::commit / PDO::rollback
  • ob_start / ob_get_clean

こういったものは気をつけて書かないと終了の関数だけが実行されず、例外安全を破ってしまう。

どうすれば「気をつけて書いている」ことになるのか考えてみた。

finallyを都度書く

finallyを使えば、例外が起きても終了処理が呼ばれる。とりあえず、これで例外安全にはなる。

ob_start();
try {
    // 例外が起きるかもしれない関数を色々呼び出す
    doSomething();
    doSomething2();
    $stdout = ob_get_contents();
} finally {
    ob_get_clean();
}

しかし都度finallyを書くのは避けるべきだと思う。
ライブラリの使用者にfinallyの利用を強制すると、あちこちにtry ~ finallyブロックが散乱してしまい、コードが汚くなる。もう少し工夫が欲しい。

finallyを無名関数ブロックでまとめる

こういう時、無名関数のブロックを使ってまとめるとfinallyを乱発しなくて良くなる。

function ob(callable $fn) {
    try {
        ob_start();
        $fn();
        return ob_get_contents();
    } finally {
        ob_end_clean();
    }
}

使い方はこんなイメージ。

$html = ob(function(){
    require 'header.php';
    require 'template1.php';
    require 'footer.php';
});

header.phpやtemplate1.phpで例外が発生した場合、$htmlには何もセットされないかもしれないが、ob_start()で始まったバッファリングは閉じることができる。

RAII

デストラクタとローカル変数を使って確実に終了処理を呼ぶやり方をRAII (Resource Acquisition Is Initialization)と言う。主にC++でのお作法だが、PHPはガーベージコレクションが参照カウンタなので真似することができる。
デストラクタはPHP側で書くならクラスにしか設定できない。RAIIを使うためには何でもクラス化することが求められる。

// プログラム終了時には必ず自分を消すファイル
class TmpObject extends SplFileObject
{
    protected $filename;
    function __construct($filename, $open_mode = 'w', $use_include_path = false, resource $context = null)
    {
        $this->filename = $filename;
        parent::__construct($filename, $open_mode, $use_include_path, $context);
    }
    function __destruct() {
        unlink($this->filename);
    }
    //...
}

fopen/fcloseのようなファイルの関数はSplFileObjectをベースにしておくと、わざわざクラス化しなくても例外安全になる。こういう意味でも生でfopen/fcloseを使うのは可能な限り避けたほうが良い。

使い分けの考察

ob_get_cleanのように終了処理の結果が必要になる場合はRAIIでまとめることができない。finallyと無名関数ブロックを使うとエレガントである。

ただし、PHPで無名関数を使うと、関連する箇所のタイプヒントが全部callableになってしまい、コードが分かりにくくなりがちだ。

RAIIを基本にして、どうしようもないときだけfinallyを使う。そういう温度感がいいのではないだろうか。