• 締切済み

PHPでツリー表記したい

phpで掲示板を作ろうとしていまして、DBから取ったデータを木構造にしてツリー表示させたいです。 親のparent_idはnull, 子は親のidになります。 自己結合させてみたり、再帰処理を試そうとしているのですが自分のphpスキルが未熟で巧い実装ができません。表示方法を教えていただければと思います。 ↓理想です 親レコード1 └子レコード5 └子レコード4 └子レコード3 └子レコード2 └子レコード1

みんなの回答

  • Proof4
  • ベストアンサー率78% (151/192)
回答No.4

No.3の補足コメントに対して回答します。 こちらのロジック = public function display、データ構造 = 引数$treeに関するもの と解釈しましたが、質問の的を外していたらすみません。 $treeのデータ構造は、レコードの親子関係を表した下記のような配列です。処理が進むごとにTreeインスタンスの$treeそのものの構造が変化することはありません。 $tree = [  1448 => [   1449 => [],   1450 => [    1460 => [] // 3つ目の階層の一例   ],   ...  ],  1454 => [   1455 => [],   1456 => [],   ...  ] ] 質問文のデータベースの構造では親子関係による2つ目の階層までしかありませんが、id=1448にとって孫にあたるレコードの例としてid=1460のようなものも考えることができます。 No.2の回答のプログラムは階層がいくら増えても処理できるようになっています。これは再帰処理によるおかげです。上記の$treeを用いて簡単に処理を追ってみます。 $tree->display()でpublic function displayが呼ばれたとき$depth = 0であり、foreach ($tree as $i => $t) { ... }のループ処理では 1回目  $i = 1448  $t = [   1449 => [],   1450 => [    1460 => []   ]  ] の処理が行われます。 $i = 1448のデータを表示した後、$tの中身を表示するためdisplayメソッドを呼び出します。呼び出されたメソッドが終了するまで親の処理は進まないこと、親のメソッドと呼び出されたメソッドとでローカル変数の共有は一切ないことに注意してください。 呼び出されたdisplayメソッドでは$tree = $t、$depth = 1であり、ループ処理では 1回目  $i = 1449  $t = []  $i = 1449のデータを表示して終了 2回目  $i = 1450  $t = [   1460 => []  ]  $i = 1450のデータを表示した後、$tの中身を表示するためdisplayメソッドを呼び出し 呼び出されたdisplayメソッドでは$tree = $t、$depth = 2であり、ループ処理では 1回目  $i = 1460  $t = []  $i = 1460のデータを表示して終了 これで$depth = 2の処理が終了したので親の$depth = 1の処理に戻ります。 $depth = 1には残りの要素がないため、親の$depth = 0の処理に戻ります。 $depth = 0には次の要素id = 1454があるので同様にこの要素を処理していきます... といったように子要素があればどんどん深い階層へ潜っていき、末端の子要素に到達したら一つ上位の階層へ戻って残りの処理を続けています。$depthの数字に着目すると分かりやすいかもしれません。 やや長くなってしまいましたが、「データ構造がどのように変化するか」を「displayメソッドの引数がどのように変化するか」と捉えるならば、ある親$iを表示したときの子の構造を表す$tの階層を掘り下げていくように変化するといったところでしょうか。

  • Proof4
  • ベストアンサー率78% (151/192)
回答No.3

display関数に$depth引数を渡し、階層位置をもとに判別を行います。 // 木構造を再帰的に表示 public function display($tree = null, $depth = 0) {  // $treeの初期値セット  if ($tree === null) {   $tree = $this->tree;  }  echo '<table>';  foreach ($tree as $i => $t) {   $data = $this->data[$i];   // 罫線の設定   $line = $depth > 0 ? '└' : '';   // 内容の表示   echo <<<EOT   <tr>    <td><input type="checkbox"></td>    <td>{$line}{$i}</td>    <td><a href="">{$data['subject']}</a></td>    <td>{$data['post_user']}</td>    <td>{$data['created_at']}</td>   </tr> EOT;   // さらに下の階層があれば再帰的に表示   if (count($t) > 0) {    echo '<tr><td colspan="5">';    $this->display($t, $depth + 1);    echo '</td></tr>';   }  }  echo "</table>"; } ※OKWAVEの仕様上、インデントを全角スペースで表現しています。コピペの際はご注意ください。 最終的にどのようなHTMLの出力を希望されているのか把握できないため、個人的な理解にはなりますがtableが入れ子になるよう変更しました。 もし、tableをレイアウトのためだけに使用しているのならそれは避けるべきです。レイアウトはCSSで行いましょう(複数の要素を横並びにしたければgridレイアウトやflexレイアウトなどが利用できます。)

myprotein
質問者

補足

お世話になります! ご回答ありがとうございます! 無事希望通りのツリー表示ができ大変感謝いたします。 ここからは本題とは逸れてしまいますのでお手隙でしたらご回答を願いたいのですが、 こちらのロジックについては、処理が進む度にデータ構造はどのように変化していくのでしょうか?

  • Proof4
  • ベストアンサー率78% (151/192)
回答No.2

No.1の補足コメントに対して回答します。 id以外のデータも表示されるようNo.1のコードを改めて書き直しました。 コードの見通しが良くなるよう、すべての操作をTreeクラスにまとめ、disp_treeはdisplayメソッドとして定義しました。 class Tree {  private $data = [];  private $tree = [];  // データの整形と木構造の作成  public function __construct($data)  {   // id => 内容 となるようにデータ整形   foreach ($data as $d) {    $this->data[$d['id']] = $d;   }   // 木構造の作成   foreach ($data as $d) {    $hier = $this->getHierarchy($d['id']);    $tree_ref = &$this->tree; // $treeの参照    // 階層構造を上位から順にたどり、最後の階層に自身のidを追加    foreach ($hier as $h) {     if (!array_key_exists($h, $tree_ref)) {      $tree_ref[$h] = [];     }     $tree_ref = &$tree_ref[$h];    }   }  }  // あるidの親となるidを階層の浅い順から全て返す  private function getHierarchy($id)  {   $hierarchy = [$id];   do {    $parent_id = $this->data[$id]['parent_id'];    if ($parent_id == null) {     // 最上位階層に到達したら終了     break;    }    // 配列の先頭にidを追加    array_unshift($hierarchy, $parent_id);    $id = $parent_id;   } while (true);   return $hierarchy;  }  // 木構造を再帰的に表示  public function display($tree = null)  {   // $treeの初期値セット   if ($tree === null) {    $tree = $this->tree;   }   echo "<ol>";   foreach ($tree as $i => $t) {    $data = $this->data[$i];    echo "<li>";    // 内容の表示    echo <<<EOT     <ul>      <li>subject: {$data['subject']}</li>      <li>name: {$data['name']}</li>      <li>body: {$data['body']}</li>      <li>id: $i</li>     </ul> EOT;    // さらに下の階層があれば再帰的に表示    if (count($t) > 0) {     $this->display($t);    }    echo "</li>";   }   echo "</ol>";  } } $tree = new Tree($data); // 木構造の作成 $tree->display(); // 表示 ※OKWAVEの仕様上、インデントを全角スペースで表現しています。コピペの際はご注意ください。 順序付きリスト(ol)をネストすることで階層構造を表現しています。public function displayのEOTで囲まれた部分のHTMLは適宜編集してください。 現状では表示されていない項目(created_atなど)も、EOTの部分で{$data['created_at']}のようにすると出力されます。

myprotein
質問者

補足

再度回答いただきありがとうございます! そして再度補足をさせていただきたいのですが、display()のEOT周辺を変更してみたのですが、今のままですと質問時の理想通りにはいかず親レコードにも「└」がついてしまいます。子レコードだけに「└」がつくようにするにはどのように表記するのがよろしいのでしょうか? public function display($tree = null) { // $treeの初期値セット if ($tree === null) { $tree = $this->tree; } // echo "<ol>"; foreach ($tree as $i => $t) { $data = $this->data[$i]; echo "<table>"; // 内容の表示 echo <<<EOT <tr> <td><input type="checkbox"></td> <td>└{$i}</td> <td><a href="">{$data['subject']}</a></td> <td>{$data['post_user']}</td> <td>{$data['created_at']}</td> </tr> EOT; // さらに下の階層があれば再帰的に表示 if (count($t) > 0) { $this->display($t); } echo "</table>"; } // echo "</ol>"; } } 何度もすみません!ぜひよろしくお願いいたします!

  • Proof4
  • ベストアンサー率78% (151/192)
回答No.1

データベースが再帰クエリに対応しているかどうか(MySQLだと8~)で対応が大きく変わりますが、ここでは対応していないものとして回答します。 まず、データベースからのデータ取得を下記クエリで行います。 SELECT p2.* FROM posts p1 INNER JOIN posts p2 ON p1.id = p2.parent_id UNION ALL SELECT * FROM posts WHERE parent_id IS NULL ORDER BY parent_id ASC; (https://www.db-fiddle.com/f/vMdqv5DyXh6Hy2S1ChZmaA/2) その後、PHPで処理を行います。 上記のクエリでデータが $data = [  ['id' => 1448, 'parent_id' => null, 'name' => 'アメリカ', 'subject' => 'トランプ', 'body' => 'Thanks'],  ... ]; のように取得できているとき(一部カラム省略)、下記のコードで木構造として表示できます。 class Hierarchy {  private $data = [];  public function __construct($data)  {   // id => 内容 となるようにデータ整形   foreach($data as $d){    $this->data[$d['id']] = $d;   }  }  // あるidまでの親idを階層の浅い順から全て返す  public function get($id)  {   $hierarchy = [$id];   do {    $parent_id = $this->data[$id]['parent_id'];    if($parent_id == null){     // 最上位階層に到達したら終了     break;    }    // 配列の先頭にidを追加    array_unshift($hierarchy, $parent_id);    $id = $parent_id;   } while(true);   return $hierarchy;  } } function disp_tree($tree, $depth = 0){  $length = count($tree);  $cnt = 1;  foreach($tree as $i => $t){   $lines = '';   // 罫線表示   if($depth > 0){    $node = $cnt == $length ? '└' : '├';    $lines = str_repeat("│", $depth - 1) . " $node";    if($depth > 1){     $lines = ' ' . $lines;    }   }   echo "$lines $i\n";   $cnt++;   if($length > 0){    disp_tree($t, $depth + 1);   }  } } // 木構造の作成 $tree = []; $hierarchy = new Hierarchy($data); foreach($data as $d){  $hier = $hierarchy->get($d['id']);  $tree_ref = &$tree; // $treeの参照  foreach($hier as $h){   if(!array_key_exists($h, $tree_ref)){    $tree_ref[$h] = [];   }   $tree_ref = &$tree_ref[$h];  } } // 表示 disp_tree($tree); ※OKWAVEの仕様上、インデントを全角スペースで表現しています。コピペの際はご注意ください。 disp_treeをカスタマイズすることで投稿内容が表示できると思います。

myprotein
質問者

補足

ご回答いただきまして誠にありがとうございます! 返事が遅くなり申し訳ございません。色々いじったりしてみたのですが、 1行1行が何をされているのかが未だ理解できず、disp_treeをどのようにいじればid以外のデータ(nameやsubject)が表示されるようになるのかお教えいただけないでしょうか?

関連するQ&A

専門家に質問してみよう