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_filter
をAccountCollection
のメンバーにする方法もある。
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のコンテキストが異なるとき)