CSV Importerで階層のあるカスタムタクソノミーをインポートする

CSV Importerで階層のあるカスタムタクソノミーを指定するときは、CSVのカラムに規定のフォーマットでカスタムタクソノミーを指定する必要があります。CSV Importerの使い方はこちらをご覧ください。

フォーマット

カスタムタクソノミーの親子ツリーをダブルクオーテーションで囲み、それぞれのタームをカンマで区切ります。

例:「”親ターム,子ターム”」

CSVはどのような方法で作成しても良いのですが、CSV Importerに取り込むためのCSVのフォーマットを指定しやすいLibreOfficeのCalcを使った場合だと、次のようなルールで入力します。

  • 「&」で連結
  • 「”」は「””””」
  • 「,」は「”,”」

従って、「”親ターム,子ターム”」の場合は、

「=””””&親タームセル&””””&”,”&””””&子タームセル&””””」

とセルに入力すればOK。

CSV Importerはなぜか2階層までしかインポートできない

親=>子の2階層のカスタムタクソノミーの場合は、これまで説明した方法で大丈夫なのですが、僕がやりたかったのは親=>子=>孫の3階層のインポートでした。普通に「”親ターム”,”子ターム”,”孫ターム”」と入力しインポートすると、全て孫タームが無視され子タームに設定されている!どうもプラグイン自体が怪しそうだったのでコードを調べてみました。

タームを生成するコードは下記の通り。

function create_terms($taxonomy, $field) {
	if (is_taxonomy_hierarchical($taxonomy)) {
		$term_ids = array();
		foreach ($this->_parse_tax($field) as $row) {
			list($parent, $child) = $row;
			$parent_ok = true;
			if ($parent) {
				$parent_info = $this->term_exists($parent, $taxonomy);
				if (!$parent_info) {
					// create parent
					$parent_info = wp_insert_term($parent, $taxonomy);
				}
				if (!is_wp_error($parent_info)) {
					$parent_id = $parent_info['term_id'];
				} else {
					// could not find or create parent
					$parent_ok = false;
				}
			} else {
				$parent_id = 0;
			}

			if ($parent_ok) {
				$child_info = $this->term_exists($child, $taxonomy, $parent_id);
				if (!$child_info) {
					// create child
					$child_info = wp_insert_term($child, $taxonomy,
						array('parent' => $parent_id));
				}
				if (!is_wp_error($child_info)) {
					$term_ids[] = $child_info['term_id'];
				}
			}
		}
		return $term_ids;
	} else {
		return $field;
	}
}

うーむ、これは…。5行目の「list($parent, $child) = $row;」、明らかに親と子に分割している。それ以上の階層の場合はダメに決まってるわ…。そこで、何階層あってもループで処理できるよう、下記のように修正。

function create_terms($taxonomy, $field) {
	if (is_taxonomy_hierarchical($taxonomy)) {
		$term_ids = array();
		foreach ($this->_parse_tax($field) as $row) {
			$parent_ok = true;
			$cntTax = ( empty( $row ) )? 0 : count( $row );
			$i = 0;
			$_tax_id = 0;
			foreach ( $row as $_tax ) {
				if ( $parent_ok ) {
					$i ++;
					$_tax_info = $this->term_exists( $_tax, $taxonomy, $_tax_id );
					if ( !$_tax_info ) {
						// create parent
						$_tax_info = wp_insert_term( $_tax, $taxonomy, array( 'parent' => $_tax_id ) );
												
					}
					if ( !is_wp_error( $_tax_info ) ) {
						$_tax_id = $_tax_info['term_id'];
						if ( $cntTax == $i ) {
							$term_ids[] = $_tax_id;
						}
					} else {
						// could not find or create parent
						$parent_ok = false;
					}
				}
			}
		}
		return $term_ids;	// 自分の所属しているタクソノミーすべてのterm_idを返す
	} else {
		return $field;
	}
}

そしてインポート。タームが格納されているテーブルwp_termsを見ると、きちんとタームの情報が格納されていることを確認できます。

管理画面、表示画面を確認

カスタムタクソノミーの管理画面を確認。すると、全てのタームが同列に表示されていました。なぜ!タクソノミーのアーカイブページを開くと「Not Found」。

でも、管理画面で適当な孫タームの詳細画面にいくと、きちんと親タームが設定されているようなのです。うーん。一覧に戻ろうと、そのタームを編集せずにそのまま保存。すると、なんということでしょう!一覧に表示されたタームはきちんと階層が保持されているではありませんか!もちろんタクソノミーのアーカイブページもきちんと表示される。試してみたところ、登録されたタームを最更新したり、新しくタームを追加したりすると表示が正常になるようです。

ここでちょっと推測

恐らく、別のテーブルもしくはファイルにキャッシュ的なものを保存して表示する仕組みになっていて、CSVからインポートしたらそれが更新されないからおかしくなるんだろうなと。調べると、案の定、wp_optionsテーブルに「タクソノミー名_children」という項目があり、そこにタームの親子関係をserializeしたデータが格納されていました。

管理画面からタームを更新したときにはきちんと表示されるようになるということは、そのコードとCSV Importerのコードを見比べれば良いだろうと思い確認。すると困ったことに、どちらも同じ「wp_insert_term」という関数でデータを登録しており、その前後で違う処理もありません。

wp_insert_term()の処理を調べてみると、最終的に「clean_term_cache($term_id, $taxonomy);」というコードが実行されており、get_optionsテーブルのキャッシュを更新しているようです(実際には更新されていないのですが)。

CSV Importerのターム追加処理の最後の方にclean_term_cache()を追加

結果は変わらず。そうだよなぁ、wp_insert_termするときに一緒に実行されてるわけだし。

clean_term_cache()を調査

clean_term_cacheの処理を見てみると、最終的に下記のコードを実行していました。

wp_cache_delete('all_ids', $taxonomy);
wp_cache_delete('get', $taxonomy);
delete_option("{$taxonomy}_children");
// Regenerate {$taxonomy}_children
_get_term_hierarchy($taxonomy);

wp_cache_deleteはキャッシュを削除、_get_term_hierarchyはキャッシュを生成しているようです。これがキャッシュ更新の本体っぽい。どうせ変わらないだろうが…と思いながらこのコードをCSV Importerのターム追加処理の最後の方に入れて見ると…成功!

ググッてみると海外にも同じような悩みを抱えている方が。読んでみると、どうもclean_term_cache内でキャッシュを更新したかどうかの判定をする変数がstatic宣言されていて、そのために1つのスクリプトの中で1回しか判定が行われず、キャッシュが更新されないということだったらしい。

まとめ

ということで、最終的に下記のようなコードに差し替えれました。

function create_terms($taxonomy, $field) {
	if (is_taxonomy_hierarchical($taxonomy)) {
		$term_ids = array();
		foreach ($this->_parse_tax($field) as $row) {
			$parent_ok = true;
			$cntTax = ( empty( $row ) )? 0 : count( $row );
			$i = 0;
			$_tax_id = 0;
			foreach ( $row as $_tax ) {
				if ( $parent_ok ) {
					$i ++;
					$_tax_info = $this->term_exists( $_tax, $taxonomy, $_tax_id );
					if ( !$_tax_info ) {
						// create parent
						$_tax_info = wp_insert_term( $_tax, $taxonomy, array( 'parent' => $_tax_id ) );
												
					}
					if ( !is_wp_error( $_tax_info ) ) {
						$_tax_id = $_tax_info['term_id'];
						if ( $cntTax == $i ) {
							$term_ids[] = $_tax_id;
						}
					} else {
						// could not find or create parent
						$parent_ok = false;
					}
				}
			}
		}
		wp_cache_delete('all_ids', $taxonomy);
		wp_cache_delete('get', $taxonomy);
		delete_option("{$taxonomy}_children");
		_get_term_hierarchy($taxonomy);
		return $term_ids;	// 自分の所属しているタクソノミーすべてのterm_idを返す
	} else {
		return $field;
	}
}

MW WP Form

MW WP Form はショートコードベースのフォームプラグインです。多くの機能を持っており、例えば、多くのバリデーションルール、問い合わせデータの保存、そしてグラフ機能集計などを使用することができます。

さらに詳しく
Habakiri

Habakiri

Bootstrap ベースのシンプルな WordPress テーマ。レスポンシブ、多くのカスタマイズ機能。圧縮された CSS・JS を使用する高速化対策。Microformats 対応。Sass、クラスベースの functions.php。

さらに詳しく
basis-stylus

Basis

軽量なレスポンシブ Stylus/CSS フレームワーク。Flexbox ベースのグリッドシステム、疎結合なコンポーネント、バーティカルリズム。

さらに詳しく