4

I have an interesting problem... I am building an API where user specifies the location of some element in an array through string. Like this:

$path = "group1.group2.group3.element";

Given this string, I must save some value to the correct place in an array. For the example above this would be:

$results['group1']['group2']['group3']['element'] = $value;

Of course, the code needs to be generic for whatever $path user throws at me.

How would you solve this?

UPDATE - SOLUTION: using both ern0's (similar to my own) and nikc's answer as inspiration, this is the solution I decided on:

// returns reference to node in $collection as referenced by $path. For example:
//   $node =& findnode('dir.subdir', $some_array);
// In this case, $node points to $some_array['dir']['subdir'].
// If you wish to create the node if it doesn't exist, set $force to true 
// (otherwise it throws exception if the node is not found)
function &findnode($path, &$collection, $force = false)
{
    $parts = explode('.', $path);
    $where = &$collection;
    foreach ($parts as $part)
    {
        if (!isset($where[$part]))
        {
            if ($force)
                $where[$part] = array();
            else
                throw new RuntimeException('path not found');
        }
        $where =& $where[$part];
    }
    return $where;
}

$results = array();
$value = '1';
try {
    $bucket =& findnode("group1.group2.group3.element", $results, true);
} catch (Exception $e) {
    // no such path and $force was false
}
$bucket = $value; // read or write value here
var_dump($results);

Thank you all for the answers, it was a nice exercise! :)

7
  • With a descend function which traversing the path step by step ultimately returns a reference to the correct node in collection. Commented Dec 21, 2011 at 7:25
  • 2
    So, what the current 'interesting' solutions that you have come up with? And what is the problem you are facing ? Commented Dec 21, 2011 at 7:26
  • @nikc: could you give an example in an answer? Commented Dec 21, 2011 at 7:30
  • 2
    The problem is interesting, because it looks trivial, but it is not. We should find a short and elegant solution. Commented Dec 21, 2011 at 7:30
  • @em0: Exactly. I have a solution, but it seems ugly. I don't want to influence others - if there will be no suitable answer, I'll post it. Commented Dec 21, 2011 at 7:32

5 Answers 5

2

Maybe, I don't know PHP well, but I couldn't find a language element, which can insert an element into an array in any deep.

The quick and dirty solution is eval(), but as we know, it's evil. But if you're watching the input (dotted form) and the result (array indexes) more than 10 secs, you will ask: why the heck are we thinking on building custom-depth arrays and whatsoever, 'cause it took only two simple *str_replace()*s to transform the input to the result.

Edit: here's the eval version, don't use it:

 $x = str_replace(".","][",$path); 
 $x = '$result[' . $x . '] = "' . $value . '";'; 
 eval($x);

The other way is to use indirection to climb deep in a tree without knowing its depth:

$path = "group1.group2.group3.element";
$value = 55;

$x = explode(".",$path);

$result = Array();
$last = &$result;
foreach ($x as $elem) {
    $last[$elem] = Array();
    $last = &$last[$elem];
}
$last = $value;

echo("<pre>$path=$value\n");
print_r($result);

Collecting array element references for later completion is a very useful PHP feature.

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

4 Comments

Because we all have access to your local machine.
Only some, but I've tested it.
It's not an indictment of you code to provide a working, publicly accessible demonstration. It's quite common and helps others who are participating, and is a good habit to get into when answering. :) You would be surprised (eg, look below) of all the code that gets posted with no actual testing whatsoever.
@ern0: almost identical solution to mine... If nothing better comes up, I'll accept it. I don't like eval() out of principle. :)
2

Let me throw my own answer in the mix: :)

$path = "group1.group2.group3.element";
$results = array();

$parts = explode('.', $path);
$where = &$results;
foreach ($parts as $part)
{
    $where =& $where[$part];
}
$where = $value;

3 Comments

I don't like referencing a non-existent variable, that's why I am using $x = Array(); first. Maybe, it will not change in PHP, I just don't agree this kind of autovivification (I do like other forms of it; painfully, it's missing from JavaScript).
You are probably right, PHP might deprecate that in the future. They have done so on numerous occasions already.
You're on the right track here. I'll expand on this. (On another note, you should rather update your question, than add stuff as an answer. Newcomers to the question will find all relevant info easier that way.)
1

I don't think this will be the best one, but I tried to find out the solution as an exercise to myself :)

    $path = "group1.group2.group3.element"; //path
    $value = 2; //value

    $results = array(); //an array

    $t = explode(".",$path);    //explode the path into an array
    $n=count($t);   //number of items

    $i=0;   //a counter variable

    $r = &$results; //create the reference to the array

    foreach($t as $p)   //loop through each item
    {
        if($i == $n-1)      //if it reached the last element, then insert the value
        {
            $r[$p] = $value;
            break;
        }
        else    //otherwise create the sub arrays
        {
            $r[$p] = array();
            $r = &$r[$p];   

            $i++;
        }       
    }

    print_r($results);  //output the structure of array to verify it

    echo "<br>Value is: " . $results['group1']['group2']['group3']['element'];  //output the value to check

Hope it will work at your side too.. :)

2 Comments

If you recognise, that after the loop $r contains the last element, which can be used setting the value, you can throw out $i and the if branch. (And then it will be letter-by-letter same as my solution.)
oh yeah! Thanks for that.. :) I didn't noticed your solution. I was trying to figure out how to solve this right after seeing the question and posted quickly after I found the solution. :)
1

As I commented on your own answer, you are on the right track. Very close in fact. I prefer to use recursion though, but that's only a preference, this could all be done in a linear loop just as well.

To find a node (read) this works:

function &findnode(array $path, &$collection) {
    $node = array_shift($path);

    if (array_key_exists($node, $collection)) {        
        if (count($path) === 0) {
            // When we are at the end of the path, we return the node
            return $collection[$node];
        } else {
            // Otherwise, we descend a level further
            return findnode($path, $collection[$node]);
        }
    }

    throw new RuntimeException('path not found');
}

$collection = array(
    'foo' => array(
        'bar' => array(
            'baz' => 'leafnode @ foo.bar.baz'
            )
        )
    );

$path = 'foo.bar.baz';
$node =& findnode(explode('.', $path), $collection);

var_dump($node); // Output: 'leafnode @ foo.bar.baz'

To inject a node (write) we need to modify the logic slightly to create the path as we go.

function &findnode(array $path, &$collection, $create = false) {
    $node = array_shift($path);

    // If create is set and the node is missing, we create it
    if ($create === true && ! array_key_exists($node, $collection)) {
        $collection[$node] = array();
    } 

    if (array_key_exists($node, $collection)) {        
        if (count($path) === 0) {
            // When we are at the end of the path, we return the node
            return $collection[$node];
        } else {
            // Otherwise, we descend a level further
            return findnode($path, $collection[$node], $create);
        }
    }

    throw new RuntimeException('path not found');
}

$collection = array(
    'foo' => array(
        'bar' => array(
            'baz' => 'leafnode @ foo.bar.baz'
            )
        )
    );

$path = explode('.', 'baz.bar.foo');
$leaf = array_pop($path); // Store the leaf node

// Write
$node =& findnode($path, $collection, true);
$node[$leaf] = 'foo.bar.baz injected';

var_dump($collection); // Will have the new branch 'baz.bar.foo' with the injected value at the leaf

To make all this nice and pretty, you would wrap the read and write operations in their own functions. More likely all of this inside its own class.

So using the above version of findnode, we can have these two functions to read and write from/to your collection array.

function read($path, $collection) {
    $path = explode('.', $path);
    $val =& findnode($path, $collection);

    return $val;
} 

function write($value, $path, $collection) {
    $path = explode('.', $path);
    $leaf = array_pop($path);
    $node =& findnode($path, $collection, true);

    $node[$leaf] = $value;
}

NB! This is not a complete solution or the most elegant. But you can probably figure out the rest for yourself.

Comments

0

I hope below code will work,

$path = "group1.group2.group3.element";
$tempArr = explode('.', $path);
$results = array();
$arrStr = '$results';
$value = 'testing';
foreach( $tempArr as $ky=>$val) {
    $arrStr .= "['".$val."']";
    ( $ky == count($tempArr) - 1 ) ? $arrStr .= ' = $value;' : '';
}
eval($arrStr);
print_r($results);

2 Comments

$x = str_replace(".","][",$path); $x = '$result[' . $x . '] = "' . $value . '";'; eval($x); is shorter.
Not really liking the use of eval(), but I suppose it works: codepad.org/0oRonvPn

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.