PHP: callable型の引数に、privateメソッドを渡すことができたり、できなかったりする

PHPは、組み込み関数のcallable型引数にはprivateメソッドを渡せるのに、ユーザ定義関数のcallable型引数にはprivateメソッドが渡せない。

例えば、アカウントというクラスがあって、

```php
final class Account
{
    /**
     * @var bool
     */
    private $isAdmin;

    public function __construct(bool $isAdmin)
    {
        $this->isAdmin = $isAdmin;
    }

    public function isAdmin(): bool
    {
        return $this->isAdmin;
    }
}
```

それをリスト化するアカウントコレクションというクラスがあって、

```php
final class AccountCollection
{
    /**
     * @var Account[]
     */
    private $accounts;

    public function __construct(Account ...$accounts)
    {
        $this->accounts = $accounts;
    }
}
```

そいつに、管理者アカウントだけ抽出するメソッドを`array_filter`をつかって生やしたコードがあるとする:

```php
final class AccountCollection
{
    // ...

    public function getAdminAccounts(): array
    {
        return array_filter($this->accounts, [$this, 'isAdminAccount']);
    }

    private function isAdminAccount(Account $account): bool
    {
        return $account->isAdmin();
    }
}
```

`array_filter`の第二引数は`callable`型であり、それに`AccountCollection`クラスのプライベートメソッド`isAdminAccount`を渡している。

このとき、次のコードは問題なく動く:

```php
$allAccounts = new AccountCollection(
    new Account(true),
    new Account(false),
    new Account(true),
    new Account(false)
);
$adminAccounts = $allAccounts->getAdminAccounts();
var_dump(count($adminAccounts)); //=> int(2)
```

このコードで、callable型の引数にprivateメソッドが渡せることが確認できた。

次に、`array_filter`とそっくりなユーザ定義関数を作る。

```php
function my_array_filter(array $elements, callable $predicate): array
{
    $newElements = [];
    foreach ($elements as $element) {
        if ($predicate($element)) {
            $newElements[] = $element;
        }
    }
    return $newElements;
}
```

そして、AccountCollectionクラスを`array_filter`ではなく`my_array_filter`を使うように変更する。

```php
final class AccountCollection
{
    /**
     * @var Account[]
     */
    private $accounts;

    public function __construct(Account ...$accounts)
    {
        $this->accounts = $accounts;
    }

    public function getAdminAccounts(): array
    {
        return my_array_filter($this->accounts, [$this, 'isAdminAccount']);
    }

    private function isAdminAccount(Account $account): bool
    {
        return $account->isAdmin();
    }
}
```

このあとに、次のコードを実行するとFatalエラーになる:

```php
$allAccounts = new AccountCollection(
    new Account(true),
    new Account(false),
    new Account(true),
    new Account(false)
);
$adminAccounts = $allAccounts->getAdminAccounts(); 
//=> PHP Fatal error:  Uncaught TypeError: Argument 2 passed to my_array_filter() must be callable, array given
```

`my_array_filter`の第二引数は`callable`を受け付けるようになっているが、callableが渡ってきてないよというエラーだ。

`my_array_filter`にとって`AccountCollection::isAdminAccount`はprivateで不可視なため発生するエラーなのはわかるが、__`array_filter`では発生しなかったので言語仕様の一貫性に謎が残る__。

ちなみに、このFatalエラーは`Closure::fromCallable`でラップすれば回避できるので、やりたいことができないわけではない。

```php
    public function getAdminAccounts(): array
    {
        return my_array_filter(
            $this->accounts,
            Closure::fromCallable([$this, 'isAdminAccount'])
        );
    }
```

あと、もちろん、`my_array_filter`を`AccountCollection`のメンバーにする方法もある。

```php
final class AccountCollection
{
    /**
     * @var Account[]
     */
    private $accounts;

    public function __construct(Account ...$accounts)
    {
        $this->accounts = $accounts;
    }

    public function getAdminAccounts(): array
    {
        return self::my_array_filter($this->accounts, [$this, 'isAdminAccount']);
    }

    private static function my_array_filter(array $elements, callable $predicate): array
    {
        $newElements = [];
        foreach ($elements as $element) {
            if ($predicate($element)) {
                $newElements[] = $element;
            }
        }
        return $newElements;
    }

    private function isAdminAccount(Account $account): bool
    {
        return $account->isAdmin();
    }
}
```

## まとめ

* 組み込み関数の`callable`型引数にはprivateメソッドが渡せる。
* ユーザ定義関数の`callable`型引数にはprivateメソッドが渡せない($thisのコンテキストが異なるとき)