PHP8.1では新機能で『第一級 callable』が追加されました。最初聞いたときなんのこっちゃ?です。
内容を見るとなんてことない、コールバック指定の方法がよりかんたんに分かりやすくなりました。
コールバックとは実行可能な関数を変数に代入したり、関数のパラメータに渡して受け取った側で実行することです。
'(...)' を指定して無名関数を使う第一級callable
第一級callableは、コールバックを使った関数コールの新しい記法です。
百聞は一見にしかずなので、いきなりサンプルコードからいきましょう。
function test($val1, $val2)
{
return $val1 + $val2;
}
// コールバックを使った関数実行
$fn = 'test';
$result = $fn(3, 5);
var_dump($result);
// コールバックから無名関数に変換した関数実行
$fn = Closure::fromCallable('test');
$result = $fn(3, 5);
var_dump($result);
$fn = Closure::fromCallable('strlen');
$result = $fn('sample');
var_dump($result);
int(8)
int(8)
int(6)
// 第一級callableでは、コールバックとClosure::fromCallable() は同じ記法。
$fn = test(...);
$result = $fn(3, 5);
var_dump($result);
$fn = strlen(...);
$result = $fn('sample');
var_dump($result);
int(8)
int(6)
コールバック関数のオブジェクト作成時には '(...)' を使って関数コールをしているかのような記述をし、それが返す無名関数を使ってじっさいの関数コールを行います。
ちなみに '...' は、関数やメソッドの定義で使う、複数パラメータの短縮形 '...' とはまったくの無関係。
括弧も含めた '(...)' が第一級callableの記号でClosureオブジェクトを作成することを明示しています。
コールバックやClosureオブジェクトについてはこちらをどうぞ。
第一級callableはコールバックの無名関数への吸収・統合
第一級callableは従来のcallable型を使いません。代わりに無名関数のオブジェクト、Closureクラスを使います。
だからサンプルコードでも、コールバックとClosure::fromCallable()は、第一級callableでは同じ記法になる。
サンプルコードのような関数実行は通常行いません。それならふつうに test(3, 5) や strlen('sample') でいいし、そっちのほうが行数が少ないから。
第一級callableの発揮できるところは、関数のパラメータや戻り値で渡すときやクラスメソッドでコールバックを使うときです。
クラスメソッドの第一級callable
ここで、第一級callableの真骨頂、クラスメソッドでのコールバックのサンプルコードを見てみましょう。
class Test {
public function test($val1, $val2)
{
return $val1 * $val2;
}
public function get_func_old()
{
// これまでの記法
return [$this, 'test'];
}
public function get_func_new()
{
// 第一級callableの記法
return $this->test(...);
}
}
$obj = new Test();
// クラス内のコールバックの実行
$fn1 = $obj->get_func_old(3, 5);
$fn2 = $obj->get_func_new(3, 5);
$result1 = $fn1(3, 5);
$result2 = $fn2(3, 5);
var_dump($result1);
var_dump($result2);
int(15)
int(15)
クラスで第一級callableを使うときはメソッドに対して行われますが、その記述はメソッドの実行の '()' が '(...)' に変わるだけ。
従来のcallable型では見た目は配列なので、知らない人にはこれを関数実行に使うとは想像もできません。
第一級callableは関数実行と似た形式を取ります。直感的な記述にしたんですね?
似てるだけに知らないと混乱しそうな気もするが。
クラスの外からメソッドをコールバック実行
クラスの外からメソッドをコールバック実行してみましょう。
// これまでの記法
$fn = [$obj, 'test'];
$result1 = $fn(4, 6);
// 第一級callableの記法
$fn = $obj->test(...);
$result2 = $fn(4, 6);
var_dump($result1);
var_dump($result2);
int(24)
int(24)
より直感的な記述になってることがよく分かります。
'(...)' は、メソッドの実行ではなくコールバック用の無名関数を作りますよ? という意味。
クラスのstaticメソッドをコールバック実行
今度はstaticメソッドも見てみましょう。
class Test {
public static function test($val1, $val2)
{
return $val1 * $val2;
}
public static function get_func_old1()
{
// これまでの記法
return [Test::class, 'test'];
}
public static function get_func_old2()
{
// これまでの記法
return 'Test::test';
}
public static function get_func_new()
{
// 第一級callableの記法
return Test::test(...);
}
}
// クラス内のコールバックを取得
$fn = Test::get_func_old1();
$result1 = $fn(6, 8);
$fn = Test::get_func_old2();
$result2 = $fn(6, 8);
$fn = Test::get_func_new();
$result3 = $fn(6, 8);
var_dump($result1);
var_dump($result2);
var_dump($result3);
int(48)
int(48)
int(48)
従来のcallable型では、配列と文字列の2つの指定方法がありましたが、第一級callableでは1つになりました。
個人的には同じことをするのにいくつか方法があるのは好きじゃない。良い変更だと思う。
最後に、クラスの外からstaticメソッドをコールバックで実行したサンプルコードです。
// これまでの記法
$fn = [Test::class, 'test'];
$result1 = $fn(7, 9);
$fn = 'Test::test';
$result2 = $fn(7, 9);
// 第一級callableの記法
$fn = Test::test(...);
$result3 = $fn(7, 9);
var_dump($result1);
var_dump($result2);
var_dump($result3);
int(63)
int(63)
int(63)
クラスオブジェクトのメソッドとstaticメソッドを見比べると、直感的になったのがさらに実感できます。
(new Test())->test(...)
Test::test(...)
ふつうのクラスメソッドコールと同じようになってる。
コールバックから無名関数も作れる。
第一級callableでは、これまでのコールバックの文字列や配列(中身はcallable型)の記法も使えます。
function test($val1, $val2)
{
return $val1 + $val2;
}
// 一般的な第一級callable。
$fn = test(...);
$result1 = $fn(3, 5);
$fn = strlen(...);
$result2 = $fn('sample');
// コールバックを使った第一級callable
$fn = 'test'(...);
$result1 = $fn(3, 5);
$fn = 'strlen'(...);
$result2 = $fn('sample');
var_dump($result1);
var_dump($result2);
int(8)
int(6)
通常の関数ではクォーテーションの有無だけでいまいちですね?
違いはクラスメソッドのほうが分かりやすいです。
class test
{
public function test($val1, $val2)
{
return $val1 + $val2;
}
public static function static_test($val1, $val2)
{
return $val1 - $val2;
}
}
$obj = new Test();
// 一般的な第一級callable
$fn = $obj->test(...);
$result1 = $fn(10, 15);
$fn = Test::static_test(...);
$result2 = $fn(10, 15);
// コールバックを使った第一級callable
$fn = [$obj, 'test'](...);
$result3 = $fn(10, 15);
$fn = [Test::class, 'static_test'](...);
$result4 = $fn(10, 15);
$fn = 'Test::static_test'(...);
$result5 = $fn(10, 15);
var_dump($result1);
var_dump($result2);
var_dump($result3);
var_dump($result4);
var_dump($result5);
int(25)
int(-5)
int(25)
int(-5)
int(-5)
直感的でシンプルなものが無くなったような...。
ふつうの関数やメソッドを使ってるかのような('()' と '(...)' のちがいで判別。)記法がすでにあるので、あえてこれを使う意味はありません。
あるとすれば、関数のパラメータでcallable型を渡してるときでしょうか?
第一級callbleにコードを修正するとき、パラメータを渡す側で '(...)' 形式に変える箇所が多くなります。
そういったことも考えた下位互換の仕様なんでしょう。
PHP8.0以下のバージョンではどうなるか?
第一級callableの '(...)' は、PHP8.1以降で使えます。それ以前はどうなるか見てみましょう。
最初のサンプルコードを使って実行します。結果の予想は Syntax Error(文法エラー)。
function test($val1, $val2)
{
return $val1 + $val2;
}
// 第一級callableでは、コールバックとClosure::fromCallable() は同じ記法。
$fn = test(...);
$result = $fn(3, 5);
var_dump($result);
$fn = strlen(...);
$result = $fn('sample');
var_dump($result);
PHP Parse error: syntax error, unexpected token ")" in /home/vagrant/php-test.php on line 7
予想通りです。')' の直前の '...' が文法として認識されません。
PHP公式ドキュメント