5

I have a class which stores values with a multi-level associative array:

I need to add a way to access and modify nested values. Here is a working solution for my problem, but it is rather slow. Is there a better way of doing this?

Note: The use of get / set functions is not mandatory, but there needs to be an efficient way to define a default value.

class Demo {
    protected $_values = array();

    function __construct(array $values) {
        $this->_values = $values;
    }

    public function get($name, $default = null) {
        $token = strtok($name, '.#');
        $node = $this->_values;
        while ($token !== false) {
            if (!isset($node[$token]))
                return $default;
            $node = $node[$token];
            $token = strtok('.#');
        }
        return $node;
    }

    public function set($name, $value) {
        $next_token = strtok($name, '.#');
        $node = &$this->_values;

        while ($next_token !== false) {
            $token = $next_token;
            $next_token = strtok('.#');

            if ($next_token === false) {
                $node[ $token ] = $value;
                break;
            }
            else if (!isset($node[ $token ]))
                $node[ $token ] = array();

            $node = &$node[ $token ];
        }

        unset($node);
    }

}

Which would be used as follows:

$test = new Demo(array(
    'simple'  => 27,
    'general' => array(
        0 => array(
            'something'    => 'Hello World!',
            'message'      => 'Another message',
            'special'      => array(
                'number'       => 27
            )
        ),
        1 => array(
            'something'    => 'Hello World! #2',
            'message'      => 'Another message #2'
        ),
    )
));

$simple = $test->get('simple'); // === 27

$general_0_something = $test->get('general#0.something'); // === 'Hello World!'

$general_0_special_number = $test->get('general#0.special.number'); === 27

Note: 'general.0.something' is the same as 'general#0.something', the alternative punctuation is for the purpose of clarity.

20
  • Why do you save and get the data in different formats? Commented Apr 5, 2011 at 11:01
  • 1
    You save it as array, but you request the data with an string as parameter. Commented Apr 5, 2011 at 11:05
  • 1
    @Lea Hayes: I suspect you might be looking for the same functionality as provided by the Zend_Dom_Query component of Zend Framework. You might consider having a look at it: framework.zend.com/manual/en/zend.dom.query.html Commented Apr 5, 2011 at 11:16
  • 2
    @Lea Hayes: And what's wrong with your current method of doing it? It seems OK, especially if you want a nice getter syntax. Commented Apr 5, 2011 at 11:19
  • 2
    @Lea Hayes: I've had an attempt at the getter by using preg_split in stead of strtok, but it was twice as slow. So I think your solution is pretty fast as it is. I'd be surprised to see a faster solution. Commented Apr 5, 2011 at 14:32

4 Answers 4

3

Well, the question was interesting enough that I couldn't resist tinkering a bit more. :-)

So, here are my conclusions. Your implementation is probably the most straightforward and clear. And it's working, so I wouldn't really bother about searching for another solution. In fact, how much calls are you gonna get in the end? Is the difference in performance worth the trouble (I mean between "super ultra blazingly fast" and "almost half as fast")?

Put aside though, if performance is really an issue (getting thousands of calls), then there's a way to reduce the execution time if you repetitively lookup the array.

In your version the greatest burden falls on string operations in your get function. Everything that touches string manipulation is doomed to fail in this context. And that was indeed the case with all my initial attempts at solving this problem.

It's hard not to touch strings if we want such a syntax, but we can at least limit how much string operations we do.

If you create a hash map (hash table) so that you can flatten your multidimensional array to a one level deep structure, then most of the computations done are a one time expense. It pays off, because this way you can almost directly lookup your values by the string provided in your get call.

I've come up with something roughly like this:

<?php

class Demo {
    protected $_values = array();
    protected $_valuesByHash = array();

    function createHashMap(&$array, $path = null) {
        foreach ($array as $key => &$value) {
            if (is_array($value)) {
                $this->createHashMap($value, $path.$key.'.');
            } else {
                $this->_valuesByHash[$path.$key] =& $value;
            }
        }
    }

    function __construct(array $values) {
        $this->_values = $values;
        $this->createHashMap($this->_values);

        // Check that references indeed work
        // $this->_values['general'][0]['special']['number'] = 28;
        // print_r($this->_values);
        // print_r($this->_valuesByHash);
        // $this->_valuesByHash['general.0.special.number'] = 29;
        // print_r($this->_values);
        // print_r($this->_valuesByHash);
    }

    public function get($hash, $default = null) {
        return isset($this->_valuesByHash[$hash]) ? $this->_valuesByHash[$hash] : $default;
    }
}


$test = new Demo(array(
    'simple'  => 27,
    'general' => array(
        '0' => array(
            'something'    => 'Hello World!',
            'message'      => 'Another message',
            'special'      => array(
                'number'       => 27
            )
        ),
        '1' => array(
            'something'    => 'Hello World! #2',
            'message'      => 'Another message #2'
        ),
    )
));

$start = microtime(true);

for ($i = 0; $i < 10000; ++$i) {
    $simple = $test->get('simple', 'default');
    $general_0_something = $test->get('general.0.something', 'default');
    $general_0_special_number = $test->get('general.0.special.number', 'default');
}

$stop = microtime(true);

echo $stop-$start;

?>

The setter is not yet implemented, and you would have to modify it for alternative syntax (# separator), but I think it conveys the idea.

At least on my testbed it takes half the time to execute this compared to the original implementation. Still raw array access is faster, but the difference in my case is around 30-40%. At the moment that was the best I could achieve. I hope that your actual case is not big enough that I've hit some memory constraints on the way. :-)

Sign up to request clarification or add additional context in comments.

2 Comments

super stuff!! Thank you so much for persevering with this!
+1 Cool, totally different approach and goal than mine, but more in line with the OP's reasoning.
3

Ok, my first approached missed the goal I was aiming for. Here is the solution to using native PHP array syntax (at least for access) and still being able to set a default value.

Update: Added missing functionality for get/set and on the fly converting.

By the way, this is not an approach to take if you are optimizing for performance. This is perhaps 20 times slower than regular array access.

class Demo extends ArrayObject {
    protected $_default;
    public function __construct($array,$default = null) {
        parent::__construct($array);
        $this->_default = $default;
    }
    public function  offsetGet($index) {
        if (!parent::offsetExists($index)) return $this->_default;
        $ret = parent::offsetGet($index);
        if ($ret && is_array($ret)) {
            parent::offsetSet($index, $this->newObject($ret));
            return parent::offsetGet($index);
        }
        return $ret;
    }
    protected function newObject(array $array=null) {
        return new self($array,$this->_default);
    }
}

Init

$test = new Demo(array(
    'general' => array(
        0 => array(
            'something'    => 'Hello World!'
        )
    )
),'Default Value');

Result

$something = $test['general'][0]['something']; // 'Hello World!'
$notfound = $test['general'][0]['notfound']; // 'Default Value'

8 Comments

@Peter: no offense, but this is 'pretty' fugly. ;)
None taken, I know that, this was sort of a failed attempt of making it 'pretty'.
@Peter: I see what you tried to do though, especially with the offsetGet part. That would have been my path of reasoning as well probably. Only to end up with pretty much the same as with what you ended up with. :)
@Peter: that is very nice actually! Problem is; I think OP wants to be able to set a default value with the getter, in stead of a 'global' default value. Now, your first attempt actually had this, but I think it kind of defeats the purpose, as OP wanted to circumvent doing $value = isset( $test['general'][0]['notfound'] ) ? $test['general'][0]['notfound'] : 'some default value'; Which is not much different then $test->setDefault( 'some default value' ); $value = $test['general'][0]['notfound'];. Nonetheless, this is a decent way to access multidimensional elements in an ArrayObject!
So just for that, I am gonna upvote this, with the note that it is probably not exactly what OP was looking for. But of course OP is the only one to be the judge of that. :)
|
1

You're looking for something like that? Essentially the get() method uses references to descend into the $values array and breaks out of the method if a requirement could not be met.

class Demo {
    protected $_values = array();

    public function __construct(array $values) {
        $this->_values = $values;
    }

    public function get($name, $default = null) {
        $parts  = preg_split('/[#.]/', $name);
        if (!is_array($parts) || empty($parts)) {
            return null;
        }

        $value  = &$this->_values;
        foreach ($parts as $p) {
            if (array_key_exists($p, $value)) {
                $value  = &$value[$p];
            } else {
                return null;
            }
        }

        return $value;
    }

    /**
     * setter missing
     */
}

$test = new Demo(array(
    'simple'  => 2,
    'general' => array(
        0 => array(
                'something'    => 'Hello World!',
                'message'      => 'Another message',
                'special'      => array(
                    'number'       => 4
                )
            ),
        1 => array(
                'something'    => 'Hello World! #2',
                'message'      => 'Another message #2'
            )
    )
));

$v = $test->get('simple'); 
var_dump($v);

$v = $test->get('general'); 
var_dump($v);

$v = $test->get('general.0'); 
var_dump($v);

$v = $test->get('general#0'); 
var_dump($v);

$v = $test->get('general.0.something'); 
var_dump($v);

$v = $test->get('general#0.something'); 
var_dump($v);

$v = $test->get('general.0.message'); 
var_dump($v);

$v = $test->get('general#0.message'); 
var_dump($v);

$v = $test->get('general.0.special'); 
var_dump($v);

$v = $test->get('general#0.special'); 
var_dump($v);

$v = $test->get('general.0.special.number'); 
var_dump($v);

$v = $test->get('general#0.special.number'); 
var_dump($v);

$v = $test->get('general.1'); 
var_dump($v);

$v = $test->get('general#1'); 
var_dump($v);

$v = $test->get('general.1.something'); 
var_dump($v);

$v = $test->get('general#1.something'); 
var_dump($v);

$v = $test->get('general.1.message'); 
var_dump($v);

$v = $test->get('general#1.message'); 
var_dump($v);

Comments

0

This is how multidimensional array work in general in PHP:

$data = array(
    'general' => array(
         0 => array(
             'something'    => 'Hello World!'
         )
    )
);

To receive Hello World:

echo $data['general'][0]['something'];

1 Comment

Ideally this needs to go through a getter method so that a default can be added. Otherwise there are countless isset's

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.