k-holyのPHPとか諸々メモ

Webで働くk-holyがPHP(スクリプト言語)とか諸々のことをメモしていきます。ソースコードはだいたいWindowsで動かしてます。

エラーハンドラが原因でSmartyのエラーが発生していた件

Smarty(3.1.8)を使っていますが、いつからかこんなエラーが発生するようになりました。

 'filemtime(): stat failed for /path/to/compiled_template.html.php' in /path/to/vendor/Smarty/sysplugins/smarty_resource.php on line 693

エラーメッセージから、ファイルの権限でエラーになってるのかなと考えましたが、ディレクトリに書き込み権限はあるし、Smartyが特殊なことしてない限り、ファイルの所有者はPHP実行ユーザーだから何も問題ないはず。
エラーが発生するのはテンプレートファイルの初回コンパイル時だけなので、もしやと思ってソースを見たところ…。

Smarty_Template_Source::getCompiled()より

    /**
     * get a Compiled Object of this source
     *
     * @param Smarty_Internal_Template $_template template objet
     * @return Smarty_Template_Compiled compiled object
     */
    public function getCompiled(Smarty_Internal_Template $_template)
    {
        // check runtime cache
        $_cache_key = $this->unique_resource . '#' . $_template->compile_id;
        if (isset(Smarty_Resource::$compileds[$_cache_key])) {
            return Smarty_Resource::$compileds[$_cache_key];
        }

        $compiled = new Smarty_Template_Compiled($this);
        $this->handler->populateCompiledFilepath($compiled, $_template);
        $compiled->timestamp = @filemtime($compiled->filepath);
        $compiled->exists = !!$compiled->timestamp;

        // runtime cache
        Smarty_Resource::$compileds[$_cache_key] = $compiled;

        return $compiled;
    }

$this->handler->populateCompiledFilepath()で、コンパイル済テンプレートファイルのパスを生成しているらしいのですが、その後ファイルの存在チェックなどせずにエラー制御演算子付きでfilemtime()が実行されていました。
個人的には行儀の悪いコードだとは思いますが(結局 $compiled->exists = !!$compiled->timestamp; とかしてるしなんでfile_exists()しないの…)、標準のエラーハンドラではここでエラーが発生してもなかったことにされるため、通常は問題にならないのでしょう。

ただ、自分の場合は独自のエラーハンドラを使ってるため、エラーが表示されたということです。
自作エラーハンドラでもerror_reporting()を使って適切にハンドリングしていれば、おそらく出力されることのないエラーなんでしょうけど、表示はしたくないけどログには記録したいケースやユニットテストのことを考えて、クラス内部ではerror_reporting()を使わずにクラス独自のエラーレベルを利用する作りにしていたのがまずかったようです。

このようなエラーも同時に発生していたのですが、多分原因は同じでしょう。

 'unlink(/path/to/compiled_template.html.php): No such file or directory' in /path/to/vendor/Smarty/sysplugins/smarty_internal_write_file.php on line 49

ちなみに、上記エラー発生箇所ではerror_reporting()で一時的にエラーレベルを変更してエラー出力を隠した上で、@unlink()とされてました。

/**
 * Smarty Internal Write File Class
 *
 * @package Smarty
 * @subpackage PluginsInternal
 */
class Smarty_Internal_Write_File {

    /**
     * Writes file in a safe way to disk
     *
     * @param string $_filepath complete filepath
     * @param string $_contents file content
     * @param Smarty $smarty    smarty instance
     * @return boolean true
     */
    public static function writeFile($_filepath, $_contents, Smarty $smarty)
    {
        $_error_reporting = error_reporting();
        error_reporting($_error_reporting & ~E_NOTICE & ~E_WARNING);
        if ($smarty->_file_perms !== null) {
            $old_umask = umask(0);
        }

        $_dirpath = dirname($_filepath);
        // if subdirs, create dir structure
        if ($_dirpath !== '.' && !file_exists($_dirpath)) {
            mkdir($_dirpath, $smarty->_dir_perms === null ? 0777 : $smarty->_dir_perms, true);
        }

        // write to tmp file, then move to overt file lock race condition
        $_tmp_file = $_dirpath . DS . uniqid('wrt', true);
        if (!file_put_contents($_tmp_file, $_contents)) {
            error_reporting($_error_reporting);
            throw new SmartyException("unable to write file {$_tmp_file}");
            return false;
        }

        // remove original file
        @unlink($_filepath);

        // rename tmp file
        $success = rename($_tmp_file, $_filepath);
        if (!$success) {
            error_reporting($_error_reporting);
            throw new SmartyException("unable to write file {$_filepath}");
            return false;
        }

        if ($smarty->_file_perms !== null) {
            // set file permissions
            chmod($_filepath, $smarty->_file_perms);
            umask($old_umask);
        }
        error_reporting($_error_reporting);
        return true;
    }

}

unlink()以外はちゃんと戻り値を見てSmartyExceptionをスローしてますし、行儀良いですね。

とりあえずこのままだとダメなので、自作エラーハンドラの方をerror_reporting()の値を見て適宜returnするよう修正しました。

せっかくなので、いくつかのフレームワークで引数なしのerror_reporting()関数の利用状況とその使い方を調べてみました。

  • CakePHP(ErrorHandlerクラス)…error_reprting() === 0 以外はdeubg設定ONならデバッガに出力、debug設定OFFならログ書き込み。戻り値はCakeLog::write()依存。(true返してる)
  • FuelPHP(Fuel\Core\Errorクラス)…ログは無条件で書き込み、Fuel::$envがPRODUCTIONかつ発生したエラーのレベルがerror_reporting()に含まれる場合はErrorExceptionに詰め替えてエラー表示。ただしエラー発生回数をカウントしてて、設定値を超えた場合はその旨の警告のみ出力。戻り値はtrue。
  • Slim(Slimクラス)…発生したエラーのレベルがerror_reporting()に含まれる場合はErrorExceptionをスロー。戻り値はtrue。(超シンプル…!)
  • Symfony(Symfony\Component\HttpKernel\Debug\ErrorHandlerクラス)…ErrorHandler::register()時に引数で指定されたエラーレベルをプロパティに設定(指定しなければerror_reporting()の値が設定される)、発生したエラーのレベルがerror_reporting()とプロパティに設定したエラーレベルの両方に含まれる場合はメッセージ整形してErrorExceptionをスロー。戻り値はfalse。

(いずれも今日の昼頃にGitHubから取得したバージョン。ZendFrameworkでは引数なしのerror_reporting()はLoggerでしか使われていませんでした)

比較してみると、それぞれの特徴がエラーハンドラの実装にも垣間見えるようで面白いですね。

拙作エラー処理クラス Phanda_ErrorGitHubで公開していますので、興味ある方はご参照ください。
https://github.com/k-holy/Phanda_Error