親子関係の配列を階層ごと取得したい
何がやりたいかというと自分自身のユニークIDと親になるデータのIDを持っている配列があって、階層の深さは不明。こういう場合、その親子関係にある配列を階層ごと取得したい。ちょっとわかりづらいので、例題を。以下のようなデータ配列があるとします。
$terms = array( array( 'id' => 1, 'parent_id' => 0, 'name' => 'Size' ), array( 'id' => 2, 'parent_id' => 0, 'name' => 'Gender' ), array( 'id' => 3, 'parent_id' => 1, 'name' => 'L' ), array( 'id' => 4, 'parent_id' => 1, 'name' => 'M' ), array( 'id' => 5, 'parent_id' => 2, 'name' => 'Men' ), array( 'id' => 6, 'parent_id' => 5, 'name' => 'Pants' ), array( 'id' => 7, 'parent_id' => 2, 'name' => 'Women' ), array( 'id' => 8, 'parent_id' => 7, 'name' => 'Skirt' ) );
parent_id
が親要素のid
を指しています。parent_id
が「0」のデータはルートカテゴリーを表します。この配列データを使って、以下のような形を作りたい。
1:Size →3:L →4:M 2:Gender →5:Men →→6:Pants →7:Women →→8:Skirt
- 1→3
- 1→4
- 2→5→6
- 2→7→8
つまり親子関係が成立しているカテゴリ(例題の場合4つ)を配列で取得したい。要はパンくずリスト作るんですけどね。これを実現する方法を考えてみる。
再帰処理を考える
こういう操作系ロジックを考えるときには、まずどちらから操作を始めるか考えます。ルートカテゴリはわかっていて末端のカテゴリが何階層になるかわからない。しかもカテゴリによって階層の深さが違うとなれば、末端のカテゴリの集合を作ってそれからルートになるまで繰り返す(再帰)処理を作ればよいと考えました。
まず末端のカテゴリーである証明をしなければいけません。証明する条件は「parent_id
に自身のid
が含まれていないこと」になります。そうですよね。誰の親でもない=末端のカテゴリーという話。ですので、まず親カテゴリーだけのIDの集合を作って、それから自身のIDと比較して存在チェックを行います。
$parent_ids = array(); foreach ( $terms as $term ) { $parent_ids[] = $term['parent_id']; } var_dump( $parent_ids );
5行目のダンプで表示した内容は以下の通り。
array (size=8) 0 => int 0 1 => int 0 2 => int 1 3 => int 1 4 => int 2 5 => int 5 6 => int 2 7 => int 7
PHPには特定のcolumnだけ抜き出して新しい配列を作成してくれる便利関数array_column
があるのですが、PHPのバージョンが5系だと5.5以上でないと対応していないので、まだちょっと使えない。7系はもちろん使用可能(PHPのバージョン上げない人多い気がする)。この関数を使用するとシンプルに以下のように書けます。
$parent_ids = array_column( $terms, 'parent_id' );
「0,0,1,1,2,5,2,7」が子カテゴリーを持つ親IDの集合になります。これを変数$parent_ids
の配列に格納しました。この配列に存在しないID=末端のカテゴリーとなるので、PHPのin_array
関数で存在チェックを行います。
$term_bottom = array(); foreach ( $terms as $term ) { if ( !in_array( $term['id'], $parent_ids ) ) { $term_bottom[] = $term['id']; } } var_dump( $term_bottom );
これもダンプで確認。表示した内容は以下の通り。
array (size=4) 0 => int 3 1 => int 4 2 => int 6 3 => int 8
「3,4,6,8」が末端カテゴリーIDの集合になります。name
も最終的に表示させたいので取得しておきます。これらを変数$term_bottom
の配列に格納しました。この配列をループさせて再帰処理を実行する関数を作成しようと思います。
再帰処理を実行する関数の作成
まずは先程の末端カテゴリー配列をループさせ、その中で再帰処理を実行する関数をコールします。以下のような処理を追加。
$categories = array(); foreach ( $term_bottom as $term_id ) { $categories[] = $this->set_ids( $term_id, $terms ); } var_dump( $categories );
3行目で再帰処理を実行する関数$this->set_ids()
をコールしています(Class内で作成したprivate関数なので$this)。この関数を以下のように組みました。
private function set_ids ( $id, $terms, $args = array() ) { if ( $id == 0 ) { return array_reverse( $args ); } else { $args[] = $id; foreach ( $terms as $term ) { if ( $term['id'] == $id ) { return $this->set_ids( $term['parent_id'], $terms, $args ); } } } }
引数
引数は全部で3つ。
- 第1引数:最初は末端カテゴリーID/再帰時は親ID
- 第3引数:カテゴリー階層配列(idはユニーク)
- 第4引数:データを格納していく配列
処理の流れ
関数コール後の処理の流れをカテゴリー構成「2→7→8」を作成する例を元に解説。
- 最初のコールで、
$this->set_ids( 8, $terms );
を渡す。 - 関数2行目:$idは8なのでFalse分岐へ
- 関数5行目:$args配列にID8をプッシュ
- 関数7〜11行:ID8とマッチするidを探して見つかったら関数をコール
- 関数9行:関数コールで、ID8の親ID7をセット。
$this->set_ids( 7, $terms, $args );
を渡す。 - 関数2行目:2回目のコール、$idは7なのでFalse分岐へ
- 関数5行目:$args配列にID7をプッシュ
- 関数7〜11行:ID7とマッチするidを探して見つかったら関数をコール
- 関数9行:関数コールで、ID7の親ID2をセット。
$this->set_ids( 2, $terms, $args );
を渡す。 - 関数2行目:3回目のコール、$idは2なのでFalse分岐へ
- 関数5行目:$args配列にID2をプッシュ
- 関数7〜11行:2とマッチするidを探して見つかったら関数をコール
- 関数9行:関数コールで、ID2の親ID0をセット。
$this->set_ids( 0, $terms, $args );
を渡す。 - 関数2行目:4回目のコール、$idは0なのでTrue分岐へ
- 関数3行目:$args配列には「8→7→2」の順でプッシュされているので、「親→子」の順にするために、
array_reverse
関数で順序反転。 - 反転した配列をreturnして終了
これで1度目のループが完了で、$categories
配列に$args
配列の値がプッシュされます。
再帰処理の結果
最後に$categories
配列のダンプ結果!
array (size=4) 0 => array (size=2) 0 => int 1 1 => int 3 1 => array (size=2) 0 => int 1 1 => int 4 2 => array (size=3) 0 => int 2 1 => int 5 2 => int 6 3 => array (size=3) 0 => int 2 1 => int 7 2 => int 8
できた!\(^o^)/
あとは名前を配列から取得してリストつくればOK。実はこれWordPressプラグインのパンくずリストの実装(カテゴリー)に使おうと思っていました。が、実はWordPrssのカテゴリー階層取得には簡単にできる関数が準備されてあるようです。次回はその関数get_ancestors
のお話を。
処理全文(再帰処理のサンプルソースコード)
最後に再帰処理のサンプルソースコードを貼り付けておきます。
new Sample(); class Sample { public function __construct () { $this->list_page_render(); } public function list_page_render () { echo 'Current PHP version: ' . phpversion() . PHP_EOL; // カテゴリー階層配列(idはユニーク) $terms = array( array( 'id' => 1, 'parent_id' => 0, 'name' => 'Size' ), array( 'id' => 2, 'parent_id' => 0, 'name' => 'Gender' ), array( 'id' => 3, 'parent_id' => 1, 'name' => 'L' ), array( 'id' => 4, 'parent_id' => 1, 'name' => 'M' ), array( 'id' => 5, 'parent_id' => 2, 'name' => 'Men' ), array( 'id' => 6, 'parent_id' => 5, 'name' => 'Pants' ), array( 'id' => 7, 'parent_id' => 2, 'name' => 'Women' ), array( 'id' => 8, 'parent_id' => 7, 'name' => 'Skirt' ) ); // 親IDだけの配列を作成 $parent_ids = array(); foreach ( $terms as $term ) { $parent_ids[] = $term['parent_id']; } // カテゴリーの最下層のidを配列に保持 $term_bottom = array(); foreach ( $terms as $term ) { if ( !in_array( $term['id'], $parent_ids ) ) { $term_bottom[] = $term['id']; } } // 最下層の配列をループして木構造の頂点まで($parent_id = 0) $categories = array(); foreach ( $term_bottom as $term_id ) { $categories[] = $this->set_ids( $term_id, $terms ); } var_dump( $categories ); } private function set_ids ( $id, $terms, $args = array() ) { if ( $id == 0 ) { return array_reverse( $args ); } else { $args[] = $id; foreach ( $terms as $term ) { if ( $term['id'] == $id ) { return $this->set_ids( $term['parent_id'], $terms, $args ); } } } } }