array_map() や usort() のように「関数そのものを渡す」場面では、これまで 'strlen' という文字列や [ $obj, 'method' ] という配列を書いてきました。関数名が文字列なので、エディタの補完は効かず、綴りを間違えても実行するまで気づけません。PHP 8.1 で追加された第一級callable構文を使うと、strlen(...) と書くだけで同じものを「呼び出せる値」として取り出せます。この記事では、その書き方と従来の記法との違い、使えないケースまでを解説します。
目次
strlen(…) と書くと Closure が返ってくる
第一級callable構文は、関数やメソッドの呼び出しの引数の位置に ... だけを書く記法です。この ... はコードに実際に打ち込む3つのドットで、可変長引数の ...$args やスプレッド展開とは別物です。「引数はまだ渡さない、呼び出す準備だけした状態を値としてほしい」という意味だと考えてください。
// strlen を「呼び出す」のではなく、「呼び出せる値」として取り出す
$length = strlen(...);
var_dump( $length instanceof Closure ); // bool(true)
// 取り出した値は、あとから普通の関数のように呼べる
echo $length( 'hello' ); // 5
返ってくるのは Closure オブジェクトです。無名関数(function () { ... })を書いたときと同じ型なので、callable を受け取る関数にはそのまま渡せます。つまり array_map( strtoupper(...), $words ) のような書き方ができるわけです。
ラッパーの無名関数を書いていた人には、記述量の差が分かりやすいと思います。次の3つはどれも同じ結果になります。
$words = [ 'php', 'closure' ];
// 1. 文字列で関数名を渡す(従来)
array_map( 'strtoupper', $words );
// 2. 無名関数でくるむ(従来)
array_map( fn ( string $w ) => strtoupper( $w ), $words );
// 3. 第一級callable構文(PHP 8.1 以降)
array_map( strtoupper(...), $words );
関数・メソッド・静的メソッドのどれでも書ける
対象は組み込み関数だけではありません。呼び出しの構文がそのまま使えるので、メソッドでも静的メソッドでも同じ要領で書けます。
| 対象 | 書き方 |
|---|---|
| 関数(組み込み・自作) | strlen(...) |
| オブジェクトのメソッド | $obj->method(...) |
| クラス内から自分のメソッド | $this->method(...) |
| 静的メソッド | Foo::bar(...) |
__invoke() を持つオブジェクト | $obj(...) |
| インスタンスの生成 | new Foo(...) は書けない(後述) |
class Formatter
{
public function trim( string $text ): string
{
return trim( $text );
}
public static function upper( string $text ): string
{
return strtoupper( $text );
}
public function all(): array
{
// クラスの中からは $this->method(...) と書ける
return [ $this->trim(...), self::upper(...) ];
}
}
$formatter = new Formatter();
$trim = $formatter->trim(...); // メソッド
$upper = Formatter::upper(...); // 静的メソッド
echo $trim( ' php ' ); // php
echo $upper( 'php' ); // PHP
$obj->method(...) で作った Closure は、そのオブジェクトを内部に抱えています。あとで $obj を別の変数に入れ替えても、作った時点のオブジェクトに対して実行されます。
従来の callable 記法との違い
PHP には以前から、関数を値として扱う書き方がいくつもありました。第一級callable構文はそれらを置き換えるもので、公式マニュアルでも Closure::fromCallable() より推奨されています。何がどう違うのかを整理します。
| 書き方 | 特徴 |
|---|---|
'strlen' | 関数名がただの文字列。補完も定義ジャンプも効かず、名前を間違えても実際に呼ばれるまで気づけない |
[ $obj, 'method' ] | 同じく文字列頼み。渡した先(クラスの外)から呼ばれるため、private / protected メソッドは指定できない |
[ Foo::class, 'bar' ] | 静的メソッド用。クラス名だけは補完が効くが、メソッド名は文字列のまま |
Closure::fromCallable( [ $obj, 'method' ] ) | PHP 7.1 以降。Closure は得られ、スコープも引き継がれるが、記述が長くメソッド名は文字列のまま |
strlen(...) | PHP 8.1 以降。名前を構文として解釈するので補完・ジャンプ・静的解析が効き、存在しない名前は値を作った時点でエラーになる |
タイプミスが実行時ではなく解析時に分かる
文字列の callable は、PHP にとっては最後まで「ただの文字列」です。'stlren' と打ち間違えても、その値が実際に呼ばれるまでエラーになりません。ループの奥やキューの処理で初めて落ちる、という発見の遅さがやっかいでした。
// 文字列: この行は何事もなく通る。呼ばれたときに初めて落ちる
$callback = 'stlren';
var_dump( is_callable( $callback ) ); // bool(false)
// 第一級callable構文: この行で Error になる
// Error: Call to undefined function stlren()
$callback = stlren(...);
さらに大きいのは、実行する前の段階での恩恵です。名前が構文の一部になったことで、エディタは通常の関数呼び出しと同じように扱えます。補完が出る、定義にジャンプできる、リネームのリファクタリングが追随する、PHPStan や Psalm といった静的解析ツールが引数の型まで検査できる——文字列のままでは得られなかったものが一通り手に入ります。
private / protected メソッドも渡せる
ここが実務でいちばん効いてくる違いです。第一級callable構文は、Closure を作った場所のスコープを保持します。クラスの中で $this->compare(...) と書けば、compare() が private であっても、その Closure は外部の usort() から問題なく呼び出せます。
class Sorter
{
// 外に公開したくない比較ロジック
private function compare( array $a, array $b ): int
{
return $a['price'] <=> $b['price'];
}
public function sort( array $items ): array
{
// 従来: usort( $items, [ $this, 'compare' ] ); は
// private なので Error になり、public にするしかなかった
usort( $items, $this->compare(...) );
return $items;
}
}
配列形式の [ $this, 'compare' ] は、usort() の側(クラスの外)から呼ばれるため private にアクセスできず、エラーになります。「コールバックに渡したいだけなのに public にせざるを得ない」という妥協が不要になりました。なお Closure::fromCallable() も同じくスコープを引き継ぐので、こちらを使っていた場合は素直に書き換えられます。
use function で取り込んだ名前がそのまま使える
名前空間を使っていると、文字列の callable には完全修飾名('App\Util\shout')を書く必要がありました。第一級callable構文は通常の関数呼び出しと同じ解決をするので、use function で取り込んだ短い名前をそのまま書けます。
namespace App;
use function App\Util\shout;
// 従来は完全修飾名を文字列で書く必要があった
array_map( 'App\Util\shout', $words );
// 取り込んだ名前をそのまま書ける
array_map( shout(...), $words );
実際に書き換えてみる
コールバックを受け取る標準関数との相性がとても良い構文です。よく使う3つを並べてみます。
$words = [ 'php', 'closure', 'callable' ];
// 各要素を大文字にする
$upper = array_map( strtoupper(...), $words );
// オブジェクトのメソッドで絞り込む
$validator = new Validator();
$valid = array_filter( $rows, $validator->isValid(...) );
// 自分の private メソッドで並べ替える(クラスの中)
usort( $items, $this->compare(...) );
array_filter() は第2引数を省略すると「値が偽とみなされる要素」を落とす動きになりますが、こうして判定メソッドを渡せば、条件の中身はクラス側に置いたまま呼び出しを1行に保てます。
WordPress のフック登録に使う
プラグインやテーマをクラスで書いていると、add_action( 'init', [ $this, 'register_post_type' ] ) のような登録を大量に書くことになります。この配列も第一級callable構文に置き換えられます。しかも前述のスコープの話が効くので、フックに登録するメソッドを private のままにできます。
class My_Recipe_Plugin
{
public function register(): void
{
// [ $this, 'register_post_type' ] では private を指定できないが、
// 第一級callable構文ならスコープを引き継ぐので呼び出せる
add_action( 'init', $this->register_post_type(...) );
add_filter( 'the_content', $this->append_notice(...) );
}
private function register_post_type(): void
{
register_post_type(
'recipe',
array(
'label' => 'レシピ',
'public' => true,
'has_archive' => true,
'show_in_rest' => true,
)
);
}
private function append_notice( string $content ): string
{
if ( ! is_singular( 'recipe' ) ) {
return $content;
}
return $content . '<p class="recipe-notice">分量は目安です。</p>';
}
}
( new My_Recipe_Plugin() )->register();
公開する必要のないメソッドを public にせずに済むので、クラスの外に見せるインターフェースが register() だけに絞れます。ただし解除には注意が必要で、この点は後述します。
得られるのは Closure なので、Closure の機能が使える
戻り値が Closure のインスタンスである以上、無名関数に対してできることは一通りできます。たとえば bindTo() で $this を別のオブジェクトに差し替えられます。
class Greeter
{
public string $name = 'Alice';
public function hello(): string
{
return "Hi, {$this->name}";
}
}
$hello = ( new Greeter() )->hello(...);
echo $hello(); // Hi, Alice
$bob = new Greeter();
$bob->name = 'Bob';
// $this を差し替えた新しい Closure が返る
$helloBob = $hello->bindTo( $bob, Greeter::class );
echo $helloBob(); // Hi, Bob
is_callable() との関係も単純です。Closure は常に呼び出し可能なので、is_callable( strlen(...) ) は必ず true を返します。文字列の callable では「この名前の関数は存在するか」を調べる意味がありましたが、第一級callable構文では値を作る時点で存在チェックが済んでいるため、その用途での is_callable() は出番がなくなります。ReflectionFunction に渡して引数の情報を調べる、といったことも可能です。
使えないケースと注意したいこと
PHP 8.1 以上が必要
この構文は PHP 8.1 で導入されました。PHP 8.0 以前では構文エラー(Parse error)になり、その1ファイルが丸ごと読み込めなくなります。レンタルサーバーなどでバージョンを固定されている環境に配布するコードでは、動作要件を確認してから使ってください。同等のことは Closure::fromCallable()(PHP 7.1 以降)で書けます。
... の位置に引数は書けない
「一部の引数だけ先に埋めておく」という部分適用はできません。... は単独で書く決まりで、他の引数と混ぜると Parse error になります。
// Parse error: syntax error, unexpected token ","
$pad = str_pad( ..., 10 );
// 先に引数を埋めたいときは、これまでどおり無名関数を使う
$pad = fn ( string $s ): string => str_pad( $s, 10 );
new Foo(…) は書けない
コンストラクターは対象外です。new と組み合わせると Error: Cannot create Closure for new expression になります。「ID の配列を DTO の配列に変換したい」といった用途では、無名関数で包むことになります。
// Error: Cannot create Closure for new expression
$factory = new User(...);
// 無名関数で包むか、
$users = array_map( fn ( array $row ): User => new User( $row ), $rows );
// 静的なファクトリメソッドを用意しておけば (...) が使える
$users = array_map( User::fromArray(...), $rows );
ついでに、?->(ヌル安全演算子)とも組み合わせられません。$obj?->method(...) は Cannot combine nullsafe operator with Closure creation というエラーになります。オブジェクトが null かどうかは、事前に判定しておいてください。
__call() 経由のメソッドも書ける
意外に思われるかもしれませんが、実体のないメソッドでも、__call() や __callStatic() が定義されていれば Closure を作れます。__invoke() を持つオブジェクトに対する $obj(...) も同様です。
class Magic
{
public function __call( string $name, array $args ): string
{
return "{$name}: " . implode( ',', $args );
}
}
$magic = new Magic();
// where() は定義されていないが、__call() があるので Closure を作れる
$where = $magic->where(...);
echo $where( 'status', 'publish' ); // where: status,publish
ただし、この場合は実体のメソッドが無いため、引数の数や型のチェックは __call() の中身次第になります。エディタの補完や静的解析も当然効きません。第一級callable構文の利点はほとんど失われるので、マジックメソッドに頼っているクラスでは「書けるが、恩恵は薄い」と理解しておくとよいでしょう。
書くたびに別の Closure ができる
同じメソッドから作っても、(...) を書いた回数だけ別々の Closure オブジェクトが生成されます。$obj->m(...) === $obj->m(...) は false です。これが問題になるのは、登録したコールバックを名前で探して解除する仕組みを使うときです。
WordPress の remove_action() / remove_filter() が典型で、これらは登録時と同じコールバックを渡す必要があります。配列の [ $obj, 'method' ] なら書き直しても同一とみなされますが、Closure はオブジェクトそのものが一致しないと解除できません。
$callback = $this->append_notice(...);
add_filter( 'the_content', $callback );
// 解除するときは、登録したのと同じ Closure を渡す
remove_filter( 'the_content', $callback );
// これは別物なので解除できない
// remove_filter( 'the_content', $this->append_notice(...) );
あとから外す可能性があるフックは、作った Closure をプロパティなどに持っておくか、従来どおり配列で登録しておくのが安全です。
まとめ
第一級callable構文は、関数呼び出しの引数の位置に ... と書くことで、その関数やメソッドを表す Closure を取り出す記法です。関数(strlen(...))、メソッド($obj->method(...)、$this->method(...))、静的メソッド(Foo::bar(...))に使えて、array_map() や usort()、WordPress の add_action() にそのまま渡せます。
文字列や配列の callable と比べた利点は、名前が構文として解釈されることです。補完や定義ジャンプ、静的解析が効き、存在しない名前は Closure を作った時点でエラーになります。加えて書いた場所のスコープを引き継ぐため、private メソッドをコールバックとして渡すために public へ変える必要がなくなりました。
一方で、PHP 8.1 以上が必要で、引数の部分適用や new Foo(...)、ヌル安全演算子との組み合わせはできません。呼ぶたびに別のオブジェクトができる点も、フックの解除まわりでは気に留めておいてください。まずは [ $this, 'method' ] と書いている箇所を $this->method(...) に置き換えるところから始めると、効果を実感しやすいはずです。