モヒカンは正義

渋谷で働く怪しいWebエンジニアの生きた証と備忘録

PHPでマジックメソッド __toString 内で例外を投げるとfatalになる #PHPの不思議な世界

f:id:pinkumohikan:20170611154720p:plain

はじめに

PHPは素敵な言語です。

※ 「素敵」という表現は主観的なものであり、何を素敵と思うかは個々の自由である。そのため異論は一切認めない。

この #PHPの不思議な世界 という謎のタグの付いている記事は、職業プログラマーとしてPHPコードを書いていて「ふぇー」と思った仕様や、「ふーん」と思った仕様についてダラダラとコードと感想を書いていくコーナーです。 PHPをdisる意図は決してなく、日頃ご飯を食べさせて頂いているPHP様には足を向けて寝ることはできません。

お題

マジックメソッド __toString 内で例外を投げるとfatalになる

前説

PHPには、マジックメソッドと呼ばれる仕組みが存在します。

特定の条件が満たされた時に、明示的にcallしなくても勝手に規定のメソッドがcallされます。 メソッド名の先頭がアンダースコア二つ __ で始まっているのが特徴です。

PHP: マジックメソッド - Manual

解説

マジックメソッド toString は、文字列化メソッドです。 文字列が期待されている関数やメソッドに対してオブジェクトを渡した際に、 toString が呼ばれます。

スキームとドメイン、パスしかないシンプルなURLクラスを考えます。

class Url
{
    private $scheme;
    private $domain;
    private $path;

    public function __construct($scheme, $domain, $path)
    {
        $this->scheme = $scheme;
        $this->domain = $domain;
        $this->path = $path;
    }

    public function __toString()
    {
        return sprintf(
            '%s://%s/%s',
            $this->scheme,
            $this->domain,
            $this->path
        );
    }
}

上記クラスをnewしてインスタンスを生成し、引数として文字列を期待する関数に渡してあげると、 __toString メソッドで定義した方法で文字列化して出力されます。

$ php -a
Interactive shell

php > print PHP_VERSION;
5.6.29
php >
php > require_once 'Url.php';
php > $url = new Url('https', 'pinkumohikan.com', 'feed');
php > print $url;
https://pinkumohikan.com/feed

本題

一言で言うと表題の通りで、「マジックメソッド __toString 内で例外を投げると、fatal errorになります」。

※ __toString はインスタンスが保持しているデータを文字列化する方法を定義するためのマジックメソッドなので、例外を投げたくなることは普通はありません。というかそういう設計をしてはいけません。振りではありません。

試しに、先ほどのUrlクラスの __toString メソッドを、仕事をせずに例外を投げるだけのゴミみたいな実装に書き換えたFreedomUrlクラスを用意します。

class FreedomUrl
{
    private $scheme;
    private $domain;
    private $path;

    public function __construct($scheme, $domain, $path)
    {
        $this->scheme = $scheme;
        $this->domain = $domain;
        $this->path = $path;
    }

    public function __toString()
    {
        throw new \RuntimeException('I am freedom!!!!');
    }
}

実行してみます。

$ php -a
Interactive shell

php > print PHP_VERSION;
5.6.29
php >
php > require_once 'FreedomUrl.php';
php > $url = new FreedomUrl('https', 'pinkumohikan.com', 'feed');
php > print $url;
PHP Fatal error:  Method FreedomUrl::__toString() must not throw an exception in php shell code on line 0
PHP Stack trace:
PHP   1. {main}() php shell code:0

Method FreedomUrl::__toString() must not throw an exception と、非常にお怒りです。

上位でtry ~ catch構文で拾おうとしてもダメです。 see __toString()でExceptionが発生すると死ぬ - Qiita

このことはPHPリファレンスの __toString メソッドの説明 にも、「絶対例外投げんなよ!絶対だぞ!」と書かれています。

警告 __toString() メソッド内から例外を投げることはできません。そうした場合、致命的なエラーが発生します。

こういうエラーを見たら、おそらく __toString メソッドが「オブジェクトが持つデータの文字列化」以上の責務を負っていると思われるので、是非設計を見直してあげて下さい。