在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等,减少外部代码,统一封装,作为组件这样处理便可以理解了。