从数据库结果生成多维数组的递归函数
Posted
技术标签:
【中文标题】从数据库结果生成多维数组的递归函数【英文标题】:Recursive function to generate multidimensional array from database result 【发布时间】:2012-01-25 02:50:47 【问题描述】:我正在寻找编写一个函数,该函数采用一组页面/类别(来自平面数据库结果)并根据父 ID 生成一组嵌套的页面/类别项。我想递归地执行此操作,以便可以完成任何级别的嵌套。
例如:我在一个查询中获取所有页面,这就是数据库表的样子
+-------+---------------+---------------------------+
| id | parent_id | title |
+-------+---------------+---------------------------+
| 1 | 0 | Parent Page |
| 2 | 1 | Sub Page |
| 3 | 2 | Sub Sub Page |
| 4 | 0 | Another Parent Page |
+-------+---------------+---------------------------+
这是我想在我的视图文件中处理的数组:
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
)
)
我已经查看并尝试了几乎所有遇到的解决方案(Stack Overflow 上有很多解决方案,但没有运气得到足够通用的东西,适用于页面和类别。
这是我得到的最接近的,但它不起作用,因为我将孩子分配给第一级父母。
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);
【问题讨论】:
你不能只使用所有 parent_id 的数组和页面的另一个数组吗? 【参考方案1】:一些非常简单的通用树构建:
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);
算法很简单:
-
获取所有元素的数组和当前父级的 id(最初是
0
/nothing/null
/whatever)。
循环遍历所有元素。
如果元素的parent_id
与您在1. 中获得的当前父ID 匹配,则该元素是父元素的子元素。将其放入您当前孩子的列表中(此处:$branch
)。
使用您刚刚在 3. 中标识的元素的 id 递归调用该函数,即找到该元素的所有子元素,并将它们添加为 children
元素。
返回您找到的孩子的列表。
换句话说,这个函数的一次执行会返回一个元素列表,这些元素是给定父 ID 的子元素。用buildTree($myArray, 1)
调用它,它会返回一个父id为1的元素列表。最初调用这个函数时父id为0,所以返回没有父id的元素,即根节点。该函数以递归方式调用自身以查找子项的子项。
【讨论】:
很高兴它有帮助。注意:这有点低效,因为它总是将整个$elements
数组向下传递。对于无关紧要的小型数组,但对于大型数据集,您需要在传递之前从中删除已经匹配的元素。不过,这变得有些混乱,所以为了您更容易理解,我把它简单化了。 :)
@deceze 我也想看看凌乱的版本。提前致谢!
请有人解释一下 buildTree() 中的第一个参数 'array' 是什么?那应该是您给初始页面数组等的var,例如'$tree = array'吗?有人还可以解释最后一行'$tree = buildTree($rows)',因为'$rows' 没有在任何地方定义吗?最后,我正在努力使用 html 标记来生成嵌套列表。
@user array
是$elements
的类型提示,第一个参数就是array $elements
。 $rows
是一组数据库结果,类似于问题中的结果。
@user 我已经添加了解释。忽略$children = buildTree(...)
部分,函数应该非常明显和简单。【参考方案2】:
$categoryArr = Array();
while($categoryRow = mysql_fetch_array($category_query_result))
$categoryArr[] = array('parentid'=>$categoryRow['parent_id'],
'id'=>$categoryRow['id']);
【讨论】:
【参考方案3】:我知道这个问题很老,但我遇到了一个非常相似的问题 - 除了大量数据。经过一番努力,我设法在结果集的一次传递中构建了树 - 使用引用。这段代码并不漂亮,但它可以运行并且运行速度非常快。它是非递归的——也就是说,只有一次通过结果集,然后在最后一次通过 array_filter
:
$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);
在此数据上执行时:
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 |
+------+-------------+
最后一个print_r
产生这个输出:
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
(
)
)
)
)
)
这正是我想要的。
【讨论】:
虽然解决方案很聪明,但是这段代码有错误,它在不同的情况下给了我不同的结果 @Mohammadhzp 去年我一直在生产中使用这个解决方案并且没有任何问题。如果你的数据不同,你会得到不同的结果:) @AleksG :我在评论之前赞成您的回答,我必须将 isset($elem['children']) 添加到 array_filter 回调中,类似于 return isset($elem['children'] ) && is_null($elem['n_parent_id']);让它正常工作 @Mohammadhzp 随着您的更改,如果***元素没有任何子元素,则代码将不起作用 - 它将完全从数组中删除。 @AleksG ,使用最新版本的 php 这对我来说不是结果,如果我删除 isset($elem['children']) 并且***元素包含子项,我会得到该***元素的两个不同数组(一个有孩子,一个没有)“我刚刚在 2 分钟前再次测试,没有 isset() 我得到不同的(错误)结果”【参考方案4】:从这里的其他答案中汲取灵感,我想出了自己的版本,用于通过使用 对一个 关联数组 递归 (任意深度)进行分组>自定义函数列表以在每个级别获得分组键。
这是original more complex variant 的简化版本(带有更多用于调整旋钮的参数)。请注意,它使用一个简单的iterative function groupByFn
作为子例程来执行各个级别的分组。
/**
* - Groups a (non-associative) array items recursively, essentially converting it into a nested
* tree or JSON like structure. Inspiration taken from: https://***.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://***.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://***.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
);
请查看更大的collection of array utility functions 和unit-tests 的要点
【讨论】:
【参考方案5】:public function testTree()
$array = [
['id'=>7,'parent_id'=>3],
['id'=>1,'parent_id'=>0],
['id'=>2,'parent_id'=>0],
['id'=>3,'parent_id'=>1],
['id'=>4,'parent_id'=>1],
['id'=>5,'parent_id'=>2],
['id'=>6,'parent_id'=>1],
['id'=>8,'parent_id'=>4],
['id'=>9,'parent_id'=>4],
['id'=>10,'parent_id'=>0]
];
$res = $this->buildTree($array);
print_r($res);
public function buildTree($array,$id_key = 'id',$parent_key = 'parent_id')
$res = [];
foreach($array as $y)
$array_with_id[$y[$id_key]] = $y;
foreach($array_with_id as $key => $element)
if($element[$parent_key])
$array_with_id[$element[$parent_key]]['childrens'][$key] = &$array_with_id[$key];
else
$res[$element[$id_key]] = &$array_with_id[$key];
return $res;
递归提供了太多的操作,我认为这是最好的方法。
【讨论】:
以上是关于从数据库结果生成多维数组的递归函数的主要内容,如果未能解决你的问题,请参考以下文章