我知道这现在是一个相当古老的话题,但这是我今天第一次遇到的事情,我认为如果我用自己的发现扩展上面所说的内容,可能会对其他人有所帮助。
据我所知,这不是 PHP 中的错误。事实上,我怀疑 PHP 解释器必须付出特别的努力才能如此具体地检测和报告这个问题。它与您访问“foo”变量的方式有关。
Registry::getInstance()->foo
当 PHP 看到这部分语句时,它做的第一件事就是检查对象实例是否有一个名为“foo”的可公开访问的变量。在这种情况下,它没有,所以下一步是调用其中一个魔术方法,要么是 __set()(如果你试图替换“foo”的当前值),要么是 __get()(如果你是试图访问该值)。
Registry::getInstance()->foo = array(1, 2, 3);
在这个语句中,你试图用 array(1, 2, 3)替换"foo" 的值,所以 PHP 用 $key = "foo" 和 $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;
功能强大,但非常冗长,因此不推荐。
方法二
让你的 __get() 函数通过引用返回,正如 cillosis 所建议的那样(没有必要让你的 __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'];” 语句可能会影响表达式的两边。但是,我认为允许通过读取操作更改变量的内容从根本上说是不好的,因为它会导致一些严重讨厌的错误(因此我觉得上述数组行为是一个 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”情况并做出相应的行为。一切都很好。相比之下,循环“知道”任何对注册表的写入操作都会失败,除非写入的值是符合“RegistryEntry”接口的对象,因此它不会费心检查以确保 $entry 确实是这样的对象可以节省不必要的开销。现在让我们假设在尝试读取任何尚不存在的注册表项之后的某个时间到达此循环的非常罕见的情况。砰!
在上述场景中,循环会生成一个致命错误“调用非对象上的成员函数 someMethod()”(如果警告是坏事,致命错误就是灾难)。发现这实际上是由上个月更新添加的程序中其他地方看似无害的读取操作引起的,这并不是一件容易的事。
就个人而言,我也会避免这种方法,因为虽然它在大多数情况下表现得很好,但如果被激怒,它真的会咬你一口。令人高兴的是,有一个更简单的解决方案可用。
方法三
只是不要定义 __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() 的时间是当你想给调用者只读访问某些私有/受保护属性的时候,在这种情况下你不想通过引用返回任何东西。
我希望这个对你有用。:)