Плоский объект PHP в иерархический объект

По этой теме уже было несколько вопросов, но обычно они касаются превращения иерархических объектов в плоские объекты, что для меня не проблема. Однако в настоящее время я полностью запутался в создании иерархического объекта из плоского объекта.

Предположим, что следующая упрощенная таблица базы данных

id | parent 
 1 |   0
 2 |   1 
 3 |   1
 4 |   3
 5 |   4

После запроса к базе данных возвращается следующий объект: $queryResult:

[{id:1, parent:0}, 
 {id:2, parent:1}, 
 {id:3, parent:1}, 
 {id:4, parent:3},
 {id:5, parent:4}]

То, что я пытаюсь выполнить с помощью рекурсивного метода, создает объект, идентичный следующему:

[{
   id:1,
   children:[
      {id:2},
      {id:3,
          children:[
              {id:4, 
               children:[
                  {id:5}
               ]
              }
          ]
      }  
   ]
}] 

У меня есть некоторые начинания, но мой мозг просто не может идти в ногу... Есть ли у кого-нибудь предложения по рекурсивной функции, которая возвращает желаемый объект? Ниже приведен грязный кусок кода (думаю, набросок), который у меня сейчас есть. Он работает правильно, пока функция не будет вызвана рекурсивно, и она не начнет дублировать записи.

function CreateObject($object, $new = array()){

    for($i = 0; $i < count($object); $i++){
        $found = false;
        foreach($object[$i] as $key => $value){
            if ($key == 'parent'){
                if ($value == 0){
                    array_push($new, $object[$i]);
                } else {
                    //push into parent
                    for($j = 0; $j < count($new); $j++){
                        foreach($new[$j] as $k => $v){
                            if ($k == 'id' && $v == $value){
                                $found = true;
                               
                                if (property_exists($new[$j], 'children')){
                                    array_push($new[$j]->children, $object[$i]);
                                } else {
                                    $new[$j]->children = array();
                                    array_push($new[$j]->children, $object[$i]);
                                }

                            }
                        }
                        
                        if (!$found){
                            foreach($new[$j] as $k => $v){
                                if ($k == 'children'){
                                    CreateObject($object, $v);
                                }
                            }
                           break;
                        }  
                    }
                }
            }
        }
    } 
    
    return $new;
}


$hierarchalObject = CreateObject($flatObject);

С быстро созданным объектом: (это приводит к точно такому же объекту, возвращаемому запросом)

$flatObject = array(
    array('id' => 1, 'parent' => 0),
    array('id' => 2, 'parent' => 1),
    array('id' => 5, 'parent' => 2),
    array('id' => 6, 'parent' => 2),
    );

$flatObject = json_decode(json_encode($flatObject));

возвращает:

[
    {
        "id": 1,
        "parent": 0,
        "children": [
            {
                "id": 2,
                "parent": 1,
                "children": [
                    {
                        "id": 5,
                        "parent": 2
                    },
                    {
                        "id": 6,
                        "parent": 2
                    },
                    {      //DUPLICATES HERE
                        "id": 5,
                        "parent": 2
                    },
                    {
                        "id": 6,
                        "parent": 2
                    }
                ]
            }
        ]
    }
]

Я старался быть максимально полным. Любая помощь будет принята с благодарностью для моего больного мозга.

ОБНОВЛЕНИЕ

Благодаря ответу C3iG3 следующая функция отлично работает в моей ситуации. Это слегка измененная версия ответа C3iG3, включающая все свойства узла, а также не включающая дочернее свойство, если оно пустое.

<?php
        function unflatten ( $data )
        {
            /* Create new node objects */
            function create_new_node ( $o ) {
                return new class ( $o ) {
                    public $id;

                    public function __construct ( $o ) { 
                        //get all properties and assign to node
                        foreach($o as $key => $value){
                            if ($key != 'parent'){
                                $this->$key = $value;
                            }
                        }
                    }
                };
            }            
        
            $roots = array(); // store root nodes
            $nodes = array(); // store all nodes
        
            foreach ( $data as $o ) 
            {
                $node = isset( $nodes[$o->id] ) ? $nodes[$o->id] : null; // check if node was already created
        
                if ( !$node ) {
                    $node = create_new_node($o);
                }
        
                $nodes[$o->id] = $node; // keep track of node
        
                if ( $o->parent ) // node has a parent
                {
                    if ( !isset( $nodes[$o->parent] ) ) { // if parent node was not already created
                        $nodes[$o->parent] = create_new_node( $o->parent );
                    }
                    $nodes[$o->parent]->children[] = $node; // add node to parent
                } else {
                    $roots[] = $node;
                }
            }
        
            return $roots;
        }
?>

person Richard V    schedule 18.12.2020    source источник
comment
Самое простое, что я могу придумать, это установить индекс для каждого элемента в дочернем массиве в качестве идентификатора, чтобы избежать дублирования. Таким образом, 1_. Тогда вместо использования array_push, я думаю, вы можете просто установить значение, например $new[$j]->children[$k] = $object[$i];   -  person WOUNDEDStevenJones    schedule 18.12.2020
comment
Спасибо за понимание WOUNDEDStevenJones, это оказалось неправильным подходом.   -  person Richard V    schedule 19.12.2020


Ответы (1)


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

Для рекурсивной функции нет очевидных отправных точек. Если вы начинаете с родительского объекта, вы не знаете, какие дочерние элементы привязать к родителю, поэтому нет способа разделить проблему (невозможно выполнить рекурсию вниз по иерархии). Вы можете рекурсивно подняться по иерархии (следуя родителям), но поскольку это никоим образом не разделяет проблему, рекурсивное написание не принесет пользы. Конечно, вы можете написать рекурсивное решение, которое создает узлы, обходит существующую иерархию и вставляет их, но это будет неэффективно для больших иерархий.

Вместо этого эта проблема лучше подходит для итеративного решения.

<?php

function unflatten ( $data )
{
    /* Create new node objects */
    function create_new_node ( $id ) {
        return new class ( $id ) {
            public $id;
            public $children = array();
            
            public function __construct ( $id ) { $this->id = $id; }
        };
    }

    $roots = array(); // store root nodes
    $nodes = array(); // store all nodes

    foreach ( $data as $o ) 
    {
        $node = isset( $nodes[$o->id] ) ? $nodes[$o->id] : null; // check if node was already created

        if ( !$node ) {
            $node = create_new_node( $o->id );
        }

        $nodes[$o->id] = $node; // keep track of node

        if ( $o->parent ) // node has a parent
        {
            if ( !isset( $nodes[$o->parent] ) ) { // if parent node was not already created
                $nodes[$o->parent] = create_new_node( $o->parent );
            }
            $nodes[$o->parent]->children[] = $node; // add node to parent
        } else {
            $roots[] = $node;
        }
    }

    return $roots;
}

$queryResult = json_decode('[{"id":1, "parent":0}, {"id":2, "parent":1}, {"id":3, "parent":1}, {"id":4, "parent":3}, {"id":5, "parent":4}]' );

echo json_encode( unflatten( $queryResult ) );

Это создаст следующую структуру:

[{
    "id": 1,
    "children": [{
        "id": 2,
        "children": []
    }, 
    {
        "id": 3,
        "children": [{
            "id": 4,
            "children": [{
                "id": 5,
                "children": []
            }]
        }]
    }]
}]

Это решение требует единственного прохода по исходному набору объектов и связывает их с родительскими объектами за постоянное время. Необходимость отслеживать все существующие узлы в массиве nodes приводит к небольшим накладным расходам.

Это решение можно переписать, чтобы оно было рекурсивным, но в конечном итоге оно будет моделировать циклическую структуру (что является хорошим показателем того, что задача лучше подходит для итеративного написания).

person C3iG3    schedule 19.12.2020
comment
СПАСИБО! Я все больше и больше запутывался... Я использовал рекурсивную функцию для чтения иерархического вывода, которая отлично работает, поэтому я подумал, что для обратного тоже нужна рекурсивная функция. Я немного изменил ваш код, чтобы он работал, если узел также имеет дополнительные свойства. - person Richard V; 19.12.2020