1. ホーム
  2. PHP

【PHP】第一級callable構文(…)の使い方|関数やメソッドをそのまま値として渡す方法

Share

array_map() や usort() のように「関数そのものを渡す」場面では、これまで 'strlen' という文字列や [ $obj, 'method' ] という配列を書いてきました。関数名が文字列なので、エディタの補完は効かず、綴りを間違えても実行するまで気づけません。PHP 8.1 で追加された第一級callable構文を使うと、strlen(...) と書くだけで同じものを「呼び出せる値」として取り出せます。この記事では、その書き方と従来の記法との違い、使えないケースまでを解説します。

strlen(…) と書くと Closure が返ってくる

第一級callable構文は、関数やメソッドの呼び出しの引数の位置に ... だけを書く記法です。この ... はコードに実際に打ち込む3つのドットで、可変長引数の ...$args やスプレッド展開とは別物です。「引数はまだ渡さない、呼び出す準備だけした状態を値としてほしい」という意味だと考えてください。

example.php
// strlen を「呼び出す」のではなく、「呼び出せる値」として取り出す
$length = strlen(...);

var_dump( $length instanceof Closure ); // bool(true)

// 取り出した値は、あとから普通の関数のように呼べる
echo $length( 'hello' ); // 5

返ってくるのは Closure オブジェクトです。無名関数(function () { ... })を書いたときと同じ型なので、callable を受け取る関数にはそのまま渡せます。つまり array_map( strtoupper(...), $words ) のような書き方ができるわけです。

ラッパーの無名関数を書いていた人には、記述量の差が分かりやすいと思います。次の3つはどれも同じ結果になります。

example.php
$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(...) は書けない(後述)
Formatter.php
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' と打ち間違えても、その値が実際に呼ばれるまでエラーになりません。ループの奥やキューの処理で初めて落ちる、という発見の遅さがやっかいでした。

example.php
// 文字列: この行は何事もなく通る。呼ばれたときに初めて落ちる
$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() から問題なく呼び出せます。

Sorter.php
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 で取り込んだ短い名前をそのまま書けます。

example.php
namespace App;

use function App\Util\shout;

// 従来は完全修飾名を文字列で書く必要があった
array_map( 'App\Util\shout', $words );

// 取り込んだ名前をそのまま書ける
array_map( shout(...), $words );

実際に書き換えてみる

コールバックを受け取る標準関数との相性がとても良い構文です。よく使う3つを並べてみます。

example.php
$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 のままにできます。

functions.php
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 を別のオブジェクトに差し替えられます。

example.php
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 になります。

NG 例
// 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 の配列に変換したい」といった用途では、無名関数で包むことになります。

example.php
// 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(...) も同様です。

example.php
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 はオブジェクトそのものが一致しないと解除できません。

functions.php
$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(...) に置き換えるところから始めると、効果を実感しやすいはずです。

参考ページ