PHP5でSTRICT有効でPEARを使う(NOTICE、STRICTをあやつる)

NOTICE、STRICTは怪しいコードを指摘してくれる、優れたエラーモードで次のように書くことで有効になります。

error_reporting(E_ALL|E_STRICT);

ところが、PEARの多くのモジュールはPHP4でも動作するよう作られているため、STRICTを有効にすると、大量のSTRICTが発生します。

そこで・・・

次のことを実現するクラスを作りました。

  • 自分で作るスクリプトは NOTICE、STRICT 有効で実行する。
  • PEARモジュールは NOTICE、STRICT を個別に無視設定して実行する。

さらに、Exception、PEAR_Exceptinon もまとめて扱うことで、PHPのエラーに関する出力を統合してみました。
PEARと書いてきましたが、NOTICE、STRICTが発生するライブラリ全般に使えます。また、今回作ったのはPHP5用ですが Exception まわりの処理を削除すれば、PHP4で NOTICE を操ることはできます。

素のPHPと、ErrorHandlerクラス使用の例

次のスクリプトを使いました。

<?php
error_reporting(E_ALL|E_STRICT);
mb_internal_encoding('UTF-8');
mb_regex_encoding('UTF-8');
date_default_timezone_set('Asia/Tokyo');
ini_set('display_errors', 1);
ini_set('default_charset', 'UTF-8');
echo 'phpversion : ' . phpversion() . '<br />';

//require_once('ErrorHandler.php');
//$error_handle = ErrorHandler::singleton();
//$error_handle->addIgnoreError(array('errfile' => '#/PEAR/PEAR.php$#i', 'errno' => E_STRICT));

require_once('PEAR.php');    // requireしただけでSTRICTになる。
echo $xxx;                   // 未定義変数なのでNOTICEになる。
trigger_error('ユーザエラーを投げる', E_USER_ERROR);
?>

次が素のPHPで実行したとき。(PEAR.phpでSTRICTが発生してるのを確認してください)

phpversion : 5.2.2

Strict Standards: Assigning the return value of new by reference is deprecated in C:\xampp\php\PEAR\PEAR.php on line 563

Strict Standards: Assigning the return value of new by reference is deprecated in C:\xampp\php\PEAR\PEAR.php on line 566

Notice: Undefined variable: xxx in C:\xampp\htdocs\errorhandler\test1.php on line 15

Fatal error: ユーザエラーを投げる in C:\xampp\htdocs\errorhandler\test1.php on line 16

次に、先のスクリプトのコメント3行を生かして、ErrorHandlerクラスを使うと、先ほどのSTRICTが無くなってるのが分かります。

phpversion : 5.2.2

E_NOTICE(8)
Undefined variable: xxx
C:\xampp\htdocs\errorhandler\test1.php (15) 
#0 C:\xampp\htdocs\errorhandler\test1.php(15): ErrorHandler->error_handler(8, 'Undefined varia...', 'C:\xampp\htdocs...', 15, Array)
#1 {main}

date : 2007-11-11 11:11:11
----------------------------------------------------------------
E_USER_ERROR(256)
ユーザエラーを投げる
C:\xampp\htdocs\errorhandler\test1.php (16) 
#0 [internal function]: ErrorHandler->error_handler(256, '???????????????...', 'C:\xampp\htdocs...', 16, Array)
#1 C:\xampp\htdocs\errorhandler\test1.php(16): trigger_error('???????????????...', 256)
#2 {main}

-- $_GET -------------------------------------------------------
Array
(
:

ErrorHandlerクラスの使い方

まず基本はこれだけです。

ini_set('display_errors', 1);
error_reporting(E_ALL|E_STRICT);
require_once('ErrorHandler.php');
$error_handle = ErrorHandler::singleton();

次に、STRICTなどを個別に操る方法です。これで「/PEAR/PEAR.php で発生する STRICT を無視」を設定できます。errfileへは正規表現で条件を記述します。

$error_handle->addIgnoreError(array('errfile' => '#/PEAR/PEAR.php$#i', 'errno' => E_STRICT));

次の例は「STRICT かつ errstr が Non-static method PEAR::isError()... を無視」です。errstrが'='で始まっていますが、これは完全一致を意味します。正規表現でも記述できます。

$error_handle->addIgnoreError(array('errno' => E_STRICT, 'errstr' => '=Non-static method PEAR::isError() should not be called statically, assuming $this from incompatible context'));

実はPEARの場合、手っ取り早いのは次の設定です。

$error_handle->addIgnoreError(array('errfile' => '#/PEAR/#i'));
$error_handle->addIgnoreError(array('errstr' => '#/PEAR/#i'));

しかしながら、自分で書いたスクリプト上で次のようなSTRICTが発生することもあるので、そのような場合は、順次設定を追加する必要があります。

'Non-static method Mail::factory() should not be called statically, assuming $this from incompatible context'

ErrorHandlerクラスの解説

詳しくはソース参照

  • set_error_handler()、set_exception_handler() でエラー・未catchの例外を受けるハンドラをセットしています。
  • エラー・未catch例外発生時は、display_errors、error_reporting() から判断して表示します。
  • ただし、無視するエラーとして登録されていれば無視します。

ErrorHandlerクラスソース

ErrorHandler.php

<?php
require_once('PEAR/Exception.php');

class ErrorHandler
{
    /**
     * 無視するエラー
     *
     * @var array
     */
    protected $_ignore_error = array();

    /**
     * シングルトン
     *
     * @param array $opt_ オプション
     * @return Weed_ErrorHandler
     */
    static public function singleton()
    {
        static $singleton = null;
        if ($singleton === null) {
            $class_name = get_class();
            $singleton = new $class_name();
            
            // エラーハンドラ設定
            set_error_handler(array($singleton, 'error_handler'));
            
            // キャッチされなかった例外のハンドラ設定
            set_exception_handler(array($singleton, 'uncatched_exception_handler'));
        }
        return $singleton;
    }
    
    /**
     * 無視するエラーを登録
     *
     * 'errno' 'errstr' 'errfile' 'errline' をキーとして条件記述する。
     * 
     * 'errstr' 'errfile' は正規表現で指定する。
     *    array('errfile' => '#/PEAR/PEAR.php$#i', 'errno' => E_STRICT)
     * 
     * 完全一致で指定するときは先頭文字を'='で始めてもよい。
     *       array('errno' => E_STRICT, 'errstr' => '=Non-static method PEAR::isError() should not be called statically, assuming $this from incompatible context')
     * 
     * @param array $errinfo_ 条件
     */
    public function addIgnoreError($errinfo_ = array())
    {
        $this->_ignore_error[] = $errinfo_;
    }

    /**
     * 無視してもよいエラーか
     *
     * @param array $errinfo_
     * @return bool
     */
    public function isIgnoreError($errinfo_ = array())
    {
        if (isset($errinfo_['errfile'])) {
            // windows 対応、ファイルパスの区切りを変換
            $errinfo_['errfile'] = strtoupper(strtr($errinfo_['errfile'], '\\', '/'));
        }
        
        foreach ($this->_ignore_error as $ignore) {
            $hit_flg = true;
            foreach (array_keys($ignore) as $key) {
                if (!isset($errinfo_[$key])) {
                    $hit_flg = false;
                    break;
                }
                if ($key === 'errstr' || $key === 'errfile') {
                    if ($ignore[$key]{0} === '=') {
                        if ($ignore[$key] !== ('=' . $errinfo_[$key])) {
                            $hit_flg = false;
                            break;
                        }
                    } else if (0 === preg_match($ignore[$key], $errinfo_[$key])) {
                        $hit_flg = false;
                        break;
                    }
                } else if ($key === 'errno' || $key === 'errline') {
                    if ($ignore[$key] !== $errinfo_[$key]) {
                        $hit_flg = false;
                        break;
                    }
                } else {
                    return false;
                }
            }
            if ($hit_flg === true) {
                return true;
            }
        }
        return false;
    }
    
    /**
     * 例外を文字列に変換
     *
     * @param Exception $e_
     * @return string
     */
    public static function exceptionToString($e_)
    {
        $msg = '';
        $exx = new PEAR_Exception('', $e_);
        $causes = $exx->getCause();
        if ($causes != null) {
            if (!is_array($causes)) {
                $causes = array($causes);
            }
            foreach ($causes  as $i => $cause) {
                /* @var $cause Exception */
                if ($cause instanceof PEAR_Exception) {
                    if ($cause->getCause() !== null) {
                        $msg .= self::exceptionToString($cause->getCause());
                    }
                } else {
                    if (! $cause instanceof Exception) {
                        $msg .= str_repeat(' ', $i) . (string)$cause . PHP_EOL;
                        continue;
                    }
                }
                $msg .= str_repeat(' ', $i) . get_class($cause) . ': ';
                $msg .= $cause->getMessage() . ' in ' . $cause->getFile();
                $msg .= ' on line ' . $cause->getLine() . PHP_EOL;
                $msg .= $cause->getTraceAsString() . PHP_EOL;
//                $msg .= print_r($cause->getTrace(), 1) . PHP_EOL;
            }
        }
        return $msg;
    }
    
    /**
     * エラーハンドラ定義
     *
     * @param int $errno
     * @param string $errstr
     * @param string $errfile
     * @param int $errline
     */
    public function error_handler($errno, $errstr, $errfile, $errline)
    {
        // @ でエラー出力を抑制していても、エラーハンドラがCallされる。
        // ただし、@ の場合は一時的に error_reporting() == 0 となるので
        // これをチェックすることで、@ の有り無しを判別する。
        if (error_reporting() === 0) {
            return;
        }
        
        // error_reporting にセットされているエラーレベルではない場合、
        // 除外する。
        if ((error_reporting() & $errno) === 0) {
            return;
        }
        
        // 無視するエラーかチェック
        $errinfo = array(
            'errno'    => $errno,
            'errstr'   => $errstr,
            'errfile'  => $errfile,
            'errline'  => $errline,
        );
        if ($this->isIgnoreError($errinfo) === true) {
            return;
        }
        
        // エラー番号とエラーレベルのマッピング
        $errlev = array(
            E_USER_ERROR   => 'E_USER_ERROR',
            E_ERROR        => 'E_ERROR',
            E_USER_WARNING => 'E_USER_WARNING',
            E_WARNING      => 'E_WARNING',
            E_USER_NOTICE  => 'E_USER_NOTICE',
            E_NOTICE       => 'E_NOTICE',
            E_STRICT       => 'E_STRICT'
        );
    
        $add_msg = 'errno';
        if (isset($errlev[$errno])) {
            $add_msg = $errlev[$errno];
        }
        $add_msg .= '(' . $errno . ')' . PHP_EOL;
        $add_msg .= $errstr . PHP_EOL;
        $add_msg .= $errfile . ' (' . $errline . ') ' . PHP_EOL;
        
        $exx = null;
        try {
            throw new Exception();
        }
        catch (Exception $e) {
            $exx = $e;    
        }
        $trace = self::exceptionToString($exx);
        $arr = preg_split('/[\r\n]+/', $trace);
        array_shift($arr);
        $trace = join(PHP_EOL, $arr);
        $add_msg .= $trace . PHP_EOL;
        
        $exit_flg = true;
        if ($errlev[$errno] === 'E_NOTICE' || $errlev[$errno] === 'E_WARNING' || $errlev[$errno] === 'E_STRICT') {
            $exit_flg = false;
        }
        self::_error_output($add_msg, $exit_flg);
    }
    
    /**
     * キャッチされなかった例外用のハンドラ定義
     *
     * @param Exception $exception
     */
    public function uncatched_exception_handler($exception) {
        $add_msg= '';
        $add_msg .= 'UNCATCHED EXCEPTION' . PHP_EOL;
        $add_msg .= self::exceptionToString($exception) . PHP_EOL;
        $exit_flg = true;
        self::_error_output($add_msg, $exit_flg);
    }
    
    /**
     * エラーを画面などに出力
     *
     * @param string $add_msg_
     * @param bool $exit_flg_
     */
    protected static function _error_output($add_msg_, $exit_flg_)
    {
        // メッセージ作成
        if ($exit_flg_) {
            $msg = '';
            $msg .= 'date : ' . date('Y-m-d H:i:s') . PHP_EOL;
            $msg .= '----------------------------------------------------------------' . PHP_EOL;
            $msg .= $add_msg_;
            if (isset($_GET)) {
                $msg .= '-- $_GET -------------------------------------------------------' . PHP_EOL;
                $msg .= print_r($_GET, true) . PHP_EOL;
            }
            if (isset($_POST)) {
                $msg .= '-- $_POST ------------------------------------------------------' . PHP_EOL;
                $msg .= print_r($_POST, true) . PHP_EOL;
            }
            if (isset($_FILES)) {
                $msg .= '-- $_FILES -----------------------------------------------------' . PHP_EOL;
                $msg .= print_r($_FILES, true) . PHP_EOL;
            }
            if (isset($_COOKIE)) {
                $msg .= '-- $_COOKIE ----------------------------------------------------' . PHP_EOL;
                $msg .= print_r($_COOKIE, true) . PHP_EOL;
            }
            if (isset($_SESSION)) {
                $msg .= '-- $_SESSION ---------------------------------------------------' . PHP_EOL;
                $msg .= print_r($_SESSION, true) . PHP_EOL;
            }
            if (isset($_SERVER)) {
                $msg .= '-- $_SERVER ----------------------------------------------------' . PHP_EOL;
                $msg .= print_r($_SERVER, true) . PHP_EOL;
            }
        } else {
            $msg = $add_msg_;
        }
        
        // 記録
//        if ((int)ini_get('log_errors') !== 0) {
//            error_log($msg);
//        }
        
//        // ファイルへ出力
//        list($usec, $sec) = explode(" ", microtime());
//        $filename = D_DIR_ERRLOG . date('YmdHis') . '-' . $usec . '-' . $errlev[$errno] . '.log';
//        $h = fopen($filename, "w");
//        fputs($h, $msg);
//        fclose($h);
        
        // 画面へ表示
        if ((int)ini_get('display_errors') !== 0) {
            echo '<pre>' . htmlentities($msg, ENT_QUOTES, mb_internal_encoding()) . '</pre>';
        }
    
        if ($exit_flg_) {
            exit;
        }
    }
}

?>