LoginSignup
12
3

More than 1 year has passed since last update.

【PHP8.2】call_user_func()と$callable()の矛盾を解消する

Last updated at Posted at 2021-10-25

PHP8.1もまだリリースされていないというのに、早々とPHP8.2に導入される機能がひとつ決まりました。
導入というか削減ですが。

そんなわけで以下はPHP8.2のRFC、Deprecate partially supported callablesの日本語訳です。

Deprecate partially supported callables

Introduction

このRFCは、call_user_func($callable)でサポートされているが、$callable()ではサポートされていない構文を非推奨とします。

Proposal

以下の構文は、is_callablecall_user_funcでは有効な構文ですが、$callable()には非対応です。

"self::method"
"parent::method"
"static::method"
["self", "method"]
["parent", "method"]
["static", "method"]
["Foo", "Bar::method"]
[new Foo, "Bar::method"]

このRFCでは、これらの構文をPHP8.2で非推奨とし、PHP9で非対応とします。
この構文はcall_user_func()array_map()等では非推奨の警告を発生します。
is_callable()callableは、サポートされなくなるまで非推奨の警告は発生しません。

通常のコールバック構文、functionFoo::method["Foo", "method"][new Foo, "method"]はこのRFCによる影響はありません。

Discussion

このRFCは、コールバックにおける二つの問題を解決しようとしています。

ひとつめは、callableの定義が矛盾している現状を解決することです。
最近はコールバックには$callable()を使うことが推奨されていますが、実際にはcallableであっても$callable()できないものが存在しました。

この矛盾の解決方法はふたつあります。
callableのサポートを廃止するか、$callable()の呼び出しをサポートするか、です。

しかし、ここでふたつめの問題が発生します。
例文では、最後の二つを除いて呼び出しはコンテキストに依存します。
self::methodが参照するメソッドは、何処から呼び出されたかによって変わってしまいます。

このRFCふたつめの目的は、コールバックのコンテキスト依存性を減らすことです。
このRFCが通過すると、あと残っているのはメソッドの可視性だけになります。
すなわち、Foo::barはあるスコープでは可視だけど別のスコープでは不可視になるということです。
将来的にコールバックがpublicメソッドに限定されたりすればこの依存性も解消しますが、このRFCでは可視性については取り扱いません。

Backward Incompatible Changes

ここで非推奨とされるコールバックは、大抵は単純に置き換えることで解決します。
たとえば"self"self::classになります。

"self::method"       -> self::class . "::method"
"parent::method"     -> parent::class . "::method"
"static::method"     -> static::class . "::method"
["self", "method"]   -> [self::class, "method"]
["parent", "method"] -> [parent::class, "method"]
["static", "method"] -> [static::class, "method"]

後者の形式は、コンテキストに依存しません。
コールバックが作成された場所ではなく、呼び出す側のself/parent/staticスコープを参照するようになります。

PHP8.1未満との互換性が必要なければ、ファーストクラスのコールバック構文self::method(...)を使うことも可能です。

[new Foo, "Bar::method"]は単純に置き換えができませんが、ほとんどの人はこんな使い方自体を見たことがないでしょう。

class Bar {
    public function method() {}
}
class Foo extends Bar {
    public function method() {}
}

このコールバック構文の考え方としては、[new Foo, "method"]Bar::method()をオーバーライドしたFoo::method()を呼び出します。
[new Foo, "Bar::method"][new Foo, "parent::method"]は、オーバーライドされる前のBar::method()を呼び出すということです。

Fooスコープ内では["Bar", "method"]、あるいは[parent::class, "method"]と呼び出せばいいです。
これは静的メソッドではなくスコープされたメソッド呼び出しであり、要はparent::method()と同じです。

PHPでは、オーバーライドされたメソッドをスコープの外側から呼び出すための、わかりやすい仕組みはありません。
どうしても必要であればリフレクションやクロージャを使って実現することはできます。

// リフレクション
(new ReflectionMethod("Bar", "method"))->invoke(new Foo);
// クロージャ
Closure::fromCallable([new Bar, "method"])->bindTo(new Foo)();

Vote

投票期間は2021/10/08から2021/10/22で、有権者の2/3の賛成で受理されます。
本RFCは賛成32反対0の全会一致で可決されました。

感想

class Foo{
    public function bar()
    {
        $callable = 'self::hoge';
        is_callable($callable);    // true
        call_user_func($callable); // "Foo::hoge"
        $callable();               // Fatal error: Class 'self' not found
    }

    public function hoge()
    {
        var_dump(__METHOD__);
    }
}

(new Foo())->bar();

is_callableはtrueだしcall_user_funcでは実際に呼ぶこともできるのに、$callable()ではエラーになってしまいます。

そんなわけでcall_user_funcのこのような使い方はPHP8.2でE_DEPRECATEDにし、PHP9ではエラーにします。
is_callableはPHP8.2で呼び出しは可能なのでtrueのままですが、PHP9ではfalseになります。
今後はこのような使い方をしないようにしましょう。

$callable()が動くようにしなかったのは、本文中にもあるとおり、$callable()を書く位置によって動いたり動かなかったりするからということのようです。
静的なcallableを全然使ったことがないのでよくわからないのですが、このような書式が有用な事例ってあったりするんですかね。
むしろ実際に使っている事例ってあるんですかね。

12
3
0

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
12
3