3

I need to find a specific key in an array, and return both its value and the path to find that key. Example:

$array = array(
  'fs1' => array(
    'id1' => 0,
    'foo' => 1,
    'fs2' => array(
      'id2' => 1,
      'foo2' => 2,
      'fs3' => array(
        'id3' => null,
      ),
      'fs4' => array(
        'id4' => 4,
        'bar' => 1,
      ),
    ),
  ),
);

search($array, 'fs3'); // Returns ('fs1.fs2.fs3', array('id3' => null))
search($array, 'fs2'); // Returns ('fs1.fs2',     array('id2' => 1, ... ))

I've been able to recurse through the array to find the correct key and return the data using RecursiveArrayIterator (shown below), but I don't know the best way to keep track of what path I'm currently on.

$i = new RecursiveIteratorIterator
    new RecursiveArrayIterator($array),
    RecursiveIteratorIterator::SELF_FIRST);
foreach ($i as $key => value) {
  if ($key === $search) {
    return $value;
  }
}
nachito
  • 6,975
  • 2
  • 25
  • 44
  • Are the keys always unique? That kind of defeats the purpose of having the array as multi-dimensional when all keys would fit in a single dimension. Do you just want to return the first instance or an array of all? – Jonathan Kuhn Dec 23 '14 at 19:40
  • @JonathanKuhn The keys are not necessarily unique, but it is fine to return the first result. I've inherited the structure of the search array so changing it isn't an option. – nachito Dec 23 '14 at 19:45
  • Seems to me like you could track your progress just using a recursive function and a simple foreach loop. – Jonathan Kuhn Dec 23 '14 at 19:51
  • That's what I was going to suggest -- just use a simple recursive function instead. – JMM Dec 23 '14 at 19:53
  • 1
    To get the keys from `recursiveIteratorIterator`, check this out: http://stackoverflow.com/questions/16855211/php-recursive-iterator-parent-key-of-current-array-iteration – Jonathan Kuhn Dec 23 '14 at 20:03

4 Answers4

6

Just for completion sake and future visitors. Combining the example code above and the answer I commented about to get the keys. Here is a working function that will return the requested results with one small change. In my return array I return the keys path and value instead of the requested 0 and $search for the keys. I find this more verbose and easier to handle.

<?php
$array = array(
    'fs1' => array(
        'id1' => 0,
        'foo' => 1,
        'fs2' => array(
            'id2' => 1,
            'foo2' => 2,
            'fs3' => array(
                'id3' => null,
            ),
            'fs4' => array(
                'id4' => 4,
                'bar' => 1,
            ),
        ),
    ),
);

function search($array, $searchKey=''){
    //create a recursive iterator to loop over the array recursively
    $iter = new RecursiveIteratorIterator(
        new RecursiveArrayIterator($array),
        RecursiveIteratorIterator::SELF_FIRST);

    //loop over the iterator
    foreach ($iter as $key => $value) {
        //if the key matches our search
        if ($key === $searchKey) {
            //add the current key
            $keys = array($key);
            //loop up the recursive chain
            for($i=$iter->getDepth()-1;$i>=0;$i--){
                //add each parent key
                array_unshift($keys, $iter->getSubIterator($i)->key());
            }
            //return our output array
            return array('path'=>implode('.', $keys), 'value'=>$value);
        }
    }
    //return false if not found
    return false;
}

$searchResult1 = search($array, 'fs2');
$searchResult2 = search($array, 'fs3');
echo "<pre>";
print_r($searchResult1);
print_r($searchResult2);

outputs:

Array
(
    [path] => fs1.fs2
    [value] => Array
        (
            [id2] => 1
            [foo2] => 2
            [fs3] => Array
                (
                    [id3] => 
                )

            [fs4] => Array
                (
                    [id4] => 4
                    [bar] => 1
                )

        )

)
Array
(
    [path] => fs1.fs2.fs3
    [value] => Array
        (
            [id3] => 
        )

)
Jonathan Kuhn
  • 15,279
  • 3
  • 32
  • 43
  • You saved my life. Thank you! I spent many many hours in writing/rewriting functions and whatever to accomplish this result and then after few more hours of searching I found this. – xZero May 15 '15 at 11:02
1

It looks like you are assuming the keys will always be unique. I do not assume that. So, your function must return multiple values. What I would do is simply write a recursive function:

function search($array, $key, $path='')
{
    foreach($array as $k=>$v)
    {
        if($k == $key) yield array($path==''?$k:$path.'.'.$k, array($k=>$v));
        if(is_array($v))
        { // I don't know a better way to do the following...
            $gen = search($v, $key, $path==''?$k:$path.'.'.$k);
            foreach($gen as $v) yield($v);
        }
    }
}

This is a recursive generator. It returns a generator, containing all hits. It is used very much like an array:

$gen = search($array, 'fs3');
foreach($gen as $ret)
    print_r($ret); // Prints out each answer from the generator
kainaw
  • 4,256
  • 1
  • 18
  • 38
1

There is already an answer in wich RecursiveIteratorIterator used. My solution might not be always optimal, I have not tested estimation time. It can be optimized by redefining the callHasChildren method of RecursiveIteratorIterator, so there will be no children when key is found. But this is outside the domain.

Here is the approach where you do not have to use explicit inner loops:

function findKeyPathAndValue(array $array, $keyToSearch)
{
    $iterator = new RecursiveIteratorIterator(
        new RecursiveArrayIterator($array),
        RecursiveIteratorIterator::CHILD_FIRST
    );

    $path = [];
    $value = null;
    $depthOfTheFoundKey = null;
    foreach ($iterator as $key => $current) {
        if (
            $key === $keyToSearch
            || $iterator->getDepth() < $depthOfTheFoundKey
        ) {
            if (is_null($depthOfTheFoundKey)) {
                $value = $current;
            }

            array_unshift($path, $key);
            $depthOfTheFoundKey = $iterator->getDepth();
        }
    }

    if (is_null($depthOfTheFoundKey)) {
        return false;
    }

    return [
        'path' => implode('.', $path),
        'value' => $value
    ];
}

Pay attention to RecursiveIteratorIterator::CHILD_FIRST. This flag reverses the order of iteration. So we can prepare our path using only one loop - this is actually the main purpose of recursive iterators. They hide all the inner loops from you.

Here is working demo.

sevavietl
  • 3,762
  • 1
  • 14
  • 21
1

In case you need to return array with all items matching certain key, you can use php generators

function recursiveFind(array $haystack, string $needle, $glue = '.'): ?\Generator
{
    $recursive = new \RecursiveIteratorIterator(
        new \RecursiveArrayIterator($haystack),
        \RecursiveIteratorIterator::SELF_FIRST
    );

    foreach ($recursive as $key => $value) {
        //if the key matches our search
        if ($key === $needle) {
            //add the current key
            $keys = [$key];
            //loop up the recursive chain
            for ($i = $recursive->getDepth() - 1; $i >= 0; $i--) {
                array_unshift($keys, $recursive->getSubIterator($i)->key());
            }

            yield [
                'path' => implode($glue, $keys),
                'value' => $value
            ];
        }
    }
}

usage:

foreach (recursiveFind($arrayToSearch, 'keyName') as $result) {
    var_dump($result);
}
Nazariy
  • 6,028
  • 5
  • 37
  • 61