Есть ли способ обнаружить круговые массивы в чистом PHP?

Я пытаюсь реализовать свою собственную функцию сериализации /var _dump в PHP. Это кажется невозможным, если есть возможность круговых массивов (что есть).

В последних версиях PHP var_dump обнаруживает круговые массивы:

php > $a = array();
php > $a[] = &$a;
php > var_dump($a);
array(1) {
  [0]=>
  &array(1) {
    [0]=>
    *RECURSION*
  }
}

Как мне реализовать собственный метод сериализации в PHP, который может обнаруживать аналогично? Я не могу просто отслеживать, какие массивы я посетил, потому что строгое сравнение массивов в PHP возвращает true для разных массивов, которые содержат одни и те же элементы, и сравнение круговых массивов вызывает фатальную ошибку. В любом случае.

php > $b = array(1,2);
php > $c = array(1,2);
php > var_dump($b === $c);
bool(true)
php > $a = array();
php > $a[] = &$a;
php > var_dump($a === $a);
PHP Fatal error:  Nesting level too deep - recursive dependency? in php shell code on line 1

Я искал способ найти уникальный идентификатор (указатель) для массива, но я не могу его найти. spl_object_hash работает только с объектами, а не с массивами. Если я передаю несколько разных массивов объектам, они получают одинаковое значение spl_object_hash (почему?).

EDIT:

Вызов print_r, var_dump или сериализация в каждом массиве, а затем использование какого-либо механизма для обнаружения присутствия рекурсии, обнаруженного этими методами, является кошмаром с алгоритмической сложностью и в основном сделает использование слишком медленным, чтобы быть практичным на больших вложенных массивах.

ПРИНЯТЫЙ ОТВЕТ:

Я принял ответ ниже, который первым предложил временно изменить массив, чтобы убедиться, что он действительно тот же, что и другой массив. Это отвечает на вопрос "как сравнить два массива для идентификации?" из которого обнаружение рекурсии тривиально.

Ответы

Ответ 1

Метод isRecursiveArray (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);

Ответ 2

Смешной метод (я знаю, что это глупо:)), но вы можете изменить его и отслеживать "путь" к рекурсивному элементу. Это просто идея:) Исходя из свойства сериализованной строки, когда начинается рекурсия, она будет такой же, как строка для исходного массива. Как вы можете видеть - я пробовал это во многих разных вариантах, и может быть, что-то может "обмануть" его, но "обнаруживает" все перечисленные рекурсии. И я не пытался рекурсивные массивы с объектами.

$a = array('b1'=>'a1','b2'=>'a2','b4'=>'a3','b5'=>'R:1;}}}');
$a['a1'] = &$a;
$a['b6'] = &$a;
$a['b6'][] = array(1,2,&$a);
$b = serialize($a); 
print_r($a);
function WalkArrayRecursive(&$array_name, &$temp){
    if (is_array($array_name)){
        foreach ($array_name as $k => &$v){
           if (is_array($v)){
                if (strpos($temp, preg_replace('#R:\d+;\}+$#', '', 
                               serialize($v)))===0) 
                { 
                  echo "\n Recursion detected at " . $k ."\n"; 
                  continue; 
                }
                WalkArrayRecursive($v, $temp);
            }
        }
    }
}
WalkArrayRecursive($a, $b);

regexp - это ситуация, когда элемент с рекурсией находится на "конце" массива. и, да, эта рекурсия связана со всем массивом. Можно сделать рекурсию подэлементов, но мне слишком поздно думать о них. Так или иначе, каждый элемент массива должен быть проверен для рекурсии в своих подэлементах. Точно так же, как и выше, через выход функции print_r или поиск определенной записи для рекурсии в сериализованной строке (R:4;} что-то вроде этого). И отслеживание должно начинаться с этого элемента, сравнивая все ниже с помощью script. Все это только в том случае, если вы хотите определить, где начинается рекурсия, а не только, есть ли у вас это или нет.

ps: но лучше всего, как я думаю, лучше написать собственную функцию unserialize из serailized string, созданной самой php.

Ответ 3

Мой подход состоит в том, чтобы иметь временный массив, который содержит копию всех объектов, которые уже были итерации. вот так:

// We use this to detect recursion.
global $recursion;
$recursion = [];

function dump( $data, $label, $level = 0 ) {
    global $recursion;

    // Some nice output for debugging/testing...
    echo "\n";
    echo str_repeat( "  ", $level );
    echo $label . " (" . gettype( $data ) . ") ";

    // -- start of our recursion detection logic
    if ( is_object( $data ) ) {
        foreach ( $recursion as $done ) {
            if ( $done === $data ) {
                echo "*RECURSION*";
                return;
            }
        }

        // This is the key-line: Remember that we processed this item!
        $recursion[] = $data;
    }
    // -- end of recursion check

    if ( is_array( $data ) || is_object( $data ) ) {
        foreach ( (array) $data as $key => $item ) {
            dump( $item, $key, $level + 1 );
        }
    } else {
        echo "= " . $data;
    }
}

И вот какой-то быстрый демо-код, чтобы проиллюстрировать, как это работает:

$obj = new StdClass();
$obj->arr = [];
$obj->arr[] = 'Foo';
$obj->arr[] = $obj;
$obj->arr[] = 'Bar';
$obj->final = 12345;
$obj->a2 = $obj->arr;

dump( $obj, 'obj' );

Этот script будет генерировать следующий вывод:

obj (object) 
  arr (array) 
    0 (string) = Foo
    1 (object) *RECURSION*
    2 (string) = Bar
  final (integer) = 12345
  a2 (array) 
    0 (string) = Foo
    1 (object) *RECURSION*
    2 (string) = Bar

Ответ 4

Это не изящно, но решает вашу проблему (по крайней мере, если у вас нет кого-то, использующего * RECURSION * в качестве значения).

<?php
$a[] = &$a;
if(strpos(print_r($a,1),'*RECURSION*') !== FALSE) echo 1;