我想使用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;
}
}
?>
当我尝试访问此类时,我得到“间接修改重载属性无效”通知。
Registry::getInstance()->foo = array(1, 2, 3); // Works
Registry::getInstance()->foo[] = 4; // Does not work
我做错了什么?
答案 0 :(得分:102)
我知道这是一个相当古老的话题,但这是我今天第一次遇到的事情,如果我用我自己的发现扩展上述内容,我认为这可能对其他人有所帮助。
据我所知,这不是PHP中的错误。事实上,我怀疑PHP解释器必须特别努力检测和报告此问题。它涉及你访问&#34; foo&#34;的方式。变量
Registry::getInstance()->foo
当PHP看到你的语句的这一部分时,它所做的第一件事是检查对象实例是否有一个名为&#34; foo&#34;的公共可访问变量。在这种情况下,它没有,所以下一步是调用其中一个魔术方法,__ set()(如果你试图替换&#34; foo&#34;的当前值),或者__get()(如果您尝试访问该值)。
Registry::getInstance()->foo = array(1, 2, 3);
在此声明中,您试图替换&#34; foo&#34;的值。 with array(1,2,3),所以PHP用$ key =&#34; foo&#34;来调用你的__set()方法和$ value = array(1,2,3),一切正常。
Registry::getInstance()->foo[] = 4;
但是,在本声明中,您检索&#34; foo&#34;的值。以便您可以修改它(在这种情况下,将其视为一个数组并附加一个新元素)。代码暗示您要修改&#34; foo&#34;的值。由实例持有,但实际上你实际修改__get()返回的foo的临时副本,因此PHP发出警告(如果你出现了类似的情况)通过引用而不是按值将Registry :: getInstance() - &gt; foo传递给函数。)
您可以通过几种方法解决此问题。
方法1
你可以写出&#34; foo&#34;的价值。转换为变量,修改该变量,然后将其写回,即
$var = Registry::getInstance()->foo;
$var[] = 4;
Registry::getInstance()->foo = $var;
功能齐全,但非常冗长,所以不推荐。
方法2
让你的__get()函数按照cillosis的建议通过引用返回(不需要让你的__set()函数通过引用返回,因为它根本不应该返回一个值)。在这种情况下,您需要知道PHP只能返回对已存在的变量的引用,并且如果违反了此约束,则可能会发出通知或行为异常。如果我们看看cillosis&#39; __get()函数适用于你的类(如果你确实选择沿着这条路走下去,由于下面解释的原因,坚持使用__get()的这个实现,并在从你的注册表中读取之前虔诚地进行存在检查): / p>
function &__get( $index )
{
if( array_key_exists( $index, $this->_registry ) )
{
return $this->_registry[ $index ];
}
return;
}
如果您的应用程序永远不会尝试获取您的注册表中尚未存在的值,那么这很好。但是当您执行此操作时,您将点击&#34; return;&#34;声明并得到一个&#34;只有变量引用才能通过引用返回&#34;警告,你可以通过创建一个回退变量并返回它来解决这个问题,因为这样可以让你对重载属性的间接修改没有任何影响&#34;出于与以前相同的原因再次发出警告。如果你的程序没有任何警告(并且警告是坏事,因为它们会污染你的错误日志并影响你的代码到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上)输出类似&#34; array(1){[&#34; foo&#34;] =&gt; &amp; NULL}&#34;,表示&#34; $ var2 =&amp; $ VAR1 [&#39;富&#39;];&#34;语句可能会影响表达式的双方。但是,我认为允许变量的内容被读取操作改变是根本不好的,因为它可能导致一些严重的错误错误(因此我觉得上面的数组行为 是一个PHP错误。)
例如,假设您只是在注册表中存储对象,并且如果$ value不是对象,则修改__set()函数以引发异常。存储在注册表中的任何对象也必须符合特殊的&#34; RegistryEntry&#34;接口,声明&#34; someMethod()&#34;方法必须定义。因此,注册表类的文档指出调用者可以尝试访问注册表中的任何值,结果将是检索有效的&#34; RegistryEntry&#34; object,如果该对象不存在,则返回null。我们还假设您进一步修改注册表以实现Iterator接口,以便人们可以使用foreach结构遍历所有注册表项。现在想象下面的代码:
function doSomethingToRegistryEntry($entryName)
{
$entry = Registry::getInstance()->$entryName;
if ($entry !== null)
{
// Do something
}
}
...
foreach (Registry::getInstance() as $key => $entry)
{
$entry->someMethod();
}
这里的理性是doSomethingToRegistryEntry()函数知道从注册表中读取任意条目是不安全的,因为它们可能存在也可能不存在,因此它会检查&#34; null& #34;案件和行为相应。一切都很好。相比之下,循环&#34;知道&#34;除非写入的值是符合&#34; RegistryEntry&#34;的对象,否则对注册表的任何写操作都将失败。接口,所以它不会检查以确保$ entry确实是一个节省不必要开销的对象。现在让我们假设在尝试读取任何尚不存在的注册表项之后的某个时间达到此循环是非常罕见的情况。砰!
在上面描述的场景中,循环会生成致命错误&#34;调用非对象上的成员函数someMethod()&#34; (如果警告是坏事,致命错误就是灾难)。发现这实际上是由上个月更新添加的程序中其他地方看似无害的读取操作引起的,并不是直截了当的。
就个人而言,我也会避免使用这种方法,因为虽然它在大多数情况下表现得很好,但如果被激怒它会让你感到很难受。令人高兴的是,有一个很多更简单的解决方案。
方法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()的时间是指您希望为调用者提供对某些私有/受保护属性的只读访问权限,在这种情况下,您不希望通过引用返回任何内容。 / p>
我希望这会有所帮助。 :)
答案 1 :(得分:21)
此行为已被多次报告为错误:
我不清楚讨论的结果是什么,虽然它似乎与“按价值”和“按参考”传递的价值观有关。我在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;
}
}
}
注意他们如何使用&__get
和&__set
,以及在分配值时使用& $value
。我认为这是使这项工作的方法。
答案 2 :(得分: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
符号,因为此函数不会返回任何内容,因此无需参考。