カテゴリーや階層メニューのように、親子関係を持つデータをネスト表示する処理はWeb開発でよくあります。ポイントは再帰で各階層を処理することと、データベースから来るフラットな配列(parent_id付き)をツリー構造に変換することです。この記事では両方を、そのまま動くコードで解説します。
この記事の結論:入れ子データの表示は再帰関数で行います(子があれば自分自身を呼び出す)。DBから取得したデータは多くがフラットな配列(
parent_id 付き)なので、まずツリー構造に変換してから表示します。表示時は htmlspecialchars() でXSS対策を忘れずに。入れ子になったサンプルデータ
まず、すでに入れ子になっているデータ構造です。各要素が name と children(子要素の配列)を持ちます。
入れ子データ
<?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(クロスサイトスクリプティング)の原因になります。フラットな配列をツリーに変換する(実務の本番)
実際にはデータベースから取得する時点では、各行が id と parent_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するとき注意することは?
A
htmlspecialchars() でエスケープしてください。ユーザー入力やDBの値をそのまま出力すると、XSSの脆弱性になります。Q無限ループになってしまいました。
Aデータに循環参照(親をたどると自分に戻る)がある可能性があります。データを修正するか、処理済みのIDを記録して再訪を防いでください。
まとめ
PHPで親子データをネスト表示するポイントを整理します。
- 表示は再帰関数(子があれば自分を呼ぶ)で何階層でも対応
- DBのフラットなデータは
parent_idでツリーに変換してから表示 - 出力は
htmlspecialchars()でXSS対策 - 循環参照・過度な深さに注意(無限ループ防止)
関連として、foreachで配列を順番に操作する方法・配列に要素を追加する方法・Array to string conversion エラーの対処方法もあわせて読むと、PHPの配列・階層データ処理に強くなれます。

