bindTo的指向改变

Posted by

在webman框架下,利用EasyWeChat SDK自定义Logger时,意外发现会引发进程崩溃退出,涉及代码如下,主要利用extend来装载。

public function getMiniApp(): Application
{
    // ... 初始化小程序配置

    $app = Factory::miniProgram($config);
    
    // 配置自定义 logger
    $app->logger->extend('stdout', function () {
        return $this->reloadLogger();  // 这里导致进程崩溃
    });
    
    return $app;
}

public function reloadLogger(): Logger
{
    $configs = config('log', []);
    $logger = new Logger('default');
    // ... 配置 logger handlers
    return $logger;
}

定位看了一下sdk底层extend的处理方式,传进来的闭包,它这里使用了bindTo,并且两个参数都是$this。很少用到这个方法,查看一下文档解释。

class LogManager implements LoggerInterface
{
 
    /**
     * Register a custom driver creator Closure.
     *
     * @param  string  $driver
     * @param \Closure $callback
     *
     * @return $this
     */
    public function extend(string $driver, \Closure $callback): LogManager
    {
        $this->customCreators[$driver] = $callback->bindTo($this, $this);

        return $this;
    } 

} 

bindTo方法签名

/**
 * Duplicates the closure with a new bound object and class scope
 * @link https://secure.php.net/manual/en/closure.bindto.php
 * @param object|null $newThis The object to which the given anonymous function should be bound, or NULL for the closure to be unbound.
 * @param object|class-string|null $newScope The class scope to which associate the closure is to be associated, or 'static' to keep the current one.
 * If an object is given, the type of the object will be used instead.
 * This determines the visibility of protected and private methods of the bound object.
 * @return Closure|null Returns the newly created Closure object or null on failure
 */
#[Pure]
public function bindTo(?object $newThis, object|string|null $newScope = 'static'): ?Closure {}
参数作用说明
$newThis改变闭包内 $this 的指向闭包内 $this 将指向传入的对象
$newScope改变闭包的访问作用域控制闭包能访问哪个类的私有/保护成员

在代码中,这里通过`$this->reloadLogger()`来获取Logger,而sdk的extend方法把这个闭包的$this的作用域指向了LogManager,于是导致在执行时找不到reloadLogger方法。

那bindTo第二个参数的具体怎么使用呢?

<?php

class Original {
    private $secret = 'Original Secret';
    protected $data = 'Original Data';

    public function getClosure() {
        return function() {
            echo "Scope: " . get_class($this) . "\n";
            echo "Private: {$this->secret}\n";
            echo "Protected: {$this->data}\n";
        };
    }
}

class Target {
    private $secret = 'Target Secret';
    protected $data = 'Target Data';

    /**
     * @return void
     */
    public function targetMethod(): void
    {
        echo "\n this is from target method";
    }
}

$original = new Original();
$target = new Target();
$closure = $original->getClosure();

echo "=== 情况 1: bindTo(\$target) - 保持原作用域 ===\n";
$bound1 = $closure->bindTo($target);
try {
    $bound1();
    // ❌ $this 是 Target,但作用域是 Original
    // 访问 $this->secret 会失败(Target 没有 Original 的 $secret)
} catch (Error $e) {
    echo "错误: 无法访问\n";
}

echo "\n=== 情况 2: bindTo(\$target, Target::class) - 改变作用域 ===\n";
$bound2 = $closure->bindTo($target, $target);
$bound2();
// ✅ 输出:
// Scope: Target
// Private: Target Secret
// Protected: Target Data

echo "\n=== 情况 3: bindTo(\$target, null) - 移除作用域 ===\n";
$bound3 = $closure->bindTo($target, null);
try {
    $bound3();
} catch (Error $e) {
    echo "错误: 无法访问私有/保护成员\n";
    // ❌ 没有作用域,无法访问任何私有/保护属性
}

即这里LogManager使用bindTo将该闭包函数的$this指向了自身,同时还允许该闭包访问自己内部的受保护的属性和方法,这样能调用它内部函数比如prepareHandlers、formatter等,减少外部代码,统一封装,作为组件这样处理便可以理解了。

Leave a Reply

您的邮箱地址不会被公开。 必填项已用 * 标注