看到一个很有意思的项目 https://github.com/php-defer/php-defer,这个项目只用了10行左右的代码就实现了Go语言中的defer,看来灵活运用数据结构还是很重要的。项目源码如下:
<?php
function defer(?SplStack &$context, callable $callback): void {
    $context ??= new class extends SplStack {
        public function __destruct() {
            while ($this->count() > 0) {
                call_user_func($this->pop());
            }
        }
    };
    $context->push($callback);
}
略显抽象,怎么来理解它呢?$context实际上就是一个栈变量,通过&达到作用域内共用一个栈的效果,作用域内所有defer的回调函数都放到$context栈中。
关键点在于new class extends SplStack一个匿名类继承并改写了SplStack类的__destruct(),使得$context销毁时可以逆序调用入栈的回调函数,用法如下。
<?php
function foo(): void {
    defer($_, function () {
        echo "first defer\n";
    });
    defer($_, function () {
        echo "second defer\n";
    });
    echo "before exception\n";
    throw new Exception('My exception');
}
try {
    foo();
} catch (Exception $e) {
    echo $e->getMessage(), "\n";
}
相当于下面的Go代码。
package main
import "fmt"
func foo() {
    defer fmt.Println("first defer")
    defer fmt.Println("second defer")
    fmt.Println("before exception")
    panic("My exception")
}
func main() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println(r)
        }
    }()
    foo()
}
调用结果如下。
$ php defer.php
before exception
second defer
first defer
My exception
跟我们设想的一样,defer回调函数确实是逆序输出的,跟Go语言的效果一致,可以再看看return的。
<?php
function foo(): string {
    defer($_, function () {
        echo "first defer\n";
    });
    defer($_, function () {
        echo "second defer\n";
    });
    return "foo";
}
echo foo(), "\n";
对应以下Go代码。
package main
import "fmt"
func foo() string {
    defer fmt.Println("first defer")
    defer fmt.Println("second defer")
    return "foo"
}
func main() {
    fmt.Println(foo())
}
输出如下:
$ php defer.php
second defer
first defer
foo
无论是异常还是返回,该defer函数的行为都跟Go语言的defer语句一致。
小小改造一下它,让它可以接收参数。
<?php
function defer(?SplStack &$context, callable $callback, ...$args): void {
    $context ??= new class extends SplStack {
        public function __destruct() {
            while ($this->count() > 0) {
                $item = $this->pop();
                $func = array_shift($item);
                if (empty($item)) {
                    call_user_func($func);
                } else {
                    call_user_func($func, ...$item);
                }
            }
        }
    };
    $context->push([$callback, ...$args]);
}
变化之处就在增加不限数量的参数...$args,入栈时,以数组形式入栈,弹栈时,先获取数据的第一个元素,调用函数分为带参数和不带参数的区别,用法跟先前几乎一样。
<?php
defer($_, function ($item) {
    printf("item: %s\n", $item);
}, "first");
defer($_, function ($item) {
    printf("item: %s\n", $item);
}, "second");