call_user_func() と $function() の動きが違った
PHPにおいてメタプログラミング的にメソッドを動的に呼び出す方法は何種類が用意されています。
- call_user_func()
- call_user_func_array()
-
variable function(式)
$methodName()
$object->$methodName()
Foo::$methodName()
[$object, $methodName]()
[$className, $methodName]()
- など
これらは、呼び出し方は違えども、基本的にどれも関数を実行するにあたって、同じ動きをするものだと思っていました。
まずは以下の例をご覧ください。
<?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にコードを書いてますのでそちらで動きを確かめてみてください。
繰り返しになりますが ChildClass::byCallUserFunc()
は call_user_func([\ParentClass::class, "whoAmI"])
のように ParentClass
の whoAmI
というように指定しているにも関わらず 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
得られた返答
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_func
は variable function(式)
とは違って、インスタンスの場合はそれに自動的に束縛されてしまう動きをしてしまうようです。
なので、書いてもらってるように式を使ったほうが直感的には違和感が少なそうなので、特に使い分けを考えずに variable function(式)
を使っていったほうが良さそうというお話でした。
Discussion