Я хочу использовать реестр для хранения некоторых объектов. Вот простая реализация класса реестра.
Когда я пытаюсь получить доступ к этому классу, я получаю уведомление "Непрямая модификация перегруженного свойства без эффекта".
Ответ 2
Я знаю, что сейчас это довольно старая тема, но сегодня я впервые столкнулся с ней, и я подумал, что это может быть полезно для других, если я расскажу о том, что было сказано выше, с моими собственными выводами.
Насколько я могу судить, это не ошибка в PHP. На самом деле, я подозреваю, что интерпретатор PHP должен приложить особые усилия, чтобы обнаружить и сообщить об этой проблеме так конкретно. Это относится к способу доступа к переменной "foo" .
Registry::getInstance()->foo
Когда PHP видит эту часть ваших утверждений, первое, что она делает, это проверить, имеет ли экземпляр объекта общедоступную переменную, называемую "foo" . В этом случае это не так, поэтому следующим шагом будет вызов одного из магических методов: либо __set() (если вы пытаетесь заменить текущее значение "foo" ), либо __get() (если вы пытаясь получить доступ к этому значению).
Registry::getInstance()->foo = array(1, 2, 3);
В этом заявлении вы пытаетесь заменить значение "foo" на массив (1, 2, 3), поэтому PHP вызывает ваш метод __set() с помощью $key = "foo" и $value = array (1, 2, 3), и все работает нормально.
Registry::getInstance()->foo[] = 4;
Однако в этом утверждении вы извлекаете значение "foo" , чтобы его можно было изменить (в этом случае, рассматривая его как массив и добавляя новый элемент). Код подразумевает, что вы хотите изменить значение "foo" , хранящееся в экземпляре, но на самом деле вы фактически изменяете временную копию foo, возвращаемую __get(), и поэтому PHP выдает предупреждение (аналогичная ситуация возникает, если вы pass Registry:: getInstance() → foo для функции по ссылке вместо значения).
У вас есть несколько возможностей для решения этой проблемы.
Метод 1
Вы можете записать значение "foo" в переменную, изменить эту переменную, а затем записать ее обратно, т.е.
$var = Registry::getInstance()->foo;
$var[] = 4;
Registry::getInstance()->foo = $var;
Функциональный, но ужасно подробный и поэтому не рекомендуется.
Метод 2
Восстановите функцию __get() по ссылке, как это было предложено cillosis (нет необходимости возвращать вашу функцию __set() по ссылке, так как она не должна вообще возвращать значение). В этом случае вам нужно знать, что PHP может только возвращать ссылки на уже существующие переменные и может выдавать уведомления или вести себя странно, если это ограничение нарушено. Если мы посмотрим на функцию cillosis '__get(), адаптированную для вашего класса (если вы решите пойти по этому маршруту, то по причинам, которые объясняются ниже, придерживайтесь этой реализации __get() и религиозно выполняйте проверку существования перед любым чтением из реестра):
function &__get( $index )
{
if( array_key_exists( $index, $this->_registry ) )
{
return $this->_registry[ $index ];
}
return;
}
Это прекрасно, если ваше приложение никогда не пытается получить значение, которое еще не существует в вашем реестре, но в тот момент, когда вы это сделаете, вы попадете в "return"; и получите сообщение "Только ссылки на ссылки на переменные должны быть возвращены ссылкой", и вы не можете исправить это, создав резервную переменную и вернув это вместо этого, так как это даст вам предупреждение "Непрямая модификация перегруженного свойства" снова по тем же причинам, что и раньше. Если ваша программа не может иметь никаких предупреждений (и предупреждения - это Bad Thing, потому что они могут загрязнять ваш журнал ошибок и влиять на переносимость вашего кода на другие версии/конфигурации 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() знает, что для чтения произвольных записей из реестра небезопасно, поскольку они могут существовать или не существовать, поэтому он проверяет "нулевой" случай и ведет себя соответствующим образом. Все хорошо и хорошо. Напротив, цикл "знает", что любая операция записи в реестр потерпела бы неудачу, если только написанное значение не было объектом, соответствующим интерфейсу "RegistryEntry" , поэтому не нужно проверять, чтобы убедиться, что $entry действительно такой объект для сохранения ненужных накладных расходов. Теперь предположим, что существует очень редкое обстоятельство, при котором этот цикл достигается когда-то после попытки прочитать любую запись реестра, которая еще не существует. Взрывы!
В описанном выше сценарии цикл будет генерировать фатальную ошибку "Вызовите функцию-член someMethod() для не-объекта" (и если предупреждения - "Плохие вещи", фатальными ошибками являются "Катастрофы" ). Выяснив, что на самом деле это вызвано, казалось бы, безобидной операцией чтения в другом месте программы, добавленной обновлением в прошлом месяце, не будет простой.
Лично я бы избегал этого метода, потому что, хотя он может показаться хорошо себя вести большую часть времени, он может действительно сильно укусить вас, если его спровоцировать. К счастью, есть гораздо более простое решение.
Метод 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() - это когда вы хотите предоставить доступным только для чтения пользователям доступ к определенным частным/защищенным свойствам, и в этом случае вы не хотите возвращать что-либо по ссылке.
Я надеюсь, что это поможет.:)