Stringableインターフェースで「文字列っぽいオブジェクト」を型で表現する

Stringableとは、__toStringメソッドを持つクラスに自動的に付与されるインターフェースで、文字列と文字列化可能なオブジェクトを同じ型として受け取れるようにする仕組みです。

__toStringを実装したオブジェクトをechoするだけなら昔からできましたが、「文字列か、それとも文字列を返せるオブジェクトか、どちらでも受け取りたい」という関数の引数を型で表現する手段は長らくありませんでした。PHP 8.0で入ったStringableは、その隙間を埋めるための地味だけど便利なインターフェースです。

__toStringさえあれば勝手にStringableになる

ここが一番の特徴だと思います。明示的に implements Stringable と書かなくても、__toString() を定義したクラスにはPHPエンジンが自動的にStringableを実装済みとして扱ってくれます。

class Money {
    public function __construct(private int $cents) {}
    public function __toString(): string {
        return number_format($this->cents / 100, 2);
    }
}

$m = new Money(12345);
var_dump($m instanceof Stringable); // bool(true)
echo $m; // 123.45

implements Stringable を書かなくても instanceof が true になるのを初めて見たときは少し驚きました。ただ、後から読む人のためにも明示的に implements Stringable と書いておくのがマナーだと思っています。書いても書かなくても挙動は変わりません。

string|Stringableで受け口を広げるにはどう書く?

Stringableの本来の使いどころは、関数やメソッドの引数を「文字列そのもの」と「文字列を返せるオブジェクト」の両方に開放したいときです。union型で string|Stringable と書けば、素の文字列も __toString を持つオブジェクトも同じ引数で受けられます。

function toUpper(string|Stringable $value): string {
    return mb_strtoupper((string) $value);
}

echo toUpper('hello');   // HELLO
echo toUpper(new Money(500)); // 5.00 (数字だけなのでそのまま大文字化はされません)

ポイントは (string) にキャストしてから使っている部分です。string|Stringable のままだとmb_strtoupperのようなstring専用関数にそのまま渡せないので、関数内部で明示的に文字列へキャストする一手間が要りますね。

ライブラリ側の型宣言はどう変わるか

Stringableが入る前は、こういう関数の引数はただ string とだけ宣言しておき、呼び出し側が文字列を渡す前提にするか、mixed で受けて実行時にチェックするしかありませんでした。Stringableのおかげで「文字列か、文字列として振る舞えるオブジェクトか」を静的解析の段階で表現できるようになったのは地味に大きな変化だと思います。

interface Loggable {
    public function log(string|Stringable $message): void;
}

PHP本体の関数がこの型を積極的に使っているわけではなく、あくまで自分たちのコードやライブラリのAPI設計で使う道具、という位置づけです。

まとめ

Stringableは、__toStringを持つクラスに勝手に付いてくるインターフェースで、明示的に書いても書かなくても動作は同じです。実務的な使いどころは、自作の関数やメソッドの引数を string|Stringable にして、文字列とオブジェクトの両方を型レベルで受け入れられるようにすることだと思います。地味な仕組みですが、型宣言の表現力が一段広がる話なので、覚えておいて損はないという気がしています。

よくある質問

Q. implements Stringable を書き忘れても大丈夫ですか?
A. __toStringさえ定義されていればPHPエンジンが自動的にStringableとして扱うので、instanceof Stringableはtrueになります。ただし可読性のために明示的に書くのが推奨されます。

Q. string|Stringableで受け取った値をstring専用関数にそのまま渡せますか?
A. 渡せません。mb_strtoupperなどstring型のみを受け付ける関数に渡す前に、(string)でキャストする必要があります。

Q. Stringableはどのバージョンから使えますか?
A. PHP 8.0で追加されたインターフェースです。それより前のバージョンでは使えません。

類似投稿

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です