はじめに
どうも、最近「盾の勇者の成り上がり」の一話をみて「あぁ、この感じの闇堕ち必至展開か」と思ったら想像以上に面白くて徹夜で全話みたけんつです。
今日は Laravel を使っている時に雑に使った call_user_func 関連でめちゃくちゃ詰まったのでその話をまとめていこうかなと思います。
TL;DR
- call_user_func で Array や String で第一引数の Callback を渡しても動く
- PHPStan でコケる
- コケないようにすると、引数を渡せなくなる
- 使わないほうが楽
前提
PHPStan を利用する環境下での話です。
PHP のバージョンは以下の通り。
PHP 7.3.6-1+ubuntu16.04.1+deb.sury.org+1 (cli) (built: May 31 2019 11:06:26) ( NTS )
PHPStan は以下のバージョンを使用する。
Using version ^0.11.12 for phpstan/phpstan
コードは以下のものを前提とします。
<?php // main.php require './vendor/autoload.php'; Sample\A::all();
<?php // A.php namespace Sample; class A { public static function say(string $name, int $age) { echo "Name: {$name}, Age: {$age}\n"; } public static function bye(string $name, int $age) { echo "Bye, Name: {$name}, Age: {$age}\n"; } public static function all() { $name = 'Jack'; $age = 20; $functions = [ 'say', 'bye' ]; foreach ($functions as $function) { call_user_func([__CLASS__, $function], $name, $age); } } }
{ "name": "lrf141/sample", "authors": [ { "name": "lrf141", } ], "require": {}, "require-dev": { "phpstan/phpstan": "^0.11.12" }, "autoload": { "psr-4": { "Sample\\": "./" } } }
何が起こるか
実行することは問題なくできる。
$ php main.php Name: Jack, Age: 20 Bye, Name: Jack, Age: 20
ただ PHPStan を実行すると問題が起こる。
$ ./vendor/bin/phpstan analyze -l 7 ./A.php 1/1 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100% ------ ------------------------------------------------------------------------------ Line A.php ------ ------------------------------------------------------------------------------ 23 Parameter #1 $function of function call_user_func expects callable(): mixed, array('Sample\\A', 'bye'|'say') given. ------ ------------------------------------------------------------------------------ [ERROR] Found 1 error
まぁ見事にコケるわけです。
そこで call_user_func の引数を見て見るわけですよ。
https://www.php.net/manual/ja/function.call-user-func.php
call_user_func ( callable $callback [, mixed $parameter [, mixed $... ]] ) : mixed
なるほど?
まぁ確かに PHPStan でもそういう解析結果だしそれはそうだなっていう納得はある。
ここで callable に当てはまらないものってなんだろうかと調べてみる
https://www.php.net/manual/ja/language.types.callable.php
PHP 関数はその名前を単に文字列として渡します。 どのようなビルトインまたはユーザー定義の関数も渡すことができます。 ただし、 array(), echo, empty(), eval(), exit(), isset(), list(), print あるいは unset() といった言語構造はコールバックとしては使えないことに注意しましょう。
なるほど?
原因はこれっぽい。とおもって、公式のサンプルコードを見てみた。
<?php class myclass { static function say_hello() { echo "Hello!\n"; } } $classname = "myclass"; call_user_func(array($classname, 'say_hello')); call_user_func($classname .'::say_hello'); // 5.2.3 以降 $myobject = new myclass(); call_user_func(array($myobject, 'say_hello')); ?>
がっつり array 使っていた。
じゃあ string に変えてみようと思って変えてみた。
call_user_func(__CLASS__.'::'.$function, $name, $age);
$ ./vendor/bin/phpstan analyze -l 7 ./A.php 1/1 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100% ------ -------------------------------------------------------------------------------------------- Line A.php ------ -------------------------------------------------------------------------------------------- 23 Parameter #1 $function of function call_user_func expects callable(): mixed, string given. ------ -------------------------------------------------------------------------------------------- [ERROR] Found 1 error
なんでそうなった…。
PHP 関数はその名前を単に文字列として渡します。 どのようなビルトインまたはユーザー定義の関数も渡すことができます。
こう書いているのだが…。
これは call_user_func_array でも同様に起こる。
じゃあこれを以下の様に書き換える。
call_user_func(self::$function(), $name, $age);
すると実行時に
$ php main.php PHP Fatal error: Uncaught ArgumentCountError: Too few arguments to function Sample\A::say(), 0 passed in /home/lrf141/phpProject/waste/A.php on line 23 and exactly 2 expected in /home/lrf141/phpProject/waste/A.php:6 Stack trace: #0 /home/lrf141/phpProject/waste/A.php(23): Sample\A::say() #1 /home/lrf141/phpProject/waste/main.php(5): Sample\A::all() #2 {main} thrown in /home/lrf141/phpProject/waste/A.php on line 6
引数を渡せないっぽい。
self::$function($name, $age);
こうすると解決した。
$ ./vendor/bin/phpstan analyze -l 7 ./A.php 1/1 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100% [OK] No errors
おわりに
誰かどうしてこうなるか教えて欲しい。
追記
Already did :) https://t.co/PCImS37JT0
— Ondřej Mirtes (@OndrejMirtes) July 31, 2019
github.com
PHPStan のバグを踏みぬいたっぽい