PHP задает магический метод с массивом как имена

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

Класс выглядит следующим образом:

class settings { private $_settings = array(); //some functions to fill the array public function __set($name, $value) { echo 'inside the __set method'; //do some stuff } } 

И код для использования этого класса:

 $foo = new settings(); //do some stuff with the class, so the internal settings array is as followed: //array( // somename => somevalue // bar => array ( // baz = someothervalue // qux = 42 // ) // ) $foo->somename = something; //this works, __set method is called correctly $foo->bar['baz'] = somethingelse; //Doesn't work, __set method isn't called at all 

Как я могу заставить эту последнюю работу работать?

    При доступе к массиву с использованием этого метода он фактически переходит через __get. Чтобы установить параметр в этом массиве, который был возвращен, он должен быть возвращен как ссылка: &__get($name)

    Если вы не хотите, чтобы каждый элемент, возвращаемый как массив, работал так же, как и родительский объект, в этом случае вы должны взглянуть на источник объекта Zend_Config Zend Framework для хорошего способа сделать это. (Он возвращает новый экземпляр самого себя с подматрицей в качестве параметра).

    Это будет работать:

     $settings = new Settings(); $settings->foo = 'foo'; $settings->bar = array('bar'); 

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

    Редактировать после комментариев (не отвечать на вопрос выше)

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

    • настройки можно сохранить в файл или базу данных
    • возможно, потребуется обновить другие части приложения
    • настройки должны быть проверены до их изменения
    • должен использовать $setting->foo[subsetting] над $setting->data[foo[subsetting]]
    • класс настроек должен предоставлять доступ к данным настроек для других классов
    • в первый раз создается экземпляр, параметры должны быть загружены из файла

    Теперь для всего класса достаточно много чего сделать. Судя по требованиям, которые вы пытаетесь создать автономный реестр Singleton , который по шкале от 1 (плохого) до 10 (апокалиптический) является идеей уровня 11 в моей книге.

    В соответствии с принципом единой ответственности (S в SOLID ) класс должен иметь одну и только причину изменения . Если вы посмотрите на свои требования, вы заметите, что существует определенная причина для его изменения. И если вы посмотрите на GRASP, вы заметите, что ваш класс занимает больше ролей, чем должен.

    В деталях:

    настройки можно сохранить в файл или базу данных

    Это как минимум два ответа: доступ к db и доступ к файлам. Некоторым людям, возможно, захочется дополнительно различать чтение из файла и сохранение в файл. Давайте теперь проигнорируем часть БД и просто сосредоточимся на доступе к файлам и простейшей вещи, которая могла бы работать на данный момент.

    Вы уже сказали, что ваш массив настроек – это просто немой ключ / хранилище значений, что в значительной степени относится к массивам на PHP. Кроме того, в PHP вы можете include массивы из файла, когда они написаны так:

     <?php // settings.php return array( 'foo' => 'bar' ); 

    Итак, технически вам не нужно ничего делать, кроме

     $settings = include 'settings.php'; echo $settings['foo']; // prints 'bar'; 

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

    Сохранение массива в качестве включаемого файла не является трудным либо благодаря var_export и file_put_contents . Мы можем легко создать класс Service для этого, например

     class ArrayToFileService { public function export($filePath, array $data) { file_put_contents($filePath, $this->getIncludableArrayString($data)); } protected function getIncludableArrayString($data) { return sprintf('<?php return %s;', var_export($data, true)); } } 

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

    Все, что вам нужно сделать, чтобы сохранить настройки

     $arrayToFileService = new ArrayToFileService; $arrayToFileService->export('settings.php', $settings); 

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

    возможно, потребуется обновить другие части приложения

    Я не уверен, зачем вам это нужно. Учитывая, что наш массив параметров может содержать произвольные данные, вы не можете заранее знать, какие части приложения могут нуждаться в обновлении. Кроме того, знание того, как обновлять другие части приложения, не является ответственностью контейнера данных. Нам нужен механизм, который сообщает различным частям приложения, когда массив обновляется. Конечно, мы не можем сделать это с помощью простого старого массива, потому что это не объект. К счастью, PHP позволяет нам получить доступ к объекту, подобному массиву, используя ArrayAccess:

     class HashMap implements ArrayAccess { protected $data; public function __construct(array $initialData = array()) { $this->data = $initialData; } public function offsetExists($offset) { return isset($this->data[$offset]); } public function offsetGet($offset) { return $this->data[$offset]; } public function offsetSet($offset, $value) { $this->data[$offset] = $value; } public function offsetUnset($offset) { unset($this->data[$offset]); } public function getArrayCopy() { return $this->data; } } не class HashMap implements ArrayAccess { protected $data; public function __construct(array $initialData = array()) { $this->data = $initialData; } public function offsetExists($offset) { return isset($this->data[$offset]); } public function offsetGet($offset) { return $this->data[$offset]; } public function offsetSet($offset, $value) { $this->data[$offset] = $value; } public function offsetUnset($offset) { unset($this->data[$offset]); } public function getArrayCopy() { return $this->data; } } 

    Методы, начинающиеся со offset* , требуются интерфейсом. Метод getArrayCopy существует, поэтому мы можем использовать его с нашим ArrayToFileService . Мы могли бы также добавить интерфейс IteratorAggregate чтобы объект вел себя еще больше как массив, но поскольку это не является требованием прямо сейчас, нам это не нужно . Теперь, чтобы разрешить произвольное обновление, мы добавляем шаблон Subject / Observer , реализуя SplSubject :

     class ObservableHashMap implements ArrayAccess, SplSubject … protected $observers; public function __construct(array $initialData = array()) { $this->data = $initialData; $this->observers = new SplObjectStorage; } public function attach(SplObserver $observer) { $this->observers->attach($observer); } public function detach(SplObserver $observer) { $this->observers->detach($observer); } public function notify() { foreach ($this->observers as $observers) { $observers->update($this); } } } 

    Это позволяет нам регистрировать произвольные объекты, реализующие интерфейс SplObserver с SplObserver ObservableHashMap (переименованный из HashMap ) и notify об изменениях. Было бы лучше, если бы Наблюдаемая часть была автономным классом, чтобы иметь возможность повторно использовать ее для других классов. Для этого мы могли бы сделать Наблюдаемую часть частью декоратора или черты . Мы также можем отделить Субъект и Наблюдателей, добавив EventDispatcher для посредничества между ними, но пока этого должно быть достаточно.

    Теперь, чтобы уведомить наблюдателя, мы должны изменить все методы класса, которые должны инициировать уведомление, например

     public function offsetSet($offset, $value) { $this->data[$offset] = $value; $this->notify(); } 

    Всякий раз, когда вы вызываете offsetSet() или используете [] для изменения значения в HashMap , любые зарегистрированные наблюдатели будут уведомлены и переданы весь экземпляр HashMap . Затем они могут проверить этот экземпляр, чтобы увидеть, изменилось ли что-то важное и реагировало по мере необходимости, например, предположим, что SomeComponent

     class SomeComponent implements SplObserver { public function update(SplSubject $subject) { echo 'something changed'; } } 

    И тогда вы просто делаете

     $data = include 'settings.php'; $settings = new ObservableHashMap($data); $settings->attach(new SomeComponent); $settings['foo'] = 'foobarbaz'; // will print 'something changed' 

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

    настройки должны быть проверены до их изменения

    Это легко. Вы не делаете этого внутри объекта hashmap / settings вообще. Учитывая, что HashMap – это просто тупой контейнер, содержащий произвольные данные, которые предполагается использовать другими классами, вы помещаете проверку в те классы, которые используют данные. Задача решена.

    должен использовать $setting->foo[subsetting] над $setting->data[foo[subsetting]]

    Ну, да. Как вы, наверное, уже догадались, вышеупомянутая реализация не использует эту нотацию. Он использует $settings['foo'] = 'bar' и вы не можете использовать $settings['foo']['bar'] с ArrayAccess (по крайней мере, насколько мне известно). Так что это несколько ограничивает.

    класс настроек должен предоставлять доступ к данным настроек для других классов

    Это и следующее требование пахнут как Singleton для меня. Если да, подумайте еще раз. Все, что вам когда-либо понадобится, – это создать экземпляр класса настроек один раз в вашем бутстрапе. Вы создаете все остальные классы, необходимые для выполнения запроса там, поэтому вы можете вводить все значения параметров прямо там. Нет необходимости, чтобы класс Settings был доступен по всему миру. Создавайте, вводите, отбрасывайте.

    в первый раз создается экземпляр, параметры должны быть загружены из файла

    См. Выше.

    Партия $ foo-> bar фактически вызывает __get, эта функция должна (в вашем случае) возвращать массив.

    возвращение правильного массива в __get будет тогда вашим решением.

    Как уже было сказано, это потому, что это массив, который хранится в $foo->bar , который изменяется, а не член класса. Единственный способ вызвать поведение __set в «массиве» – это создать класс, реализующий интерфейс offsetSet метод offsetSet , однако это приведет к тому, что вы сохраните настройки в одном и том же объекте.

    Достаточно аккуратная и обычная работа заключается в использовании путей с точечным ограничением:

     class Settings { protected $__settings = array(); // Saves a lot of code duplication in get/set methods. protected function get_or_set($key, $value = null) { $ref =& $this->__settings; $parts = explode('.', $key); // Find the last array section while(count($parts) > 1) { $part = array_shift($parts); if(!isset($ref[$part])) $ref[$part] = array(); $ref =& $ref[$part]; } // Perform the appropriate action. $part = array_shift($parts); if($value) $ref[$part] = $value; return $ref[$part]; } public function get($key) { return $this->get_or_set($key); } public function set($key, $value) { return $this->get_or_set($key, $value); } public function dump() { print_r($this->__settings); } } $foo = new Settings(); $foo->set('somename', 'something'); $foo->set('bar.baz', 'somethingelse'); $foo->dump(); /*Array ( [somename] => something [bar] => Array ( [baz] => somethingelse ) )*/ 

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

     public function set(/* ... */) { /* ... */ if(strpos($key, 'display.theme') == 0) /* update the theme */ /* ... */ }