PHP7調査(23)致命的エラーが例外としてキャッチできるようになった

  • 76
    Like
  • 2
    Comment
More than 1 year has passed since last update.

(2015/07/02追記:PHP 7.0.0alpha2に合わせて修正しました)

PHPのエラーは、エラー文言を表示するだけの警告・注意と、その場で処理を終了してしまう致命的エラー(fatal error)の2種類に大別できます。

ところで、PHP5の致命的エラーには不便な点があります。それは、set_error_handler()やその他の方法でエラーハンドリングできず、必ず終了してしまう点です。これでは致命的エラーをユニットテストするのも不便ですし、ReactPHPのようにサーバ動作させるようなプログラムも安心して使えません。

PHP7では致命的エラーが例外として実現されるようになり、エラーハンドリングの自由度が格段に上がりました。この変更の概要を紹介します。

例外のクラス階層の変更

まず、PHP7で例外クラスの階層がどう変わるかを説明します。PHP5までの例外のクラス階層は次のようなものでした。

  • Exception
    • ErrorException
    • その他SPL例外

一方、PHP7からは次のようにThrowableインターフェースが実装され、ExceptionErrorThrowableを実装する形になりました。また、Errorの子供として TypeError, ParseErrorの2つが新設されています。

致命的エラーが例外になる仕組み

PHP7では、致命的エラーの多くがErrorをスローするように実装が変わりました。Errorがトップレベルまで処理されなかった場合は、従来と同じ致命的エラーが表示されてPHPの処理が終了します。

このErrorExceptionの子供ではないため、PHP5向けコードのcatch (Exception $e)ではキャッチされません。つまり、過去のコードをPHP7で動かした場合はPHP5と同じ動作をするように設計されています。

<?php
$foo = null;
try {
    $foo->bar(); // 例外スロー、次のキャッチブロックに入る
} catch (Error $e) {
    var_dump($e->getMessage());
}
echo "baz\n";
$foo->bar(); // 例外キャッチしていないため致命的エラーになる
$ php7 /tmp/foo.php
string(39) "Call to a member function bar() on null"
baz
PHP Fatal error:  Uncaught Error: Call to a member function bar() on null in /tmp/foo.php:9

文法エラーも例外になった

PHP5までは文法エラーもキャッチ不可能なエラーでしたが、PHP7ではParseExceptionをスローするようになりました。これを利用して、PHP7からはinclude()eval()で発生した文法エラーをキャッチできます。

<?php
try {
    include("bar.php");
} catch (ParseError $e) {
    var_dump($e->getMessage());
}
echo "baz\n"; // 致命的エラーより後まで処理が進む
$ php7 /tmp/foo.php
string(36) "syntax error, unexpected end of file"
baz

ただし、トップレベルのスクリプト自身の文法エラーはキャッチできません。

注意点1

残念ながら、全ての致命的エラーが例外になったわけではありません。

  • スタートアップ処理でのエラーやメモリ不足など、真の致命的エラー
  • 本来なら例外にすべきなんだけど、実装上の都合で例外に再実装するのがつらくて残っている致命的エラー

上記のような2種類の致命的エラーがまだ残っている状況です。PHPらしいといえばらしい気がしますが、ちょっと悲しいですね。

注意点2

警告・注意レベルのエラーはPHP7でも変わっていません。あらゆるエラーを統一的に扱いたい場合は、set_error_handler()を使ってエラーハンドラで例外スローして、catch (Throwable $e)でキャッチしましょう。

参照

本件、興味がある人はRFCを読んだ方がいいと思います。