PHP
PHPの罠っぽい仕様

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

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

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

final class Account

{
/**
* @var bool
*/

private $isAdmin;

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

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

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

final class AccountCollection

{
/**
* @var Account[]
*/

private $accounts;

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

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

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を渡している。

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

$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とそっくりなユーザ定義関数を作る。

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を使うように変更する。

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エラーになる:

$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でラップすれば回避できるので、やりたいことができないわけではない。

    public function getAdminAccounts(): array

{
return my_array_filter(
$this->accounts,
Closure::fromCallable([$this, 'isAdminAccount'])
);
}

あと、もちろん、my_array_filterAccountCollectionのメンバーにする方法もある。

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のコンテキストが異なるとき)