7
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

【調査】PHPのクロージャ(無名関数)の仕様

Last updated at Posted at 2022-04-28

Laravelのindex.phpを順番に読んでいたらクロージャ(無名関数)を知らないと何やっているのかわからないコードが出てきたのでPHPの公式を参照しながら調査しました!

無名関数(クロージャ)とはそもそも何か?

無名関数はクロージャとも呼ばれ、関数名を指定せずに関数を作成できるようにするものです。

クロージャの例
spl_autoload_register(function ($class_name) {
    include $class_name . '.php';
});

上の例はcallable型の引数に、コールバック関数としてクロージャを渡している例です。

クロージャはcallable型の引数に使えるだけではなく、変数の値として使用することもできます。
変数の値として使用する場合は定義したクロージャを自動でClosureインスタンスに変換します。
このClosureクラスはクロージャそのものを表すクラスでクロージャの設定を少し変えることが可能です。それについては後ほど触れます。
以下は公式ドキュメントからの引用です。変数に代入しているので裏でClosureインスタンスが作成されています。

変数への無名関数代入の例
<?php
$greet = function($name)
{
    printf("Hello %s\r\n", $name);
};
$greet('World');
$greet('PHP');
?>

クロージャの外部の変数を使う方法

ここら辺からやや難しくなってきます。
useを使うことでクロージャの外で宣言している変数を使えます。逆に言うとuseを使わない限り外で宣言している変数は使えません。
これに関しては実際にプログラムを見た方が分かりやすかったです。
下記は公式ドキュメントに記載されているコードにメモ書きを記載したものです。
そのままコピペで動くはずですので是非試してみてください。※最初にエラーが表示されるかと思いますが未定義変数のエラーであれば想定通りです。

親のスコープからの変数の引き継ぎ
<?php
$message = 'hello';

// [1]"use" がない場合はクロージャ内のローカル変数を参照していることになる。
// 何も宣言していないので宣言してないよって通知される。
$example = function () {
    var_dump($message);
};
$example();
// Notice: Undefined variable: message in /example.php on line 6
//NULL

// [2]useを使うことでクロージャの外で宣言している変数を使えます。
$example = function () use ($message) {
    var_dump($message);
};
$example();
//string(5) "hello"

// [3]引き継がれた変数の値は、関数が定義された時点のものであり、関数が呼ばれた時点のものではありません。
// つまり[2]の時点で定義されていたものです。
// どうしてこんなことが起きるかというと、useで変数を渡すときに参照渡しではなく値渡しをしている為です。
// 参照渡しはメモリ番地を渡しているだけなのでメモリ内の値が変更された場合変更が反映されます。
// 一方、値渡しは値そのものをコピーして受け取っているのでコピー元が変更されても影響を受けることはありません。
$message = 'world';
$example();
// string(5) "hello"

// $message をリセットします
$message = 'hello';

// [4]useを使って参照渡しで引き継ぐことも可能です。
$example = function () use (&$message) {
    var_dump($message);
};
$example();
// string(5) "hello"

// [5]クロージャに参照渡しした場合はクロージャの外で変更された値が反映されます
$message = 'world';
$example();
// string(5) "world"

// [6]クロージャには引数も渡せます。
$example = function ($arg) use ($message) {
    var_dump($arg . ' ' . $message);
};
$example("hello");
// string(11) "hello world"

// [7]戻り値の型はuseの後に置きます
$example = function () use ($message): string {
    return "hello $message";
};
var_dump($example());
//string(11) "hello world"
?>

上記はグローバルスコープの変数を使用しましたが、クロージャが宣言されている関数の中の変数も使うことができます。
例は公式ドキュメントからの引用ですが大体使い方は一緒なので特に新しい感じはありません。

クロージャのスコープ
<?php
// 基本的なショッピングカートで、追加した商品の一覧や各商品の数量を表示します。
// カート内の商品の合計金額を計算するメソッドでは、クロージャをコールバックとして使用します。
class Cart
{
    const PRICE_BUTTER  = 1.00;
    const PRICE_MILK    = 3.00;
    const PRICE_EGGS    = 6.95;

    protected $products = array();
    
    public function add($product, $quantity)
    {
        $this->products[$product] = $quantity;
    }
    
    public function getQuantity($product)
    {
        return isset($this->products[$product]) ? $this->products[$product] :
               FALSE;
    }
    
    public function getTotal($tax)
    {
        $total = 0.00;
        
        $callback =
            function ($quantity, $product) use ($tax, &$total)
            {
                $pricePerItem = constant(__CLASS__ . "::PRICE_" .
                    strtoupper($product));
                $total += ($pricePerItem * $quantity) * ($tax + 1.0);
            };
        
        // 配列の全ての要素にユーザー定義の関数を適用する
        array_walk($this->products, $callback);
        // 四捨五入して返却
        return round($total, 2);
    }
}

$my_cart = new Cart;

// カートに商品を追加します
$my_cart->add('butter', 1);
$my_cart->add('milk', 3);
$my_cart->add('eggs', 6);

// 合計に消費税 5% を付加した金額を表示します
print $my_cart->getTotal(0.05) . "\n";
// 結果は 54.29 です
?>

クロージャ内の$this

クロージャをクラス内で宣言した場合は、宣言したクラスに自動的に紐づけられます。
つまりデフォルトではクロージャで$thisを使う場合はクロージャを宣言したクラスとなります。
下記も公式ドキュメントからの引用です。

$this の自動バインド
<?php

class Test
{
    public function testing()
    {
        return function() {
            var_dump($this);
        };
    }
}

$object = new Test;
$function = $object->testing();
$function();
// 出力はobject(Test)#1 (0) {}
?>

繰り返しになりますが上記のようにインスタンス化してから使われるクロージャーは自動で$thisに自クラスが設定されます。
ですが必ずしもインスタンスに紐づいているわけありません。クロージャにもインスタンスに紐づかない静的クロージャが存在します。

静的クロージャ(静的無名関数)

静的クロージャを宣言した場合は自動でのインスタンスの紐づけがされなくなります。
インスタンスに紐づけされないということは、クロージャがどのインスタンスにも紐づいていない状態ということで、$thisがnullになります。
下記も公式ドキュメントからの引用です。

静的無名関数内での $this の使用例
<?php

class Foo
{
    function __construct()
    {
        $func = static function() {
            var_dump($this);
        };
        $func();
    }
};
new Foo();
// Notice: Undefined variable: this in %s on line %d
// NULL
?>

Closureクラス

ここまででクロージャとインスタンスの紐づけについて触れてきましたがこれは後で変更することが可能です。
変数にクロージャを代入した場合、クロージャはClosureクラスに自動で変換されると書きました。
このClosureクラスを使用すればクロージャの設定を変更することが可能です。
今回調査したのはClosureクラスの関数のうち、bind関数、bindTo関数の2つだけです。他にもいくつかありますのでまた機会があれば調査します。
bind関数、bindTo関数が使えるようになると$thisの値にかかわるインスタンスを変更したり、指定したクラスのprivateメンバーを参照できたりするようになります。

bind関数(Closure::bind)

Closure::bind — バインドされたオブジェクトとクラスのスコープでクロージャを複製する。

クロージャを複製するとありますが渡したクロージャに引数の値を設定して返してくれるくらいにとらえていても問題なさそうです。
厳密には複製なので紐づくインスタンス違いでクロージャが欲しいときなどは新しい変数などに格納すれば紐づくインスタンス違いのクロージャを2つ作ることも可能なはずです。

定義
public static Closure::bind(Closure $closure, ?object $newThis, object|string|null $newScope = "static"): ?Closure

引数は以下のようになっています。
closure
設定をしたいクロージャ
newThis
指定したクロージャを紐づけるインスタンス。$thisの値がこれで決まる。
静的クロージャの場合はnullを設定します。もし静的クロージャをインスタンスに紐づけようとした場合、静的クロージャにインスタンスを紐づけることはできませんと警告が出ます。
newScope

無名関数からどのクラスのprivateメンバーやprotectedメンバーにアクセスできるのかが決まります。

つまり指定したクロージャがnewScopeで指定したクラスのprivateメンバーやprotectedメンバーにアクセスできるようにするための引数です。
静的クロージャはnewThisにnullしか設定できませんが、newScopeは設定できます。
newScopeで指定したクラスの中に関数を新しく定義するようなイメージだとクロージャからprivateメンバーにアクセスできることがイメージしやすい気がします。

以下はLaravelプロジェクトのvendor/Composer/autoload_static.phpから抜粋しました。
newThisをnullにすることでインスタンスに紐づかない静的クロージャにして、
更にnewScopeにClassLoader::classを指定することでClassLoaderクラスのprivateメンバーにアクセスできるClosureを作成しています。

bind関数使用例
\Closure::bind(function () use ($loader) {
            $loader->prefixLengthsPsr4 = ComposerStaticInitf2c0c51de1e125fbd476c42d0d8cd85c::$prefixLengthsPsr4;
            $loader->prefixDirsPsr4 = ComposerStaticInitf2c0c51de1e125fbd476c42d0d8cd85c::$prefixDirsPsr4;
            $loader->prefixesPsr0 = ComposerStaticInitf2c0c51de1e125fbd476c42d0d8cd85c::$prefixesPsr0;
            $loader->classMap = ComposerStaticInitf2c0c51de1e125fbd476c42d0d8cd85c::$classMap;
        }, null, ClassLoader::class);

ほんのり理解したようなところで公式ドキュメントに載っていた例を参考に4パターンほど試してみました。
以下を実行すればたぶん納得できるかと思います。

<?php
class A {
    public static $pubStafoo = 1;
    private static $priStafoo = 2;
    public $pubfoo = 3;
    private $prifoo = 4;
}

// そもそも静的無名関数として定義されているので注意
$cl1 = static function() {
    return A::$pubStafoo . A::$priStafoo;
};

$cl2 = static function() {
    //コメントアウトを外すとprivateアクセスができないためエラーになる。
    //var_dump(A::$priStafoo);
    return A::$pubStafoo;
};

// こっちは普通のクロージャ
$cl3 = function() {
    return $this->pubfoo . $this->prifoo;
};

$cl4 = function() {
    //コメントアウトを外すとprivateアクセスができないためエラーになる。
    //var_dump($this->prifoo);
    return $this->pubfoo;
};

// 静的無名関数のスコープにAクラスを指定したのでAクラス内のprivateメンバーにアクセスできる。
$bcl1 = Closure::bind($cl1, null, 'A');

// 静的無名関数はインスタンスに紐づけることはできないので警告が出る。
//$bcl1 = Closure::bind($cl1, new A(), 'A');

// 静的無名関数のスコープを設定しなかったのでpublicメンバーにしかアクセスできない。
$bcl2 = Closure::bind($cl2, null);

// クロージャをAのインスタンスに紐づけることでthisが使える。更にスコープにAクラスを指定したのでAクラス内のprivateメンバーにアクセスできる。
$bcl3 = Closure::bind($cl3, new A(), 'A');

// クロージャをAのインスタンスに紐づけることでthisが使える。スコープを指定しないとpublicのものにしかアクセスできなくなる。
$bcl4 = Closure::bind($cl4, new A());

echo $bcl1(), "\n";
echo $bcl2(), "\n";
echo $bcl3(), "\n";
echo $bcl4(), "\n";
?>

bindTo関数

bind関数に似たような関数でClosureクラスのbindTo()という関数もあります。

定義
public Closure::bindTo(?object $newThis, object|string|null $newScope = "static"): ?Closure

bindTo関数もできることはbind関数と同じで、引数のnewThis、newScopeをクロージャに設定します。
違う点は呼び出し方法とそれに伴う引数くらいです。
bindは定義を見ていただけると分かると思いますが静的な関数なのでインスタンス化せずに使いますが、bindToはインスタンス化しないと使えない関数です。
インスタンス化とは言いますが変数に設定したときに自動でClosureに変換されるのでわざわざnewするものではありません。

bindToは以下のように呼び出します。公式ドキュメントに載っていたものにメモ書きしたものです。

<?php

class A {
    function __construct($val) {
        $this->val = $val;
    }
    function getClosure() {
        // このオブジェクトとスコープにバインドしたクロージャを返します。
        return function() { return $this->val; };
    }
}

$ob1 = new A(1);
$ob2 = new A(2);

// クロージャを受け取る。この時Closureインスタンスに変換されています。
// クラス内で定義されたクロージャは自動的に定義されたクラスのインスタンスに紐づけられます。
$cl = $ob1->getClosure();
echo $cl(), "\n";
// bindToはClosureインスタンスから呼び出すようにします。
// この例ではスコープは指定せず紐づけるインスタンスだけ指定しています。
// もともとob1に紐づいていたところをob2に紐づくように変更されるので出力結果が変わるはずです。
$cl = $cl->bindTo($ob2);
echo $cl(), "\n";
?>

メモ書き

今回はあくまでも仕様について理解するための調査だったので、実際にどういうときにクロージャ使うのかなど発展的な内容はまだまだだと感じています。
ただ仕様を知っているので何とか対応できる気がします。今すぐには使わなくてもこの記事をメモ代わりにいざ使うときに役立てたいです。

7
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?