😀

PHP8の組み込み例外まとめ

2022/08/03に公開

きっかけ

PHP7時代から徐々に、PHP8が使われ始めて個人的にも知りたかったので、まとめてみました。
細かい用途を詳しく知りたかったため、それ以外はおまけです。大まかな分類を知りたい方はmpywさんのこの記事を参照したほうが良いと思います。

まずは、Throwableなクラスがどのような継承関係にあるのか以下にまとめてみました。
※インデントは継承関係
※(新)マークは7.2と比べて新しく追加されたもの。
cf.)https://www.php.net/manual/ja/class.error

Error
   ArithmeticError
      DivisionByZeroError
   AssertionError
   CompileError(新)
      ParseError
   FiberError(新)
   TypeError
      ArgumentCountError
   UnhandledMatchError(新)
   ValueError(新)
Exception
   ClosedGeneratorException
   DOMException
   ErrorException
   IntlException
   JsonException(新)
   LogicException
      BadFunctionCallException
         BadMethodCallException
      DomainException
      InvalidArgumentException
      LengthException
      OutOfRangeException
   PharException
   ReflectionException
   RuntimeException
      OutOfBoundsException
      OverflowException
      PDOException
      RangeException
      UnderflowException
      UnexpectedValueException
   SodiumException

LogicExceptionとRuntimeExceptionの違い

例外を扱う際に最大の問題が、このLogicExceptionとRuntimeExceptionの違いだと思います。
まず、PHPのドキュメントにおける定義に関して見ていきましょう。

  • LogicException

プログラムのロジック内でのエラーを表す例外です。 この類の例外が出た場合は、自分が書いたコードを修正すべきです。
(英原文:Exception that represents error in the program logic. This kind of exception should lead directly to a fix in your code.)

  • RuntimeException

実行時にだけ発生するようなエラーの際にスローされます。
(英原文:Exception thrown if an error which can only be found on runtime occurs.)

この文から読み取れることとして
LogicExceptionはプログラムを組む際に起きる例外として、定義されます。
つまり、アプリケーションとして体をなさないような内部処理を行うだけのライブラリ(DBなどの接続を取り持つようなものは除くという意味)では、LogicExceptionをthrowするのが推奨されています。

それに対して、RuntimeExceptionではエンドユーザから送られてくるものが異常だったり、外部システム側の通信障害など、開発側では対応することのできない例外が発生したときに、throwするものです。
特にPDOExceptionがRuntimeExceptionに所属していることが、わかりやすいでしょう。

イメージとして、LogicExceptionはクリーンアーキテクチャの文脈において、より内側の方で発生する例外、RuntimeExceptionはより外側で発生する例外。となります。

つまり、LogicExceptionを捕捉して、RuntimeExceptionとして投げ直すことはあっても、
RuntimeExceptionを捕捉して、LogicExceptionを投げ直すことはありえません。

これを念頭においた上で、各例外の詳細を見てみましょう。

各例外の詳細

Throwable

全ての例外はこのInterfaceを実装しています。
また、このInterfaceを継承していないオブジェクトをthrowすることはできません。

Exception

全てのユーザランドコードから投げられる例外はこれを継承したものをthrowするのを推奨しています。

Exceptionを直接継承している例外

  • ClosedGeneratorException
  • DOMException
  • JsonException
  • IntlException
  • PharException
  • ReflectionException
  • SodiumException

これらの例外は各ライブラリからthrowされる例外で、基本的にユーザがこれらをthrowすることはないです。(多分)

ErrorException

特殊な例外です。
最初にご紹介させていただいたmpywさんの記事を参照してください。

LogicException

BadFunctionCallException

LogicExceptionを親に持ちます。
不正な関数実行をした時に、throwする例外です。
類似の例外はRuntimeException側にはありません。

BadMethodCallException

BadfunctionCallExceptionを親に持ちます。
不正な関数実行が行われたうち、特に対象がクラスのメソッドだった場合にthrowする例外です。
同様に類似の例外はRuntimeException側にありまあせん。

DomainException

LogicExceptionを親に持ちます。
そのうち、定義したデータドメインに値が従わないときthrowされます。
RuntimeException側の類似の例外として、RangeExceptionが挙げられます。
ユーザ入力などによって、引き起こされる場合RangeExeptionを、値を引き回している途中で起こる場合、こちらを使用することになります。

InvalidArgumentException

LogicExceptionを親に持ちます。
引数の型が一致しなかった場合に、throwする例外です。
RuntimeException側の類似の例外として、UnexpectedExceptionが挙げられます。

LengthException
OutOfRangeException

LogicExceptionを親に持ちます。
配列(連想配列を含む)のキーが見つからない場合にthrowされるれ以外です。
RuntimeException側の類似の例外として、OutOfBoundsExceptionが挙げられます。

RuntimeException

RangeException

DomainExceptionのRuntimeException版です。

UnexpectedValueException

InvalidArgumentExceptionのRuntimeException版です。

OutOfBoundsException

OutofRangeExceptionのRuntimeException版です。

OverflowException

予期しない数値のオーバフローが起こった際に起こすべき例外です。
これは、PHP上での実装問題としてではなく、コンピュータ自体のアーキテクチャの要請としての例外のため、RuntimeExceptionを継承しているようです。

UnderflowException

上記のアンダーフロー版です。

PDOException

データベース接続用オブジェクトPDOの起こす例外です。
これは外部システムによって起こる、例外のため、RuntimeExceptionになっています。
(正確には、SQLの間違いなどにおいても起こりますが、基本的に外部要因なので、RuntimeExceptionになっている、と考えれば良いと思います。)

Error

全てのPHP内部エラーを表すクラスです。
これ自体をユーザ側で実装することはありませんが、catchする際は意識する必要があります。

ArithmeticError

関数などの処理で数学的にありえない引数の渡し方、呼び出し方をした際に発生するErrorです。

DivisionByZeroError

その名の通り、0での除算を行った際に発生するErrorです。
親がArithmeticError。

AssertionError

assert関数に失敗したときに発生するError。

CompileError

新設のエラーです。
もともと、ParseErrorとErrorが直接継承関係にありましたが、これが挟まるようになりました。
新設された背景としては、JITやOpCache周りの拡充などがあると考えられます。(勝手な予想)

ParseError

親がErrorからCompileErrorに変更されました。
requireやinclude、evalなどの、コード内でコードを評価するような関数を使用したときに、パースが失敗すると発生します。

FiberError

新設のErrorです。
同様にPHP8で追加された非同期処理用クラスFiberの取り扱いを間違った際に発生します。

TypeError

型が間違った場合に発生します。
発生する条件は下記

  • クラスのプロパティに設定されている値が、プロパティで宣言されている型と一致しない場合。
  • 関数に渡された引数の型が、関数の宣言時に指定された型と一致しない場合。
  • 関数の戻り値の型が、関数の宣言時に指定された型と一致しない場合。
    cf.) https://www.php.net/manual/ja/class.typeerror
ArgumentCountError

関数あるいはメソッドに渡された引数が少なすぎる場合にスローされます。
注) 多い場合は何も起こりません。

UnhandledMatchError

新設のErrorです。
switchと比較してmatch式は必ずいずれかの分岐に入る想定で実装されています。
どの分岐にも一致しなかった場合、このErrorが発生します。

ValueError

PHP8に移行するときの最大の敵です
今までPHPの組み込み関数では、その処理に失敗した際、falseを返すものが多くありました。
しかし、これからはこのErrorが返るようになります。
ValueErrorはユーザランドで新しくthrowすることも可能ですが、基本的にはPHPの組み込み関数からthrowされるべきです。

Discussion