キャッシュありでテンプレートで発生したFatalが表示されないバグ

どんな問題か?

setup.phpから以下設定しておく。

  • キャッシュあり。
  • 更新周期0s

test.php

<?php
include_once dirname(__FILE__) . '/__init__.php';
Rhaco::import('tag.HtmlParser');

$p = new HtmlParser('test.html');
$p->write();
?>

test.html

<html>
<head>
</head>
<body>

<?php
echo phpversion() . "<br/>\n";

// Strict Standards
//function & aaa(){
//$ret = & new stdClass();
//return $ret;
//}
//$ccc = & aaa();

//echo $xxx;	// Notice:

{}}		// Parse error:

//xxx();	// Fatal error:
?>

</body>
</html>

test.php を実行するとテンプレートで以下となる。

  • Parser errorが発生しても画面に表示されない。
  • Noticeが発生したときのログが成形されていない。(これはついでに修正してみました)

画面イメージは次の通り。

Parser error 修正前

Parser error が発生してるのに画面上はわからない。

Notice 修正前

ログされてるが1行にまとまっていない。

修正してみた。

ob_start、register_shutdown_function あたり挙動がPHP4、PHP5で異なる。 - gounx2の日記 を踏まえて修正してみた。

ポイントは。

エラーの検出は2つ考慮が必要

  • Noticeなどerror_handlerで取得できるエラー
  • Parser errorなどerror_handlerで取得できないエラー

あと、ob_get_clean の実行タイミング。

rhaco/io/Snapshot.php(rev3175)

<?php
Rhaco::import("resources.Message");
Rhaco::import("io.FileUtil");
Rhaco::import("util.Logger");
Rhaco::import("lang.Variable");
Rhaco::import("lang.ArrayUtil");
Rhaco::import("lang.Env");
Rhaco::import("tag.model.TemplateFormatter");

/**
 * スナップショットを操作するクラス
 * @author Kazutaka Tokushima
 * @license New BSD License
 * @copyright Copyright 2005- rhaco project. All rights reserved.
 */
class Snapshot{
    var $start = false;
    var $url = "";
    var $variables = array();
    var $buffer = "";
    var $id = 0;

    /**
     * スナップショットの取得を開始する
     *
     * @param string $url
     * @param array $variables
     * @return Snapshot
     */
    function Snapshot($url="",$variables=array()){
        /***
         * $snap = new Snapshot();
         * print("A");
         * eq("A",$snap->get());
         *
         * $snap1 = new Snapshot();
         * print("A");
         * $snap2 = new Snapshot();
         * print("a");
         * $snap3 = new Snapshot();
         * print("1");
         * eq("1",$snap3->get());
         * eq("a",$snap2->get());
         * print("B");
         * eq("AB",$snap1->get());
         */
        $id = sizeof(Rhaco::getVariable("RHACO_CORE_SNAPSHOT_COUNT"));
        Rhaco::addVariable("RHACO_CORE_SNAPSHOT_COUNT",$id,$id);
        $this->id = $id;
        $this->start = true;
        $this->url = $url;
        $this->variables = ArrayUtil::arrays($variables);
        Rhaco::register_shutdown(array($this,"close"));
        Logger::deep_debug(Message::_("start snapshot({1})",$this->id));
$GLOBALS['BufferedErrors']=Array();
set_error_handler(array($this, '_error_handler'));
        ob_start();
    }

// Snapshot中のエラーを捕捉するためのハンドラ
function _error_handler($errno, $errstr, $errfile, $errline, $errcontext) {
    if(@Rhaco::error($errno, $errstr, $errfile, $errline, $errcontext)) return true;
    $errorTypes = Array(
        E_ERROR => 'Fatal error',                               // 実際には補足できない。
        E_WARNING => 'Warning',
        E_PARSE => 'Parse error',                               // 実際には補足できない。
        E_NOTICE => 'Notice',
        E_CORE_ERROR => 'Fatal Core Error',                     // 実際には補足できない。
        E_CORE_WARNING => 'Core Warning',                       // 実際には捕捉できない。
        E_COMPILE_ERROR => 'Compilation Error',                 // 実際には捕捉できない。
        E_COMPILE_WARNING => 'Compilation Warning',             // 実際には捕捉できない。
        E_USER_ERROR => 'Fatal error',
        E_USER_WARNING => 'Warning',
        E_USER_NOTICE => 'Notice',
        2048 => 'Strict Standards',
        4096 => 'Catchable Fatal Error'                         // 実際には捕捉できない。
    );
    $errmsg = sprintf(
        "%s: %s in %s on line %d",
        $errorTypes[$errno],
        $errstr,
        $errfile,
        $errline);
    $GLOBALS['BufferedErrors'][]=array('type'=>'warning', 'msg'=>$errmsg);
    return false;
}

    /**
     * スナップショットの取得を終了する
     */
    function close(){
        /*** unit("io.SnapshotTest"); */
        if($this->start){
            if($this->buffer === ""){
$flg = 1; // get()が呼ばれずclose()されたとき、スクリプトの実行が中断される重大なエラー発生と判断する。
                $this->buffer = ob_get_contents();
//              if(preg_match("/Fatal error.+on line.+/",$this->buffer,$match)){
//                  Logger::error(str_replace(array("<b>","</b>","<br />"),array("","",""),$match[0]));
//              }

// error_handler で捕捉できないエラー(Fatal/Parse)は、ob_get_contentsから探して判断する。
// これができるのはPHP5だけ。PHP4は、ob_get_contentsが空文字列になってる。
$arr=array('Fatal error','Parse error');
foreach($arr as $key){
if(preg_match("/".$key.".+on line.+/",$this->buffer,$match)){
$errmsg = str_replace(array("<b>","</b>","<br />"),array("","",""),$match[0]);
$GLOBALS['BufferedErrors'][]=array('type'=>'error', 'msg'=>$errmsg);
}
}
            }
//PHP5のとき、shutdown関数内でcleanするとエラーが表示されないので、
//重大なエラーでスクリプト中断されたときは、cleanしないようにしてみた。
//「shutdownのときは」とかで見れたほうがより確実なのかも。
if(!isset($flg)){
            ob_get_clean();
}
restore_error_handler();
//捕捉したエラーをログ出力
foreach($GLOBALS['BufferedErrors'] as $err){
    if($err['type'] === 'error') Logger::error($err['msg']);
    else                         Logger::warning($err['msg']);
}
            $this->start = false;
            Rhaco::clearVariable("RHACO_CORE_SNAPSHOT_COUNT",$this->id);
            Logger::deep_debug(Message::_("end snapshot({1})",$this->id));
        }
    }


rhaco/tag/TagParser.php(rev3237)
Snapshot側でエラーのログを全て行うので、ここでのNoticeをログする処理は削除した。

    /**
     * テンプレートをフォーマットし取得する
     * @param string $templateFileName テンプレートファイルパス(resources/templates)からの相対
     * @param string $remotePath 相対パス変換用のルートパス
     * @param array $variables テンプレートで利用する変数(hash)
     * @return string
     */
    function read($filename="",$variables=array(),$remotePath=""){
        /*** unit("tag.TagParserTest"); */
        $this->filename = empty($filename) ? $this->filename : $filename;
        if(empty($this->filename)) return ExceptionTrigger::raise(new NotFoundException("template"));
        $filename = Url::parseAbsolute($this->path,$this->filename);
        $variables = $this->_setSpecialVariables(array_merge(ArrayUtil::arrays($variables),ArrayUtil::arrays($this->variables)));
        $cacheurl = $this->_getCacheUrl($filename);
        $this->tmpurl = empty($remotePath) ? $this->url : $remotePath;
        $rhaco_tag_parse_src = null;

        if(!Variable::bool(Rhaco::constant("NOT_MAKE_CACHE")) && Variable::bool(Rhaco::constant("TEMPLATE_CACHE")) &&
            !Cache::isExpiry($cacheurl,Rhaco::constant("TEMPLATE_CACHE_TIME",86400)) && (FileUtil::time($filename) < Cache::time($cacheurl))
        ){
            $rhaco_tag_parse_src = Cache::execute($cacheurl,$variables);
        }else{
            $rhaco_tag_parser_read_src = $this->_parse($this->_getTemplateSource($filename));
            if(Variable::bool(Rhaco::constant("TEMPLATE_CACHE")) && !Variable::bool(Rhaco::constant("NOT_MAKE_CACHE"))){
                Cache::set($cacheurl,$rhaco_tag_parser_read_src);
                $rhaco_tag_parse_src = Cache::execute($cacheurl,$variables);
            }
            if($rhaco_tag_parse_src === null){
                $rhaco_snapshot = new Snapshot();
                Rhaco::execute($rhaco_tag_parser_read_src,$variables);
                $rhaco_tag_parse_src = $rhaco_snapshot->get();
            }
            unset($rhaco_snapshot,$rhaco_tag_parser_read_src);
        }
        unset($filename,$variables,$cacheurl);
        $src = StringUtil::encode($this->_callFilter("publish",$this->_call($rhaco_tag_parse_src,"_doRead")),$this->encodeType);
//      if(preg_match_all("/Notice.+/",$src,$match)) Logger::warning($match);
        return $src;
    }

修正後の画面イメージは次の通り。

Parser error 修正後

Notice 修正後