PHPの例外

# 定義済み例外とSPL例外

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

 * [定義済み例外](http://php.net/manual/ja/reserved.exceptions.php)
 * [SPL例外](http://php.net/manual/ja/spl.exceptions.php#spl.exceptions.tree)

# SPL例外

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

## 「守るべき３つのルール」

1. 例外は例外の時に使用する (Exceptions are exceptions)
2. 制御構造のために例外を用いない (Never use exceptions for control flow)
3. 値を渡すために例外を用いない (Never ever use exceptions for parameter passing)
 
３つのルールとありますが「例外は真に例外的な時のみ用いるのであって、通常の制御のために用いてならない」という事だと思います。

例外とは例えば以下のような場合です。([An Overview of Exceptions in PHP](https://tournasdimitrios1.wordpress.com/2014/07/05/an-overview-of-exceptions-in-php-2/)より）
 
* データベースが接続できない
* Web APIをアクセスできなかった
* ファイルシステムのエラー
* テンプレートファイルや設定ファイルのパースエラー

## 「特別な例外を作る」

* 例外は"特別化(specialization)すべき ＝ 独自のものを作るべき 
* 組み込みの例外を拡張する ([PHPマニュアル: 例外を拡張する](http://php.net/manual/ja/language.exceptions.extending.php))

```php
class YourException extends Exception
{
}
```

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

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

class FileNotExistsException extends \RuntimeException
{
}

throw new FileNotExistsException($file);         // Better
```

# LogicExceptionとRuntimeExceptionの区別

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

* **LogicException**はコンパイル時またはアプリケーション設計で検出ができる例外
* **RuntimeException**は実行時のみ検出できる例外。全てのデータベース例外のベースクラス

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

### LogicExceptionの例

```php
<?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の例

```php
$fo = new SplFileObject($file); // RuntimeException
```
ファイルはアクセス不可能かもしれないし、存在しないかもしれません。しかしプログラム自体には間違いはありません。[SplFileObject](http://php.net/manual/ja/splfileobject.construct.php)はファイルが開けない場合`RuntimeException`例外を発生します 

```php
$fo = new SplFileObject($file);
$fo->setFlags(SplFileObject::DROP_NEWLINE);
$data = array();
foreach($fo as $l) {
    if (/*** CHECK DATA ***/) {
        throw new Exception((); // RuntimeException
    }
    $data[] = $l;
}
```
外部のファイルを用いているので、データは読み込むたびに変わります。これは実行時の例外です。

```php
!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`です。

### 混在する例

```php
$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];
}
```
かけ算をするのには「２つの数字が必要」という前提条件が満たされず**データドメイン**の例外をthrowしています。

注) [DomainException](http://php.net/manual/ja/class.domainexception.php)は説明が分かりにくいという指摘があり([#47097](https://bugs.php.net/bug.php?id=47097) )説明が加えられた経緯もあります [#291058]((http://svn.php.net/viewvc/phpdoc/en/trunk/reference/spl/domainexception.xml?r1=291059&r2=291058&pathrev=291059))　マニュアルのユーザーノートには「0は除数にならない」「"曜日"にfooはない」など扱うデータがその取り扱い範囲（＝データドメイン）ではないという例外と説明されています。

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

# OutOfRangeExceptionとOutOfBoundsExceptionの区別

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

> `OutOfRangeException` - 無効なインデックスを要求した場合にスローされる例外です。 これは、コンパイル時に検出しなければなりません。(LogicException)
> `OutOfBoundsException` - 値が有効なキーでなかった場合にスローされる例外です。 これは、コンパイル時には検出できないエラーです。(RuntimeException)

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

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

# SPL例外使用の是非

SPL例外の足りない点を検討すると[SPL Improvements: Exceptions](https://wiki.php.net/rfc/spl-improvements/exceptions)と指摘されてるようにドキュメンテーションが不十分で開発者の共通言語として十分かというのもあるかと思います。

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

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

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

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

# まとめ

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

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

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

## リファレンス

* [Standard PHP Libraryスライド]()
* [Exceptional PHP: Introduction to Exceptions](http://www.brandonsavage.net/exceptional-php-introduction-to-exceptions/)
* [SPL Improvements: Exceptions](https://wiki.php.net/rfc/spl-improvements/exceptions)
* [An Overview of Exceptions in PHP](https://tournasdimitrios1.wordpress.com/2014/07/05/an-overview-of-exceptions-in-php-2/)
* [例外 Advent Calendar 2014](http://qiita.com/advent-calendar/2014/exception)
* [OutOfRangeException vs. OutOfBoundsException](http://stackoverflow.com/questions/8193798/outofrangeexception-vs-outofboundsexception)