46

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

我做错了什么?

4

3 回答 3

115

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

据我所知,这不是 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() 的时间是当你想给调用者只读访问某些私有/受保护属性的时候,在这种情况下你不想通过引用返回任何东西。

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

于 2013-11-03T04:08:12.660 回答
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;
      }
   }
}

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

于 2012-11-16T18:00:13.317 回答
0

在不起作用的示例中

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因为这个函数应该什么都不返回,所以没有什么可以引用的。

于 2015-06-08T14:17:30.630 回答