[PHP] 親、子、孫のカテゴリを登録・表示するサンプルを作成してみた【再帰関数版】



Nプログラマ(@Nprog128)です。

Hugoのテンプレートでサイドバーのカテゴリを階層構造でしたくなり、そのプロトタイプとしてPHPでサンプルを作りました。

なプ

どうしてPHPなの? Hugoのテンプレートでいきなり作ればいいじゃん。

まぁ、、、たしかにそうなのですが、理由はHugoのテンプレートに慣れていないことですね。

なので、使い慣れているPHPを使った、というわけでございます。

作りたいものは複数の親子関係を持ったカテゴリが設定されたファイルを読み込んで、読み込んだものを以下のように表示するプログラムです。

  • ファイルのリスト
    • ファイル1には、book, java のカテゴリが設定
    • ファイル2には、book, php, java のカテゴリが設定
    • ファイル3には、programming, php のカテゴリが設定
    • ファイル3には、programming, rugy のカテゴリが設定

ファイルリストを読み込んだ結果のイメージです。

1.
2├── book
3│   ├── java
4│   └── php
5│       └── laravel
6└── programming
7    ├── php
8    └── ruby

たぶん再帰呼び出しを使えば、できるんじゃないかなって思っています。

普段はあまり使わないので、使い方を思い出すのに時間がかかります。。。(笑)

なプ

とにかく関数の中でその関数自身を呼べばいいんですよねっ?(汗)

と、まぁこんな感じなんで、まずは復習として再帰で小さな足し算をするプログラムを作ってみます。

まずはコードを作ってみて再帰の復習

/img/article/2020/03/26/01.jpg
再帰で計算

よく例に出てくるような1から10までの足し算をするプログラムを作ってみます。

サッと作ってみたところ、こんな感じになりました。

factorial.php コードを開く
factorial.php
 1<?php
 2function factorial(int $n)
 3{
 4    if ($n == 1) {
 5        return 1;
 6    }
 7
 8    return $n + factorial($n - 1);
 9}
10
11$result= factorial(10);
12
13print_r("result: " . $result . "\n");
1> php factorial.php
2result: 55

自分でも確認しておきます。

1,2,3,4,5,6,7,8,9,10で、両側を足し算していくと11の組が5つできるので、11 * 5 = 55ですね!(ガウスの計算方法でしたっけ?)

再帰で重要なのは終了条件です。

この場合は$nが1になったら、再帰呼び出しをするのではなく1を返しています。

これがないとスタックに関数が積まれ続けてオーバーフローしてしまいます。

なプ

手抜きですが、ザックリと再帰の復習終わり!

作ったプログラム

いきなりですが、作ったプログラムをドカンと載せます。

なプ

結構長いよ。

作ったプログラム コードを開く
作ったプログラム
 1<?php
 2
 3class Category {
 4    function __construct($name, $depth) {
 5        $this->name = $name;
 6        $this->depth = $depth;
 7    }
 8    public $name = '';
 9    public $count = 1;
10    public $depth = 0;
11    public $children = []; // key => valueの連想配列
12
13   /**
14    * @param array $nodes 中身の例 [['parent_name' => 'root', 'name' => 'category', depth => '0']]]
15    */
16    public function add_child(array $nodes, Category $cur, $depth)
17    {
18        $node = $nodes[$depth];
19        $child = null;
20        // 子が存在する
21        if (isset($cur->children[$node['name']])) {
22            $child = $cur->children[$node['name']];
23            $child->count++;
24        }
25        else {
26            $child = new Category($node['name'], $node['depth']);
27            $cur->children[$child->name] = $child;
28        }
29        // 読み込んだデータで現在位置から見て、次の子が存在するなら再帰呼び出し
30        if (isset ($nodes[$depth + 1])) {
31            $cur->add_child($nodes, $child, $depth + 1);
32        }
33    }
34
35    public function show()
36    {
37        $children = array_values($this->children);
38        for ($i = 0; $i < count($children); $i++) {
39            $child = $children[$i];
40            print_r(str_repeat(' ', $child->depth) . $child->name . "\n");
41            $child->show();
42        }
43    }
44}
45
46$files = [
47  'file1' => [
48      'cat' => '本',
49      'subcat' => 'プログラミング',
50      'subsubcat' => 'php',
51  ],
52  'file2' => [
53      'cat' => '仕事',
54      'subcat' => 'プログラミング',
55      'subsubcat' => 'php',
56  ],
57  'file3' => [
58      'cat' => '仕事',
59      'subcat' => '転職',
60      'subsubcat' => 'it業界',
61  ],
62  'file4' => [
63      'cat' => '本',
64      'subcat' => '自己啓発',
65  ],
66  'file5' => [
67      'cat' => 'Amazon',
68  ],
69];
70
71$root = new Category('root', 0, null);
72
73$keys = ['cat', 'subcat', 'subsubcat'];
74foreach ($files as $file) {
75    $tmp = [];
76    foreach ($keys as $i => $key) {
77        if (isset($file[$key])) {
78            $tmp[] = [
79                'name'  => $file[$key],
80                'depth' => $i,
81            ];
82        }
83    }
84    $root->add_child($tmp, $root, 0);
85}
86
87$root->show($root);
88

概要

各ファイルに設定されているカテゴリ(親、子、孫の順番で記載)を読み込み、それらを階層構造で表示するプログラムです。

例えば、ファイルに[本,プログラミング,PHP]というカテゴリが設定されているものを読み込むと、階層構造ではこんな風に表示されます。

12 プログラミング
3  php

ファイルは複数あるので、[本,お金,税金]がついたファイルや[仕事,転職]がついたファイルなどがリストとなっており、これらを重複なく表示します。

また、階層構造があるので、[本,PHP]と[プログラミング,PHP]の場合は、PHPは親が異なるので別物扱いになり表示されます。

カテゴリの書き方には順番があり、左から親、子、孫、になります。

上の例を表にするとこんな感じです。

カテゴリ名 種類
プログラミング
PHP

ちなみに、子 or 孫だけのカテゴリは作ることはできません。

親がいなければ子や孫は存在しないですからね。

なプ

概要の説明、終わりです。

コードの説明

それでは先程のコードの要所要所を説明していきます。

Categoryクラスについて

これは読み込んだカテゴリを保持するクラスです。

自身のカテゴリ数を持っており、同じカテゴリが読み込まれた場合はカテゴリ数をカウントすることができます。

また、自分からみた子カテゴリを複数持つことができます。

 1<?php
 2
 3class Category {
 4    function __construct($name, $depth) {
 5        $this->name = $name;
 6        $this->depth = $depth;
 7    }
 8    public $name = '';
 9    public $count = 1;
10    public $depth = 0;
11    public $children = []; // key => valueの連想配列
12
13   /**
14    * @param array $nodes 中身の例 [['parent_name' => 'root', 'name' => 'category', depth => '0']]]
15    */
16    public function add_child(array $nodes, Category $cur, $depth)
17    {
18        $node = $nodes[$depth];
19        $child = null;
20        // 子が存在する
21        if (isset($cur->children[$node['name']])) {
22            $child = $cur->children[$node['name']];
23            $child->count++;
24        }
25        else {
26            $child = new Category($node['name'], $node['depth']);
27            $cur->children[$child->name] = $child;
28        }
29        // 読み込んだデータで現在位置から見て、次の子が存在するなら再帰呼び出し
30        if (isset ($nodes[$depth + 1])) {
31            $cur->add_child($nodes, $child, $depth + 1);
32        }
33    }
34
35    public function show()
36    {
37        $children = array_values($this->children);
38        for ($i = 0; $i < count($children); $i++) {
39            $child = $children[$i];
40            print_r(str_repeat(' ', $child->depth) . $child->name . "\n");
41            $child->show();
42        }
43    }
44}

コンストラクタ

インスタンス生成時には、カテゴリ名とカテゴリの深さの指定が必要になります。

1<?php
2    function __construct($name, $depth) {
3        $this->name = $name;
4        $this->depth = $depth;
5    }

メソッド: add_child

これは読み込んだカテゴリを親、子、孫と繋げていくメソッドで、再帰的に呼び出して使います。

 1<?php
 2    public function add_child(array $nodes, Category $cur, $depth)
 3    {
 4        $node = $nodes[$depth];
 5        $child = null;
 6        // 子が存在する
 7        if (isset($cur->children[$node['name']])) {
 8            $child = $cur->children[$node['name']];
 9            $child->count++;
10        }
11        else {
12            $child = new Category($node['name'], $node['depth']);
13            $cur->children[$child->name] = $child;
14        }
15        // 読み込んだデータで現在位置から見て、次の子が存在するなら再帰呼び出し
16        if (isset ($nodes[$depth + 1])) {
17            $cur->add_child($nodes, $child, $depth + 1);
18        }
19    }

$nodesには読み込んだ[親, 子, 孫]のカテゴリが常に入ってきて、$depthの値でどの階層のカテゴリを読み込んでいるかを把握します。

$depthは[親,子,孫]の配列のインデックスを使っているので、親なら0、子なら1、孫なら2が入ってきます。

要素 インデックス
0
1
2

$curには現在のCategoryのインスタンスが入ってきます。

初回は木構造のRootのカテゴリが入ってきて、その後は生成された子かすでに登録されている子が入っていきます。

子の登録について

$nodesから現在の階層をカテゴリを取り出し、自分の子にぶら下がっているかを調べます。

子がいない場合は、新しく現在の階層にいるカテゴリを作って、自分の子に登録します。

子がいる場合は、その子を取り出してカテゴリ数であるcountを1つ加算します。

最後にissetで次のカテゴリがあるかを調べて、次のカテゴリがある場合は再帰呼び出しをします。

この時点で階層が一つ深くなるので、$depthに+1をした引数を渡しています。

$nodesは使い回すので、次の再帰呼び出しにそのまま渡してあげます。

再帰呼び出しの終了条件は、$nodes内で次の子が存在しない場合です。

この例で行くと、孫の次は存在しないので孫を読み込んだらカテゴリの登録が終了します。

カテゴリ一覧の表示について

先程のadd_childで登録されたカテゴリを全て表示するメソッドです。

登録されている子を取り出し、自身のnameを表示した後、showメソッドを再帰呼び出ししています。

なプ

深さ優先探索(バックトラック法)というやつですかね。

表示するときに階層構造が分かりやすいようにstr_repeat関数を使って、深さに応じて空白を表示しています。

こんな感じに表示されます。

12 プログラミング
3  php

カテゴリの一覧を準備

カテゴリ一覧を読み込む部分はそんなに重要ではないので、手を抜いて予めPHPの配列で定義して準備しました。

カテゴリが入った配列 コードを開く
カテゴリが入った配列
 1<?php
 2$files = [
 3  'file1' => [
 4      'cat' => '本',
 5      'subcat' => 'プログラミング',
 6      'subsubcat' => 'php',
 7  ],
 8  'file2' => [
 9      'cat' => '仕事',
10      'subcat' => 'プログラミング',
11      'subsubcat' => 'php',
12  ],
13  'file3' => [
14      'cat' => '仕事',
15      'subcat' => '転職',
16      'subsubcat' => 'it業界',
17  ],
18  'file4' => [
19      'cat' => '本',
20      'subcat' => '自己啓発',
21  ],
22  'file5' => [
23      'cat' => 'Amazon',
24  ],
25];
なプ

先程の[親,子,孫]のカテゴリが、cat, subcat, subsubcatの値である[book,programming,php]に相当します。

項目名 キー名
cat book
subcat programming
subsubcat php

本来はファイル読み込みなどゴリゴリ書くのですが、省略しますm(_ _)m

カテゴリを読み込みながら登録する

先程のファイルの配列をループでクルクルと回しながら、カテゴリを登録していきます。

 1<?php
 2$root = new Category('root', 0, null);
 3
 4$keys = ['cat', 'subcat', 'subsubcat'];
 5foreach ($files as $file) {
 6    $tmp = [];
 7    foreach ($keys as $i => $key) {
 8        if (isset($file[$key])) {
 9            $tmp[] = [
10                'name'  => $file[$key],
11                'depth' => $i,
12            ];
13        }
14    }
15    $root->add_child($tmp, $root, 0);
16}

add_childの$nodes引数に渡すため、$tmpという一時変数へカテゴリの情報を詰めていきます。

親、子、孫のカテゴリ名は決まっているので、これらをkeysという配列に入れてループでクルクル回してカテゴリを詰めます。

issetのチェックで、親のみのカテゴリ([親])や子までのカテゴリ([親,子])に対応することができます。

なプ

先程のadd_childメソッドの終了条件は、次の子が存在しないことをご確認ください。

issetが通れば、名前と深さ(keysのindexはそのままカテゴリの深さに使える)の連想配列を作成し、$tmpに詰めて終わりです。

showメソッド(登録されたカテゴリ一覧を表示する)

カテゴリを全て読み込んで登録が終わったら、登録されたカテゴリ一覧を表示します。

 1<?php
 2    public function show()
 3    {
 4        $children = array_values($this->children);
 5        for ($i = 0; $i < count($children); $i++) {
 6            $child = $children[$i];
 7            print_r(str_repeat(' ', $child->depth) . $child->name . "\n");
 8            $child->show();
 9        }
10    }

自分の子の一覧を取得し、階層の深さの分だけスペースを表示した後、自分のカテゴリ名を表示します。

その後、再帰呼び出しでshowメソッドを呼んで更に自分の子を表示していきます。

実行結果

作成したコードを実行してみた結果は、このようになりました。

 1 2 プログラミング
 3  php
 4 自己啓発
 5仕事
 6 プログラミング
 7  php
 8 転職
 9  it業界
10Amazon

うまく動作しているようです。

本と仕事のカテゴリの下にぶら下がっているカテゴリは同じですが、親が異なるので別物扱いになっていることを確認できました。

本のカテゴリは2つありますが、同じカテゴリにまとめられています。

なプ

重複なく表示できているし、良さげ。

オマケ: ulリストで表示するには

先程のshowメソッドでは空白スペースで階層構造を実現していました。

htmlのulリストで表示するために、こんな風に実装してみました。

 1<?php
 2
 3public function show()
 4{
 5    $children = array_values($this->children);
 6    for ($i = 0; $i < count($children); $i++) {
 7        $child = $children[$i];
 8        if ($i == 0) {
 9            print_r('<ul>');
10        }
11        print_r('<li>' . $child->name . '</li>');
12        $child->show();
13        if ($i == count($children) - 1) {
14            print_r('</ul>');
15        }
16    }
17}
18

先程の内容とshowメソッドと同じですが、ulタグとliタグを表示するようにしてあります。

子を表示する時、要素の最初ならulの開始タグを表示、要素の終わりならulの終了タグを表示しています。

おわりに

今回は、PHPで再帰関数を使った親、子、孫のカテゴリを登録・表示するサンプルを作成してみた、という内容でした。

結構ゴリゴリと書いてしまい、コードが膨らんでしまいました。

このコードを今度はHugoのテンプレートに移植したいと思います。

なプ

作れんのかな。。。

それでは、このへんで。
バイナリー!

\ ちょっとお買い物 /


関連した記事