データベースの結果から多次元配列を生成する再帰関数


82

(フラットデータベースの結果から)ページ/カテゴリの配列を取得し、親IDに基づいてネストされたページ/カテゴリアイテムの配列を生成する関数を作成しようとしています。これを再帰的に実行して、任意のレベルのネストを実行できるようにします。

例:1つのクエリですべてのページをフェッチしていますが、これはデータベーステーブルの外観です。

+-------+---------------+---------------------------+
|   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には多くのソリューションがありますが、ページとカテゴリの両方で機能する十分に一般的なものを取得することができませんでした。

これが私が得た最も近いものですが、子を第1レベルの親に割り当てているため、機能しません。

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

1
すべてのparent_idの配列とページの別の配列を操作するだけでは不十分ですか?
djot 2011

回答:


234

いくつかの非常に単純で一般的なツリー構築:

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

アルゴリズムは非常に単純です。

  1. すべての要素の配列と現在の親のID(最初は0/ noneth / null/ whatever)を取得します。
  2. すべての要素をループします。
  3. parent_id要素のが1で取得した現在の親IDと一致する場合、その要素は親の子です。現在の子のリストに入れてください(ここ:) $branch
  4. 3.で特定した要素のIDを使用して関数を再帰的に呼び出します。つまり、その要素のすべての子を検索し、それらをchildren要素として追加します。
  5. 見つかった子のリストを返します。

つまり、この関数を1回実行すると、指定された親IDの子である要素のリストが返されます。で呼び出すとbuildTree($myArray, 1)、親IDが1の要素のリストが返されます。最初にこの関数は親IDが0で呼び出されるため、親IDのない要素(ルートノード)が返されます。この関数は、自分自身を再帰的に呼び出して、子供の子を見つけます。


1
お役に立ててうれしいです。注:これは常に$elements配列全体を渡すため、やや非効率的です。小さな配列の場合はほとんど問題になりませんが、大きなデータセットの場合は、既に一致している要素を削除してから渡す必要があります。でもそれはやや面倒になるので、わかりやすくするためにシンプルにしておきました。:)
deceze

6
@deceze乱雑なバージョンも見たいです。前もって感謝します!
イェンスTörnell

buildTree()の最初の引数 'array'が何であるかを誰かが説明できますか?これは、ページの最初の配列などに指定する変数である必要があります。例: '$ tree = array'?'$ rows'はどこにも定義されていないので、誰かが最後の行 '$ tree = buildTree($ rows)'を説明することもできますか?最後に、ネストされたリストを生成するためにHTMLマークアップに苦労しています。
玄米2013年

1
@userは、arrayあるタイプのヒントのための$elements最初の引数は、単純です、array $elements$rows質問のようなデータベース結果の配列です。
–deceze

1
@user説明を追加しました。$children = buildTree(...)パーツを無視すると、関数は非常に明白で単純なはずです。
–deceze

13

私はこの質問が古いことを知っていますが、非常に大量のデータを除いて、非常によく似た問題に直面していました。苦労した後、参照を使用して、結果セットの1つのパスでツリーを構築することができました。このコードはきれいではありませんが、機能し、非常に高速に機能します。これは非再帰的です。つまり、結果セットのパスは1つだけarray_filterで、最後に1つだけです。

$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

@Mohammadhzp私は昨年、このソリューションを本番環境で使用してきましたが、問題はありませんでした。データが異なると、異なる結果が得られます:)
Aleks G 2015

@AleksG:コメントの前にあなたの答えに賛成します、私はisset($ elem ['children'])をarray_filterコールバックに追加しなければなりませんでした。 n_parent_id ']); それを正しく機能させるために
Mohammadhzp

@Mohammadhzp変更により、最上位要素に子がない場合、コードは機能しなくなります。配列から完全に削除されます。
Aleks G 2015

@AleksG、最新バージョンのphpを使用しているので、isset($ elem ['children'])を削除し、トップレベルの要素に子が含まれていると、そのトップレベルの2つの異なる配列が取得されます。 element(1つは子あり、もう1つはなし)「2分前に再度テストし、isset()なしで異なる(間違った)結果が得られました」
Mohammadhzp

0

ここで他の回答からインスピレーションを得て、カスタム関数のリストを使用して各レベルでグループ化キー取得することにより、assoc配列の配列を 再帰的に(任意の深さまで)グループ化する独自のバージョンを思いつきました。

これは、元のより複雑なバリアントの簡略版です(ノブを微調整するためのパラメーターが増えています)。個々のレベルでグループ化を実行するためのサブルーチンとして、単純な反復関数groupByFnを採用していることに注意してください。

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

ユニットテストと一緒に配列ユーティリティ関数のより大きなコレクションの要点をチェックアウトしてください


-1

phpを使用してmysqlの結果を配列に取得し、それを使用することができます。

$categoryArr = Array();
while($categoryRow = mysql_fetch_array($category_query_result)){
    $categoryArr[] = array('parentid'=>$categoryRow['parent_id'],
            'id'=>$categoryRow['id']);
   }
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.