つい先日PHP8.4がリリースされたばかりだというのに、PHP8.5に搭載される新機能が早々とひとつ決まっていました。
ということで以下は該当のRFC、Support Closures in constant expressionsの紹介です。
PHP RFC: Support Closures in constant expressions
Introduction
いくつかのPHP機能は、定数式のみを受け入れるようになっています。
これらの式は、大まかに"不変値"とみなせる限られた演算を含めることができます。
特にアトリビュートの引数は定数式のみを受け入れる構造ですが、クロージャは許可されていません。
クロージャはPHPオペコードであり、一部機能を除けば不変値であるため、定数式とみなすことができます。
定数式としてクロージャを使用できることで、幾つかのユースケースが生まれます。
以下は引数$callback
をnullableにすることなく、デフォルト値としてクロージャを設定した、ユーザランドによるarray_filter()の実装例です。
function my_array_filter(
array $array,
Closure $callback = static function ($item) { return !empty($item); },
) {
$result = [];
foreach ($array as $item) {
if ($callback($item)) {
$result[] = $item;
}
}
return $result;
}
var_dump(my_array_filter([
0, 1, 2,
'', 'foo', 'bar',
]));
Proposal
このRFCでは、定数式にクロージャを含めることを提案します。
次の個所が対象となります。
・アトリビュートの引数。
・プロパティと引数のデフォルト値。
・定数、クラス定数。
Constraints
クロージャを定数式として使用する場合、以下の制限がつきます。
スコープがないため、use
を用いて変数をキャプチャすることはできません。
また、矢印記号を用いたアロー関数は暗黙的に変数をキャプチャするため、サポートしません。
この制限は、定数式として変数を使うことができないという制約と一致しています。
クロージャはstaticでなければなりません。
つまり$this
にアクセスすることはできません。
$this
はクロージャの再評価が必要となるため、一回しか評価されない定数式として取り扱うことはできません。
これらの制限は、コンパイル時に検証されます。
Scoping
他の定数式と同様に、定数式として定義されたクロージャは、それが配置されているコンテキストのスコープにあります。
即ち、プロパティデフォルト値のクロージャは、同クラスのprivateプロパティ、メソッド、クラス定数にアクセスできます。
これは、コンストラクタでプロパティに定義したクロージャがアクセスできるのと同じ理由です。
同様に、アトリビュート引数のクロージャは、同クラスのprivateメンバーにアクセス可能です。
Closures in sub-expressions
クロージャは定数式内の他の演算と同様に動作するため、演算の一部になる場合があります。
クロージャを式の一部として使うことは特に便利ではありませんが、動作はします。
ただ、デフォルト値としてクロージャのリストを定義することは、便利になる例のひとつです。
function foo(
string $input,
array $callbacks = [
static function ($value) {
return \strtoupper($value);
},
static function ($value) {
return \preg_replace('/[^A-Z]/', '', $value);
},
]
) {
foreach ($callbacks as $callback) {
$input = $callback($input);
}
return $input;
}
var_dump(foo('Hello, World!')); // string(10) "HELLOWORLD"
new
にクロージャを渡す例。
class MyObject
{
public function __construct(private Closure $callback) {}
}
const Foo = new MyObject(static function () {
return 'foo';
});
Use Cases
ユースケース。
アトリビュートベースの検証ライブラリによる、カスタムフィールド検証の例。
final class Locale
{
#[Validator\Custom(static function (string $languageCode): bool {
return \preg_match('/^[a-z][a-z]$/', $languageCode);
})]
public string $languageCode;
}
テストライブラリによる、テストケースの生成。
final class CalculatorTest
{
#[Test\CaseGenerator(static function (): iterable {
for ($i = -10; $i <= 10; $i++) {
yield [$i, $i, 0];
yield [$i, 0, $i];
yield [0, $i, ($i * -1)];
}
})]
public function testSubtraction(int $minuend, int $subtrahend, int $result)
{
\assert(Calculator::subtract($minuend, $subtrahend) === $result);
}
}
アトリビュートベースのシリアライズライブラリによる、カスタムフォーマット。
final class LogEntry
{
public string $message;
#[Serialize\Custom(static function (string $severity): string {
return \strtoupper($severity);
})]
public string $severity;
}
Backward Incompatible Changes
互換性のない変更点はありません。
以前は、定数式としてクロージャを書くとコンパイルエラーが出ていました。
静的アナライザーとIDEは、無効だった式が有効になる他の全てのRFCと同じく、この式をエラーにしないようにする必要があります。
Proposed PHP Version(s)
PHP8.5
RFC Impact
Opcacheは、SHMに定数式を保存する部分を修正する必要があります。
プルリクエストには既に修正が含まれており、Opcache・JITを有効にした状態で全てのテストに合格します。
Open Issues
既知の問題はありません。
Unaffected PHP Functionality
影響を受けないPHP機能。
影響を受けるのは定数式だけであり、以前はクロージャを書けなかったところに書けるようになるだけです。
Future Scope
将来の展望であり、このRFCには含まれていません。
・staticでないクロージャのサポート
・第一級callableのサポート
・変数キャプチャのサポート
Proposed Voting Choices
本RFCは2024/11/13から2024/11/27まで投票が行われ、賛成19反対0の全員賛成で受理されました。
Patches and Tests
Implementation
感想
一番わかりやすい例は定数だと思うので、RFCには真っ先にこれを乗っけてほしかった。
const CLOSURE = static function () {
echo "called", PHP_EOL;
};
var_dump(CLOSURE); // object(Closure)
(CLOSURE)(); // called
// PHP8.4 Fatal error: Constant expression contains invalid operations
これまで定数には固定値、もしくは単純な式しか書くことができませんでしたが、クロージャも置けるようになったのでより複雑なことが可能になります。
プルリクにはより多くのテストケース・使用例が載っているので参考にするとよいでしょう。
ただまあ、私はPHPの特殊な使い方をあんまりしていないせいもありますが、正直メリットがよくわかりませんでした。
メリットとして挙げられている例も、アトリビュート以外はこれまでの書き方をしてもそんなに苦労せずに同じことができますし、アトリビュートは普通に使っているだけでは滅多に遭遇しませんからね。
しかしRFC提出からわずか一ヶ月で受理、さらに全員賛成という結果が示すように支持は圧倒的であり、もっと深い使い方をしている人にとっては非常に役立つ文法なのではないかと思われます。