Edited at

PHPの例外


定義済み例外とSPL例外

PHPにはPHP本体が持つ定義済み例外とPHPに標準でバンドルされるStandard PHP Library(SPL)の例外のSPL例外の2種類の例外があります。SPLは標準で組み込まれるのでPHPの機能として標準的に使うことも可能ですが、マニュアルでも個別のページを持っています。


SPL例外

SPLの作者はPDOなど多数のパッケーのleadを務められているMarcus Börgerさんです。(PECLサイト参照 https://pecl.php.net/user/helly) MarcusさんがSPLのために作成したスライドStandard PHP Libraryから例外の部分を2つ抜粋します。


「守るべき3つのルール」


  1. 例外は例外の時に使用する (Exceptions are exceptions)

  2. 制御構造のために例外を用いない (Never use exceptions for control flow)

  3. 値を渡すために例外を用いない (Never ever use exceptions for parameter passing)

3つのルールとありますが「例外は真に例外的な時のみ用いるのであって、通常の制御のために用いてならない」という事だと思います。

例外とは例えば以下のような場合です。(An Overview of Exceptions in PHPより)


  • データベースが接続できない

  • Web APIをアクセスできなかった

  • ファイルシステムのエラー

  • テンプレートファイルや設定ファイルのパースエラー


「特別な例外を作る」

class YourException extends Exception

{
}

以下のようなRuntimeExceptionをそのまま使うより、例外クラスを継承してFileNotExistExceptionを作成することを勧めています。

throw new \RuntimeException("$fileがありません");   // OK

class FileNotExistsException extends \RuntimeException
{
}

throw new FileNotExistsException($file); // Better


LogicExceptionとRuntimeExceptionの区別

PHPマニュアルではそれぞれ、「プログラムのロジック内でのエラーを表す例外です。 この類の例外が出た場合は、自分が書いたコードを修正すべきです」「実行時にだけ発生するようなエラーの際にスローされます。」と説明がありますが多くの人が指摘するようにこれでは良くわかりません。Marcusさんの説明は実例を伴ってもう少し明快です。



  • LogicExceptionはコンパイル時またはアプリケーション設計で検出ができる例外


  • RuntimeExceptionは実行時のみ検出できる例外。全てのデータベース例外のベースクラス

LogicExceptionはコード検査すれば発見できる例外(=バグ)、RuntimeExceptionはプログラムを実行しなければ分からない例外、つまりプログラムが前提すると外部の条件や入力によって起こる例外と考えればいいでしょう。


LogicExceptionの例

<?php

abstract class MyIteratorWrapper implements Iterator
{
private $it;

public function __construct(Iterator $it)
{
$this->it = $it;
}

public function __call($func, $args)
{
$callee = [$this->it, $func];
if (!is_callable($callee)) {
throw new BadMethodCallException(); // LogicException
}

return call_user_func_array($callee, $args);
}
}

存在しないメソッドをコールしているのはアプリケーション設計の問題です。実行前にプログラムを検査することで検出可能です。


RuntimeExceptionの例

$fo = new SplFileObject($file); // RuntimeException

ファイルはアクセス不可能かもしれないし、存在しないかもしれません。しかしプログラム自体には間違いはありません。SplFileObjectはファイルが開けない場合RuntimeException例外を発生します

$fo = new SplFileObject($file);

$fo->setFlags(SplFileObject::DROP_NEWLINE);
$data = array();
foreach($fo as $l) {
if (/*** CHECK DATA ***/) {
throw new Exception((); // RuntimeException
}
$data[] = $l;
}

外部のファイルを用いているので、データは読み込むたびに変わります。これは実行時の例外です。

!preg_match($regex, $l)                 // UnexpectValueException

count($l=split(',', $l)) !=3 // RangeException
count($data) < 10) // UnderflowException
count($data) > 99) // OverflowException
count($data) < 10 || count($data) > 99) // OutOfBoundsException

これらも全てRuntimeExceptionです。


混在する例

$fo = new SplFileObject($file); // RuntimeException

$fo->setFlags(SplFileObject::DROP_NEWLINE);
$data = array();
foreach($fo as $l) {
if (!preg_match('/\d,\d/', $l)) {
throw new UnexpectedValueException(); // RuntimeException
}
$data[] = $l;
}
if (count($data) < 10) throw new UnderflowException(); // RuntimeException
// maybe more precessing code
foreach($data as $v) {
if (count($v) !== 2) {
throw new DomainException(); // LogicException
}
$v = $v[0] * $v[1];
}

かけ算をするのには「2つの数字が必要」という前提条件が満たされずデータドメインの例外をthrowしています。

注) DomainExceptionは説明が分かりにくいという指摘があり(#47097 )説明が加えられた経緯もあります #291058 マニュアルのユーザーノートには「0は除数にならない」「"曜日"にfooはない」など扱うデータがその取り扱い範囲(=データドメイン)ではないという例外と説明されています。

LogicExceptionはプロダクションサイトでは決してスローされてはならない例外、RuntimeExceptionはプロダクションサイトでもスローされることがある例外とも言えます。


OutOfRangeExceptionとOutOfBoundsExceptionの区別

この2つの例外は名前も役割も似ています。


OutOfRangeException - 無効なインデックスを要求した場合にスローされる例外です。 これは、コンパイル時に検出しなければなりません。(LogicException)

OutOfBoundsException - 値が有効なキーでなかった場合にスローされる例外です。 これは、コンパイル時には検出できないエラーです。(RuntimeException)


これらの区別を考えるときもLogicException/RuntimeExceptionの使い分けと同じように考えます。

コードのみで範囲が決定してそれに違反するものはOutOfRangeException、実行時にデータが外部から読まれてその結果無効なキーが使われるもの等実行時に範囲外が判明するのがOutOfBoundsException。OutOfRangeExceptionはバグですが、OutOfBoundsExceptionは実行の条件によってスローされる例外です。


SPL例外使用の是非

SPL例外の足りない点を検討するとSPL Improvements: Exceptionsと指摘されてるようにドキュメンテーションが不十分で開発者の共通言語として十分かというのもあるかと思います。

多くのライブラリやフレームワークがSPL例外を使用していて、それらの使用を勧めるブログ記事も多数あります。しかし一方でそうでない意見も少数ながらあります。StackOverflowで多くの投稿のある@go_ohさんのこのような意見を目にしました。


しかし率直に言えば、SPLの例外の階層構造はメチャクチャ。だから自分の例外を(\Exceptionを拡張して)好きに作るといいと思うよ


 - http://stackoverflow.com/questions/14171714/php-runtime-or-logic-exception

私自身はSPL例外を長期間使っていますが、一方でGordonさんの意見にも一理あると思います。実際にAuraPHPもSPL例外は未使用で全ての独自例外をPHP組み込み例外\Exceptionから継承していますが、十分にクリーンで機能しています。


まとめ


  • PHPにはSPLとして階層構造を持った例外機構が用意されていますが、他のSPL同様にSPL例外の利用はオプションです。また例外の考え方は言語や使用者にとって多様で、多くのディスカッションや意見があります。


  • Exceptions are exceptionsという考え方は大切です。例外をgotoの代わりにプログラムの実行位置を変えるために使ったり、swtichifの代わりに使うのはcontrol flowのために例外を使っているアンチパターンです。値を渡すための使うのも同様。


  • 「書き込み権限がありません」とメッセージで例外を文で説明しないで、特別なエラークラス(NotWritableException)で区別するようにします(ドメイン固有の例外をそれぞれ用意すれば、Exceptionsフォルダを見ればそのプロジェクトではどのような例外が起こり得るのかが分かるようになります。)



リファレンス