無名関数はクロージャとも呼ばれ、名前のない関数です。値が期待される場所にfunction ($param) { ... }と書き、変数に代入したり、ほかの関数に渡したり、返したりします。use ($var)で周りのコードの変数を持ち込めます。
<?php
$greet = function (string $name): string {
return "Hello, $name";
}; // the semicolon ends the assignment
echo $greet('Ada'), "\n";
$greeting = 'Welcome';
$welcome = function (string $name) use ($greeting): string {
return "$greeting, $name";
};
echo $welcome('Linus'), "\n";
var_dump($greet instanceof Closure);
すべての無名関数は組み込みクラスClosureのオブジェクトなので、保存したり、受け渡したり、あとで$greet(...)で呼び出したりできます。
useキーワードは値でキャプチャする
クロージャは自分では外側の変数を参照できません。use ($x)は、クロージャが実行されるときではなく、作られた瞬間の$xの値をクロージャにコピーします。
<?php
$discount = 10;
$apply = function (float $price) use ($discount): float {
return $price - $discount;
};
$discount = 50; // changed after the closure was created
echo $apply(100), "\n"; // still uses 10
// Several variables
$currency = 'EUR';
$rate = 1.08;
$convert = function (float $usd) use ($rate, $currency): string {
return number_format($usd / $rate, 2) . " $currency";
};
echo $convert(54), "\n";
参照によるuse:&$var
use (&$x)はコピーではなく変数そのものをクロージャに渡します。クロージャの中での変更は外から見え、外での変更も中から見えます。コールバックで合計に足したり、カウンターを保持したりするのはこの方法です。
<?php
$total = 0;
$addToTotal = function (int $amount) use (&$total): void {
$total += $amount;
};
$addToTotal(10);
$addToTotal(32);
echo $total, "\n";
// A counter that remembers its state between calls
function makeCounter(): Closure
{
$count = 0;
return function () use (&$count): int {
return ++$count;
};
}
$next = makeCounter();
echo $next(), $next(), $next(), "\n";
$other = makeCounter(); // its own $count
echo $other(), "\n";
makeCounter()を呼ぶたびに新しい$countが作られ、返されたクロージャは関数が戻ったあともそれを保持し続けます。「クロージャ」という言葉が指しているのはこれで、関数が必要な変数を閉じ込めるのです。
コールバックとしてクロージャを渡す
array_map()、array_filter()、usort()、array_walk()のような関数は、関数を引数として受け取ります。クロージャを使えば、必要なだけ文を書いてその場で関数を書けます。
<?php
$products = [
['name' => 'Laptop', 'price' => 1200, 'stock' => 3],
['name' => 'Mouse', 'price' => 25, 'stock' => 0],
['name' => 'Monitor', 'price' => 300, 'stock' => 7],
];
$minPrice = 100;
$available = array_filter($products, function ($p) use ($minPrice) {
return $p['stock'] > 0 && $p['price'] >= $minPrice;
});
usort($available, function ($a, $b) {
return $b['price'] <=> $a['price']; // most expensive first
});
$labels = array_map(function ($p) {
$label = strtoupper($p['name']);
return "$label ({$p['stock']} left)";
}, $available);
print_r($labels);
本体が1つの式なら、fn($p) => $p['price']のようなアロー関数のほうが短く、useも要りません。
callable:関数名、メソッド、strlen(...)
callable型の引数はクロージャ以外も受け付けます。関数名の文字列、オブジェクトとメソッド名の配列、クラス名と静的メソッド名の配列も該当します。PHP 8.1以降は、ファーストクラスcallable構文strlen(...)で、そのどれでも直接Closureにできます。
<?php
class Formatter
{
public function shout(string $s): string { return strtoupper($s) . '!'; }
public static function whisper(string $s): string { return strtolower($s) . '...'; }
}
$words = ['Hello', 'World'];
$f = new Formatter();
print_r(array_map('strtoupper', $words)); // function name
print_r(array_map([$f, 'shout'], $words)); // object method
print_r(array_map([Formatter::class, 'whisper'], $words)); // static method
// PHP 8.1 first-class callable syntax
$len = strlen(...);
$shout = $f->shout(...);
$whisper = Formatter::whisper(...);
echo $len('closure'), ' ', $shout('hey'), ' ', $whisper('PSST'), "\n";
function apply(callable $fn, string $value): string
{
return $fn($value);
}
echo apply('ucfirst', 'php'), "\n";
var_dump(is_callable('no_such_function'));
(...)の形はその行が実行された時点でチェックされ、タイプミスがあればその場でErrorが投げられます。'strtoupr'のような文字列は、誰かが呼び出そうとするまでただの文字列です。
クラスの中のクロージャ:$thisとstatic
メソッドの中で作られたクロージャには自動的に$thisが結びつけられるので、オブジェクトのプロパティを使えます。$thisが不要ならstatic functionにしましょう。オブジェクトを生かし続けるのも防げます。
<?php
class Cart
{
private array $items = [];
private float $taxRate = 0.2;
public function add(string $name, float $price): void
{
$this->items[$name] = $price;
}
public function pricesWithTax(): array
{
// $this is available inside the closure
return array_map(function (float $price): float {
return round($price * (1 + $this->taxRate), 2);
}, $this->items);
}
public function names(): array
{
return array_map(static function (string $name): string {
return ucfirst($name);
}, array_keys($this->items));
}
}
$cart = new Cart();
$cart->add('book', 12.5);
$cart->add('pen', 1.99);
print_r($cart->pricesWithTax());
print_r($cart->names());
useの書き忘れ
クロージャでいちばんよくあるバグは、外側の変数をuseに並べずに使うことです。クロージャの中ではその変数は存在しません。
$minAge = 18;
$adults = array_filter($people, function ($p) {
return $p['age'] >= $minAge; // $minAge is not in use (...)
});
PHP prints: Warning: Undefined variable $minAge
中では$minAgeはnullで、どんな年齢も>= nullの比較でtrueになるので、全員が絞り込みを通ってしまいます。警告を表示しないサーバーでは、何も教えてくれません。変数をuseに加えましょう。
<?php
$people = [['name' => 'Ada', 'age' => 36], ['name' => 'Tom', 'age' => 12]];
$minAge = 18;
$adults = array_filter($people, function ($p) use ($minAge) {
return $p['age'] >= $minAge;
});
print_r(array_column($adults, 'name'));
// Or an arrow function, which captures $minAge automatically
$adults2 = array_filter($people, fn($p) => $p['age'] >= $minAge);
print_r(array_column($adults2, 'name'));