Есть ли способ обнаружить круговые массивы в чистом 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;