Función recursiva para generar una matriz multidimensional a partir del resultado de la base de datos

81

Estoy buscando escribir una función que tome una matriz de páginas / categorías (de un resultado de base de datos plano) y genere una matriz de elementos de página / categoría anidados basados ​​en los identificadores de los padres. Me gustaría hacer esto de forma recursiva, para poder realizar cualquier nivel de anidación.

Por ejemplo: estoy obteniendo todas las páginas en una consulta, y así es como se ve la tabla de la base de datos

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

Y esta es la matriz con la que me gustaría terminar para procesar en mis archivos de vista:

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
        )
)

He examinado y probado casi todas las soluciones con las que me he encontrado (hay muchas de ellas aquí en Stack Overflow, pero no he tenido suerte en conseguir algo lo suficientemente genérico que funcione tanto para páginas como para categorías.

Aquí está lo más cerca que he estado, pero no funciona porque estoy asignando a los niños al padre de primer nivel.

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
fuente
1
¿No puede simplemente trabajar con una matriz de todos los parent_ids y otra matriz para sus páginas?
djot

Respuestas:

233

Una construcción de árboles genérica y muy simple:

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);

El algoritmo es bastante simple:

  1. Tome la matriz de todos los elementos y la identificación del padre actual (inicialmente 0/ nada / null/ lo que sea).
  2. Recorre todos los elementos.
  3. Si el parent_idde un elemento coincide con la identificación principal actual que obtuvo en 1., el elemento es un elemento secundario del principal. Ponlo en tu lista de hijos actuales (aquí :) $branch.
  4. Llame a la función de forma recursiva con el id del elemento que acaba de identificar en 3., es decir, busque todos los hijos de ese elemento y agréguelos como childrenelemento.
  5. Devuelva su lista de niños encontrados.

En otras palabras, una ejecución de esta función devuelve una lista de elementos que son hijos de la identificación principal dada. Llámelo con buildTree($myArray, 1), devolverá una lista de elementos que tienen la identificación principal 1. Inicialmente, esta función se llama con la identificación principal 0, por lo que se devuelven los elementos sin identificación principal, que son nodos raíz. La función se llama a sí misma de forma recursiva para encontrar hijos de hijos.

deceze
fuente
1
Me alegro de que ayude. Nota: esto es algo ineficiente ya que siempre pasa toda la $elementsmatriz. Para arreglos pequeños, eso apenas importa, pero para conjuntos de datos grandes, querrá eliminar el elemento que ya coincide antes de pasarlo. Sin embargo, eso se vuelve algo complicado, así que lo dejé simple para que lo entiendas más fácilmente. :)
diciembre
6
@deceze Me gustaría ver la versión desordenada también. ¡Gracias por adelantado!
Jens Törnell
¿Alguien puede explicar cuál es el primer argumento 'matriz' en buildTree ()? ¿Debería ser la var que le da a la matriz inicial de páginas, etc., por ejemplo, '$ árbol = matriz'? ¿Alguien también puede explicar la última línea '$ tree = buildTree ($ rows)', ya que '$ rows' no está definido en ninguna parte? Por último, estoy luchando con el marcado HTML para generar la lista anidada.
Brownrice
1
@user arrayes un indicio de tipo para $elements, el primer argumento es simple array $elements. $rowses una matriz de resultados de bases de datos como la de la pregunta.
diciembre
1
@usuario He añadido una explicación. Ignore la $children = buildTree(...)parte y la función debería ser bastante obvia y simple.
diciembre
13

Sé que esta pregunta es antigua, pero estaba enfrentando un problema muy similar, excepto con una gran cantidad de datos. Después de un poco de lucha, logré construir el árbol en una pasada del conjunto de resultados, usando referencias. Este código no es bonito, pero funciona y funciona bastante rápido. No es recursivo, es decir, solo hay una pasada sobre el conjunto de resultados y luego una array_filteral final:

$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);

Cuando se ejecuta en estos datos:

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 |
+------+-------------+

El último print_rproduce esta salida:

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
                                (
                                )

                        )

                )

        )

)

Que es exactamente lo que estaba buscando.

Aleks G
fuente
si bien la solución es inteligente, pero este código tiene un error, me dio diferentes resultados en diferentes situaciones
Mohammadhzp
@Mohammadhzp He estado usando esta solución en producción durante el último año y no tuve ningún problema con ella. Si sus datos son diferentes, obtendrá resultados diferentes :)
Aleks G
@AleksG: voté a favor de su respuesta antes de comentar, tuve que agregar isset ($ elem ['children']) a la devolución de llamada array_filter, algo como esto return isset ($ elem ['children']) && is_null ($ elem [' n_parent_id ']); para que funcione correctamente
Mohammadhzp
@Mohammadhzp Con su cambio, el código no funcionará si un elemento de nivel superior no tiene hijos; se eliminará completamente de la matriz.
Aleks G
@AleksG, con el uso de la última versión de php, eso no es lo que me ocurre, si elimino isset ($ elem ['children']) y el elemento de nivel superior contiene niños, obtendría dos matrices diferentes de ese nivel superior elemento (uno con niños y otro sin) "Acabo de probar de nuevo hace 2 minutos y sin isset () obtengo un resultado diferente (incorrecto)"
Mohammadhzp
0

Inspirándome en otras respuestas aquí, se me ocurrió mi propia versión para agrupar una matriz de matrices asociadas de forma recursiva (a cualquier profundidad arbitraria), utilizando la lista de funciones personalizadas para obtener claves de agrupación en cada nivel .

Aquí hay una versión simplificada de la variante original más compleja (con más parámetros para ajustar las perillas). Tenga en cuenta que emplea una función iterativagroupByFn simple como subrutina para realizar agrupaciones a niveles individuales.

/**
 * - 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
        );
    }
}

Verifique la esencia para una colección más grande de funciones de utilidad de matriz junto con pruebas unitarias

y2k-shubham
fuente
-1

Es posible usar php para obtener el resultado de mysql en una matriz y luego usarlo.

$categoryArr = Array();
while($categoryRow = mysql_fetch_array($category_query_result)){
    $categoryArr[] = array('parentid'=>$categoryRow['parent_id'],
            'id'=>$categoryRow['id']);
   }
Mustafa
fuente