获取“间接修改重载属性无效”通知

我想用一个registry来存储一些对象。 这是一个简单的registry类实现。

<?php final class Registry { private $_registry; private static $_instance; private function __construct() { $this->_registry = array(); } public function __get($key) { return (isset($this->_registry[$key]) == true) ? $this->_registry[$key] : null; } public function __set($key, $value) { $this->_registry[$key] = $value; } public function __isset($key) { return isset($this->_registry[$key]); } public static function getInstance() { if (self::$_instance == null) self::$_instance = new self(); return self::$_instance; } } ?> 

当我尝试访问这个类时,我得到“间接修改重载属性无效”的通知。

 Registry::getInstance()->foo = array(1, 2, 3); // Works Registry::getInstance()->foo[] = 4; // Does not work 

我做错了什么?

这种行为已被报告为一个错误几次:

我不清楚讨论的结果是什么,尽pipe看来这与“按价值”和“借鉴”通过的价值观有关。 我在一些类似的代码中find的解决scheme是这样的:

 function &__get( $index ) { if( array_key_exists( $index, self::$_array ) ) { return self::$_array[ $index ]; } return; } function &__set( $index, $value ) { if( !empty($index) ) { if( is_object( $value ) || is_array( $value) ) { self::$_array[ $index ] =& $value; } else { self::$_array[ $index ] =& $value; } } } 

请注意,他们如何使用&__get&__set以及在赋值时使用& $value 。 我认为这是做这个工作的方法。

我知道这是一个老话题,但今天我第一次遇到了这个问题,如果用我自己的发现来扩充上述内容的话,我认为这可能对别人有所帮助。

据我所知,这不是在PHP中的错误。 事实上,我怀疑PHP解释器必须特别努力检测和报告这个问题。 它涉及到你访问“foo”variables的方式。

 Registry::getInstance()->foo 

当PHP看到这部分语句时,首先检查对象实例是否有一个名为“foo”的可公开访问的variables。 在这种情况下,它不会,所以下一步是调用其中一个魔法方法,无论是__set()(如果您试图replace当前值“foo”)或__get()(如果您试图访问该值)。

 Registry::getInstance()->foo = array(1, 2, 3); 

在这个语句中,你试图用数组(1,2,3) replace “foo”的值,所以PHP用$ key =“foo”和$ value = array(1,2,3)调用你的__set 3),一切正常。

 Registry::getInstance()->foo[] = 4; 

但是,在这个语句中,您正在检索 “foo”的值,以便您可以修改它(在这种情况下,将其视为一个数组并添加一个新元素)。 代码意味着你想修改实例持有的“foo”的值,但实际上你实际上是在修改由__get()返回的foo的临时副本 ,所以PHP会发出警告(如果你有类似的情况出现将Registry :: getInstance() – > foo通过引用而不是按值传递给函数)。

有几个解决此问题的选项。

方法1

您可以将“foo”的值写入variables,修改该variables,然后将其写回,即

 $var = Registry::getInstance()->foo; $var[] = 4; Registry::getInstance()->foo = $var; 

function,但可怕的详细,所以不build议。

方法2

按照cillosis的build议让你的__get()函数返回引用(不需要通过引用返回__set()函数,因为它根本不应该返回值)。 在这种情况下,您需要知道PHP只能返回已经存在的variables的引用,并且如果违反了这个约束,可能会发出通知或行为exception。 如果我们看看适合你的课程的cillosis'__get()函数(如果你select沿着这条路线走下去,那么由于下面解释的原因,坚持__get()的这个实现并且在读取之前虔诚地进行存在检查从您的registry):

 function &__get( $index ) { if( array_key_exists( $index, $this->_registry ) ) { return $this->_registry[ $index ]; } return; } 

如果您的应用程序永远不会尝试获取您的registry中尚不存在的值,那么这很好,但是当您执行此操作时,您将会遇到“返回”。 语句并得到一个“只能通过引用返回variables引用”的警告,并且你不能通过创build一个回退variables来解决这个问题,而是返回这个variables,因为这会给你“间接修改重载属性没有任何效果”的警告再次出于同样的原因。 如果你的程序不能有任何警告(并且警告是一件坏事,因为它们可能会污染你的错误日志,并影响你的代码到PHP的其他版本/configuration的可移植性),那么你的__get()方法将不得不创build条目在返回之前不存在,即

 function &__get( $index ) { if (!array_key_exists( $index, $this->_registry )) { // Use whatever default value is appropriate here $this->_registry[ $index ] = null; } return $this->_registry[ $index ]; } 

顺便说一句,PHP本身似乎做了一些非常类似于它的数组,这就是:

 $var1 = array(); $var2 =& $var1['foo']; var_dump($var1); 

上面的代码(至less在某些版本的PHP中)输出类似于“array(1){[”foo“] =>&NULL}”“,意思是”$ var2 =&$ var1 ['foo']; 声明可能会影响expression式的两边 。 但是,我认为,通过读取操作来改变variables的内容是非常糟糕的,因为它会导致一些严重的问题(因此我觉得上面的数组行为一个PHP错误)。

例如,让我们假设你只会将对象存储在registry中,并且如果$ value不是对象,则修改__set()函数以引发exception。 存储在registry中的任何对象都必须符合特殊的“RegistryEntry”接口,该接口声明必须定义“someMethod()”方法。 因此,registry类的文档声明调用者可以尝试访问registry中的任何值,结果将是检索有效的“RegistryEntry”对象,如果该对象不存在,则返回null。 我们还假设您进一步修改您的registry以实现Iterator接口,以便人们可以使用foreach构造遍历所有registry项。 现在想象下面的代码:

 function doSomethingToRegistryEntry($entryName) { $entry = Registry::getInstance()->$entryName; if ($entry !== null) { // Do something } } ... foreach (Registry::getInstance() as $key => $entry) { $entry->someMethod(); } 

这里的理由是,doSomethingToRegistryEntry()函数知道从registry读取任意条目是不安全的,因为它们可能存在也可能不存在,所以它检查“null”情况并相应地进行处理。 一切顺利。 相比之下,循环“知道”registry的任何写入操作都将失败,除非写入的值是符合“RegistryEntry”接口的对象,所以它不必检查以确保$条目确实这样的对象可以节省不必要的开销。 现在让我们假设在尝试读取任何尚不存在的registry条目之后,有一个非常罕见的情况,在这个环境下达到这个循环。 砰!

在上面描述的情况下,循环会产生一个致命的错误 “调用一个非对象的成员函数someMethod()”(如果警告是坏事,致命错误是灾难)。 发现这实际上是由上个月更新中添加的程序中某个看起来无害的读取操作引起的,并不容易。

就个人而言,我也会避免使用这种方法,因为虽然大多数情况下performance得很好,但如果被激怒的话,它确实会让你难以接受。 令人高兴的是,有一个简单的解决scheme可用。

方法3

不要定义__get(),__set()或__isset()! 然后,PHP将在运行时为您创build属性,并使其可公开访问,以便您可以在需要时直接访问它们。 根本不需要担心引用,如果你希望你的registry是可迭代的,你仍然可以通过实现IteratorAggregate接口来实现。 鉴于你在原来的问题中给出的例子,我相信这是你最好的select。

 final class Registry implements IteratorAggregate { private static $_instance; private function __construct() { } public static function getInstance() { if (self::$_instance == null) self::$_instance = new self(); return self::$_instance; } public function getIterator() { // The ArrayIterator() class is provided by PHP return new ArrayIterator($this); } } 

实现__get()和__isset()的时间是当你想给调用者只读访问某些私有/受保护的属性,在这种情况下,你不想通过引用返回任何东西。

我希望这个对你有用。 🙂

在不起作用的例子

 Registry::getInstance()->foo[] = 4; // Does not work 

您首先执行__get ,然后使用返回的值将数组添加到数组中。 所以你需要通过引用来传递__get的结果:

 public function &__get($key) { $value = NULL; if ($this->__isset($key)) { $value = $this->_registry[$key]; } return $value; } 

我们需要使用$value因为只有variables可以通过引用传递。 我们不需要为__set添加& sign,因为这个函数不会返回任何东西,所以没有什么可以参考的。