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

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

概要

PEAR_ErrorStack の利用方法の紹介

導入

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

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($msg, 3, '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 つのうちひとつを実行する必要があります。

エラーの発生を制御する

エラー生成のきめこまやかな制御が必要になる状況は多々あります。 一般的なエラー処理コールバック (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', false, false, true);
?>

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());
?>