SPLの例外の意味をphp-srcから探る

PHPのSPL系組み込み例外は全部で13個あるんですが、正直、使い分けがよく分かりませんでした。公式ヘルプを見ても、わかったような、わからんような。。

(全部うしろに`Exception`って付くので省略してます)

* Logic系
    * BadFunctionCall
        * BadMethodCall
    * Domain
    * InvalidArgument
    * Length
    * OutOfRange
* Runtime系
    * OutOfBounds
    * Overflow
    * Underflow
    * Range
    * UnexpectedValue

名前の似ているOutOfRangeとOutOfBoundsが全然違うって辺りが罠。

どういうときに使われるのかをPHP本体のソースコードからgrepしてみます。
そもそも、SPLというオブジェクト指向のライブラリを作る上で導入されたのがSPLの例外だったはずだから、本家の使い分けが一番信用に足るはず。。

ソースコードはPHP7.1を使っています。

## Logic

> プログラムのロジック内でのエラーを表す例外です。 この類の例外が出た場合は、自分が書いたコードを修正すべきです。 

「てめーの使い方がおかしーんだよ」という意味合い。普通発生しない、発生したらコードを直すはずなので、catchしないのがお約束。
SPL内でも直接throwされていることがある。

http://lxr.php.net/search?q=&defs=&refs=spl_ce_LogicException&path=&hist=&type=&project=PHP-7.1

```
"%s::getIterator() must return an object that implements Traversable"
"Can't seek file %s to negative line "
"Can't truncate file %s"
"Cannot use SplFileObject with directories"
"Class to downcast to not found or not base class or does not implement Traversable"
"Function '%s' not %s (%s)", ZSTR_VAL(func_name), alfi.func_ptr ? "callable" : "found"
"Function spl_autoload_call() cannot be registered"
"Illegal value passed (%s)"
"Passed array does not specify %s %smethod (%s)", alfi.func_ptr ? "a callable" : "an existing", !obj_ptr ? "static " : ""
"Passed array specifies a non static method but no object (%s)"
"Unable to unregister invalid function (%s)"
```

以下の例外には当てはまらないのだけど、使い方がおかしいことを伝えるために使う。
「`parent::__construct()`を呼び忘れてるせいで、オブジェクトが変な状態になってるよ」というのは面白い。

"The object is in an invalid state as the parent constructor was not called"

## Logic > BadFunctionCall

> 未定義の関数をコールバックが参照したり、引数を指定しなかったりした場合にスローされる例外です。 

throwしてるケースなかった。BadMethodCallの親クラスとしてしか使われてないっぽい。

http://lxr.php.net/search?q=&defs=&refs=spl_ce_BadFunctionCallException&path=&hist=&type=&project=PHP-7.1

## Logic > BadFunctionCall > BadMethodCall

> 未定義のメソッドをコールバックが参照したり、引数を指定しなかったりした場合にスローされる例外です。 

状態によってメソッドを呼ばれたくないようなケースかな。メソッドを呼ばないように修正するべき。

ext/以下だと、pharが結構throwしている。

http://lxr.php.net/search?q=&defs=&refs=spl_ce_BadMethodCallException&path=&hist=&type=&project=PHP-7.1

```
"Accessing the key of an EmptyIterator"
"Accessing the value of an EmptyIterator"
"Cannot call constructor twice"
"Cannot create a directory in magic \".phar\" directory"
"Iterator %s returned an invalid stream handle"
"Iterator %s returns an SplFileInfo object, so base directory must be specified"
"New name is too long"
```

"Cannot call constructor twice"

「constructorは2回呼べない」これとか面白いね。

## Logic > Domain

> 定義したデータドメインに値が従わないときにスローされる例外です。 

型は合ってるんだけど微妙に変な値だったとき、みたいな感じか。

http://lxr.php.net/search?q=&defs=&refs=spl_ce_DomainException&path=&hist=&type=&project=PHP-7.1

使われてるの一箇所だけだった。

```
"Maximum line length must be greater than or equal zero"
```

`SplFileObject->setMaxLineLen()`にマイナスの数字を渡すと発生。

## Logic > InvalidArgument

> 引数の型が期待する型と一致しなかった場合にスローされる例外です。 

型が間違っていたら投げる。PHP7未満だとscalar型のチェックと併せて使うだろうし、PHP7以降ももう少し複雑な型を使いたい場合は使ってもいいかもしれない（AかつBのinstanceofであるオブジェクト、などは今も記述できない）

SPLとPharが一部発生させている。

http://lxr.php.net/search?q=&defs=&refs=spl_ce_InvalidArgumentException&path=&hist=&type=&project=PHP-7.1

```
"An instance of RecursiveIterator or IteratorAggregate creating it is required"
"Cannot extract to \"%s...\", destination directory is too long for filesystem"
"Flags must contain only one of CALL_TOSTRING, TOSTRING_USE_KEY, TOSTRING_USE_CURRENT, TOSTRING_USE_INNER"
"Illegal mode "
"Info must be NULL, integer or string"
"Key duplication error"
"Passed variable is not an array or object, using empty array instead"
"Sub-Iterator is associated with NULL"
"Unsetting flag CALL_TO_STRING is not possible"
"Unsetting flag TOSTRING_USE_INNER is not possible"
"array must contain only positive integer keys"
"array size cannot be less than zero"
"integer overflow detected"
```

"array size cannot be less than zero"なんかは、DomainExceptionの例と同じですね。。使い分けがしっかりしてないみたい。


## Logic > Length

> 長さが無効な場合にスローされる例外です。

使われてるところ見つからなかった。。何に使えばいいんだ。。

http://lxr.php.net/search?q=&defs=&refs=spl_ce_LengthException&path=&hist=&type=&project=PHP-7.1

## Logic > OutOfRange

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

\ArrayAccess的なクラスを想定してて、コンパイル時判定できるような無効インデックスが指定されたら投げる。例えば型間違いとかかなぁ。

intlとSplDoublyLinkedListなどなど。

http://lxr.php.net/search?q=&defs=&refs=spl_ce_OutOfRangeException&path=&hist=&type=&project=PHP-7.1

```
"Offset invalid or out of range"
"Offset invalid"
"Offset out of range"
"Parameter count must either be -1 or a value greater than or equal 0"
"Parameter max_depth must be >= -1"
"Parameter offset must be >= 0"
"Running past end of ResourceBundle"
"Use RecursiveTreeIterator::PREFIX_* constant"
```

と思ったのだが、Offsetが範囲より大きい場合、なんていうのはコンパイル時検出なんてできるのかコレ…？？

## Runtime

> 実行時にだけ発生するようなエラーの際にスローされます。 

よくよく考えると不思議な説明文だ…。例外とは実行時に発生するものに決まっているだろうに。
ちなみに `PDOException` や `mysqli_sql_exception` は `RuntimeException` の子クラス。

実行時に起こりうる = キャッチして対処するべき？
ただ、見てる感じだと「それはバグだろ…」感のあるものも多い。

直接投げているところは結構ある。

http://lxr.php.net/search?q=&defs=&refs=spl_ce_RuntimeException&path=&hist=&type=&project=PHP-7.1

```
"An iterator cannot be used with foreach by reference"
"Called current() with non valid sub iterator"
"Called key() with non valid sub iterator"
"Can't extract from an empty heap"
"Can't peek at an empty datastructure"
"Can't peek at an empty heap"
"Can't pop from an empty datastructure"
"Can't shift from an empty datastructure"
"Cannot create SplFileInfo for empty path"
"Cannot open file '%s'", intern->file_name_len ? intern->file_name : ""
"Cannot read from file %s"
"Cannot rewind file %s"
"Could not open file"
"Directory name must not be empty."
"Failed to call sub iterator method"
"Hash needs to be a string"
"Heap is corrupted, heap properties are no longer ensured."
"Index invalid or out of range"
"Internal error, function '%s' not found. Please report"
"Iterators' LIFO/FIFO modes for SplStack/SplQueue objects are frozen"
"Object not initialized"
"Operation not supported"
"Unable to read link %s, error: %s"
"phar error: unable to open file \"%s\" to add to phar archive"
"phar error: unable to open file \"%s\" to add to phar archive, open_basedir restrictions prevent this"
```

file系は何となく納得感あるかな。

## Runtime > OutOfBounds

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

型は合ってたけどデータ的にはみ出てるケースかな。
iterator系でいくつかある。

http://lxr.php.net/search?q=&defs=&refs=spl_ce_OutOfBoundsException&path=&hist=&type=&project=PHP-7.1

```
"Cannot seek to " ZEND_LONG_FMT " which is behind offset " ZEND_LONG_FMT " plus count "
"Cannot seek to " ZEND_LONG_FMT " which is below the offset "
"Seek position " ZEND_LONG_FMT " is out of range"
```

## Runtime > Overflow

> いっぱいになっているコンテナに要素を追加した場合にスローされる例外です。 

**使われてない！！**

http://lxr.php.net/search?q=&defs=&refs=spl_ce_OverflowException&path=&hist=&type=&project=PHP-7.1

コンテナに上限設定することってあるのか？

## Runtime > Underflow

> 空のコンテナ上で無効な操作 (要素の削除など) を試みたときにスローされる例外です。 

**使われてない！！**

http://lxr.php.net/search?q=&defs=&refs=spl_ce_UnderflowException&path=&hist=&type=&project=PHP-7.1

これは、、バグのような気もするけど、要素を含むかどうかは実行時しか分からないので、Runtime系であるべき、ってことかな？

## Runtime > Range

> プログラムの実行時に範囲エラーが発生したことを示すときにスローされる例外です。 通常は、アンダーフローやオーバーフロー以外の計算エラーが発生したことを意味します。 これは、実行時版の DomainException です。

**使われてない!**
 
http://lxr.php.net/search?q=&defs=&refs=spl_ce_RangeException&path=&hist=&type=&project=PHP-7.1

何だろう、アンダーフローやオーバーフローに似ているけど、それ以外のケース… 値域に歯抜けがあって、そこを指定された場合とかか？

## Runtime > UnexpectedValue

> いくつかの値のセットに一致しない値であった際にスローされる例外です。 これが発生する例としては、ある関数が別の関数をコールしていて、 その関数の返り値が特定の型や値である (そして計算やバッファ関連のエラーがない) ことを想定している場合があります。 

関数の中で関数を呼んだところ、結果が変。そんなときに使うという感じか。

```php
<?php
function hoge(Foo $foo) {
    $resp = $foo->doSomething(); //ドキュメントによると、intを返すはず
    if (!is_int($resp)) {
       throw new \UnexpectedValueException;
    }
//...
}
```

普段はいちいちチェックしないだろうけど、こんな風にcallableを渡してもらい中で実行するなら、出自が胡散臭いのでちゃんとチェックしそう。

これも返り値型宣言によってある程度減らせるような気がする。

pharでいっぱい出てきた。

http://lxr.php.net/search?q=&defs=&refs=spl_ce_UnexpectedValueException&path=&hist=&type=&project=PHP-7.1

```
"%s"
"Could not resolve file path"
"Error at offset " ZEND_LONG_FMT " of %zd bytes"
"Error at offset %zd of %zd bytes"
"Iterator %s returned a file that could not be opened \"%s\""
"Iterator %s returned a path \"%s\" that is not in the base directory \"%s\""
"Iterator %s returned a path \"%s\" that open_basedir prevents opening"
"Iterator %s returned an invalid key (must return a string)"
"Iterator %s returned an invalid value (must return a string)"
"Iterator %s returned no value"
"Iterator %v returned an invalid key (too long)"
"Object not found"
"Objects returned by RecursiveIterator::getChildren() must implement RecursiveIterator"
"phar error: unable to open phar \"%s\""
"phar error: unable to read stub of phar \"%s\" (cannot create %s filter)"
```


## よく分からなくなってきた。。

PHPの言語仕様的には、チェック例外というものは存在しないです。
Throwableなクラスであれば何でもthrowできます。発生しうる例外はキャッチしてもいいし投げ抜けても問題ありません。

で、Exceptionの下の例外に何か明確な使用ルールがあるかというと、言うほど明確なルールはなさそうに見えます。

もっと致命的なもの、メモリが足りないとかはError系で飛んで行くので、RuntimeExceptionをcatchしてれば大丈夫というわけでもなく、うーん。
PHP-FIGとかでこの辺決まってもいいんだろうけど、どうせ守る人いないんだろうな…

結論はない、です