First-class 可调用语法
概述
First-class 可调用语法(First-class Callable Syntax)是 PHP 8.1 引入的特性,允许使用 function_name(...) 的简洁语法从可调用结构创建 Closure 对象。这是 Closure::fromCallable() 的语法糖,使代码更简洁,尤其在将函数或方法作为回调传递时非常实用。
PHP 版本说明
First-class 可调用语法自 PHP 8.1 引入。它是 Closure::fromCallable() 的替代写法,行为完全一致。支持用于普通函数、类方法、静态方法和闭包。
基础概念
什么是 First-class 可调用
"First-class"(一等)意味着函数/方法可以像值一样被传递、存储和操作。PHP 8.1 的 func(...) 语法让创建可调用引用变得更简单。
与 Closure::fromCallable 对比
| 写法 | 引入版本 | 说明 |
|---|---|---|
Closure::fromCallable('strlen') | PHP 7.1 | 完整写法 |
strlen(...) | PHP 8.1 | 简洁语法糖 |
[$obj, 'method'](...) | PHP 8.1 | 对象方法引用 |
ClassName::method(...) | PHP 8.1 | 静态方法引用 |
语法与代码
基本语法
使用 函数名(...) 创建函数的 Closure 引用。
php
<?php
declare(strict_types=1);
// 从普通函数创建 Closure
$strlen = strlen(...);
echo $strlen('hello') . "\n"; // 5
$strtoupper = strtoupper(...);
echo $strtoupper('hello') . "\n"; // HELLO
// 等价的旧写法
$strlenOld = Closure::fromCallable('strlen');
echo $strlenOld('hello') . "\n"; // 5
// 创建的仍然是 Closure 对象
echo get_class($strlen) . "\n"; // Closure对象方法引用
php
<?php
declare(strict_types=1);
class StringHelper
{
public function format(string $text): string
{
return trim(strtolower($text));
}
public function repeat(string $text, int $times = 2): string
{
return str_repeat($text, $times);
}
public static function slug(string $text): string
{
return strtolower(preg_replace('/[^a-z0-9]+/', '-', $text));
}
}
$helper = new StringHelper();
// 从实例方法创建 Closure
$formatter = $helper->format(...);
echo $formatter(' HELLO WORLD ') . "\n"; // hello world
// 从静态方法创建 Closure
$slugMaker = StringHelper::slug(...);
echo $slugMaker('Hello World PHP') . "\n"; // hello-world-php
// 使用数组语法
$repeater = [$helper, 'repeat'](...);
echo $repeater('Hi ', 3) . "\n"; // Hi Hi Hi
// 静态方法的数组语法
$slugMaker2 = [StringHelper::class, 'slug'](...);
echo $slugMaker2('Test String') . "\n"; // test-string在数组函数中使用
First-class 可调用语法最常见的用途是作为数组函数的回调。
php
<?php
declare(strict_types=1);
$names = ['alice', 'BOB', 'Charlie', 'DAVE'];
// 使用 First-class 语法
$upper = array_map(strtoupper(...), $names);
print_r($upper);
// Array ( [0] => ALICE [1] => BOB [2] => CHARLIE [3] => DAVE )
$lower = array_map(strtolower(...), $names);
print_r($lower);
// Array ( [0] => alice [1] => bob [2] => charlie [3] => dave )
$trimmed = array_map(trim(...), [' hello ', ' world ']);
print_r($trimmed);
// Array ( [0] => hello [1] => world )
// array_filter
$values = [0, 1, '', 'hello', false, null, 42];
$filtered = array_filter($values, fn($v) => $v !== null && $v !== '' && $v !== 0 && $v !== false);
// 自定义类方法作为回调
class NumberValidator
{
public function isPositive(int $n): bool
{
return $n > 0;
}
public function isEven(int $n): bool
{
return $n % 2 === 0;
}
}
$validator = new NumberValidator();
$numbers = [-3, -1, 0, 2, 5, 8, 10];
$positives = array_filter($numbers, $validator->isPositive(...));
print_r($positives); // Array ( [3] => 2 [4] => 5 [5] => 8 [6] => 10 )
$evens = array_filter($numbers, $validator->isEven(...));
print_r($evens); // Array ( [3] => 2 [5] => 8 [6] => 10 )与闭包和箭头函数配合
php
<?php
declare(strict_types=1);
// 将 first-class callable 作为参数传递
function applyTransformers(string $input, callable ...$transformers): string
{
$result = $input;
foreach ($transformers as $transformer) {
$result = $transformer($result);
}
return $result;
}
// 组合多个转换
$result = applyTransformers(
' Hello, World! ',
trim(...),
strtolower(...),
fn(string $s): string => str_replace('world', 'PHP', $s),
ucfirst(...)
);
echo $result . "\n"; // Hello, php!命名参数兼容
First-class 可调用语法与 PHP 8.0 的命名参数完全兼容。
php
<?php
declare(strict_types=1);
// array_slice(string|int $array, int $offset, ?int $length, bool $preserve_keys)
$slicer = array_slice(...);
$fruits = ['apple', 'banana', 'cherry', 'date', 'elderberry'];
// 使用命名参数调用 first-class callable
echo implode(', ', $slicer($fruits, offset: 2, preserve_keys: true)) . "\n";
// cherry, date, elderberry
// htmlspecialchars(string $string, int $flags, string $encoding, bool $double_encode)
$escape = htmlspecialchars(...);
$html = '<b>Bold</b> & "Quoted"';
echo $escape($html, flags: ENT_QUOTES, encoding: 'UTF-8') . "\n";
// <b>Bold</b> & "Quoted"详细说明
内部机制
func(...) 语法在编译时转换为 Closure::fromCallable('func')。两者行为完全一致。
php
<?php
declare(strict_types=1);
// 以下两种写法完全等价
$closure1 = strlen(...);
$closure2 = Closure::fromCallable('strlen');
// 都返回 Closure 实例
echo $closure1('test') . "\n"; // 4
echo $closure2('test') . "\n"; // 4
// 都支持 call_user_func 调用
echo call_user_func($closure1, 'hello') . "\n"; // 5
echo call_user_func($closure2, 'hello') . "\n"; // 5作用域和 $this 绑定
使用实例方法创建的 First-class callable 会正确绑定 $this 上下文。
php
<?php
declare(strict_types=1);
class Counter
{
private int $count = 0;
public function increment(): int
{
return ++$this->count;
}
public function getCount(): int
{
return $this->count;
}
public function getIncrementer(): Closure
{
// 返回绑定到当前实例的闭包
return $this->increment(...);
}
}
$counter = new Counter();
$incrementer = $counter->getIncrementer();
echo $incrementer() . "\n"; // 1
echo $incrementer() . "\n"; // 2
echo $incrementer() . "\n"; // 3
echo $counter->getCount() . "\n"; // 3实战示例
事件处理器注册
php
<?php
declare(strict_types=1);
class EventDispatcher
{
/** @var array<string, Closure[]> */
private array $handlers = [];
public function register(string $event, callable $handler): void
{
// 自动转换为 Closure
$this->handlers[$event][] = Closure::fromCallable($handler);
}
public function dispatch(string $event, mixed ...$data): void
{
foreach ($this->handlers[$event] ?? [] as $handler) {
$handler(...$data);
}
}
}
class UserService
{
public function onUserCreated(array $user): void
{
echo "UserService: New user {$user['name']}\n";
}
public function onUserDeleted(int $userId): void
{
echo "UserService: User #{$userId} deleted\n";
}
}
class EmailService
{
public function sendWelcome(array $user): void
{
echo "EmailService: Welcome email to {$user['email']}\n";
}
}
$dispatcher = new EventDispatcher();
$userService = new UserService();
$emailService = new EmailService();
// 使用 first-class callable 语法注册事件处理器
$dispatcher->register('user.created', $userService->onUserCreated(...));
$dispatcher->register('user.created', $emailService->sendWelcome(...));
$dispatcher->register('user.deleted', $userService->onUserDeleted(...));
$dispatcher->dispatch('user.created', ['name' => 'Alice', 'email' => 'alice@example.com']);
$dispatcher->dispatch('user.deleted', 42);数据处理管道
php
<?php
declare(strict_types=1);
// 使用 first-class callable 构建可复用的数据处理管道
function pipe(callable ...$operations): Closure
{
return fn(mixed $input): mixed => array_reduce(
$operations,
fn(mixed $value, callable $op): mixed => $op($value),
$input
);
}
// 定义管道
$processText = pipe(
trim(...),
strtolower(...),
fn(string $s): string => preg_replace('/\s+/', ' ', $s),
);
echo $processText(' HELLO WORLD ') . "\n"; // hello world
$processNumber = pipe(
fn(mixed $v): int => (int) $v,
abs(...),
fn(int $n): int => $n * 2,
);
echo $processNumber('-42') . "\n"; // 84注意事项
语法限制
...不能省略,必须有括号
php
<?php
declare(strict_types=1);
// 正确
$closure = strlen(...);
// 错误:不能省略 (...)
// $closure = strlen; // 解析为常量或未定义标识符
// $closure = strlen(); // 这是调用函数,不是创建引用- 不能对表达式使用此语法
php
<?php
declare(strict_types=1);
// 正确:使用变量
$func = 'strlen';
// $closure = $func(...); // PHP 8.1 中不支持(仅支持字面量函数名/方法名)
// 替代方案
$closure = Closure::fromCallable($func);- 不能直接用于闭包变量
php
<?php
declare(strict_types=1);
$myClosure = fn(string $s): string => strtoupper($s);
// $ref = $myClosure(...); // 不支持
// $ref = Closure::fromCallable($myClosure); // 这是可以的
// 但闭包本身就是 Closure,可以直接使用
echo $myClosure('hello') . "\n"; // HELLO最佳实践
1. 优先使用 First-class 语法替代 Closure::fromCallable
php
<?php
declare(strict_types=1);
// 推荐(PHP 8.1+)
$mappers = array_map(strtoupper(...), ['a', 'b', 'c']);
// 不推荐
$mappersOld = array_map(Closure::fromCallable('strtoupper'), ['a', 'b', 'c']);
// 不推荐(使用箭头函数重复包装)
$mappersArrow = array_map(fn(string $s) => strtoupper($s), ['a', 'b', 'c']);2. 方法引用让代码更具声明性
php
<?php
declare(strict_types=1);
class Formatter
{
public function bold(string $text): string
{
return "**{$text}**";
}
public function italic(string $text): string
{
return "_{$text}_";
}
}
$formatter = new Formatter();
$words = ['hello', 'world', 'php'];
// 推荐:直接引用方法
$boldWords = array_map($formatter->bold(...), $words);
// 不推荐:每次创建新闭包
$boldWordsOld = array_map(fn(string $w) => $formatter->bold($w), $words);3. 在容器/服务注册中使用
php
<?php
declare(strict_types=1);
class Container
{
/** @var array<string, callable> */
private array $bindings = [];
public function bind(string $key, callable $resolver): void
{
$this->bindings[$key] = $resolver;
}
public function resolve(string $key): mixed
{
return ($this->bindings[$key])();
}
}
class Logger
{
public static function create(): self
{
return new self();
}
}
class Database
{
public static function connect(string $dsn): self
{
return new self();
}
}
$container = new Container();
// 使用 first-class callable 注册服务
$container->bind('logger', Logger::create(...));
$container->bind('db', fn() => Database::connect('mysql://localhost'));