LoginSignup
7
4

More than 3 years have passed since last update.

【PHP7.4】__toString()が例外を吐けるようになる

Last updated at Posted at 2019-05-27

プロパティの設定を必須にしたかったとしましょう。

class HOGE{
    public $var;
    public function __toString(){
        if(!$this->var){
            throw new \Exception('$var must set.');
        }
        return sprintf('$var is %1$s', $this->var);
    }
}

try{
    echo new HOGE();
}catch(\Exception $e){
    var_dump($e);
}

何の変哲も無いように見えるコードですが、これ動きません。
Fatal error: Method HOGE::__toString() must not throw an exception, caught ExceptionというFatalエラーを吐いて死にます。

実は__toString()メソッド内では例外を出すことができないのです。
他のあらゆるマジックメソッドにはこんな制限はないのに、__toString()だけそういうことになっています。
よくわかりませんね。

しかしまあ、そんなよくわからない制限なら要らないよね、ということでAllow throwing exceptions from __toStringのRFCが提出されました。

PHP RFC: Allow throwing exceptions from __toString()

Introduction

__toString()からの例外スローは禁止されており、致命的エラーになります。
これは__toString()から任意のコードを呼び出すことを困難にし、一般的なAPIとして使用することが難しくなります。
このRFCは、この制限を取り除くことを目的としています。

現在のような動作になっている理由は、PHPエンジンと標準ライブラリのあらゆるところで文字列変換が行われており、そして全ての箇所が例外を正しく処理できるようになっているわけではないからです。

技術的観点からすると、この制限は無駄でしかありません。
なぜならば、文字列変換中の例外は、回復可能なエラーを例外に変換するエラーハンドラによって引き起こされる可能性があるからです。

set_error_handler(function() {
    throw new Exception();
});

try {
    (string) new stdClass;
} catch (Exception $e) {
    echo "(string) threw an exception...\n";
}

実際、Symfonyは現在の制限を回避するためにこの抜け穴を使用しています。
残念ながら、これはPHP8で削除予定の$errcontextパラメータに依存しています。

Proposal

__toString()からの例外スローを許可します。
もう致命的なエラーは発生しません。

さらにPHP7のエラーポリシーに基づいて、could not be converted to stringおよび__toString() must return a string valueというrecoverable fatal errorを、適切なError exceptionに変更します。

Extension Guidelines

エクステンション開発者は、文字列変換からの例外を適切に処理するため、以下のガイドラインを考慮に入れてください。

zval_get_string()convert_to_string()および類似の関数は、例外を生成した場合でも文字列を生成します。この文字列を含めてください。必ずしも従う必要はありませんが、そうすることができます。
・文字列変換がエラーハンドラによって例外になった場合の結果は、オブジェクトから文字列への変換の場合は空文字列、配列の場合は"Array"という文字列になります。これは以前と同じ動作です。
・通常は(EG(exception))で例外がスローされたかどうかを確認すれば十分です。

zend_string *str = zval_get_string(val);
if (EG(exception)) {
    // リソース解放など
    return;
}

・失敗する可能性のある操作を行うヘルパーAPIが多数追加されます。

// zval_get_string()に似ているが、変換に失敗したらNULLを返す
zend_string *str = zval_try_get_string(val);
if (!str) {
    // リソース解放とか
    return;
}
// Main code.
zend_string_release(str);

// zval_get_tmp_string()に似ているが、変換に失敗したらNULLを返す
zend_string *tmp, *str = zval_try_get_tmp_string(val, &tmp);
if (!str) {
    // リソース解放とか
    return;
}
// Main code.
zend_tmp_string_release(tmp);

// convert_to_string()に似ているが、変換に成功したか否かのbooleanを返す
if (!try_convert_to_string(val)) {
    // リソース解放とか
    return;
}
// Main code.

try_convert_to_string()は、変換失敗時には元の値を返しません。そのため、これを使った方が、convert_to_string()と例外チェックを使うより安全です。
・あらゆる文字列変換操作にチェックを行うことができますが、チェックを省略しても通常は余剰な警告が発生するだけです。気をつけて実装しなければならないのは、主にデータベースのような、永続的な構造を変更する操作です。

Backward Incompatible Changes

recoverable fatal errorからError exceptionへの変更は、破壊的変更です。

Vote

投票開始は2019/05/22、終了が2019/06/05、投票数の2/3+1の賛成票で受理されます。

2019/05/27現在、賛成32反対0で、ほぼ確実に導入決定です。

感想

これによって、あらゆるメソッドから例外を出すことができるようになりました。

さて、なんでこんな簡単そうな変更が今まで残っていたの?
かというと、変更が簡単ではないからです。

最初に報告があったのはなんと2011年1月であり、2012年にはあのNikitaが実装が大変なんじゃよーと言っています。
しかし結局全てのcommitがNikita自身の手によって行われました。
最終的な修正は111ファイル1000行以上に及びます。
どんだけ働いてんだこの人

PHPフォーラムは基本的に保守的な人が多く、破壊的な変更はあまり受理されない傾向にあるのですが、Nikitaについては既に、Nikitaが言うなら仕方ないというレベルに達しているようです。
三項演算子のDeprecateとか、他の人が提案してたら絶対通らなかったよね。

しかしこれ、てっきりuninitialized__toString()したときのために作ったものなのかと思ったのですが、特に関係なかったみたいですね。

そして修正規模のわりに、あまり重要な使いどころが思いつかないというか、そもそも個人的には__toString()自体まず使うことがないですね。
デバッグにはvar_dump()という超絶便利関数がありますし、文字列化したいなら適当にメソッド生やします。

7
4
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
4