Help us understand the problem. What is going on with this article?

PhpStorm の throws のチェックがうるさい

More than 1 year has passed since last update.

PhpStorm は、呼び出したメソッドの phpdoc の @throws をヒントに try-catch の不足などをインスペクションすることができます。が、これが現状の PHP コミュニティの例外の意味付けのブレ (PHPの例外はすべて非検査例外 v.s. RuntimeException以外は検査例外) に合わなくて、いろいろなライブラリを使うとすぐに黄色くなってしまいます。

また、「すべての Throwable 発生に割り込んで何かしておきたい」というような場合も、

catch (\Throwable $e) {
    // なにかやる
    throw $e;
}

とすると、メソッドの doc コメントに @throws が足りないよという警告が出て、それに従ってしまうとこんど、コール側に Throws の catch が必要だよという、わけのわからない警告になってしまいます。

そんなこんなでインスペクションを切ってしまうと、こんどは、ビジネスロジック上の例外を捕捉必須のつもりで投げてるところで、捕捉漏れが起きることに...

というわけで、ひとまず大真面目に例外のハンドリングをどう書けばいいか考えました。

PHP >= 7.1 です。

/**
 * @throws RecoverbleDbException
 */
function doSometiong()
{
    $db->beginTransaction(); // RecoverbleDbException が起きるかもしれない

    try {

        // ...

    } catch (DuplicatedOperationException $e) {  // ... 1
        // 自己解決できる例外は中でしれっと処理
        $db->rollback();
        return;
    } catch (RecoverbleDbException $e) {  // ... 2
        $db->rollback();
        // 検査例外はコール側にキャッチさせるべき例外なのでそのまま投げて @throws に明記。
        throw $e;
    } catch (\RuntimeException | \LogicException | \Error $e) {  // ... 3
        $db->rollback();
        // 非検査例外は想定外で予測不能、もれなくキャッチさせるのは不可能なので
        // @throws に書かれている必要はない。
        throw $e;
    } catch (\Exception | \Throwable $e) {  // ... 4
        $db->rollback();
        // もしここに漏れてくるなら、それは検査例外の捕捉漏れか、現在の PHP
        // にない第三の Throwable ということになる。
        // 検査例外が漏れてくるということは実装に捕捉漏れがある証拠、あるいは
        // 仕様をフォローできていない PHP メジャーバージョンに上げたということ。
        // どちらも、LogicException で実装ミスを報告する。
        throw new \LogicException("Unhandled Exception or Throwable:" . get_class($e), 0, $e);
    }
}
  1. 既知の例外のうち自己解決するもの
  2. 既知の検査例外を呼び出し側に任せたい場合
  3. 非検査例外
  4. 未知の検査例外の検出

1 と 2 は問題ないですね。2 の方だけが @throws に登場し、こいつは呼び出し側に処理する責任がある。@throws にあるのは「呼び出ロジックが絶対にキャッチしなければならない例外」です。API インターフェース仕様の一部って扱いですね。

3 は非検査例外です。これは throw $e やっていても phpdoc には何も書かないのが普通です。PhpStorm は書いても無視します。2 と 3 は PHP 7.1 なら | で合体させてもいいですね。

さて問題は 4 です。「既知の検査例外」と「すべての非検査例外」をキャッチしたあと、漏れてくる Exception とは何でしょう? そうですね、あってはいけないはずのフローです。検査例外は @throws によって呼び出し側に何らかのハンドリングが必須だとしているのですから、思ってもいない例外が漏れてくるはずがありません。なので、プログラムが正常なら起きないはずの LogicException に例外を翻訳するべきです。(PHP に UnexpectedExceptionException みたいなのがあれば良かったんですが、適当なのがないので)

... でも

これ、コンパイラが絶対にリリースを許さないとかなけでば、こういう連鎖って実質無理じゃね? というのが現実でして...えぇ

正直めんどくさい。めんどくさいものは流行らない。

というわけで、PhpStorm のインスペクション機能をオフにせずに、都合よく警告を抑制したいというレベルまで意識を下げると、人知れずこっそりこういう...

/**
 * @throws RecoverbleDbException
 */
function doSometiong()
{
    try {

        // ...

    } catch (RecoverbleDbException | \Throwable $e) {
        $db->rollback();
        (function () use ($e) { throw $e; })();
    }
}

クロージャーは静的な呼び出し階層解析に含まれないので、これで一応黄色いのはなくなります。throw が発生するフローが静的に辿れなくても、べつに @thorws に「出るからね」というインターフェースを宣言するのは間違いじゃないですし。

ま、PhpStorm があとちょっとこうなったらな、なんですよ。例外捕捉をチェックしないクラスの設定、こういうふうにカスタマイズできるようになったらなとずっと思っているんだけど、2018.1 でもだめでした。

  • Error 型とその派生クラス
  • LogicException 型とその派生クラス
  • RuntimeException 型とその派生クラス
  • Throwable 型 (実装を含まない) ← これと
  • Exception 型 (派生クラスを含まない) ← これ追加

まあダメですね... もし Exception で捕まえて throw したら、握りつぶしたかに見えながら、実際は何が出てくるかわからない怖いメソッドになりますね。うーん... まあもともと PHP がそういう世界観だからねといえばそうかもしれないけど。

結論はありません。PSRがんばれ

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away