より進んだエラー処理のための PEAR_ErrorStack の利用方法

より進んだエラー処理のための PEAR_ErrorStack の利用方法 – シンプルで、かつ進んだエラー処理を行うための PEAR_ErrorStack の利用

Synopsis

PEAR_ErrorStack の利用方法の紹介

導入

このクラスは、PEAR パッケージ の一部として提供されており、以下のような特徴があります。

  • 単体テストが十分に行なわれており、ドキュメントもきちんと作成されている

  • 動作が機敏 - PEAR_Error をはるかに上回る

  • パッケージ固有のエラー処理

  • エラーレベル(notice/warning/error/exception)の指定

  • エラーに関連するデータがエラーメッセージとは別に保存される

  • エラーの階層化 - 親エラーを指定可能

  • エラーメッセージの動的な生成機能により、 同一のエラーオブジェクトに対して異なるエラーメッセージを 生成することが可能

  • エラーメッセージの生成・エラーコンテキストの生成・ エラー処理機能において、洗練されたコールバック機能が利用可能。 エラーコンテキストの表示, カスタムエラーメッセージの生成, および エラー生成の制御 も参照ください。

PEAR_ErrorStack では、スタック形式でのエラーの生成と処理を実装しています。 この形式は、PEAR_Error の実装形式に比べてはるかに優れています。 PEAR_Error では、エラーの生成やエラーハンドリングを PEAR_Error オブジェクトのコンストラクタで集中管理しています。 ひとたびオブジェクトが生成されたら、ひき続いて、 メソッドの返り値をチェックするか、単一のグローバルなコールバックを用いるかして、 すべてのエラー処理を完了させてやる必要があります。 さらに、PEAR_Error ではエラーの発生元をたどることがほぼ不可能ですし、 エラーの生成の際には、PEAR の基底クラスの大きくて重い一連のメソッドがコールされる ことになります。

<?php
// 昔ながらの PEAR_Error の使用法
require_once 'PEAR.php';
class 
myobj
{
    
// $err がどこで発生したのかがわからない
    
function errorCallback($err)
    {
        
$this->display($err->getMessage());
        
$this->log($err->getMessage());
    }

    function 
log($msg)
    {
        
error_log($msg3'somefile.log')
    }

    function 
display($msg)
    {
        echo 
$msg '<br />';
    }
}

$myobj = new myobj;

// コールバックを利用する
PEAR::setErrorHandling(PEAR_ERROR_CALLBACK, array(&$myobj'errorCallback'));

$ret SomePackage::doSomething();
if (
PEAR::isError($ret)) {
    
// 何かの処理をする - このエラーは画面に表示され、ログにも記録される
}
PEAR::pushErrorHandling(PEAR_ERROR_RETURN);

$ret SomePackage::doSomething();
if (
PEAR::isError($ret)) {
    
// 何かの処理をする - このエラーは画面にはあらわれないし、ログにも記録されない
}
PEAR::popErrorHandling();
?>

PEAR_ErrorStack クラスは Log パッケージを参考に作られており、 エラーの識別や、さらにはエラーの自動再パッケージも容易に行うことができます。

<?php
// PEAR_ErrorStack を用いたエラー処理
require_once 'PEAR/ErrorStack.php';
require_once 
'Log.php';
define('MYPACKAGE_ERROR_DBERROR'1);
class 
myobj
{
    var 
$_stack;
    function 
myobj()
    {
        
$this->_stack = &PEAR_ErrorStack::singleton('MyPackage');
    }

    function 
errorCallback($err)
    {
        switch(
$err['package']){
            case 
'MyPackage':
                
// エラースタックに、エラーのログを残すことだけを
                // 指定する。スタックには積み込まない。
                
return PEAR_ERRORSTACK_LOG;
                break;
            case 
'InternalDbPackage':
                
// エンドユーザにわかりやすいように、これらのエラーを
                // mypackagefor のエラーとしてパッケージしなおす。
                
$this->_stack->push(MYPACKAGE_ERROR_DBERROR'error',
                    array(
'dbmessage' => $err['message'],
                          
'dbcode' => $err['code'],
                          
'We are having Connection problems, please' .
                          
'try again in a few moments'),
                    
''$err); // エラーを再パッケージする
                // 内部の DB エラースタックに、エラーを無視して
                // 何事もなかったように振舞うように伝える。
                
return PEAR_ERRORSTACK_IGNORE;
                break;
        } 
// switch
    
}
}

$myobj = &new myobj;
// 自分のパッケージ用と内部 DB パッケージ用にエラースタックを分ける
$dbstack = &PEAR_ErrorStack::singleton('InternalDbPackage');
$mystack = &PEAR_ErrorStack::singleton('MyPackage');
// PEAR::Log を用いたログ出力を設定する
$log = &Log::Factory('file''somefile.log''MyPackage error log');
$mystack->setLogger($log);
// デフォルトログとして指定し、すべてのエラースタックに利用させる
PEAR_ErrorStack::setDefaultLogger($log);

// MyPackage で発生したエラーはすべてログに記録される
$ret SomePackage::doSomething();

// どんなエラーであっても $ret をチェックする必要はない -
// エラーは完全にコードと分離されている
if ($dbstack->hasErrors()) {
    
var_dump($dbstack->getErrors();
}

// すべてのエラーに対してのデフォルトのコールバックを設定する
PEAR_ErrorStack::setDefaultCallback(array(&$myobj'errorCallback'));

// これで、すべての DB エラーが透過的に
// わかりやすい MyPackage エラーに変換される。
$ret SomePackage::doSomething();

?>

PEAR_Error があるのに、なぜまた新しいエラー処理ルーチンを作ったのでしょうか? PEAR_Error にはいくつかの問題があります。 エラーメッセージがエラークラスに保持されているにもかかわらず、コンピュータに エラーメッセージを自動的に処理させることは困難です。 さらに、 いったん PEAR_Error に保持されてしまったエラーメッセージを 翻訳することも容易ではありません。 また、エラー関連情報をエラークラスに 格納するための標準機能も存在しません。 そのうえ、エラーメッセージ関連の 問題として、PEAR_Error オブジェクト がどのパッケージで 作成されたのかもわかりません。またそのエラーの深刻度もわかりません。 致命的なエラーもそうでないエラーもまったく同じように見えてしまいます。

PEAR_Error オブジェクト の最大の欠陥は、 エラーをすべて同一のものとしてしまう設計です。 すべての PEAR_Error オブジェクト は、ただ単に PEAR_Error オブジェクト であるだけです。 エラーの深刻度や発生元を区別する方法がありません。 深刻度を定義する唯一の方法は、PEAR_ERROR_TRIGGER を指定して、 PHP の trigger_error 関数の 定数 PEAR_ERROR_TRIGGER および E_USER_NOTICE/E_USER_WARNING/E_USER_ERROR を用いることです。 しかし、この機能のために 900 行ものコードを使うのは馬鹿げています。 なぜなら trigger_error() は PHP に組み込まれているからです!

では、新しいエラーオブジェクトを使うために、まずはすべての PEAR::raiseError()PEAR::throwError() の呼び出しを書き換えましょう。こういうのを、

<?php
require_once 'PEAR.php';
// 古いやりかた
$error_specific_info 'bad';
$e PEAR::raiseError("error message - very " $error_specific_info .
    
" way to do things"MYPACKAGE_ERROR_FOO);
// 別の古いやりかた
$e PEAR::throwError("error message - very " $error_specific_info .
    
" way to do things"MYPACKAGE_ERROR_FOO);
?>

こんな風にします。

<?php
require_once 'PEAR/ErrorStack.php';
// 新しいやりかた
// バージョン 1: スタックインスタンスへのアクセス
$stack = &PEAR_ErrorStack::singleton('MyPackage');
$stack->push(MYPACKAGE_ERROR_DBERROR'error',
    array(
'query' => $query'dsn' => $dsn),
    
'Critical Database Error: Contact Administrator immediately');
// バージョン 2: 静的なシングルトンへのアクセス(若干遅い)
PEAR_ErrorStack::staticPush('MyPackage'MYPACKAGE_ERROR_DBERROR'error',
    array(
'query' => $query'dsn' => $dsn),
    
'Critical Database Error: Contact Administrator immediately');
?>

PEAR_Error のかわりに PEAR_ErrorStack パッケージを利用するために 最低限必要なのはこれだけです。

高度な機能

エラーに関連する情報の表示

エラーの生成方法をカスタマイズしたいこともあるでしょう。たとえば、 エラーを追跡するためには、 エラーが発生したファイル名・行番号およびクラス名/関数名を含めると 便利です。 デフォルトのオプションは、ほとんどの場合に十分要件を 満たします。これは PEAR_ErrorStack::getFileLine() で得られます。

すべてのエラーが PHP のソースファイル中で発生するとは限りません。 たとえばテンプレートエンジンでのコンパイルエラーはテンプレートの ソースファイル中で発生します。 データベースのエラーは、クエリーの テキストやデータベースの内部で起こることもあります。 インターネットパッケージでは、エラーは別のサーバ上で発生するかも知れません。 これらのすべてのエラー関連情報は、コンテキスト指定コールバック (context grabbing callback) を用いてエラーメッセージに含めることが可能です。

<?php
require_once 'PEAR/ErrorStack.php';
class 
DatabaseClass
{
    var 
$_dbError;
    var 
$_dbErrorMsg;
    var 
$_dbQuery;
    var 
$_dbPos;
    
/**
     * データベースパッケージのコンテキスト情報を取得する
     * @param integer エラーコード
     * @param array   エラーパラメータ情報 {@link PEAR_ErrorStack::push()}
     * @param array   debug_backtrace() の出力(このコールバックでは利用されません)
     */
    
function getErrorContext($code$params$backtrace)
    {
        
$context = array(
            
'errorcode' => $this->_dbError,
            
'errormsg' => $this->_dbErrorMsg,
            
'query' => $this->_dbQuery,
            
'pos' => $this->_dbPos,
        );
        return 
$context;
    }
}
$db = new DatabaseClass;
PEAR_ErrorStack::staticSetContextCallback('Database', array(&$db'getErrorContext'));
?>

コンテキスト情報は、外部のアプリケーションからも操作しやすいような 書式となっています。 もしコンテキスト情報をエラーメッセージに 含めたければ、エラーメッセージコールバックを用いて情報を可読形式の エラーメッセージに変換します。この方法については次のセクションで説明します。

独自のエラーメッセージの作成

エラーメッセージを効率的に生成するために、PEAR_ErrorStack では 3 つの 方法があります。 利用するためには、3 つのうちひとつを実行する必要があります。

  • PEAR_ErrorStack::setErrorMessageTemplate() をコールし、エラーコードとエラーメッセージテンプレートを関連付けた 配列を設定します。このように。

    <?php
    define
    ('ERROR_ONE'1);
    define('ERROR_TWO'2);
    define('ERROR_THREE'3);
    define('ERROR_FOUR'4);
    require_once 
    'PEAR/ErrorStack.php';
    $stack = &PEAR_ErrorStack::singleton('mypackage');
    $messages = array(
        
    ERROR_ONE => 'The gronk number %num% dropped a %thing%',
        
    ERROR_TWO => 'The %list% items were missing',
        
    ERROR_THREE => 'I like chocolate, how about %you%?',
        
    ERROR_FOUR => 'and a %partridge% in a pear %tree%',
    );
    $stack->setErrorMessageTemplate($messages);
    ?>

    置換は str_replace を用いて行われ、非常にシンプルです。 基本的に、もしパーセント記号(%) で囲まれた変数名があれば、連想配列で渡された値で置き換えられます。

    <?php
    array('varname' => 'value');
    ?>

    のような配列がメソッドに渡されれば、%varname% はすべて value に 置き換えられます。

    さらに、もし値がオブジェクトだった場合は、そのオブジェクトについて "__toString()" という名前のメソッドが あるかどうかを探し、見つかればそれを用いてオブジェクトを文字列に 変換します。 もし文字列の配列だった場合は、それらがカンマ区切りで 連結されます。

    <?php
    array('varname' => array('first''second''third'));
    // これは 'first, second, third' となります
    ?>
  • PEAR_ErrorStack::setMessageCallback() をコールし、独自のエラーメッセージを生成するための関数やメソッドを 設定します。 複雑な状況においては、おそらくこれが一番の方法でしょう。 これを利用すると、ユーザは PEAR_ErrorStack::getMessageCallback() でエラーメッセージの上書きや拡張が可能となります。 例。

    <?php
    require_once 'PEAR/ErrorStack.php';
    class 
    foo
    {
        var 
    $_oldcallback;
        function 
    callback(&$stack$err)
        {
            
    $message call_user_func_array($this->_oldcallback, array(&$stack$err));
            
    $message .= "File " $err['context']['file'];
            return 
    $message;
        }
    }
    $a = new foo;
    $stack = &PEAR_ErrorStack::singleton('otherpackage');
    $a->_oldcallback $stack->getMessageCallback('otherpackage');
    $stack->setMessageCallback(array(&$a'callback'));
    ?>
  • PEAR_ErrorStack を継承したクラスを作成し、 PEAR_ErrorStack::getErrorMessageTemplate() あるいは PEAR_ErrorStack::getErrorMessage() をオーバーライドします。 このクラスが別のパッケージ/アプリケーションからも 利用できることを保証するため、このコードをクラス宣言の直後に入れてください。

    <?php
    PEAR_ErrorStack
    ::singleton('mypackage'falsenull'MyPEAR_ErrorStack');
    ?>

エラーの発生を制御する

エラー生成のきめこまやかな制御が必要になる状況は多々あります。 一般的なエラー処理コールバック (generic error handling callback) では、 発生するエラーはすべてひとつのコールバックで処理されるようになっています。 PEAR_ErrorStack では個々のパッケージについて別々のコールバックを 利用できますが、 PEAR_ErrorStack::staticPushCallback() メソッドを用いて一般的なエラー処理コールバックを行うことも可能です。 これは、PEAR_Error の PEAR_ERROR_CALLBACK モードと同じです。

PEAR_ErrorStack の真の強みは、このコールバックにあります。 PEAR_Error のコールバックではエラーメッセージに対して変更を加えることができません。 すべてのエラー処理はコールバック関数あるいはメソッドの中で完結する 必要があります。 PEAR_ErrorStack のコールバックでは、3 つの定数を用いて エラー処理を変更することができます。

PEAR_ERRORSTACK_IGNORE はスタックに対し、エラーを無視して 何事も起こらなかったように振る舞わせます。エラーはログに記録されず、 スタックにも保存されません。しかし、 PEAR_ErrorStack::push() からは返されます。

PEAR_ERRORSTACK_PUSH はスタックに対し、エラーを保存するが ログには記録しないことを指示します。

PEAR_ERRORSTACK_LOG はスタックに対し、エラーを保存せずに ログにだけ記録することを指示します。

<?php
define
('ERROR_CODE_ONE',1);
define('ERROR_CODE_TWO',2);
define('ERROR_CODE_THREE',3);
require_once 
'PEAR/ErrorStack.php';
require_once 
'Log.php';
function 
somecallback($err)
{
    switch(
$err['code']){
        case 
ERROR_CODE_ONE:
                return 
PEAR_ERRORSTACK_IGNORE;
                break;
        case 
ERROR_CODE_TWO:
                return 
PEAR_ERRORSTACK_PUSH;
                break;
        case 
ERROR_CODE_THREE:
                return 
PEAR_ERRORSTACK_LOG;
                break;
    } 
// switch
}
$log = &Log::factory('display');
$stack = &PEAR_ErrorStack::singleton('mypackage');
$stack->setLogger($log);
$stack->pushCallback('somecallback');
$stack->push(ERROR_CODE_ONE);
$stack->push(ERROR_CODE_TWO);
$stack->push(ERROR_CODE_THREE);
var_dump(PEAR_ErrorStack::staticGetErrors());

// simulate PEAR_ERROR_CALLBACK, with specific callback for mypackage
// every other package will only log errors, only mypackage's errors
// are pushed on the stack, conditionally
class myclass {
    function 
acallback($err)
    {
        return 
PEAR_ERRORSTACK_LOG;
    }
}
$stack2 PEAR_ErrorStack::singleton('anotherpackage');
$stack3 = &PEAR_ErrorStack::singleton('thirdpackage');
PEAR_ErrorStack::setDefaultCallback(array('myclass''acallback'));
?>

エラーを別のパッケージに移す

エラーコールバックの最も解りやすい使用法としては、いくつもの ユーザレベルのアプリケーションがシステムレベルのパッケージを利用する場合に よく行われる方法が挙げられます。 たとえば、PEAR DB パッケージを用いたコンテンツ管理システム (CMS)を書いているとしましょう。ユーザがフォーラムへの投稿のために リンクをクリックしたときに、データベースのエラーが表示されるのは あまりよくありません。 このような場合にデータベースエラーを MyPackage のエラーに再パッケージするために PEAR_ErrorStack が用いられます。

<?php
define
('MYPACKAGE_ERROR_DBDOWN',1);
require_once 
'PEAR/ErrorStack.php';
function 
repackage($err)
{
    if (
$err['package'] == 'DB') {
        
$mystack = &PEAR_ErrorStack::singleton('mypackage');
        
$mystack->push(MYPACKAGE_ERROR_DBDOWN'error', array('olderror' => $err));
        
// DB エラーを無視し、mypackage のエラーとして記録する
        
return PEAR_ERRORSTACK_IGNORE;
    }
}
?>

@ 演算子をエミュレートする

PEAR_Error の PEAR::expectError() メソッドは、強力ですが 使いにくい面もあります。 通常の PHP のエラーであれば、@ 演算子を以下のように用いることで 出力を抑制することができます。

<?php
@file_get_contents();
?>

PEAR_ErrorStack でこの動作を再現するのは簡単です。

<?php
define
('ERROR_CODE_SOMETHING'1);
require_once 
'PEAR/ErrorStack.php';
function 
silence($err)
{
    
// すべてのエラーを無視する
    
return PEAR_ERRORSTACK_IGNORE;
}
$stack = &PEAR_ErrorStack::singleton('mypackage');
$stack->pushCallback('silence');
$stack->push(ERROR_CODE_SOMETHING);
?>

PEAR_ErrorStack はこれを一歩進め、さらに 2 つの定数を用いることで 「ログにのみ記録する」「スタックにのみ記録する」といった制御が可能です。 最後に、特定のエラーだけを選び出してそれ以外を無視する例を示します。

<?php
define
('SOMEPACKAGE_ERROR_THING'1);
require_once 
'PEAR/ErrorStack.php';
function 
silenceSome($err)
{
    if (
$err['package'] != 'somepackage') {
        
// 他のすべてのパッケージのエラーを無視する
        
return PEAR_ERROR_IGNORE;
    }
    if (
$err['code'] != SOMEPACKAGE_ERROR_THING) {
        
// 他のすべてのエラーコードを無視する
        
return PEAR_ERRORSTACK_IGNORE;
    }
}
$stack = &PEAR_ErrorStack::singleton('mypackage');
$stack->pushCallback('silenceSome');
$stack->push(ERROR_CODE_SOMETHING);
?>

PEAR_Error との後方互換性、PHP 5 の例外および PEAR_Exception との前方互換性

PEAR_ErrorStack は、 PEAR::raiseError() を用いて自動的に PEAR_Error を生成するようにプログラムすることもできます。そのためには 以下のように PEAR_Error compatibility に true を設定します。

<?php
require_once 'PEAR/ErrorStack.php';
$stack = &PEAR_ErrorStack::singleton('mypackage'falsefalsetrue);
?>

PEAR_ErrorStack は新しい PEAR_Exception クラスとともに使用することが可能です。 このようなコードで、例外を変換します。 以下のコードで、返される例外クラス名を設定することができます。

<?php
require_once 'PEAR/ErrorStack.php';
require_once 
'PEAR/Exception.php';
$stack = &PEAR_ErrorStack::singleton('mypackage');
$stack->push(1'test error');
throw new 
PEAR_Exception('did not work'$stack->pop());
?>