Skip to content

Closure / Generator / Fiber 类

概述

PHP 提供了三种特殊的内部类来支持高级控制流和函数式编程特性:

  • Closure — 匿名函数的内部类,提供 bind()call() 等方法来操作闭包的绑定上下文
  • Generator — 生成器的内部类,是 yield 关键字产生的对象,提供 getReturn()getCurrent() 等方法
  • Fiber — 协程/纤程(PHP 8.1+),提供轻量级的并发执行能力,可暂停和恢复执行

这三者虽然各自服务于不同的目的,但都是 PHP 中控制流抽象的重要组成部分。

版本说明

  • Closure 类从 PHP 5.3 起可用(随匿名函数引入)
  • Generator 类从 PHP 5.5 起可用(随 yield 关键字引入)
  • Fiber 类在 PHP 8.1 中引入,PHP 8.2+ 支持在 Fiber 中使用 set_exception_handler

基础概念

Closure 类

Closure 类是所有匿名函数(闭包)的实例类型。它提供了操作闭包绑定上下文($this 和作用域)的方法。

Generator 类

Generator 实现了 Iterator 接口,是包含 yield 的函数返回的对象。生成器允许惰性求值和协程式编程。

Fiber 类

Fiber 是 PHP 8.1 引入的协程原语,允许在函数执行过程中暂停(Fiber::suspend()),并在外部恢复(Fiber->resume()),实现协作式多任务。

语法与代码

Closure 类常用方法

php
<?php

declare(strict_types=1);

class User
{
    private string $name = 'Alice';

    public function getName(): string
    {
        return $this->name;
    }
}

// Closure::bind() — 绑定闭包到指定对象
$closure = function (): string {
    return $this->name;  // 访问私有属性
};

$user = new User();

// bind 绑定闭包到 $user 对象,并允许访问私有成员
$bound = Closure::bind($closure, $user, User::class);
echo $bound();  // Alice

// Closure::bind() 创建新闭包,原闭包不变

// Closure::call() — 绑定并立即调用(PHP 7.0+)
$result = $closure->call($user);
echo $result;  // Alice
// call() 更简洁,且性能更好

// Closure::fromCallable() — 从可调用对象创建闭包
$func = Closure::fromCallable('strlen');
echo $func('hello');  // 5

$method = Closure::fromCallable([$user, 'getName']);
echo $method();       // Alice

Generator 类常用方法

php
<?php

declare(strict_types=1);

function rangeGenerator(int $start, int $end): Generator
{
    for ($i = $start; $i <= $end; $i++) {
        yield $i => $i * $i;
    }
}

$gen = rangeGenerator(1, 5);

// Generator 实现了 Iterator 接口
// current() — 获取当前 yield 的值
echo $gen->current();  // 1(值)
echo $gen->key();       // 1(键)

// send() — 向生成器发送值(作为 yield 表达式的返回值)
function echoGenerator(): Generator
{
    $input = yield '等待输入...';
    echo "收到: {$input}\n";
    $input2 = yield '再次等待...';
    echo "又收到: {$input2}\n";
}

$echoGen = echoGenerator();
echo $echoGen->current();       // 等待输入...
$echoGen->send('Hello');        // 收到: Hello, 输出: 再次等待...
$echoGen->send('World');        // 又收到: World

// getReturn() — 获取生成器函数的返回值
function sumGenerator(array $numbers): Generator
{
    $sum = 0;
    foreach ($numbers as $num) {
        $sum += $num;
        yield $sum;
    }
    return $sum;  // 最终返回值
}

$sumGen = sumGenerator([1, 2, 3, 4, 5]);
// 需要先遍历完毕
foreach ($sumGen as $partial) {
    // partial: 1, 3, 6, 10, 15
}
echo $sumGen->getReturn();  // 15

Fiber 类(PHP 8.1+)

php
<?php

declare(strict_types=1);

// 创建 Fiber
$fiber = new Fiber(function (): void {
    $value = Fiber::suspend('第一次暂停');
    echo "恢复时收到的值: {$value}\n";

    $value2 = Fiber::suspend('第二次暂停');
    echo "再次恢复收到的值: {$value2}\n";
});

// 启动 Fiber,直到第一次 suspend
$result = $fiber->start();
echo "Fiber 返回: {$result}\n";  // Fiber 返回: 第一次暂停

// 恢复 Fiber,传入值
$result = $fiber->resume('Hello');
echo "Fiber 返回: {$result}\n";  // Fiber 返回: 第二次暂停

// 再次恢复
$fiber->resume('World');
// 输出: 恢复时收到的值: Hello
//        再次恢复收到的值: World

// Fiber::throw() — 向 Fiber 抛出异常
$fiber2 = new Fiber(function (): void {
    try {
        Fiber::suspend('暂停');
    } catch (RuntimeException $e) {
        echo "捕获到异常: {$e->getMessage()}\n";
    }
});

$fiber2->start();
$fiber2->throw(new RuntimeException('出错了'));
// 输出: 捕获到异常: 出错了

详细说明

Closure::bind() 详解

php
<?php

declare(strict_types=1);

class Order
{
    private string $status = 'pending';

    public function getStatus(): string
    {
        return $this->status;
    }
}

class OrderLogger
{
    // 使用闭包访问 Order 的私有属性
    public function getStatusExtractor(): Closure
    {
        $extractor = function (): string {
            return $this->status;
        };

        return Closure::bind($extractor, null, Order::class);
        // bind 参数:
        //   1. 闭包
        //   2. 绑定的 $this 对象(null 表示不绑定 $this)
        //   3. 作用域类(决定可访问的可见性级别)
    }
}

$logger = new OrderLogger();
$extractor = $logger->getStatusExtractor();

$order = new Order();
// 传入 $this 对象调用
$boundExtractor = Closure::bind($extractor, $order, Order::class);
echo $boundExtractor();  // pending

// 使用 call() 更简洁
echo $extractor->call($order);  // pending

Generator 高级用法

php
<?php

declare(strict_types=1);

// yield from — 委托生成器
function innerGenerator(): Generator
{
    yield 'a';
    yield 'b';
}

function outerGenerator(): Generator
{
    yield 'start';
    yield from innerGenerator();  // 委托给内部生成器
    yield 'end';
}

foreach (outerGenerator() as $value) {
    echo $value . ' ';  // start a b end
}

// yield from 返回值
function innerWithReturn(): Generator
{
    yield 'x';
    return 'inner_result';
}

function outerWithReturn(): Generator
{
    $result = yield from innerWithReturn();
    yield "inner returned: {$result}";
}

foreach (outerWithReturn() as $val) {
    echo $val . "\n";  // x, inner returned: inner_result
}

Fiber 的状态管理

php
<?php

declare(strict_types=1);

$fiber = new Fiber(function (): void {
    echo "Fiber 开始\n";
    Fiber::suspend();
    echo "Fiber 恢复\n";
});

// Fiber 状态检查
echo $fiber->getStatus();           // FiberState::Unstarted

$fiber->start();
echo $fiber->getStatus();           // FiberState::Suspended

$fiber->resume();
echo $fiber->getStatus();           // FiberState::Terminated

// Fiber 状态枚举:
// FiberState::Unstarted   — 未启动
// FiberState::Running     — 运行中
// FiberState::Suspended   — 已暂停
// FiberState::Terminated  — 已结束

Fiber 实现简单任务调度

php
<?php

declare(strict_types=1);

class SimpleScheduler
{
    /** @var array<int, Fiber> */
    private array $fibers = [];
    private int $currentId = 0;

    public function spawn(callable $task): int
    {
        $id = $this->currentId++;
        $scheduler = $this;

        $this->fibers[$id] = new Fiber(function () use ($task, $scheduler, $id): void {
            try {
                $task($id);
            } finally {
                $scheduler->remove($id);
            }
        });

        return $id;
    }

    public function remove(int $id): void
    {
        unset($this->fibers[$id]);
    }

    public function run(): void
    {
        while (!empty($this->fibers)) {
            foreach ($this->fibers as $id => $fiber) {
                if ($fiber->getStatus() === FiberStatus::Suspended) {
                    $fiber->resume();
                } elseif ($fiber->getStatus() === FiberStatus::Unstarted) {
                    $fiber->start();
                }
            }
        }
    }
}

// 注意:PHP 8.1 中 FiberStatus 是枚举类
// 使用 Fiber->isStarted()/isSuspended()/isTerminated() 检查状态

实战示例

使用 Closure 实现中间件

php
<?php

declare(strict_types=1);

class MiddlewarePipeline
{
    /** @var array<int, Closure> */
    private array $middlewares = [];

    public function add(Closure $middleware): self
    {
        $this->middlewares[] = $middleware;
        return $this;
    }

    public function run(Closure $core): mixed
    {
        // 从后向前构建中间件链
        $pipeline = array_reduce(
            array_reverse($this->middlewares),
            function (Closure $next, Closure $middleware): Closure {
                return fn (...$args): mixed => $middleware($next, ...$args);
            },
            $core
        );

        return $pipeline();
    }
}

$pipe = new MiddlewarePipeline();

$pipe->add(function (Closure $next): mixed {
    echo "[1] 请求前处理\n";
    $result = $next();
    echo "[1] 响应后处理\n";
    return $result;
});

$pipe->add(function (Closure $next): mixed {
    echo "[2] 认证检查\n";
    $result = $next();
    echo "[2] 日志记录\n";
    return $result;
});

$result = $pipe->run(function (): string {
    echo "[核心] 处理业务逻辑\n";
    return '完成';
});

// 输出:
// [1] 请求前处理
// [2] 认证检查
// [核心] 处理业务逻辑
// [2] 日志记录
// [1] 响应后处理

使用 Generator 实现管道

php
<?php

declare(strict_types=1);

function csvReaderGenerator(string $filePath): Generator
{
    $handle = fopen($filePath, 'r');
    if ($handle === false) {
        return;
    }

    $headers = fgetcsv($handle);
    if ($headers === false) {
        fclose($handle);
        return;
    }

    while (($row = fgetcsv($handle)) !== false) {
        yield array_combine($headers, $row);
    }

    fclose($handle);
}

function filterGenerator(Generator $source, Closure $filter): Generator
{
    foreach ($source as $item) {
        if ($filter($item)) {
            yield $item;
        }
    }
}

function mapGenerator(Generator $source, Closure $mapper): Generator
{
    foreach ($source as $item) {
        yield $mapper($item);
    }
}

function limitGenerator(Generator $source, int $limit): Generator
{
    $count = 0;
    foreach ($source as $item) {
        if ($count >= $limit) {
            break;
        }
        yield $item;
        $count++;
    }
}

// 惰性管道:数据按需处理,不会一次性加载到内存
// $pipeline = limitGenerator(
//     mapGenerator(
//         filterGenerator(csvReaderGenerator('data.csv'), fn($row) => $row['active']),
//         fn($row) => ['name' => $row['name'], 'email' => $row['email']]
//     ),
//     100
// );

注意事项

Fiber 的限制

php
<?php

declare(strict_types=1);

// Fiber 不能跨越某些边界
// 1. Fiber 中的异常如果不捕获,会终止 Fiber
$fiber = new Fiber(function (): void {
    throw new RuntimeException('未捕获的异常');
});

$fiber->start();  // 抛出 RuntimeException

// 2. Fiber 中不能使用 return 返回值(用 suspend 返回)
// Fiber 函数的返回值在 terminated 后不可获取

// 3. Fiber 的栈是独立的
$fiber = new Fiber(function (): void {
    global $counter;  // 不推荐,Fiber 有独立作用域
});

Fiber 注意事项

  • Fiber 不会自动并行执行,它是协作式多任务,需要手动调度
  • 主线程 Fiber 不能被 suspend(Fiber::suspend() 只能在 Fiber 内调用)
  • Fiber 中的异常如果不处理会导致 Fiber 终止,异常通过 Fiber->getException() 获取

Closure::bind() 的性能

php
<?php

declare(strict_types=1);

// Closure::call() 比 Closure::bind() + 调用 更快
// call() 是 PHP 7.0 引入的优化版本

$closure = function (): string {
    return $this->name;
};

$user = new class {
    private string $name = 'Test';
};

// 方式 1: bind + 调用(两步)
$bound = Closure::bind($closure, $user, get_class($user));
$result = $bound();

// 方式 2: call(一步,推荐)
$result = $closure->call($user);

最佳实践

  1. 优先使用 Closure::call():比 Closure::bind() 更简洁高效
  2. Generator 用于大数据集:避免一次性加载大量数据到内存
  3. Fiber 适用于 I/O 密集型任务:如并发 HTTP 请求、数据库查询等
  4. Closure::fromCallable() 类型安全:将可调用对象转为统一的 Closure 类型
  5. Generator 注意资源释放:在 finally 块中释放文件句柄等资源
php
<?php

declare(strict_types=1);

// Generator 资源安全模式
function safeGenerator(string $filePath): Generator
{
    $handle = fopen($filePath, 'r');
    if ($handle === false) {
        return;
    }

    try {
        while (($line = fgets($handle)) !== false) {
            yield rtrim($line, "\r\n");
        }
    } finally {
        // 无论生成器如何结束(遍历完成、抛异常、unset)
        // finally 块确保资源被释放
        fclose($handle);
    }
}

参考链接