Получение «косвенной модификации перегруженного имущества не имеет никакого эффекта»

Я хочу использовать реестр для хранения некоторых объектов. Вот простая реализация класса реестра.

<?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 

Что я делаю неправильно?

Такое поведение было сообщено как ошибка пару раз:

Мне непонятно, каков был результат обсуждения, хотя он, похоже, имеет отношение к значениям, передаваемым «по значению» и «по ссылке». Решение, которое я нашел в каком-то подобном коде, делало что-то вроде этого:

 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 . Я думаю, что это способ сделать эту работу.

Я знаю, что сейчас это довольно старая тема, но сегодня я впервые столкнулся с ней, и я подумал, что это может быть полезно для других, если я расскажу о том, что было сказано выше, с моими собственными выводами.

Насколько я могу судить, это не ошибка в 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 (), чтобы создать исключение, если значение $ не является объектом. Любой объект, хранящийся в реестре, должен также соответствовать специальному интерфейсу «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 () для не-объекта» (и если предупреждения – это Bad Things, фатальными ошибками являются Катастрофы). Выяснение того, что на самом деле это вызвано, казалось бы, безобидной операцией чтения в другом месте программы, добавленной обновлением в прошлом месяце, не будет просто.

Лично я бы избегал этого метода, потому что, хотя он может показаться хорошо себя вести большую часть времени, он может сильно укусить вас, если его спровоцировать. К счастью, существует гораздо более простое решение.

Способ 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 () – это когда вы хотите предоставить вызывающим абонентам доступ только для чтения к определенным частным / защищенным свойствам, и в этом случае вы не хотите возвращать что-либо по ссылке.

Я надеюсь, что это помогает. 🙂

В примере, который не работает

 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 поскольку только переменные могут передаваться по ссылке. Нам не нужно добавлять & sign в __set как эта функция ничего не должна возвращать, поэтому ссылаться на нее нет.