php 收到“间接修改重载属性无效”通知

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/13421661/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-25 05:22:41  来源:igfitidea点击:

Getting "Indirect modification of overloaded property has no effect" notice

php

提问by Nikolay Dutchuk

I want to use a Registry to store some objects. Here is a simple Registry class implementation.

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

?>

When I try to access this class, I get "Indirect modification of overloaded property has no effect" notification.

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

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

What do I do wrong?

我做错了什么?

采纳答案by Jeremy Harris

This behavior has been reported as a bug a couple times:

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

It is unclear to me what the result of the discussions was although it appears to have something to do with values being passed "by value" and "by reference". A solution that I found in some similar codedid something like this:

我不清楚讨论的结果是什么,尽管它似乎与“按值”和“按引用”传递的值有关。我在一些类似的代码中找到的解决方案做了这样的事情:

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

Notice how they use &__getand &__setand also when assigning the value use & $value. I think that is the way to make this work.

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

回答by indigo866

I know that this is now quite an old topic, but it is something that I encountered myself for the first time today, and I thought it might be helpful to others if I expanded upon what was said above with my own findings.

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

As far as I can tell, this is not a bug in PHP. In fact, I suspect the PHP interpreter has to make a special effort to detect and report this issue so specifically. It relates to the way in which you are accessing the "foo" variable.

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

Registry::getInstance()->foo

When PHP sees this part of your statements, the first thing that it does is check to see if the object instance has a publicly accessible variable called "foo". In this case, it doesn't, so the next step is to call one of the magic methods, either __set() (if you are attempting to replace the current value of "foo"), or __get() (if you are trying to access that value).

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

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

In this statement, you are attempting to replacethe value of "foo" with array(1, 2, 3), so PHP calls your __set() method with $key = "foo" and $value = array(1, 2, 3), and everything works fine.

在这个语句中,你试图用 array(1, 2, 3)替换"foo" 的值,所以 PHP 用 $key = "foo" 和 $value = array(1, 2, 3),一切正常。

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

However, in this statement, you are retrievingthe value of "foo" so that you can modify it (in this case by treating it as an array and appending a new element). The code implies that you want to modify the value of "foo" held by the instance, but in reality you are actuallymodifying a temporary copyof foo returned by __get(), and so PHP issues the warning (a similar situation arises if you pass Registry::getInstance()->foo to a function by reference instead of by value).

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

You have a few options for working around this issue.

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

Method 1

方法一

You could write the value of "foo" into a variable, modify that variable, and then write it back, i.e.

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

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

Functional, but horribly verbose and so not recommended.

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

Method 2

方法二

Have your __get() function return by reference, as suggested by cillosis (there is no need to have your __set() function return by reference, since it is not supposed to return a value at all). In this case you need to be aware that PHP can only return references to variables that already exist, and may issue notices or behave strangely if this constraint is violated. If we look at cillosis' __get() function adapted for your class (if you do choose to go down this route then, for reasons that are explained below, stick with this implementation of __get() and religiously do an existence check before any read from your registry):

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

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

    return;
}

This is fine provided your application never tries to get a value that doesn't yet exist in your registry, but the moment you do, you will hit the "return;" statement and get an "Only variable references should be returned by reference" warning, and you can't fix this by creating a fallback variable and returning that instead, since that will give you the "Indirect modification of overloaded property has no effect" warning again for the same reasons as before. If your program can't have any warnings (and warnings are a Bad Thing because they can pollute your error log and affect the portability of your code to other versions/configurations of PHP), then your __get() method would have to create entries that do not exist before returning them, i.e.

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

Incidentally, PHP itself seems to do something very similar to this with its arrays, that is:

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

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

The above code will (on at least some versions of PHP) output something like "array(1) { ["foo"]=> &NULL }", meaning the "$var2 =& $var1['foo'];" statement might affect both sidesof the expression. However, I think that it is fundamentally bad to allow the contents of a variable to be changed by a readoperation, because it can lead to some seriously nasty bugs (and hence I feel that the above array behaviour isa PHP bug).

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

For example, let us suppose that you're only ever going to store objects in your registry, and you modify your __set() function to raise an exception if $value is not an object. Any object stored in the registry must also conform to a special "RegistryEntry" interface, which declares that the "someMethod()" method must be defined. The documentation for your registry class thus states that a caller can attempt to access any value within the registry, and the result will either be retrieval of a valid "RegistryEntry" object, or null if that object does not exist. Let's also suppose that you further modify your registry to implement the Iteratorinterface so that people can loop through all registry entries using a foreach construct. Now imagine the following code:

例如,让我们假设您只打算在注册表中存储对象,并且您修改 __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();
}

The rational here is that the doSomethingToRegistryEntry() function knows that it's not safe to read arbitrary entries from the registry, since they may or may not exist, so it does a check for the "null" case and behaves accordingly. All well and good. By contrast, the loop "knows" that any writeoperation to the registry would have failed unless the value written was an object that conforms to the "RegistryEntry" interface, so it doesn't bother to check to make sure that $entry is indeed such an object to save unnecessary overhead. Now let's suppose that there's a very rare circumstance under which this loop is reached sometime after an attempt is made to read any registry entry that does not yet exist. Bang!

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

In the scenario described above, the loop would generate a fatal error"Call to a member function someMethod() on a non-object" (and if warnings are Bad Things, fatal errors are Catastrophes). Finding out that this is actually being caused by a seemingly-innocuous readoperation somewhere else in the program that was added by last month's update is not going to be straightforward.

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

Personally, I would avoid this method as well because whilst it can appear to behave well most of the time, it can really bite you hard if provoked. Happily, there is a muchsimpler solution available.

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

Method 3

方法三

Just don't define __get(), __set(), or __isset()! Then, PHP will create properties for you at runtime and make them publicly accessible, so that you can simply access them directly whenever you need to. No need to worry about references at all, and if you want your registry to be iterable, you can still do this by implementing the IteratorAggregateinterface. Given the example you gave in your original question, I believe that this is by far your best option.

只是不要定义 __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);
    }
}

The time to implement __get() and __isset() are when you want to give callers read-only access to certain private/protected properties, in which case you don't want to be returning anything by reference.

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

I hope that this helps. :)

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

回答by a.mikheychik

In the example that doesn't work

在不起作用的例子中

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

You first do the __getand then it works with the returned value to add something to array. So you need to pass result from __getby reference:

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

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

We need to use $valueas only variables can be passed by reference. We don't need to add &sign to __setas this function should return nothing, so there is nothing to reference to.

我们需要使用$valueas 只有变量可以通过引用传递。我们不需要添加&符号,__set因为这个函数应该不返回任何东西,所以没有什么可引用的。