获得"间接修改超载财产无效"的通知

Nik*_*huk 43 php

我想使用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;
    }
}

?>
Run Code Online (Sandbox Code Playgroud)

当我尝试访问这个类时,我得到"间接修改重载属性没有效果"的通知.

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

我做错了什么?

ind*_*866 106

我知道这是一个相当古老的话题,但这是我今天第一次遇到的事情,我认为如果我用自己的发现扩展上述内容,对其他人可能会有所帮助.

据我所知,这不是PHP中的错误.事实上,我怀疑PHP解释器必须特别努力检测和报告此问题.它与您访问"foo"变量的方式有关.

Registry::getInstance()->foo
Run Code Online (Sandbox Code Playgroud)

当PHP看到你的语句的这一部分时,它做的第一件事是检查对象实例是否有一个名为"foo"的可公开访问的变量.在这种情况下,它没有,所以下一步是调用一个魔术方法,__ set()(如果你试图替换"foo"的当前值),或__get()(如果你是试图访问该值).

Registry::getInstance()->foo   = array(1, 2, 3);
Run Code Online (Sandbox Code Playgroud)

在这份声明中,你正试图取代与阵列(1,2,3),那么PHP调用您的__set()方法$关键= "foo"和$值=阵列(1,2 "富"的价值, 3),一切正常.

Registry::getInstance()->foo[] = 4;
Run Code Online (Sandbox Code Playgroud)

然而,在此声明,您是检索 "foo"的值,这样就可以(通过将其视为一个数组,并追加了新的元素在这种情况下)修改它.该代码意味着要修改的"富"的实例保存的值,但在现实中你确实修改临时副本的foo)通过__get(返回,因此PHP发出警告(类似的情况出现,如果你通过引用而不是通过值将Registry :: getInstance() - > foo传递给函数).

您有几个选项可以解决此问题.

方法1

你可以"富"的值写入到一个变量,修改变量,然后将其写回,即

$var = Registry::getInstance()->foo;
$var[] = 4;
Registry::getInstance()->foo = $var;
Run Code Online (Sandbox Code Playgroud)

功能性,但可怕的冗长,所以不推荐.

方法2

让你的__get()函数按照cillosis的建议通过引用返回(不需要让你的__set()函数通过引用返回,因为它根本不应该返回一个值).在这种情况下,您需要知道PHP只能返回对已存在的变量的引用,并且如果违反了此约束,则可能会发出通知或行为异常.如果我们看一下cillosis'__get()函数适合你的班级(如果你确实选择沿着这条路走下去,那么由于下面解释的原因,坚持使用__get()的这个实现,并在任何阅读之前虔诚地进行存在检查来自您的注册表):

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

    return;
}
Run Code Online (Sandbox Code Playgroud)

如果您的应用程序永远不会尝试获取您的注册表中尚不存在的值,这很好,但是当您这样做时,您将点击"返回"; 声明并获得"只有变量引用应通过引用返回"警告,并且您无法通过创建回退变量并返回该变量来解决此问题,因为这将为您提供"间接修改过载属性无效"警告再次出于与以前相同的原因.如果你的程序没有任何警告(并且警告是坏事,因为它们可能污染你的错误日志并影响你的代码到PHP的其他版本/配置的可移植性),那么你的__get()方法将不得不创建条目在返回它们之前不存在,即

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 ];
}
Run Code Online (Sandbox Code Playgroud)

顺便说一句,PHP本身似乎与它的数组非常相似,即:

$var1 = array();
$var2 =& $var1['foo'];
var_dump($var1);
Run Code Online (Sandbox Code Playgroud)

上面的代码将(在至少某些版本的PHP上)输出类似"array(1){["foo"] =>&NULL}",意思是"$ var2 =&$ var1 ['foo'];" 语句可能会影响表达式的两面.但是,我认为允许通过读取操作更改变量的内容是根本不好的,因为它可能导致一些严重的错误错误(因此我觉得上面的数组行为一个PHP错误).

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

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

...

foreach (Registry::getInstance() as $key => $entry)
{
    $entry->someMethod();
}
Run Code Online (Sandbox Code Playgroud)

这里的理性是doSomethingToRegistryEntry()函数知道从注册表中读取任意条目是不安全的,因为它们可能存在也可能不存在,因此它会检查"null"情况并相应地运行.一切都很好.相比之下,循环"知道" 对注册表的任何操作都会失败,除非写入的值是符合"RegistryEntry"接口的对象,因此它不会检查以确保$ entry确实是这样的对象可以节省不必要的开销.现在让我们假设有一种非常罕见的情况,在尝试读取任何尚不存在的注册表项之后的某个时间达到此循环.砰!

在上面描述的场景中,循环将生成致命错误 "在非对象上调用成员函数someMethod()"(如果警告是Bad Things,则致命错误是灾难).发现这实际上是由于在上个月的更新中添加的程序中的其他地方看似无害的读取操作引起的并不是直截了当的.

就个人而言,我也会避免使用这种方法,因为虽然它在大多数情况下表现得很好,但如果被激怒它会让你很难受.令人高兴的是,有一个简单的解决方案.

方法3

只是不要定义__get(),__ set()或__isset()!然后,PHP将在运行时为您创建属性并使其可公开访问,以便您可以在需要时直接访问它们.根本不需要担心引用,如果您希望您的注册表可以迭代,您仍然可以通过实现IteratorAggregate接口来实现.鉴于您在原始问题中给出的示例,我相信这是您最好的选择.

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);
    }
}
Run Code Online (Sandbox Code Playgroud)

实现__get()和__isset()的时间是您希望为调用者提供对某些私有/受保护属性的只读访问权限,在这种情况下,您不希望通过引用返回任何内容.

我希望这个对你有用.:)


Jer*_*ris 21

此行为已多次报告为错误:

我不清楚讨论的结果是什么,虽然它似乎与"按价值"和"按参考"传递的价值观有关.我在一些类似的代码中找到的解决方案做了这样的事情:

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;
      }
   }
}
Run Code Online (Sandbox Code Playgroud)

注意他们如何使用&__get,&__set以及在分配值时使用& $value.我认为这是使这项工作的方法.

  • 使用`&__ get`代替`&__ get`为我工作以防止"间接修改重载属性没有效果"警告,但是`&__ set`触发了一个不同的错误("警告:只应通过引用返回变量引用".所以我把它留作了`__set`. (2认同)