3
$arr = array(
  'a'          => 1,
  'b'          => 15,
  'c'          => 0,
);

$arr['c'] = &$arr;

print_r($arr); // <-- CYCLE

Do you know how can I detect if I have array values that somehow point to the existing element, or cause a infinite loop?

Jordan Running
  • 102,619
  • 17
  • 182
  • 182
thelolcat
  • 10,995
  • 21
  • 60
  • 102
  • 1
    As you're iterating the array, you keep track of where you've been - basically checking if you're stepping in your own footprints while going in circle in the woods. – Marc B Feb 14 '12 at 21:49
  • `if( !in_Array( $current, $visited)){ $visited[] = &$current ; goDeeper()}` doesn't work? – Vyktor Feb 14 '12 at 22:00
  • http://stackoverflow.com/questions/9105816/is-there-a-way-to-detect-circular-arrays-in-pure-php – Cheery Feb 14 '12 at 22:10

4 Answers4

3

Use memory, Luke. When your iterator comes across the array as an elementof another one, just store reference/id of it to smth like set or list (or another suitable container, eg. array). So you memorize what array you have already processed, and then ignore it when meet next time or stop the loop.

<?php
function get_id(&$array) {
  return crc32(serialize($array)); 
}

# This will not work as expected. I leave it here just for future versions.
function get_id_alt(&$array) {
  $obj = (object) $array;
  return spl_object_hash($obj);
}

function iterate(array $input, &$reached=array(), $id_func='get_id') {
  // this is an unseen array, memorize it
  array_push($reached, $id_func($input));

  foreach($input as $key=>$item) {
    if (is_array($item)) {        // This is an array
      if (in_array($id_func($item), $reached)) {
        // We found an array that have been processed before. What will you do?
      } else {
        // Recurse!
        iterate($item, $reached);
      }
    } else {
      // It is not traversable value, do what you want here
    }
  }
}

PS: I'm using spl_object_hash as a ID-function, you may use another one, if prefered (but i don't know others that can identify the same objects, as this one does).

UPD: Using spl_object_hash doesn't give right results as of PHP 5.3.10: it treats any array as the same object regardless of its content. But using smth like %hash_fn%(serialize($array)) works well (beware of performance degradation!).

Andrew D.
  • 1,022
  • 12
  • 26
  • it says: Warning: spl_object_hash() expects parameter 1 to be object, array given in.. – thelolcat Feb 14 '12 at 22:13
  • doesn't seem to work if I have other arrays as values: http://codepad.org/NT079que (I get the "recursion" notice on them too) – thelolcat Feb 14 '12 at 22:38
  • This is because of the `spl_object_hash` wierd behaviour. I've just tried my own test with it (especially pure casting from array to object), and it gives me the same hashes for different arrays (with different items) even when I wrap them via `ArrayObject`. You suggestion about `serailize` would help there, but I'll try to solve the problem with native PHP hasher – Andrew D. Feb 14 '12 at 22:50
  • 1
    ok I found out the issue. You have to instantiate a new variable lol, like `$object_version = (object)$input;` and pass that variable to spl_object_hash. It appears that the spl thingie doesn't compare object properties, just some internal variable IDs and stuff... I thought that casting stuff internally creates a variable, but it appears it doesn't... – thelolcat Feb 14 '12 at 22:55
  • I've tried to create new variable, but that didn't help. It produces different hash for every variable. So, if get working code, show it, please. As for me, I got correct behaviour only with `crc32(serialize($arr))` but not with `spl_object_hash`. – Andrew D. Feb 14 '12 at 23:32
  • Strange PHP, so strange.. I'd better leave you suggestion with serialize, my `spl_object_hash` failed. Great thanks for your assistance) – Andrew D. Feb 14 '12 at 23:55
  • I think it can be done with splobjectstorage. Basically you need to create a object from the array then iterate trough this object instead of the array :) – thelolcat Feb 15 '12 at 00:00
  • Nope. SplObjectStorage failed (it seems like using spl_object_hash also). I give up.. I'm not pro in PHP, and some time ago i promised myself forget about this language at all (because of its bloatness architectural misses) :) If you have an opportunity, move to Python or smth else, I think. if you dive into PHP, you will often get stuck on sutff like that on your way down – Andrew D. Feb 15 '12 at 00:24
3

If print_r tells you of recursions, why don't use it? :)

// Set up bad array
$arr = array(
  'a'          => 1,
  'b'          => 15,
  'c'          => 0,
);
$arr['c'] = &$arr;

// Check print_r
if(strpos(print_r($a,1),'*RECURSION*') !== false) {
  echo "Houston, we got a problem!\n";
}

Edit: As outlined by @Vyktor, this does not work in all cases and may produce false positves, but serialize() also gives an indicator for recursion. It gives R for recursion. So we check, if there is an R outside of any string in the output of serialize():

<?php
// Set up bad array
$arr = array(
  'a'          => 1,
  'b'          => 15,
  'c'          => 0,
);
$arr['c'] = &$arr;

$str = serialize($arr);   // Serialize it
$len = strlen($str);      // Get the length

// Simple serialize "parser"
$state = 0;
$recursion_found = false;
for($i=0;$i<$len;$i++) {
  $byte = $str[$i];
  if($byte == "\"" && $state == 0) {
    $state = 1; // in string!
  } else if($byte == "\"" && $state == 1) {
    // Check if the " is escaped
    if($str[$i-1] != "\\") {
      $state = 0; // not in string
    }
  } else if($byte == "R" && $state == 0) { // any R that is not in a string
    $recursion_found = true;
  }
}

if($recursion_found) {
  echo "There is recursion!\n";
}
iblue
  • 29,609
  • 19
  • 89
  • 128
  • This doesn't answer his question + `"We should prevent using *RECURSION* when building functions"` – Vyktor Feb 14 '12 at 22:17
  • He could parse the output of `print_r` :) – iblue Feb 14 '12 at 22:18
  • `array( "We should prevent using *RECURSION* when building functions")` you'd mark this as array containing recursion – Vyktor Feb 14 '12 at 22:19
  • Yep, I know. But not, if he'd write a print_r parser. – iblue Feb 14 '12 at 22:22
  • `array( "\n [key] => *RECURSIVE*")` is unparsable as output of `print_r` isn't it? :) – Vyktor Feb 14 '12 at 22:27
  • How about `var_export()`? `var_export()` gives parseable output and should give an alert, if there is any recursion, shouldn't it? – iblue Feb 14 '12 at 22:29
  • with var_export: nesting level too deep – thelolcat Feb 14 '12 at 22:32
  • Ok, may be `print_r` and `var_export` are a bad idea, but at least, `print_r` can give some heuristics. – iblue Feb 14 '12 at 22:51
  • Wait, I got one more! How about `serialize()`? :) – iblue Feb 14 '12 at 22:52
  • No, I thought of that too, but then you would get false positives on two different arrays that happen to have the same values... – thelolcat Feb 14 '12 at 22:58
  • Can you explain this further? I've tried `serialize()` on your array and two arrays that both contain `array(1, 2, 3)`. On your array, it outputs a `R` for recursion, the others do not display it. – iblue Feb 14 '12 at 23:04
  • Yes, but consider two different variables, both with the same form of `$arr`. They will look the same serialized so it would appear there's recursion inside them, when actually it isn't. I'm not sure even spl_object_hash handles this, but I hope it does :) – thelolcat Feb 14 '12 at 23:12
  • I have updated my post, serialize detects recursion directly. – iblue Feb 14 '12 at 23:14
  • Iblue create `$arr2 = array('a' => 1, 'b' => 15, 'c' => 0);$arr2['c'] = &$arr2'`, then assign to `$arr['c'] = $arr2` and you will see what I mean. – thelolcat Feb 14 '12 at 23:17
  • Then `$arr2` will contain a recursion and will be copied to `$arr['c']`, so both `$arr` and `$arr2` will contain a recursion and the script works perfectly. – iblue Feb 14 '12 at 23:24
3

(I just posted this as an answer to the Is there a way to detect circular arrays in pure PHP? question that @Cheery mentioned, but I may be updating that answer depending on that asker's needs.)

The isRecursiveArray(array) method below detects circular/recursive arrays. It keeps track of which arrays have been visited by temporarily adding an element containing a known object reference to the end of the array.

function removeLastElementIfSame(array & $array, $reference) {
    if(end($array) === $reference) {
        unset($array[key($array)]);
    }
}

function isRecursiveArrayIteration(array & $array, $reference) {
    $last_element   = end($array);
    if($reference === $last_element) {
        return true;
    }
    $array[]    = $reference;

    foreach($array as &$element) {
        if(is_array($element)) {
            if(isRecursiveArrayIteration($element, $reference)) {
                removeLastElementIfSame($array, $reference);
                return true;
            }
        }
    }

    removeLastElementIfSame($array, $reference);

    return false;
}

function isRecursiveArray(array $array) {
    $some_reference = new stdclass();
    return isRecursiveArrayIteration($array, $some_reference);
}



$array      = array('a','b','c');
var_dump(isRecursiveArray($array));
print_r($array);



$array      = array('a','b','c');
$array[]    = $array;
var_dump(isRecursiveArray($array));
print_r($array);



$array      = array('a','b','c');
$array[]    = &$array;
var_dump(isRecursiveArray($array));
print_r($array);



$array      = array('a','b','c');
$array[]    = &$array;
$array      = array($array);
var_dump(isRecursiveArray($array));
print_r($array);
Community
  • 1
  • 1
lightster
  • 1,200
  • 9
  • 10
0

Derivative work for arrays and objects (both):

function isRecursiveArrayObjectTest(&$object, $marker)
{
    if ( is_array($object) ) {
        // Move the array pointer to the end and test if we encounter the marker (if so, its recursive)
        $last = end($object);
        if ($marker === $last) {
            return true;
        }
        // Add a marker to the end of the array to test for recursion (removed upon exit)
        $object[] = $marker;
    } elseif ( is_object($object) ) {
        // Treat objects differently
        if ( $marker === $object->__testRecursion ) {
            return true;
        }
        // Add a marker using a member that is unlikely to be defined (removed upon exit)
        $object->__testRecursion = $marker;
    } else {
        return false;
    }

    $isRecur = false;

    // Loop over the elements of the array, recursively testing each one (if it is an array|object)
    foreach($object as &$element) {
        if(is_array($element) || (is_object($element) && $element !== $marker)) {
            if(isRecursiveArrayObjectTest($element, $marker)) {
                $isRecur = true;
                break;
            }
        }
    }

    // remove marker before we leave
    if (is_array($object) && end($object) === $marker) {
        unset($object[key($object)]);
    } elseif ( is_object($object) && isset($object->__testRecursion) ) {
        unset($object->__testRecursion);
    }

    return $isRecur;
}

function isRecursiveArrayObject($object)
{
    // Create a 'marker' to detect where we have visited to determine recursion
    $marker = new stdClass();
    return isRecursiveArrayObjectTest($object, $marker);
}