开发者

PHP set magic method with array as names

I am creating a class which I will use to store and load some settings. Inside the class all settings are stored in an array. The settings can be nested, so the settings array is a multidimensional array. I want to store and load the settings using the magic methods __get and __set, so the settings can act as class members. However, since I'm using nested methods, I can't get the __set method to work when I try to access a nested setting.

The class is like this:

class settings
{
    private $_settings = array();

    //some functions to fill the array

    public function __set($name, $value)
    {
        echo 'inside the __set method';
        //do some stuff
    }
}

And the code to use this class:

$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'] = something开发者_开发知识库else; //Doesn't work, __set method isn't called at all

How can I get this last line to work?


When accessing an array using this method, it actually goes through __get instead. In order to set a parameter on that array that was returned it needs to be returned as a reference: &__get($name)

Unless, what you mean is that you want each item that is returned as an array to act the same way as the parent object, in which case you should take a look at Zend Framework's Zend_Config object source for a good way to do that. (It returns a new instance of itself with the sub-array as the parameter).


This would work:

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

But, there is no point in using magic methods or the internal array at all. When you are allowing getting and setting of random members anyway, then you can just as well make them all public.

Edit after comments (not answer to question above)

Like I already said in the comments I think your design is flawed. Let's tackle this step by step and see if we can improve it. Here is what you said about the Settings class requirements:

  • settings can be saved to a file or a database
  • settings might need to update other parts of the application
  • settings need to be validated before they are changed
  • should use $setting->foo[subsetting] over $setting->data[foo[subsetting]]
  • settings class needs to give access to the settings data for other classes
  • first time an instance is made, the settings need to be loaded from a file

Now, that is quite a lot of things to do for a single class. Judging by the requirements you are trying to build a self-persisting Singleton Registry, which on a scale of 1 (bad) to 10 (apocalyptic) is a level 11 idea in my book.

According to the Single Responsibility Principle (the S in SOLID) a class should have one and only reason to change. If you look at your requirements you will notice that there is definitely more than one reason to change it. And if you look at GRASP you will notice that your class takes on more roles than it should.

In detail:

settings can be saved to a file or a database

That is at least two responsibilites: db access and file access. Some people might want to further distinguish between reading from file and saving to file. Let's ignore the DB part for now and just focus on file access and the simplest thing that could possibly work for now.

You already said that your settings array is just a dumb key/value store, which is pretty much what arrays in PHP are. Also, in PHP you can include arrays from a file when they are written like this:

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

So, technically you dont need to do anything but

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

to load and use your Settings array from a file. This is so simple that it's barely worth writing an object for it, especially since you will only load those settings once in your bootstrap and distribute them to the classes that need them from there.

Saving an array as an includable file isnt difficult either thanks to var_export and file_put_contents. We can easily create a Service class for that, for example

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));
    }
}

Note that I deliberatly did not make the methods static despite the class having no members of it's own to operate on. Usign the class statically will add coupling between the class and any consumer of that class and that is undesirable and unneccessary.

All you have to do now to save your settings is

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

In fact, this is completely generic, so you can reuse it for any arrays you want to persist this way.

settings might need to update other parts of the application

I am not sure why you would need this. Given that our settings array can hold arbitrary data you cannot know in advance which parts of the application might need updating. Also, knowing how to update other parts of the application isnt the responsiblity of a data container. What we need is a mechanism that tells the various parts of the application when the array got updated. Of course, we cannot do that with a plain old array because its not an object. Fortunately, PHP allows us to access an object like an array by implementing 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;
    }
}

The methods starting with offset* are required by the interface. The method getArrayCopy is there so we can use it with our ArrayToFileService. We could also add the IteratorAggregate interface to have the object behave even more like an array but since that isnt a requirement right now, we dont need it. Now to allow for arbitrary updating, we add a Subject/Observer pattern by implementing 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);
        }
    }
}

This allows us to register arbitrary objects implementing the SplObserver interface with the ObservableHashMap (renamed from HashMap) class and notify them about changes. It would be somewhat prettier to have the Observable part as a standalone class to be able to reuse it for other classes as well. For this, we could make the Observable part into a Decorator or a Trait. We could also decouple Subject and Observers further by adding an EventDispatcher to mediate between the two, but for now this should suffice.

Now to notify an observer, we have to modify all methods of the class that should trigger a notification, for instance

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

Whenever you call offsetSet() or use [] to modify a value in the HashMap, any registered observers will be notified and passed the entire HashMap instance. They can then inspect that instance to see whether something important changed and react as needed, e.g. let's assume SomeComponent

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

And then you just do

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

This way, your settings class needs no knowledge about what needs to happen when a value changes. You can keep it all where it belongs: in the observers.

settings need to be validated before they are changed

That one is easy. You dont do it inside the hashmap/settings object at all. Given that the HashMap is just a dumb container holding arbitrary data that is supposed to be used by other classes, you put the validation into those classes that use the data. Problem solved.

should use $setting->foo[subsetting] over $setting->data[foo[subsetting]]

Well, yeah. As you probably have guessed already, the above implementation doesnt use this notation. It uses $settings['foo'] = 'bar' and you cannot use $settings['foo']['bar'] with ArrayAccess (at least to my knowledge). So that is somewhat of a limitation.

settings class needs to give access to the settings data for other classes

This and the next requirement smell like Singleton to me. If so, think again. All you ever need is to instantiate the settings class once in your bootstrap. You are creating all the other classes that are required to fulfill the request there, so you can inject all the settings values right there. There is no need for the Settings class to be globally accessible. Create, inject, discard.

first time an instance is made, the settings need to be loaded from a file

See above.


The part $foo->bar is actually calling __get, this function should (in your case) return an array.

returning the right array in the __get would then be your solution.


As has been stated, this is because it is the array stored in $foo->bar that is being modified rather than the class member. The only way to invoke __set behaviour on an 'array' would be to create a class implementing the ArrayAccess interface and the offsetSet method, however this would defeat the purpose of keeping the settings in the same object.

A reasonably neat and common work around is to use dot delimited paths:

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
      )
  )*/

This also makes it clearer you are not manipulating instance variables, as well as allowing arbitrary keys without fear of conflicts with instance variables. Further processing for specific keys can be achieved by simply adding key comparisons to get/set e.g.

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

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜