Skip to content

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";
// &lt;b&gt;Bold&lt;/b&gt; &amp; &quot;Quoted&quot;

详细说明

内部机制

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

注意事项

语法限制

  1. ... 不能省略,必须有括号
php
<?php

declare(strict_types=1);

// 正确
$closure = strlen(...);

// 错误:不能省略 (...)
// $closure = strlen;     // 解析为常量或未定义标识符
// $closure = strlen();   // 这是调用函数,不是创建引用
  1. 不能对表达式使用此语法
php
<?php

declare(strict_types=1);

// 正确:使用变量
$func = 'strlen';
// $closure = $func(...); // PHP 8.1 中不支持(仅支持字面量函数名/方法名)

// 替代方案
$closure = Closure::fromCallable($func);
  1. 不能直接用于闭包变量
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'));

参考链接