phpフレームワーク自作6

_170807midaigawa
暴れ川で有名なみだい川(自宅近くの山梨県南アルプス市)

例外・エラー処理

どんなに簡潔にコーディングしたとしても、いざ動かしてみると、どこかしら不具合が生じるのが普通の事と思います。
フレームワークにおいて、これをいかにすばやく発見し、修正していくことが求められていると考えます。
phpには元々、事前にキャッチする例外処理のような仕組みは無かったのですが、javaやc++のようなオブジェクト指向の概念が取り入れられることになってからコード上のエラーや、起こりうるであろうエラーをキャッチして適切に対処しようとする仕組みが備わるようになりました。
この仕組みをフレームワークの中でどのように生かしていくかを以下のように考えてみました。
尚、ここではphp7を想定して、システムを構築しております。

目次

求められる機能要件

  • デバッグ開発モードとプロダクションモードをわけて動作させる。
  • 開発時はなるべく不具合時の詳細情報を表示させる。
  • 本番環境では不具合の詳細情報を表示させない。
  • 不具合のログを適切に残す。
  • 不具合時は安全にシステムを終了させる。
  • 適切なhttpレスポンスをかえす。

フレームワークにおける、例外。エラー処理のグローバルな設定

考えられる設定場所

  1. php.iniファイルでの設定
  2. httpd.confファイルでの設定
  3. .htaccessによる設定
  4. フレームワークの中に設定ファイルを作成する。globalsetting.php

フレームワークでの設定方針

本番のシステム環境を想定してシステムを構築すべきではありますが、凡庸性を兼ね備えるフレームワークにおいては、上記の4を選択せざるを得ないとおもわれます。では、具体的にどのように設定したらいいのだろうかと考えざるを得ないことになります。幸い、php.iniファイルと同じフォルダーの中に、php.ini-developmentとphp.ini-productionというファイルがあるので、このファイルの設定の差異を設定ファイルglobalsetting.phpに反映することにしてみました。

production development
error_reporting E_ALL & ~E_DEPRECATED & ~E_STRICT E_ALL
display_errors Off On
display_startup_errors Off On
track_errors Off On
mysqlnd.collect_memory_statistics Off On
zend.assertions -1 1
opcache.huge_code_pages 1 0

開発・本番での例外エラー処理の方針

本番環境でのエラー、例外処理の取り扱い

  • 最初にキャッチされたエラー、例外のみ対象とする。
  • 簡単な画面表示をする。
  • レスポンスステータスコードを投げる。
  • Error、LogicExceptionのログを残す。

開発環境でのエラー、例外処理の取り扱い

  • なるべく。多くのエラー、例外処理の情報をトレースルートも含めて表示する。
  • ログは残さない。

例外エラー処理

例外・エラー処理の流れ
例外・エラー処理の流れ
① エラーの種別定義、グローバルな関数、設定処理
  • 基本的に、素のExceptionクラスは使わず、splExceptionクラスで分類します。
  • 100番台はエラー、300番台はロジック例外、400番台はランタイム例外、4桁は独自に定義した例外というようにコード化しています。
  • 処理のメインクラスとなるErrorFuncのインスタンスを静的メソッドで生成していますが、これは、クラスの継承(extends)、遅延静的束縛 (Late Static Bindings) による、独自例外振り分け処理用に使う為のものであります。
②-a 例外処理のキャッチ(二重try句の内側のスローされたクラス)
  • スローされたクラスでキャッチされる。
  • ErrorFuncクラスの静的メソッドである、catchAfterに処理が渡される。
  • 開発環境(デバッグモード)では、続けて、それ以外の例外、エラーキャッチも継続される。
②-b 例外処理のキャッチ(二重try句の外側のスローされたクラス)
  • PHP 7 の throw 文でスロー可能なあらゆるオブジェクトが実装する基底インターフェイスであるThrowableでキャッチされる。
②-c キャッチされない例外
  • try~catch(\throwable $e)句の外側でスローされる例外の場合である。
  • 例外ハンドラー関数であるexception_handlerにおいて、\Exceptionクラスとしてキャッチされ catchAfterに処理が渡される。
  • 開発環境(デバッグモード)においても、続けで例外、エラーキャッチ処理が行われない。
②-d エラーのキャッチ(ErrorExceptionとしてスローされるエラー)
  • エラーハンドラー関数であるerror_handlerにおいて、\ErrorExceptionクラスとしてスロー、キャッチされ、catchAfterに処理が渡される。
  • 開発環境(デバッグモード)では、続けて、それ以外の例外、エラーキャッチも継続される。
②-e エラーのキャッチ(ErrorExceptionクラスでスローされないエラー)
  • 二重try句の外側で\Throwableインターフェイスでキャッチされ、catchAfterに処理が渡される。
  • 開発環境(デバッグモード)においても、続けで例外、エラーキャッチ処理が行われない。
②-f キャッチされないエラー
  • シャットダウン時に実行されるregister_shutdown_functionにおいて、表示処理されル場合がある。
  • 開発環境(デバッグモード)では、Fatal error(致命的エラー)の表示の後、shutdown_functionでの表示まで処理が継続される場合がある。
  • 本番環境では、エラー表示はされない、「system error!」とだけ表示される。
  • Parse error(syntax error)の場合は、開発環境、本番環境関係なく、register_shutdown_functionが実行されず、Fatal errorの表示がされ、処理が終了される。
③ 開発環境モードと本番環境モードとの処理を振り分けるcatchAfter($e)
  • 開発環境モードでは、エラー、例外情報を受け取って、文字列として整形connect_ex_str()し、グローバル変数である$GLOBALS[‘exceptions’]に追記している。
  • 本番環境モードでは、funcByCode()に処理を移す。
④ 本番環境モードにおいて、エラー、例外の処理の種別を振り分ける。funcByCode($e)
  • runfirst.phpで定義した、コードから処理別に振り分け、case_○○○に処理を移す。
  • クラスの継承により、振り分け処理の独自定義を可能にさせる為、インスタントメソッドとして定義している。
⑤ エラー、例外の種別ごとの処理を定義する case_○○○($e)
  • 本番環境のみで動作する処理なので、詳細な画面表示はしない。
  • プログラムのロジックにかかわらず、実行時における、想定外の操作、運用インフラレベルの障害による例外は、原則としてログに記録しない。
  • 但し、開発段階では、本番環境モードに切り替えたとき、これらの情報がみえてこないので、ログに記録する切り替えを出来るようにする。
  • クライアントの操作による、誤ったアクセスに対しては、HTTP レスポンスステータスコード404をセットする。
  • その他の例外、エラーに対しては HTTP レスポンスステータスコード500をセットする。
⑥ログに書き出し、表示をする。logWrite($e) display($id_name, $dts)
  • ログは、globalsetting.phpのなかで定義したerror_logで設定されたパスのテキストファイルに記録される。
  • logWriteの引数である、id_name、dts[]はエラー、例外種別ごとの任意の文字列になります。
  • logWiteはフレームワーク内のレンダリング機能に渡され、処理されます。
  • 尚、開発環境モードでは、以上の動作は行われない為、dispAll()で、単純に$GLOBALS[‘exceptions’]と$GLOBALS[‘ret_url’]をecho表示している。

例外エラーキャッチ後の考えられる取り扱い処理について

開発環境では、エラー例外をすべて$GLOBALS[‘exceptions’]の中に追記してアプリケーション終了直前で画面表示していますが、グローバル空間では、容易に書き換えられてしまう可能性があり、グローバル内の変数定義を認識していなければ、意図した動作をしない可能性を秘めております。このような弊害を回避する為には、キャッチされた、エラー例外情報を、クラスの中で落としこみ、カプセル化するという手法も考えられます。その分、コード量も増え、リソースを使うことになります。ここでは、大規模システムを構築するようなアプリケーションを想定していないので、より、シンプルな方法を採用することにしました。
本番環境では、エラー例外情報をログファイルに書き出すという処理をしています。実際に、エラー例外が書き込まれたときに、アプリケーション製作者はこのエラー例外情報を得る為に、このシステムにアクセスして、ログファイルをそのつど閲覧しなければならないという手間が生じてしまいます。こうした状況を改善する為には、エラー例外情報がその都度、メール自動送信機能などで、製作者の手元に届く仕組みを考える必要があると思います。

例外処理の拡張

例外エラー処理はフレームワークにおいて、ErrorFuncクラスの中で定義されていますが、アプリケーションごとに、独自の例外処理をする事がある場合があります。そんなときのために、ErroFuncクラスの機能を引き継ぎつつも、柔軟に定義をいじれる仕組みが必要となってきます。そこで、コアのシステムはいじらずに、コアとなるErrorFuncクラスを拡張する形でアプリケーション独自の例外処理を考えてみました。ここでは、htmlでのフォーム送信処理における、バリデーションエラーを例外として捕らえ、AppErrorFuncクラスの中で定義してみました。

form_sample
form_sample
例外・エラー処理の流れ(拡張)
例外・エラー処理の流れ(拡張)

formからデータを送信した場合、サーバー側でデータをチェックし
①エラーがあれば$errorにメッセージを追記し、追記したエラーの行番号を$numに追記していく。エラーの出る箇所はフォーム送信項目ごとに複数生じる可能性があるので、エラーメッセージを$errorに追記し、データチェックが完了した時点で、例外としてスローするという手法をとっております。$numも同様の考え方で処理しております。
②最終的にエラーがあれば、追記された行番号とメッセージを区切り文字で挟んでつなげて、③MyvalidExceptionChildのインスタンスの引数としてスローする。
④MyvalidExceptionChildのコンストラクタにおいて、スーバークラス(MyvalidException)の追記されたメッセージと追記された行番号の取り出しを行い、⑤追記されたメッセージは$msgとして、⑥追記された行番号は$id_nameとして、スーバークラスのメンバーとしてセットされる。尚、$id_nameはレンダリング処理のキャッシュファイルの判別につかわれますが、詳細は次回に説明いたします。
⑦キャッチされた例外オブジェクトにエラー例外表示したときのリンク先をメンバーとして登録する。
⑧ErrorFuncを継承した、AppErrorFuncクラスのインスタンスを生成する。これは、アプリケーション独自の例外振り分け定義処理(funcByCode())を利用するためのものであります。
⑨AppErrorFuncクラスのcatchAfter()から親クラスのcatchAfter()を呼び出すことにより、⑩遅延静的束縛 (Late Static Bindings) の機能を利用して、呼び出し元クラスである、AppErrorFuncクラスのインスタンスメソッドである、funcByCode()が実行され、ここで定義された、⑪アプリケーション独自の例外キャッチ後の処理である、case_myvalidChild($e)が実行される。
⑫以降は、上記コアの処理手順と同様の処理内容になります。

例外処理の振り分け処理について

runfirst.phpでコード化した例外クラスはすべてExceptionクラスを継承しておりますが、Exceptionクラスを継承しているのは、これ以外にも存在します。たとえば「JsonException」など。また、独自に例外クラスを定義したり、インターフェースを実装することにより、インターフェースごとに例外振り分け処理をすることも考えられます。その際は、この拡張機能を利用して、コード化し処理を定義する必要があると思われます。

実装コード

runfirst.php
グローバルな設定および最初に呼び込むべきファイル

globalsetting.php
エラーや例外処理に関する設定、関数

BadRequestException.php
不正リクエスト送信。file not found に係わるカスタム例外クラス

MyvalidException.php
カスタムバリデーションに係わるカスタム例外クラス

MyvalidExceptionChild.php
アプリケーション独自のバリデーションに係わるカスタム例外クラス

ErrorFunc.php
例外エラー処理、エラー種別を定義表示するクラス

AppErrorFunc.php
例外エラー処理、エラー種別を定義表示する拡張クラス

実装サンプル

②-a 例外処理のキャッチ(二重try句の内側のスローされたクラス)

try{    
    //Error case DomainException case 
    try {
        if ($val !== 2) {  # ① error
            $msg = 'not defined variable number setting!';
            throw new \DomainException ($msg, DOMAIN); 
        }
    } catch(\DomainException $e) { # ②
        ErrorFunc::catchAfter($e);  
    }
     
    //InvalidArgumentException case 
    try {           
        $dig='k';
        if (!is_numeric($dig)) {
            $msg = 'inner2 exception!';
            throw new \InvalidArgumentException($msg, INVALID);
        }
    } catch(\InvalidArgumentException $e) { # ③
        ErrorFunc::catchAfter($e);
    }
     
    //BadFunctionCallExceptio case
    try{
        $arg = 'Unknown';
        $func = 'do' . $arg;
        if (!is_callable($func)) {
            $msg = 'Function ' . $func . ' is not callable';
            throw new \BadFunctionCallException($msg, BADFUNC);
        }
    } catch (\BadFunctionCallException $e){ # ④
        ErrorFunc::catchAfter($e);
    }

    //UnexpectedValueException
    define ("TYPE_FOO" ,'foo');
    define ("TYPE_BAR" , 'bar');

    function doSomething($x) {
        try {
            if($x != TYPE_FOO || $x != TYPE_BAR) {
                $msg ='unexpected value set!';
                throw new \UnexpectedValueException($msg, UNEXPECTED);
            }
        } catch (\UnexpectedValueException $e) { # ⑤
            ErrorFunc::catchAfter($e);
        }   
    } 
    doSomething('unknown');
} catch (\Throwable $e) {
    ErrorFunc::catchAfter($e);
}

if (defined('DBG') && !empty( $GLOBALS['exceptions'])) {
    ErrorFunc::dispAll();
}   

inside_exception[dbgモード]
二重try句の内側のスローされたexceptionクラス[dbgモード]

inside_exception2 [dbgモード]2
二重try句の内側のスローされたexceptionクラス[dbgモード]2

inside_exception[productionモード]
最初にキャッチされた例外(ErrorException)のみが表示対象になる。[productionモード]

②-b 例外処理のキャッチ(二重try句の外側のスローされたクラス)

try {
	//not defined Exception case
	$msg = 'unknown exception call!';
	throw new UnknownException($msg); # ①
} catch (\Throwable $e) { # ②
echo 'catch throwable!';
	ErrorFunc::catchAfter($e);		
} 
if (defined('DBG') && !empty( $GLOBALS['exceptions'])) {
echo "all exceptions str!";
echo "\n";
	ErrorFunc::dispAll();
}	
outside_notdefined_exception [dbgモード]
定義されてないExceptionクラスでも、ファイルのオートロードでの例外キャッチで内側のtry~catchで処理される。また、throwableでErrorとしてもキャッチされる。[dbgモード]
outside_notdefined_exception (productionモード)
定義されてないExceptionクラスはクラスファイルのオートロード処理での例外としてキャッチされる。html responseステータスコード404番が送信されている。(productionモード)
②-c キャッチされない例外

    //catchされないException case
    $msg = 'is not chatched exception call!';
    throw new \Exception($msg);
try {
    //not defined Exception case
    $msg = 'unknown exception call!';
    throw new UnknownException($msg);
} catch (\Throwable $e) {
    ErrorFunc::catchAfter($e);          
} 
if (defined('DBG') && !empty( $GLOBALS['exceptions'])) {
    ErrorFunc::dispAll();           
}   

is_not_chatched_exception [dbgモード]
exception_handlerでキャッチされる。[dbgモード]

is_not_chatched_exception[productionモード]
exception_handlerでキャッチされ、最終的にDomainExceptionで処理される。[productionモード]

②-d エラーのキャッチ(ErrorExceptionとしてスローされるエラー)

    //Error case
    $val = 3;
    $dt = $val/0;  # ①error
try{    
    //Error case and DomainException case
    try {
        if ($undefined !== 2) {  # ②error
            $msg = 'undefined number!';
            throw new \DomainException ($msg, DOMAIN); # ③
        }
    } catch(\DomainException $e) {
        ErrorFunc::catchAfter($e);  
    }
      
    if ($unknown == "") { 
        echo 'undefined variable!'; # ④error
    }
        
    //未定義の変数の評価
    echo $undefined; # ⑤error
      
    //未定義のグローバル定数の評価
    echo UNDEFINED; # ⑥error
    
    //未定義の配列オフセットの評価
    $ary = [];
    echo $ary[0];  # ⑦error
    
    //未定義のインスタンスプロパティの評価
    $obj = new stdClass;
    $obj->undefined; # ⑧error
        
    //配列と文字列の結合
    $ary = [];
    $ary . ""; // ⑨PHP 7 のコンパイル時エラー回避
       
    //配列のオフセットの型間違い
    $ary = [];
    $ary[[]] . ""; # ⑩error
       
    //組み込み関数のタイプ指定無視
    strlen([]); # ⑪error
     
    //組み込み関数の引数不足
    strlen(); # ⑫error

    //foreachの型間違い
    $num = 1;
    foreach ($num as $k => $v); # ⑬error
      
    //ユーザレベルの設定されたエラー
    if ($divisor == 0) {
        trigger_error("ゼロで割ることはできません", E_USER_ERROR); # ⑮⑯error
    }

    //__toString()未実装時の文字列キャスト
    $obj = new stdClass;
    (string)$obj; # ⑰error

    //組み込み関数の引数余剰
    strlen("", ""); # ⑱error

    //アサーションの失敗 (assert.exception=0)
    assert(false); # ⑲error

} catch (\Throwable $e) {
    ErrorFunc::catchAfter($e);     
} 

if (defined('DBG') && !empty( $GLOBALS['exceptions'])) {     
    ErrorFunc::dispAll();
}    

inside_error [dbgモード]
ErrorExceptionでキャッチされるエラー[dbgモード]

inside_error [productionモード]
ErrorExceptionでキャッチされるエラー。最初にキャッチされたエラーのみが対象。[productionモード]

②-f キャッチされないエラー

//try 内外関係なくerror表示される
//  ini_set("memory_limit", "12M");
//  str_repeat("aaa", 50000000);
try{
    ini_set("memory_limit", "12M");
    str_repeat("aaa", 50000000);
    
    ini_set("max_execution_time", "1");
    for(;;){};
    
} catch (\Throwable $e) {
    ErrorFunc::catchAfter($e);  
}
if (defined('DBG') && !empty( $GLOBALS['exceptions'])) {
    ErrorFunc::dispAll();
}

Fatal_error[dbgモード]
shutdown_functionでキャッチされるが、Fatal error表示もされている。[dbgモード]

Fatal_error [productionモード]
shutdown_functionでキャッチされるが、「system error!」とだけ表示される。[productionモード]

PDOException

if (!empty($_POST)) {
try {
	//PDOException case
	$dsn = 'mysql:dbname=myvote5;host=localhost;charset=utf8';
	$user = 'user';
	$password = 'userpass';

	try {
		$dbh = new \PDO($dsn, $user, $password, array(\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION));
		$dbh->query("SELECT wrongcolumn FROM wrongtable"); # ①error
	//	$error = $dbh->errorInfo(); 間違った場所でthrowされるので実行されない。
	} catch (\PDOException $e) {
		echo 'Connection failed!';
        ErrorFunc::catchAfter($e);	
	}
	
	try {
		$dbh->query("SELECT wrongcolumn FROM wrongtable"); # ②error
	} catch (\PDOException $e) {
		echo 'Connection failed!';	
        ErrorFunc::catchAfter($e);	
	}	
	
} catch (\Throwable $e) {
echo 'catch throwable!';
	ErrorFunc::catchAfter($e);		
	
} 
if(defined('DBG') && !empty( $GLOBALS['exceptions'])){
echo 'print all exceptions str!';		
	ErrorFunc::dispAll();			
}	

} else {			
//フォーム表示html
}

pdo_exception [dbgモード]
throw文は不要となる。PDOExceptionでキャッチすればほかのExceptionと同様に扱える。[dbgモード]

pdo_exception [productionモード]
詳細表示されない。http responseステータスコード500番が送信される。[productionモード]

MyvalidExceptionChild
フォーム送信post
フォーム送信post

myvalid_exception_after [dbgモード]
連結されたエラーメッセージとリターンアドレスが表示される。[dbgモード]

myvalid_exception_after [productionモード]
エラー状況を表すメッセージ、アドレスリンクが表示される。http response 200番が返される。[productionモード]

* 尚、本番モードにおける、画面表示については、次回のレンダリング処理について、説明を行う予定であります。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください