それが僕には楽しかったんです。

ミドルウェアとかやってます

PHPStan を利用する環境下で call_user_func あたりでハマった話

はじめに

どうも、最近「盾の勇者の成り上がり」の一話をみて「あぁ、この感じの闇堕ち必至展開か」と思ったら想像以上に面白くて徹夜で全話みたけんつです。


今日は 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                                                                                       
                                                                                                      

おわりに

誰かどうしてこうなるか教えて欲しい。

追記


github.com

PHPStan のバグを踏みぬいたっぽい