PHPで親子関係のデータをネスト表示する方法|再帰とフラット配列からのツリー化

カテゴリーや階層メニューのように、親子関係を持つデータをネスト表示する処理はWeb開発でよくあります。ポイントは再帰で各階層を処理することと、データベースから来るフラットな配列(parent_id付き)をツリー構造に変換することです。この記事では両方を、そのまま動くコードで解説します。

この記事の結論:入れ子データの表示は再帰関数で行います(子があれば自分自身を呼び出す)。DBから取得したデータは多くがフラットな配列(parent_id 付き)なので、まずツリー構造に変換してから表示します。表示時は htmlspecialchars()XSS対策を忘れずに。
スポンサーリンク

入れ子になったサンプルデータ

まず、すでに入れ子になっているデータ構造です。各要素が namechildren(子要素の配列)を持ちます。

入れ子データ
<?php
$tree = [
    [
        'name' => '果物',
        'children' => [
            ['name' => 'りんご'],
            [
                'name' => '柑橘類',
                'children' => [
                    ['name' => 'みかん'],
                    ['name' => 'レモン'],
                ],
            ],
        ],
    ],
    ['name' => '野菜'],
];

再帰関数でネスト表示する

各ノードをループで処理し、子要素があれば同じ関数を再帰呼び出しします。出力する名前は htmlspecialchars() でエスケープしてXSSを防ぎます。

ツリーをHTMLリストで表示
<?php
function displayTree(array $tree): void {
    echo '<ul>';
    foreach ($tree as $node) {
        // XSS対策: 出力は必ずエスケープ
        echo '<li>' . htmlspecialchars($node['name'], ENT_QUOTES, 'UTF-8');
        if (!empty($node['children'])) {
            displayTree($node['children']); // 再帰
        }
        echo '</li>';
    }
    echo '</ul>';
}

displayTree($tree);
名前などの値は htmlspecialchars() でエスケープして出力してください。ユーザー入力やDBの値をそのまま echo すると、<script> などが埋め込まれてXSS(クロスサイトスクリプティング)の原因になります。

フラットな配列をツリーに変換する(実務の本番)

実際にはデータベースから取得する時点では、各行が idparent_id を持つフラットな配列であることがほとんどです。これを表示用の入れ子構造に変換するのが本題です。再帰で parent_id をたどってツリーを組み立てます。

フラット配列 → ツリー構造
<?php
// DBから取得したようなフラットなデータ
$rows = [
    ['id' => 1, 'parent_id' => null, 'name' => '果物'],
    ['id' => 2, 'parent_id' => 1,    'name' => 'りんご'],
    ['id' => 3, 'parent_id' => 1,    'name' => '柑橘類'],
    ['id' => 4, 'parent_id' => 3,    'name' => 'みかん'],
    ['id' => 5, 'parent_id' => null, 'name' => '野菜'],
];

function buildTree(array $rows, $parentId = null): array {
    $branch = [];
    foreach ($rows as $row) {
        if ($row['parent_id'] === $parentId) {
            $children = buildTree($rows, $row['id']); // 子を再帰で取得
            if ($children) {
                $row['children'] = $children;
            }
            $branch[] = $row;
        }
    }
    return $branch;
}

$tree = buildTree($rows);
displayTree($tree); // 上の表示関数で出力
上の buildTree はシンプルですが、行数が多いと処理量が増えます。大量データなら、parent_id をキーにした連想配列(参照渡し)で1回のループで組み立てる方法もあります。ループ処理の基本はforeachで配列を順番に操作する方法を参照してください。

注意点:無限ループと深さ

  • 循環参照に注意:データに「親が自分の子」のような循環があると、再帰が無限ループになります。データの整合性を保つか、訪問済みIDを記録して防ぎます。
  • 階層が深すぎる場合:極端に深いと再帰が深くなりメモリを消費します。通常のカテゴリ階層では問題になりませんが、想定外の深さには上限を設けると安全です。
  • parent_id の型null 判定は === null で行うと確実です(文字列の "0" などと取り違えない)。

よくある質問(FAQ)

Qなぜ再帰を使うのですか?
A階層の深さが事前に分からないためです。再帰なら「子があれば同じ処理を繰り返す」だけで、何階層でも対応できます。ループのネストを固定で書くと、決まった深さしか扱えません。
QDBから取得したデータがフラットです。どうネストしますか?
A各行の parent_id をたどってツリーに変換します。本文の buildTree() のように、再帰で「指定した親IDを持つ子」を集めていけば、入れ子構造を組み立てられます。
Q表示でechoするとき注意することは?
Ahtmlspecialchars() でエスケープしてください。ユーザー入力やDBの値をそのまま出力すると、XSSの脆弱性になります。
Q無限ループになってしまいました。
Aデータに循環参照(親をたどると自分に戻る)がある可能性があります。データを修正するか、処理済みのIDを記録して再訪を防いでください。

まとめ

PHPで親子データをネスト表示するポイントを整理します。

  • 表示は再帰関数(子があれば自分を呼ぶ)で何階層でも対応
  • DBのフラットなデータは parent_id でツリーに変換してから表示
  • 出力は htmlspecialchars() でXSS対策
  • 循環参照・過度な深さに注意(無限ループ防止)

関連として、foreachで配列を順番に操作する方法配列に要素を追加する方法Array to string conversion エラーの対処方法もあわせて読むと、PHPの配列・階層データ処理に強くなれます。