Подтвердить что ты не робот

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

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

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

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

4b9b3361

Ответ 1

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

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

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

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

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

Я надеюсь, что это поможет.:)

Ответ 3

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

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