🤨

call_user_func() と $function() の動きが違った

2023/05/17に公開

PHPにおいてメタプログラミング的にメソッドを動的に呼び出す方法は何種類が用意されています。

これらは、呼び出し方は違えども、基本的にどれも関数を実行するにあたって、同じ動きをするものだと思っていました。

まずは以下の例をご覧ください。

<?php

class ParentClass
{
    public static function whoAmI(): void
    {
        var_dump(static::class);
    }
}

class ChildClass extends ParentClass
{
    public function byCallUserFunc(): void
    {
        call_user_func([\ParentClass::class, "whoAmI"]);
    }

    public function byVariableFunc(): void
    {
        [\ParentClass::class, "whoAmI"]();
    }
}
  • ParentClass を継承した ChildClass がいます
  • ParentClass には static method として whoAmI() があり static::class を出力します

このとき、直感的に得られる出力結果としては以下のようなものかと思います。

ParentClass::whoAmI(); // "ParentClass"
call_user_func([ParentClass::class, 'whoAmI']); // "ParentClass"

$childClass = new ChildClass();
$childClass->byCallUserFunc(); // "ParentClass"
$childClass->byVariableFunc(); // "ParentClass"

どれを実行したとしても ParentClass::whoAmI() を呼び出していることに変わりはないので、結果として "ParentClass" が返ってくることを想像します。

ところが実際には $childClass->byCallUserFunc() の時だけは結果が "ChildClass" になります。

$childClass->byCallUserFunc(); // "ChildClass" 
// 実際はこれだけChildClassが返ってくる

3v4lにコードを書いてますのでそちらで動きを確かめてみてください。
https://3v4l.org/eeh7r#v8.2.6

繰り返しになりますが ChildClass::byCallUserFunc()call_user_func([\ParentClass::class, "whoAmI"]) のように ParentClasswhoAmI というように指定しているにも関わらず static::class の結果が "ChildClass"
になってしまいます。

結論としては、以下の条件を満たす場合に、 static:: が指し示すクラスが子クラスに移動します。

  • インスタンスメソッドから親のstaticメソッドを呼び出していること
  • その際に call_user_func() を使って呼び出していること

この動きがバグのように思えたので php-src にissueを投げたところ、明確な仕様であるとまでは名言されませんでしたが、そういうものである、といった感じの回答をいただきました。

When calling a parent class's static method using call_user_func(), static::class return the child class #11249
https://github.com/php/php-src/issues/11249

得られた返答

call_user_func uses object context when called inside an object context, it works like $this::whoAmI().
call_user_func がオブジェクト内で実行された場合、オブジェクトコンテキストをもち $this::whoAmI() のように振る舞います。

Using callable expression is likely better alternative (e.g. [\ParentClass::class, "whoAmI"](); ), because it does not preserve object context.
[\ParentClass::class, "whoAmI"]() のような "式" の場合は、オブジェクトコンテキストを保持しないので、そちらを利用するとよいでしょう。

PHPのクラスにおいての object context を正確に理解できてないので何となくにはなりますが、 call_user_funcvariable function(式) とは違って、インスタンスの場合はそれに自動的に束縛されてしまう動きをしてしまうようです。

なので、書いてもらってるように式を使ったほうが直感的には違和感が少なそうなので、特に使い分けを考えずに variable function(式) を使っていったほうが良さそうというお話でした。

TANOMU

Discussion