PHP闭包访问私有属性,导致IDE(PhpStorm)提示“member has private access”错误

发布于 2022-09-11 14:30:22 字数 5610 浏览 21 评论 0

Composer的autoload_static.php里getInitializer函数访问了ClassLoader类的私有属性,导致IDE(PhpStorm)提示“member has private access”错误,如图红框所示:

图片描述

而实际上这不属于错误,而是IDE的误报,但百度、Google了好久,也没找到关闭该错误提示的方法,哪位遇到过类似的问题,怎么抑制或者取消这个错误提示,但又不能影响其他正常的错误提示呢?明明不属于错误,PhpStorm却总是显示红线提示错误,强迫症表示不能忍啊......

========================================================================================
20181019PM补充:

多位答主一开始都说代码有问题,但实际上代码确实是没有问题的,之所以觉得有问题,原因在于对闭包的理解上。鉴于PHP官方文档的晦涩含糊,下面将我自己的理解写一下供参考,如有错漏,请大家指正。

先说一下我对闭包的理解。

所谓闭包,就是封闭打包,可以理解为:定义一段代码,同时对其当时的运行上下文有选择地做一个快照,并封闭打包捆绑在一起,用于在将来的某个时候让这段代码还在当初所选择的上下文中运行。

闭包的重要意义就在于此:可以封闭打包携带外部环境(即上下文)中的变量(即外部变量),因此闭包可以从其父作用域中继承变量,即便外部环境已经销毁,也不影响闭包中之前已经封闭打包(专业术语一般称之为绑定)了的外部变量。

在PHP中,闭包中所使用到的任何其父作用域中的外部变量都应该使用use关键字传入到闭包函数中去(但自PHP 7.1起,不能传入此类变量:superglobals、$this或者和参数重名),这一点与全局变量必须通过global关键字引入貌似有点异曲同工的意味。

注意:在PHP5.3+中,匿名函数即是闭包函数(参见PHP官方文档说明),当定义了一个匿名函数时,就是隐式地创建了一个Closure闭包类的对象实例。

下面说一下上面截图中所使用到的Closure::bind方法。由于Closure::bind方法实际为Closure::bindTo方法的静态版本,所以下面重点介绍Closure::bindTo方法。

Closure::bindTo方法的定义:public Closure Closure::bindTo(object $newthis[, mixed $newscope = 'static'])
Closure::bind方法的定义:public static Closure Closure::bind(Closure $closure, object $newthis[, mixed $newscope = 'static'])

Closure::bindTo实际为Closure类的实例方法(这个双冒号::的写法实在是让人误解),而Closure::bind则为Closure类的静态方法,属于Closure::bindTo这个实例方法的静态版本。

Closure::bindTo方法作为Closure类的实例方法,其功能为将当前闭包对象(即该实例方法所属的闭包函数对象实例,简称为闭包对象)复制为一新的闭包对象,并可以绑定指定的$this对象和类作用域到该复制的新闭包对象中,然后返回该新闭包对象。

该返回的新闭包对象与当前闭包对象的函数体相同、绑定的外部变量(即use关键字所引入的变量)也相同,但因为可以绑定不同的$this对象,从而可以改变当前闭包对象函数体中所使用的$this对象的属性与方法,即在返回的新闭包对象中指向了新的$this对象的属性与方法;也因为可以绑定新的类作用域,从而可以改变外部变量的作用域,这将使得外部变量(为一个对象)的作用域为新绑定的类(该类即为外部变量对象所属的类)作用域,也就是说闭包中所引入的外部变量相当于位于该新绑定的类或者说该类的对象实例之中。

因此,如果在闭包中使用该外部变量对象的私有属性是完全符合语法规则的,因为闭包的一个重要特性即是可以将其所在的外部环境(这里指的就是新绑定的类作用域)封闭打包,从而使得在闭包中可以直接使用外部环境中的变量,即便该外部环境本身已经销毁,即便该变量在该外部环境中为私有变量。

Closure::bind方法只是Closure::bindTo方法的静态版本而已,功能类似,不再赘述。

示例:

一、Closure::bindTo()的示例
    <?php
        class A {
            function __construct($val) {
                $this->val = $val;
            }
            function getClosure() {
                //returns closure bound to this object and scope
                return function() { return $this->val; };
            }
        }
        
        $ob1 = new A(1);
        $ob2 = new A(2);
        
        $cl = $ob1->getClosure();
        echo $cl(), "\n";
        $cl = $cl->bindTo($ob2);
        echo $cl(), "\n";
    
    以上输出:
    1
    2
    
二、Closure::bind()的示例
    <?php
        class A {
            private static $sfoo = 1;
            private $ifoo = 2;
        }
        
        $cl1 = static function() {
            return A::$sfoo;
        };
        $cl2 = function() {
            return $this->ifoo;
        };
        
        $bcl1 = Closure::bind($cl1, null, 'A');
        $bcl2 = Closure::bind($cl2, new A(), 'A');
        echo $bcl1(), "\n";
        echo $bcl2(), "\n";
    
    以上输出:
    1
    2

========================================================================================
20181023再次补充:

针对答主kumfo的补充回答的回复:

1、Closure::bind函数的参数一是一个闭包(或者说匿名函数,即定义中的Closure $closure),相对于同样也是一个闭包的返回值而言,参数一所传入的可称之为源闭包,而Closure::bind函数的返回值相应地可称之为目标闭包;

2、Closure::bind函数的参数二是一个对象(即定义中的object $newthis),该参数仅在第一个参数(即源闭包)中使用了$this对象时其传入才有意义,而且也必须传入,否则如果传入null,闭包运行中将可能会报“Fatal error: Uncaught Error: Using $this when not in object context in XXX.php”之类的错误。该参数相对于源闭包中的$this对象而言,称之为“新”的$this对象(即定义中的object $newthis)。显然,参数二仅用于为源闭包中使用的$this绑定一个具体的对象,除此之外,貌似没有其他功能。至于你补充回答中的“Closure::bind()会自动把这个对象绑定到闭包的作用域,所以在闭包里访问私有属性直接以$this->这种形式访问”的说法是不准确的,这是因为闭包中能否访问$this对象的私有属性,要取决于第三个参数(是的,你没看错,闭包中的$this对象以及use关键字所引入的对象,能否访问它们的私有属性,均取决于第三个参数)。

3、Closure::bind函数的参数三是一个具有默认值'static'的参数,根据默认值来看参数三可以接受一个字符串类型的数据(即定义中的mixed $newscope = 'static'),但根据定义,貌似也可以接受其他类型,为简化说明,就以字符串类型为例来说好了。

根据官方文档,该参数用于给源闭包绑定一个类作用域(以类的完全限定名称来表示,即\Namespace\ClassName::class),相对于默认值'static'这个“旧”的作用域而言,该参数相当于给源闭包绑定了一个“新”的类作用域(你的补充回答中“题主后面做了的解释说是一个新类什么的”,是你没有仔细看我问题中的补充内容,我的补充内容中从头到尾都没有说过“新类”,只说了“新的类作用域”)。

那么该参数所绑定的类作用域(注意,不是“类的作用域”,而是“类作用域”,“类的作用域”往往容易让人理解为该类所在的外部环境,而“类作用域”特指类自身所构建的内部作用域,是该类内部的属性和方法所在的作用域或者说所处的外部环境),就是第二个参数$newthis和use关键字所引入的对象的作用域。

如果该参数没有传入实参(也就是使用其默认值'static'),或者传入了一个不是$this对象和use关键字所引入对象所属的类的类作用域(比如\OtherNamespace\OtherClassName::class),等于是源闭包内的$this对象(如果源闭包内使用了$this对象的话)和use关键字所引入的对象(如果源闭包使用use关键字引入了对象的话)所在的作用域(即两者所处的外部环境)不是两者所属的类的类作用域,而是处于其他类的类作用域或者是顶级作用域中(这相当于常规用法),显然是不能访问它们的私有属性的;而如果传入的是两者所属的类的类作用域,则相当于源闭包中的$this对象和use关键字所引入的对象(这种情况下use引入的对象与$this对象为同一类对象,但不是同一个对象,而是同一个类的不同实例)就处于它们自身所属的类之中,因而两者均能访问它们自身的私有属性就是很自然的了。

这正是闭包的特殊性所在————从代码表面上来看闭包中的对象(典型的如$this对象)明明好像已经不在其所属的类中,但实际上到底在不在其所属的类之中,取决于闭包所“封闭打包”的外部环境(即类作用域)。一般来讲,闭包所“封闭打包”(专业术语称之为“绑定”)的外部环境在该闭包定义时就已经确定了(或可称之为静态绑定,这种情况下代码看起来相对比较好理解),而Closure::bind函数无非是提供了一种可以动态地“封闭打包”闭包所处的外部环境的手段或者说工具而已(正是这种动态绑定使得代码乍看起来让人费解)。

作为一个码农,我其中一个很深的体会就是,必须细抠细节,一个细微的差别,如果没有注意到,理解的结果可能就大相径庭,甚至与实际情况南辕北辙。你的首次回答和补充回答中,都没有注意到一些我表述中的细节,因此你的两次回答对于我问题的理解都有错漏,导致得出了错误的结论。

无论如何,再次谢谢你的回答。

ps:源闭包中的$this对象和use关键字所引入的对象也可以是不属于同一个类的异种对象,但由于第三个参数只能传入其中一个对象所属类的类作用域,这将导致绑定后所返回的目标闭包在运行时必定会有其中一个对象因为访问了其私有属性而报错。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

七分※倦醒 2022-09-18 14:30:22

关于Closure::bind的解释

Closure::bind()有三个参数,官方文档有做说明,分别为:

  1. 闭包或者直接叫匿名函数;
  2. 参数一所传入的匿名函数所影响的作用域,也就是参数一所能影响的对象。
  3. 想要绑定给闭包的类作用域,或者 'static' 表示不改变。如果传入一个对象,则使用这个对象的类型名。 类作用域用来决定在闭包中 $this 对象的 私有、保护方法 的可见性。 (官方说法)

这里,注意第三点的说明,static表示不改变,也就是说,如果第三个参数传入'static',意思说闭包函数里的操作是按照正常语法逻辑来操作数据的,如private,proctected是不能直接访问的,如果第三个参数传入的是类名,那么在闭包内访问这个类的实例对象里的private,protected类型的属性或方法是允许的,可以说是不用管PHP本身面向对象的语法规则,相当于Closure::bind()这个方法改变了对象的性状。

我来看以下以下两段代码:

# 第一段 start
class Loader {
    private $prefixLengthPsr4 = "good";
    public function getPreFixLengthPsr4() {
        return $this->prefixLengthPsr4;
    }
};
$outer = new Loader();

$tt = Closure::bind(function() use($outer) {
    $outer->prefixLengthPsr4 = "change good";
},null,'Loader');

$tt();
var_dump($outer->getPreFixLengthPsr4());
# 第一段 end
# 第二段 start
class Loader {
    private $prefixLengthPsr4 = "good";
    public function getPreFixLengthPsr4() {
        return $this->prefixLengthPsr4;
    }
};
$outer = new Loader();

$tt = Closure::bind(function() {
    $outer->prefixLengthPsr4 = "change good";
},$outer,'Loader');
$tt();
var_dump($outer->getPreFixLengthPsr4());
# 第二段 end

这两段代码有什么区别呢,首先看第一段,闭包部分(我还是喜欢叫匿名函数):

function() use($outer) {
    $outer->prefixLengthPsr4 = "change good";
}

我们都知道闭包的执行依赖于执行时候的上下文环境,我想要使用当前环境的数据那么只能以use这种形式把当前的数据传入闭包,此处我传入了$outer对象,而我在Closure::bind()第三个参数指定了闭包所能影响的类的作用域为Loader这个类,而$outerLoader类的实例对象,所以我们通过Closure::bind()了之后,是可以直接访问私有属性的。

然后第二段,我给了Closure::bind()的第二个参数传入了$outer这个对象,Closure::bind()会自动把这个对象绑定到闭包的作用域,所以在闭包里访问私有属性直接以$this->这种形式访问。

然后,Closure::bind()其实返回的依旧是一个闭包,也就是返回Closure::bind()的第一个参数,但是由于Closure::bind()做了一圈(我不知道里面具体做了些什么事情)之后,把我指定的类作用域绑定到了闭包。所以我以$tt()(上面代码中的)来调用这个闭包。然后我通过$outer->getPreFixLengthPsr4()发现,这个闭包的确是把$outer里的属性修改了。

然后题主后面做了的解释说是一个新类什么的,我觉得是不正确的。我们知道函数(不管是正常的函数还是闭包,包括类的方法)的参数为对象的时候,传参都是引用传参,从上面测试可以看到闭包最后还是修改了外部传入对象的属性值可知。

以上是我做的分析,如果想要深究具体原因,那就需要看PHP源代码了,如果题主有精力的话,看完后可以回来告诉我一声具体Closure::bind()到底是干了些什么事情。

以下为原答案


你这面向对象没学好啊,这可以说已经属于语法错误了,怎么还不是错误?面向对象的private,proctected,public的特性搞清楚了没?还不能忍,有啥不能忍的。建议去看一下面向对象程序设计的语法规则,其他的话,没啥好建议。不想在这里再给你复述一遍语法规则。


然后,我回头再看了一下你想看啥,你想用这个Closure应该是想这样用吧?
我举个例子:


class Loader {
    private $prefixLengthPsr4 = "fuck";
    public function getPreFixLengthPsr4() {
        return $this->prefixLengthPsr4;
    }
};
$loader = new Loader();

$closure = Closure::bind(function() {
    $this->prefixLengthPsr4 = "what the fuck";
    return $this;
},$loader,'Loader');
别想她 2022-09-18 14:30:22

代码写漏了一个。
Closure::bind()第三个参数

newscope

想要绑定给闭包的类作用域,或者 'static' 表示不改变。如果传入一个对象,则使用这个对象的类型名。 类作用域用来决定在闭包中 $this 对象的 私有、保护方法 的可见性。 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.
学习了


搞了段代码测了一下。

分明定义两个对象

伪代码,大致相同,第16行处声明了之后然后立即调用方法,因为我没那么多东西去给测。

然后开始执行PHP的语法检查

php -l

返回内容为没有语法异常。

然后直接用cli跑代码

抛出了一个致命错误,根据异常提示得知其义为不得在外部访问受保护的属性。

但是题主说运行是没问题,那就继续,要知道在php的面相对象中还有两个魔术方法
__get__set , 接着将这两个方法写好了放进去。

然后切到 Test2 ,可以看到没有 error 了

但是还是有问题,毕竟有色差,鼠标放上去看一下。

大致意思为 这虽然是一个 受保护的成员,但是因为有 __set 方法的存在 所以我们允许了它
但是我们并不知道 __set 有没有为这个成员工作。所以我们也还是给了你语法异常提示。

问题到了这里 还有更好的解决方法吗?当然是有的

大部分语言都有一个东西 用来辅助开发 甚至有的还会辅助功能,**doc ,系列。
这里着重说 phpdoc (PHP Documentor)。(PS:突然梯子挂了上不去)具体的google自己查。

摘一段百度的

PHPDoc是PEAR下面的一个非常优秀的模块,它的目标是实现类似javadoc的功能,可以为你的代码快速生成具有相互参照,索引等功能的API文档。

具体怎么操作??

打开 Test1 在Class上面声明一个

然后回去看

诶,成了!

最后说一下。根据楼主提供的图和我实践的结果作比。我觉得题主提供的代码 应该也会报异常。
具体的可以看一下 ClassLoad 里面的实现和 XDebug 进行调试。如果确定问题存在,则就应该给相关的仓库提 issue 或者 自己提 PR。

这个问题从我被邀请 (在上班没看)。下班了在路上看到了有2个回答,其中一个答主耐心回答后,却换来的是一票反对,另外一个答主也被投了反对票。然后刚刚瞄了一眼,问题被投了一票反对。

等我坐下来准备写这个答案的时候,其中一为答主的答案不见了 可能是自己删除了。

然后我还是回答了,启机器,写代码,写答案,截图,传图。

在 问答区 尤其是PHP这块,其他的不太了解,有50%的问题都是提问者提了问 可能就当场qs了吧。

还有的就是 自己没解决方案 却还死杠的。

最后提一句,题主不像是个提问题的,倒像是个老板来 SF 搞视察一样。

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文