【问题标题】:Getting "Indirect modification of overloaded property has no effect" notice收到“间接修改重载属性无效”通知
【发布时间】:2012-11-16 17:38:39
【问题描述】:

我想使用注册表来存储一些对象。这是一个简单的 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

我做错了什么?

【问题讨论】:

    标签: php


    【解决方案1】:

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

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

    Registry::getInstance()->foo
    

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

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

    在此语句中,您尝试将“foo”的值替换为 array(1, 2, 3),因此 PHP 使用 $key = "foo" 调用您的 __set() 方法和 $value = array(1, 2, 3),一切正常。

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

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

    您有几种解决此问题的方法。

    方法一

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

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

    功能强大,但非常冗长,因此不推荐。

    方法二

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

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

    这很好,前提是您的应用程序永远不会尝试获取注册表中尚不存在的值,但是当您这样做时,您将点击“返回”;声明并获得“仅应通过引用返回变量引用”警告,并且您无法通过创建后备变量并返回该变量来解决此问题,因为这会给您“重载属性的间接修改无效”警告再次出于与以前相同的原因。如果您的程序不能有任何警告(警告是一件坏事,因为它们会污染您的错误日志并影响您的代码对 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 ];
    }
    

    顺便说一句,PHP 本身似乎对它的数组做了一些与此非常相似的事情,即:

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

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

    例如,假设您只打算在注册表中存储对象,并且您修改了 __set() 函数以在 $value 不是对象时引发异常。存储在注册表中的任何对象还必须符合特殊的“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();
    }
    

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

    在上述场景中,循环会产生一个致命错误“调用非对象上的成员函数 someMethod()”(如果警告是坏事,致命错误就是灾难)。发现这实际上是由上个月更新添加的程序中其他地方的看似无害的 read 操作引起的并不是一件容易的事。

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

    方法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);
        }
    }
    

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

    我希望这会有所帮助。 :)

    【讨论】:

    • 这是最好的答案。它值得更多的点击
    • 感谢您对问题的完整解释!现在对我来说更有意义了。
    • 感谢@indigo866 非常感谢!
    • 第三种方法非常适合亲吻,而且很有道理。不错的答案!
    【解决方案2】:

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

    我不清楚讨论的结果是什么,尽管它似乎与“按值”和“按引用”传递的值有关。我在some similar code 中找到的解决方案是这样的:

    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;
          }
       }
    }
    

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

    【讨论】:

    • 我发现了一个案例,即使这样也无法解决问题:使用ifsetor 实现可能会在测试存在时创建虚假的 NULL 值条目,请参阅this PHP.net RFC which was rejected。不,感谢 PHP 核心开发人员让我的生活更加艰难,我在工作中花费了更多的时间,而且由于没有实现正确的 ifsetor 运算符,我的代码更容易出错。
    • 使用&amp;__get 代替&amp;__get 为我工作以防止“间接修改重载属性无效”警告,但&amp;__set 触发了不同的错误(“警告:仅变量引用应该通过引用返回。”)所以我将其保留为__set
    • __set 从不返回任何内容。你只需要为__get做那个
    【解决方案3】:

    在不起作用的示例中

    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,因为只有变量可以通过引用传递。 我们不需要在__set 中添加&amp; 符号,因为这个函数应该什么都不返回,所以没有什么可引用的。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2023-04-08
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2018-05-22
      相关资源
      最近更新 更多