最近のモダンなPHPプログラムでは、エラー処理を行う際に、try-catchを利用した例外処理で対応する場合が多いです。
全てのプログラムを自作する場合は、全てのエラーを例外として統一的に扱うことができます。ですが、古いライブラリなどを利用する場合は、ライブラリの関数内でexit()や、die()を呼ばれてしまい、エラーのハンドリングを思ったように統一しづらい場合があります。
今回は、関数内でexit()や、die()が呼び出されるようなレガシーなライブラリとの共存方法について考えてみます。
PHPの例外制御の確認
exit()やdie()の前に、まずはPHPの例外の書き方について、確認してきます。
PHPでも他の言語と同じようにtry-catch構文があり、catchの中で例外の種類(クラス)を指定する形式です。
最初の例は、try-catchが有りますが、例外が発生しないパターンです。(これを今回の記事の基本形とします)
※ちなみに余談ですが、PHP_EOLはPHPの実行環境における改行コード(CRや、LFなど)を示す定数です。
<?php
echo "start script" . PHP_EOL;
try {
echo "start try" . PHP_EOL;
echo "end try" . PHP_EOL;
} catch(Exception $e) {
echo "catch exception" . PHP_EOL;
}
echo "end script" . PHP_EOL;
この実行結果は、以下のようになります。catch exception
のメッセージが表示されていないことを確認してください。
> php test.php
start script
start try
end try
end script
try-catchの中で例外を投げた時
次は、try-catchの中で例外を投げた例です。もちろん、catch句でエラーが捉えられます。
<?php
echo "start script" . PHP_EOL;
try {
echo "start try" . PHP_EOL;
throw new Exception(); // 例外を発生させる
echo "end try" . PHP_EOL;
} catch(Exception $e) {
echo "catch exception" . PHP_EOL;
}
echo "end script" . PHP_EOL;
こちらの出力結果はcatch exception
が出力されており、catch句の処理が実行されていることが分かります。
> php test.php
php test01.php
start script
start try
catch exception
end script
try-catchの中でexit()を実行したばあい
try-catchの中でexit()を投げた時は、その場で処理が終了してしまいます。このためcatch句の処理は実行されません。この振る舞いは変更できないので注意が必要です。
例に挙げたプログラムでは、try-catchで直接exit()しているので問題が分かりやすいですが、実際は別の関数を呼び出していて、その中からexit()されたりする場合も多いです。この場合はexitによる終了されたことが分かりづらいので注意が必要です。
※また、phpにはexit()以外にdie()関数もありますが、exit()とdie()は同じ振る舞いなので同様です。
<?php
echo "start script" . PHP_EOL;
try {
echo "start try" . PHP_EOL;
exit();
echo "end try" . PHP_EOL;
} catch(Exception $e) {
echo "catch exception" . PHP_EOL;
}
echo "end script" . PHP_EOL;
実行結果を見ると、catch exception
やend script
が表示されていないことが分かります
> php test.php
start script
start try
try-catchの中でのexit()を検出したい時
exit()が呼ばれた時でも、スクリプト終了時にクリーンアップコードを呼び出したい場合があります。この場合にはregister_shutdown_function()が利用できます。
register_shutdown_function()関数では、スクリプトの終了時に呼ばれる関数を登録できます。処理の最後でスクリプトが正常終了したかのフラグを立てておけば、終了処理を実行させることができます。
<?php
echo "start script" . PHP_EOL;
$isCleanExit = false;
register_shutdown_function(function() use(&$isCleanExit) {
echo "shutdown" . PHP_EOL;
if (!$isCleanExit) {
echo "do cleanup" . PHP_EOL;
}
});
try {
echo "start try" . PHP_EOL;
exit();
echo "end try" . PHP_EOL;
} catch(Exception $e) {
echo "catch exception" . PHP_EOL;
}
echo "end script" . PHP_EOL;
$isCleanExit = true;
こちらの実行結果は下記の通りです。
catch exception
や、end script
が出力されないのはこれまで通りですが、do cleanup
が出力されていることから、
register_shutdown_function()で登録した関数が最後に走っていることが分かります。
start script
start try
shutdown
do cleanup
exit()をコメントアウトしてみると以下の様にdo cleanup
の出力がなくなります。
start script
start try
shutdown
正常終了した場合でもregister_shutdown_function()に登録した関数は実行されますが、$isCleanExitがtrueなので、do cleanup
の処理が実行されません。これをもってexit()が呼ばれたかの検出が行えます。
try-catchで0除算などのエラーが発生したとき
ちなみに、try-catchの中で例外ではなく、0徐算などのエラーが発生したときのふるまいも確認します。
java等の言語では0除算は例外扱いになりますが、PHPでのデフォルトのふるまいは例外が発行されず警告のメッセージが出力されるだけです。このため、処理は続行します。
<?php
echo "start script" . PHP_EOL;
try {
echo "start try" . PHP_EOL;
$a = 1 / 0;
echo "end try" . PHP_EOL;
} catch(Exception $e) {
echo "catch exception" . PHP_EOL;
}
echo "end script" . PHP_EOL;
このスクリプトの実行結果は下記の通りです。end try
が出力されるので、後続の処理が実行されているこが確認できます。
start script
start try
PHP Warning: Division by zero in C:\Users\aka\Desktop\php\test01.php on line 7
Warning: Division by zero in C:\Users\aka\Desktop\php\test01.php on line 7
end try
end script
try-catchの中で,0除算などのエラーを補足する
set_error_handler()関数を利用すると、エラーが発生したときに特定の処理を実行させることができます。
登録したハンドラ関数で例外を発行させることで、エラーが起きたときに処理を続行させず、例外扱いにさせることもできます。
<?php
echo "start script" . PHP_EOL;
// エラーを例外に変換する
set_error_handler( function($errno, $errstr, $errfile, $errline) {
throw new ErrorException($errstr, $errno, 0, $errfile, $errline);
});
try {
echo "start try" . PHP_EOL;
$a = 1 / 0;
echo "end try" . PHP_EOL;
} catch(Exception $e) {
echo "catch exception" . PHP_EOL;
}
echo "end script" . PHP_EOL;
この出力結果は下記の通りです。end try
が出力されずに、catch exception
が出力されていることを確認してください。
start script
start try
catch exception
end script
まとめ
これから作るPHPスクリプトの実行中にexit();を実行させることはあまりないかと思います。
ですが、レガシーなライブラリを流用したプロジェクトを作成するとき、ライブラリ内でexit()されてしまう状況は依然として残っています。このような場合においても、クリーンナップの終了処理が確実に行われるか注意深くプログラムすることで品質の高いシステムを作ることができます。