Funkcja rekurencyjna do generowania wielowymiarowej tablicy na podstawie wyniku bazy danych

82

Chcę napisać funkcję, która pobiera tablicę stron / kategorii (z płaskiego wyniku bazy danych) i generuje tablicę zagnieżdżonych elementów strony / kategorii na podstawie identyfikatorów nadrzędnych. Chciałbym to zrobić rekurencyjnie, aby można było wykonać zagnieżdżenie na dowolnym poziomie.

Na przykład: Pobieram wszystkie strony w jednym zapytaniu i tak wygląda tabela bazy danych

+-------+---------------+---------------------------+
|   id  |   parent_id   |           title           |
+-------+---------------+---------------------------+
|   1   |       0       |   Parent Page             |
|   2   |       1       |   Sub Page                |
|   3   |       2       |   Sub Sub Page            |
|   4   |       0       |   Another Parent Page     |
+-------+---------------+---------------------------+

A to jest tablica, którą chciałbym ostatecznie przetworzyć w moich plikach widoku:

Array
(
    [0] => Array
        (
            [id] => 1
            [parent_id] => 0
            [title] => Parent Page
            [children] => Array
                        (
                            [0] => Array
                                (
                                    [id] => 2
                                    [parent_id] => 1
                                    [title] => Sub Page
                                    [children] => Array
                                                (
                                                    [0] => Array
                                                        (
                                                            [id] => 3
                                                            [parent_id] => 1
                                                            [title] => Sub Sub Page
                                                        )
                                                )
                                )
                        )
        )
    [1] => Array
        (
            [id] => 4
            [parent_id] => 0
            [title] => Another Parent Page
        )
)

Sprawdziłem i wypróbowałem prawie każde rozwiązanie, które napotkałem (jest ich dużo tutaj w Stack Overflow, ale nie udało mi się uzyskać czegoś wystarczająco ogólnego, który będzie działał zarówno dla stron, jak i kategorii.

Oto najbliższe, jakie uzyskałem, ale nie działa, ponieważ przydzielam dzieci rodzicowi pierwszego poziomu.

function page_walk($array, $parent_id = FALSE)
{   
    $organized_pages = array();

    $children = array();

    foreach($array as $index => $page)
    {
        if ( $page['parent_id'] == 0) // No, just spit it out and you're done
        {
            $organized_pages[$index] = $page;
        }
        else // If it does, 
        {       
            $organized_pages[$parent_id]['children'][$page['id']] = $this->page_walk($page, $parent_id);
        }
    }

    return $organized_pages;
}

function page_list($array)
{       
    $fakepages = array();
    $fakepages[0] = array('id' => 1, 'parent_id' => 0, 'title' => 'Parent Page');
    $fakepages[1] = array('id' => 2, 'parent_id' => 1, 'title' => 'Sub Page');
    $fakepages[2] = array('id' => 3, 'parent_id' => 2, 'title' => 'Sub Sub Page');
    $fakepages[3] = array('id' => 4, 'parent_id' => 3, 'title' => 'Another Parent Page');

    $pages = $this->page_walk($fakepages, 0);

    print_r($pages);
}
David Hemphill
źródło
1
Nie możesz po prostu pracować z tablicą wszystkich parent_ids i inną tablicą dla swoich stron?
djot

Odpowiedzi:

234

Bardzo proste, ogólne budowanie drzewka:

function buildTree(array $elements, $parentId = 0) {
    $branch = array();

    foreach ($elements as $element) {
        if ($element['parent_id'] == $parentId) {
            $children = buildTree($elements, $element['id']);
            if ($children) {
                $element['children'] = $children;
            }
            $branch[] = $element;
        }
    }

    return $branch;
}

$tree = buildTree($rows);

Algorytm jest dość prosty:

  1. Weź tablicę wszystkich elementów i identyfikator bieżącego rodzica (początkowo 0/ nic / null/ cokolwiek).
  2. Zapętlaj przez wszystkie elementy.
  3. Jeśli parent_idelement elementu pasuje do bieżącego identyfikatora rodzica, który otrzymałeś w 1., element jest dzieckiem rodzica. Umieść to na swojej liście obecnych dzieci (tutaj $branch:).
  4. Wywołaj funkcję rekurencyjnie z identyfikatorem elementu, który właśnie zidentyfikowałeś w 3., tj. Znajdź wszystkie dzieci tego elementu i dodaj je jako childrenelement.
  5. Zwróć listę znalezionych dzieci.

Innymi słowy, jedno wykonanie tej funkcji zwraca listę elementów, które są dziećmi o podanym identyfikatorze rodzica. Wywołaj ją za pomocą buildTree($myArray, 1), zwróci listę elementów, które mają identyfikator rodzica 1. Początkowo ta funkcja jest wywoływana z identyfikatorem rodzica równym 0, więc zwracane są elementy bez identyfikatora rodzica, które są węzłami głównymi. Funkcja wywołuje się rekurencyjnie, aby znaleźć dzieci dzieci.

zamrozić
źródło
1
Cieszę się, że to pomaga. Uwaga: jest to nieco nieefektywne, ponieważ zawsze przekazuje całą $elementstablicę w dół. W przypadku małych tablic, które nie mają znaczenia, ale w przypadku dużych zestawów danych będziesz chciał usunąć z niego już dopasowany element przed przekazaniem go. Staje się to jednak trochę bałaganiarskie, więc zostawiłem to proste dla łatwiejszego zrozumienia. :)
deceze
6
@deceze Chciałbym również zobaczyć niechlujną wersję. Z góry dziękuję!
Jens Törnell
Czy ktoś może wyjaśnić, jaki jest pierwszy argument „tablica” w buildTree ()? Czy powinna to być zmienna, którą podajesz początkowej tablicy stron itp., Np. „$ Drzewo = tablica”? Czy ktoś może również wyjaśnić ostatnią linię „$ tree = buildTree ($ rows)”, ponieważ „$ rows” nie jest nigdzie zdefiniowane? Wreszcie zmagam się ze znacznikami HTML, aby wygenerować zagnieżdżoną listę.
Brownrice
1
@user arrayto wskazówka dotycząca typu $elements, pierwszy argument to po prostu array $elements. $rowsjest tablicą wyników z bazy danych, podobną do tej z pytania.
deceze
1
@user Dodałem wyjaśnienie. Zignoruj $children = buildTree(...)część, a funkcja powinna być dość oczywista i prosta.
deceze
13

Wiem, że to pytanie jest stare, ale miałem podobny problem - z wyjątkiem bardzo dużej ilości danych. Po pewnych zmaganiach udało mi się zbudować drzewo w jednym przejściu zestawu wyników - używając referencji. Ten kod nie jest ładny, ale działa i działa dość szybko. Jest nierekurencyjny - to znaczy, istnieje tylko jedno przejście przez zbiór wyników, a następnie jedno array_filterprzejście na końcu:

$dbh = new PDO(CONNECT_STRING, USERNAME, PASSWORD);
$dbs = $dbh->query("SELECT n_id, n_parent_id from test_table order by n_parent_id, n_id");
$elems = array();

while(($row = $dbs->fetch(PDO::FETCH_ASSOC)) !== FALSE) {
    $row['children'] = array();
    $vn = "row" . $row['n_id'];
    ${$vn} = $row;
    if(!is_null($row['n_parent_id'])) {
        $vp = "parent" . $row['n_parent_id'];
        if(isset($data[$row['n_parent_id']])) {
            ${$vp} = $data[$row['n_parent_id']];
        }
        else {
            ${$vp} = array('n_id' => $row['n_parent_id'], 'n_parent_id' => null, 'children' => array());
            $data[$row['n_parent_id']] = &${$vp};
        }
        ${$vp}['children'][] = &${$vn};
        $data[$row['n_parent_id']] = ${$vp};
    }
    $data[$row['n_id']] = &${$vn};
}
$dbs->closeCursor();

$result = array_filter($data, function($elem) { return is_null($elem['n_parent_id']); });
print_r($result);

Podczas wykonywania na tych danych:

mysql> select * from test_table;
+------+-------------+
| n_id | n_parent_id |
+------+-------------+
|    1 |        NULL |
|    2 |        NULL |
|    3 |           1 |
|    4 |           1 |
|    5 |           2 |
|    6 |           2 |
|    7 |           5 |
|    8 |           5 |
+------+-------------+

Ostatni print_rgeneruje ten wynik:

Array
(
    [1] => Array
        (
            [n_id] => 1
            [n_parent_id] => 
            [children] => Array
                (
                    [3] => Array
                        (
                            [n_id] => 3
                            [n_parent_id] => 1
                            [children] => Array
                                (
                                )

                        )

                    [4] => Array
                        (
                            [n_id] => 4
                            [n_parent_id] => 1
                            [children] => Array
                                (
                                )

                        )

                )

        )

    [2] => Array
        (
            [n_id] => 2
            [n_parent_id] => 
            [children] => Array
                (
                    [5] => Array
                        (
                            [n_id] => 5
                            [n_parent_id] => 2
                            [children] => Array
                                (
                                    [7] => Array
                                        (
                                            [n_id] => 7
                                            [n_parent_id] => 5
                                            [children] => Array
                                                (
                                                )

                                        )

                                    [8] => Array
                                        (
                                            [n_id] => 8
                                            [n_parent_id] => 5
                                            [children] => Array
                                                (
                                                )

                                        )

                                )

                        )

                    [6] => Array
                        (
                            [n_id] => 6
                            [n_parent_id] => 2
                            [children] => Array
                                (
                                )

                        )

                )

        )

)

Właśnie tego szukałem.

Aleks G
źródło
chociaż rozwiązanie jest sprytne, ale ten kod zawiera błąd, dał mi różne wyniki w różnych sytuacjach
Mohammadhzp
@Mohammadhzp Korzystam z tego rozwiązania w produkcji przez ostatni rok i nie miałem z nim problemu. Jeśli twoje dane są inne, otrzymasz inne wyniki :)
Aleks G
@AleksG: Głosowałem za twoją odpowiedzią przed komentarzem, musiałem dodać isset ($ elem ['children']) do wywołania zwrotnego array_filter, coś takiego jak ten return isset ($ elem ['children']) && is_null ($ elem [' n_parent_id ']); aby to działało dobrze
Mohammadhzp
@Mohammadhzp Wraz z twoją zmianą kod nie będzie działał, jeśli element najwyższego poziomu nie ma żadnych dzieci - zostanie całkowicie usunięty z tablicy.
Aleks G
@AleksG, używając najnowszej wersji php, to nie jest to, co mi się przydarza, jeśli usunę isset ($ elem ['children']) i element najwyższego poziomu zawiera dzieci, otrzymam dwie różne tablice tego najwyższego poziomu element (jeden z dziećmi i jeden bez) "Właśnie przetestowałem ponownie 2 minuty temu i bez isset () otrzymuję inny (zły) wynik"
Mohammadhzp
0

Czerpiąc inspirację z innych odpowiedzi tutaj, wymyśliłem własną wersję grupowania tablicy asocjacyjnych tablic rekurencyjnie (do dowolnej głębokości), używając listy funkcji niestandardowych w celu uzyskania kluczy grupujących na każdym poziomie .

Oto uproszczona wersja oryginalnego, bardziej złożonego wariantu (z większą liczbą parametrów do regulacji pokręteł). Zauważ, że wykorzystuje prostą iteracyjną funkcjęgroupByFn jako podprogram do wykonywania grupowania na poszczególnych poziomach.

/**
 * - Groups a (non-associative) array items recursively, essentially converting it into a nested
 *   tree or JSON like structure. Inspiration taken from: https://stackoverflow.com/a/8587437/3679900
 * OR
 * - Converts an (non-associative) array of items into a multi-dimensional array by using series
 *   of callables $key_retrievers and recursion
 *
 * - This function is an extension to above 'groupByFn', which also groups array but only till 1 (depth) level
 *   (whereas this one does it till any number of depth levels by using recursion)
 * - Check unit-tests to understand further
 * @param array $data Array[mixed] (non-associative) array of items that has to be grouped / converted to
 *                    multi-dimensional array
 * @param array $key_retrievers Array[Callable[[mixed], int|string]]
 *                    - A list of functions applied to item one-by-one, to determine which
 *                    (key) bucket an item goes into at different levels
 *                    OR
 *                    - A list of callables each of which takes an item or input array as input and returns an int
 *                    or string which is to be used as a (grouping) key for generating multi-dimensional array.
 * @return array A nested assoc-array / multi-dimensional array generated by 'grouping' items of
 *               input $data array at different levels by application of $key_retrievers on them (one-by-one)
 */
public static function groupByFnRecursive(
    array $data,
    array $key_retrievers
): array {
    // in following expression we are checking for array-length = 0 (and not nullability)
    // why empty is better than count($arr) == 0 https://stackoverflow.com/a/2216159/3679900
    if (empty($data)) {
        // edge-case: if the input $data array is empty, return it unmodified (no need to check for other args)
        return $data;

        // in following expression we are checking for array-length = 0 (and not nullability)
        // why empty is better than count($arr) == 0 https://stackoverflow.com/a/2216159/3679900
    } elseif (empty($key_retrievers)) {
        // base-case of recursion: when all 'grouping' / 'nesting' into multi-dimensional array has been done,
        return $data;
    } else {
        // group the array by 1st key_retriever
        $grouped_data = self::groupByFn($data, $key_retrievers[0]);
        // remove 1st key_retriever from list
        array_shift($key_retrievers);

        // and then recurse into further levels
        // note that here we are able to use array_map (and need not use array_walk) because array_map can preserve
        // keys as told here:
        // https://www.php.net/manual/en/function.array-map.php#refsect1-function.array-map-returnvalues
        return array_map(
            static function (array $item) use ($key_retrievers): array {
                return self::groupByFnRecursive($item, $key_retrievers);
            },
            $grouped_data
        );
    }
}

Zapoznaj się z sednem, aby uzyskać większy zbiór funkcji narzędziowych dla tablic wraz z testami jednostkowymi

y2k-shubham
źródło
-1

Możliwe jest użycie php, aby pobrać wynik mysql do tablicy, a następnie go użyć.

$categoryArr = Array();
while($categoryRow = mysql_fetch_array($category_query_result)){
    $categoryArr[] = array('parentid'=>$categoryRow['parent_id'],
            'id'=>$categoryRow['id']);
   }
mustafa
źródło