CakePHP から wordpress のデータを扱う(3)

今度は wordpress のカテゴリ一覧、と指定したカテゴリ内の記事を拾ってみます。

wordpress はカテゴリの扱いがややこしくて、カテゴリの名前自体は wp_terms に入っているのですが…その後の wp_posts テーブルまでの関係がややこしいという。
# 更に云えば、カスタムフィールドと関係を見ていくと、なにやら大変という感じなのです。

  • terms: カテゴリ名などの名称
  • term_taxonomy: terms の分類(category, link_category など)
  • term_relationships: カテゴリとの親子関係
  • posts: 記事自体

という 4 つのテーブルが関わってきます。

いきなりはややこしいので、まずはカテゴリ一覧。

■カテゴリ一覧のビュー(views/categories/index.ctp)

<h2>カテゴリ一覧</h2>
<table>
	<tr>
		<th>term_id</th>
		<th>term_taxonomy_id</th>
		<th>name</th>
		<th>slug</th>
	</tr>
	<?php foreach($Categories as $item) : ?>
	<tr>
		<td><?php echo $item['Term']['term_id']; ?>
		<td><?php echo $item['TermTaxonomy']['term_taxonomy_id']; ?>
		<td><?php echo $item['Term']['name']; ?>
		<td><?php echo $item['Term']['slug']; ?>
	</tr>
	<?php endforeach ; ?>
</table>
<h2>カテゴリ一覧(リンク版)</h2>
<ul>
	<?php foreach($Categories as $item) : ?>
	<li>
		<a href="http://localhost/cake_wp/categories/item/<?php echo $item['Term']['slug']; ?>"><?php echo $item['Term']['name']; ?></a>
	</li>
	<?php endforeach ; ?>
</ul>

カテゴリだけを表示したいので、Term と TermTaxonomy モデルだけを使います。

■コントローラー(controllers/categories_controller.php)

<?php
class CategoriesController extends AppController {

	var $name = 'Categories';
	var $uses = array('Category');
	function index() {
		$this->set('Categories',$this->Category->findCategories());
	}
	function item($slug='') {
		$this->set('Categories',$this->Category->findPosts($slug));
		$this->set('ViewData',array(
			'slug'=>$slug,
			'name'=> $this->Category->getCategoryName($slug)
				));
	}
}
?>

(追記 2014/06/22 item function を追加)
コントローラーは CategoriesController にしました。4 つのテーブルを使うので、どれの名前も適切ではないんですよね。そして、一覧表を取る時には index メソッドを作っています。

さて、モデルはどうするかというと、Term モデル、TermTaxonomy モデルを直接利用することは避けました。先にも書きましたがデータベース上は正規化されていることが常な訳で、そのまま O/R マッピングをしてしまうと、オブジェクト指向の世界では「現実離れ」なクラスになってしまいます。なので、より自然な形に戻す(本来の形に戻す)ために、Category モデルクラスを作ります。
ここでリストを取得するために、findPosts というメソッドを新たに作っています。
# find(‘all’) とか find(‘first’) とか合わせたほうがよいのですが、ひとまず。

■モデル(models/category.php)

さて、Category モデルは実テーブルとは連結していないので、$useTable = false にします。で、問題なのは、どうやってデータを取ってくるのか?ですね。

先に Term と TermTaxonomy モデルのアソシエーションを設定します。

<!-- models/term.php -->
<?php
class Term extends AppModel
{
	var $name = 'Term';
	var $primaryKey = 'term_id';
	var $hasOne = array(
		'TermTaxonomy' => array(
			'className' => 'TermTaxonomy',
			'foreignKey' => 'term_taxonomy_id' ));
}
?>

Term.term_taxonomy_id (1)-(1) TermTaxonomy.term_taxonomy_id の関係になるので Model::$hasOne を指定。

<!-- models/term_taxonomy.php -->
<?php
class TermTaxonomy extends AppModel
{
	var $name = 'TermTaxonomy';
	var $useTable = 'term_taxonomy';
	var $primaryKey = 'term_taxonomy_id';
	var $hasOne = array(
	    'Term' => array(
			'className' => 'Term',
			'foreignKey' => 'term_id' ));
}
?>

逆向きも同じで Model::$hasOne を指定。

こうしておくと、次のように findCategories が書けます。

<!-- models/category.php -->
<?php
class Category extends AppModel
{
	var $name = 'Category';
	var $useTable = false ;
	function findCategories() {
		if (0) {
			$TermTaxonomy = ClassRegistry::init('TermTaxonomy');
			$items = $TermTaxonomy->find('all',array(
				'conditions'=>"taxonomy = 'category'"));
			return $items ;
		} else {
			$Term = ClassRegistry::init('Term');
			$items = $Term->find('all', array(
				'conditions'=>"TermTaxonomy.taxonomy = 'category'"));
			return $items ;
		}
	}
}

if 文で分けていますが、どちらも同じ記述です。
テーブルに連結していない場合は、ClassRegistry::init メソッドでモデルクラスを読み込みます。この後は、普通に検索をして OK です。
TermTaxonomy モデルを中心にした場合は、’category’ を直接検索できますが、Term モデルを中心にした場合は、TermTaxonomy.taxonomy な形で conditions を設定します。

これはどちらも結果が同じになります。

[img 20110201_07.jpg]

今度は、カテゴリ内の記事一覧を取得します。

■カテゴリ内記事のビュー(views/categories/item.ctp)

<h2>カテゴリ内の記事一覧</h2>
<?php echo $ViewData['slug']; ?> /
<?php echo $ViewData['name']; ?>
<table>
	<tr>
		<th>Post.ID</th>
		<th>Post.post_title</th>
		<th>Post.post_date</th>
		<th>Post.guid</th>
	</tr>
	<?php foreach($Categories as $item) : ?>
	<tr>
		<td><?php echo $item['Post']['ID']; ?>
		<td><?php echo $item['Post']['post_title']; ?>
		<td><?php echo $item['Post']['post_date']; ?>
		<td><?php echo $item['Post']['guid']; ?>
	</tr>
	<?php endforeach ; ?>
</table>

な感じで、Post モデルから情報を取ってきます。カテゴリのスラッグ(名前)と日本語名称は、$ViewData という変数に入れておきます。

■まずは、コントローラーをクエリで書いてしまう。

面倒なので、Category::findPosts メソッドの中身をクエリで書いてしまったのがこれです。

<!-- models/category.php -->
<?php
class Category extends AppModel
{
	var $name = 'Category';
	var $useTable = false ;
	function findCategories() {
		if (0) {
		$TermTaxonomy = ClassRegistry::init('TermTaxonomy');
		$items = $TermTaxonomy->find('all',array(
			'conditions'=>"taxonomy = 'category'"));
		return $items ;
		} else {
		$Term = ClassRegistry::init('Term');
		$items = $Term->find('all', array(
			'conditions'=>"TermTaxonomy.taxonomy = 'category'"));
		return $items ;
		}
	}

	// カテゴリ内の記事一覧を取得する
	function findPosts($slug,$max=10) {
	if(0) {

		$sql = <<< HERE
select *
from wp_term_taxonomy as TermTaxonomy
 inner join wp_term_relationships as TermRelationship
 on TermTaxonomy.term_taxonomy_id = TermRelationship.term_taxonomy_id
 inner join wp_terms as Term
  on ( Term.term_id = TermTaxonomy.term_id )
 inner join wp_posts as Post
  on ( TermRelationship.object_id = Post.ID )
  where TermTaxonomy.taxonomy = 'category'
   and Term.slug = '$slug'
 order by Post.post_date desc
 limit $max
 ;
HERE;
	return $this->query($sql);
	} else {
		$Post = ClassRegistry::init('Post');
		$items = $Post->find('all',array(
			'conditions'=>"TermTaxonomy.taxonomy = 'category'"));
		return $items ;
	}

	}

	function getCategoryName($slug) {
		$Term = ClassRegistry::init('Term');
		$item = $Term->find('first',array(
			'conditions'=>"slug = '$slug'"));
		return $item['Term']['name'];
	}
}
?>

これでも用は足りるのですが、プレフィクス「wp_」が、そのまま出てきてしまうので難点が多いですよね。ただ、複雑なアソシエーション(外部キー)で悩むよりは、このほうが安全だと思います。

■アソシエーションで設定してみる。

が、CakePHP なのでアソシエーションで頑張ってみようとモデルを作ります。結論から言うと、4 つのテーブルを連携させて動かすことができません…果たして動くのか動かないのか分からないのですが、一応、ソースを晒しておきます。

TermRelationship モデルクラスはプライマリーキーを2つ持つので、$primaryKey に対して array で指定しているのですが、これでいいんでしょうかねぇ?

<!-- models/term_relationship.php -->
<?php
class TermRelationship extends AppModel
{
	var $name = 'TermRelationship';
	var $primaryKey = array('object_id','term_taxonomy_id');
	var $belongsTo = array(
		'TermTaxonomy' => array(
			'className' => 'TermTaxonomy',
			'conditions' => 'TermTaxonomy.term_taxonomy_id = TermRelationship.term_taxonomy_id'));
	var $hasOne = array(
		'Post' => array(
			'className' => 'Post',
			'conditions' => 'Post.ID = TermRelationship.object_id'));
}
?>

Post モデルに対しては、Model::$hasOne を使い、TermTaxonomy モデルに対しては Model::$belongsTo なんですが。
Post モデルに関しては、TermRelationship への連携を Model::$hasOne で指定しておきます。

<!-- models/post.php -->
<?php
class Post extends AppModel
{
	var $name = 'Post';

	var $belongsTo = array(
	    'User' => array(
			'className' => 'User',
			'foreignKey' => 'post_author' ));
	var $hasOne = array(
		'TermRelationship' => array(
			'className' => 'TermRelationship',
			'foreignKey' => 'object_id'));
}

これを有効にして、Category::findPosts メソッドを書き換えます。

	// カテゴリ内の記事一覧を取得する
	function findPosts($slug,$max=10) {
		$TermTaxonomy = ClassRegistry::init('TermTaxonomy');
		$items = $TermTaxonomy->find('all',array(
			'conditions'=> array(
				'TermTaxonomy.taxonomy' => 'category',
				'Post.post_status' => 'publish' )));
		return $items ;
	}

これを動かしてみるのですが、SQL文でエラーが出ます。

SELECT
 `TermTaxonomy`.`term_taxonomy_id`,
 `TermTaxonomy`.`term_id`,
 `TermTaxonomy`.`taxonomy`,
 `TermTaxonomy`.`description`,
 `TermTaxonomy`.`parent`,
 `TermTaxonomy`.`count`,
 `Post`.`ID`,
 `Post`.`post_author`,
 `Post`.`post_date`,
 `Post`.`post_date_gmt`,
 `Post`.`post_content`,
 `Post`.`post_title`,
 `Post`.`post_excerpt`,
 `Post`.`post_status`,
 `Post`.`comment_status`,
 `Post`.`ping_status`,
 `Post`.`post_password`,
 `Post`.`post_name`,
 `Post`.`to_ping`,
 `Post`.`pinged`,
 `Post`.`post_modified`,
 `Post`.`post_modified_gmt`,
 `Post`.`post_content_filtered`,
 `Post`.`post_parent`,
 `Post`.`guid`,
 `Post`.`menu_order`,
 `Post`.`post_type`,
 `Post`.`post_mime_type`,
 `Post`.`comment_count`,
 `Term`.`term_id`,
 `Term`.`name`,
 `Term`.`slug`,
 `Term`.`term_group`
FROM
 `wp_term_taxonomy` AS `TermTaxonomy`
   LEFT JOIN `wp_posts` AS `Post`
    ON (`TermRelationship`.`object_id` = `Post`.`ID` AND `TermTaxonomy`.`post_id` = `Post`.`id`)
   LEFT JOIN `wp_terms` AS `Term`
    ON (`Term`.`term_id` = `TermTaxonomy`.`term_taxonomy_id`)
WHERE
 `TermTaxonomy`.`taxonomy` = 'category'
AND `Post`.`post_status` = 'publish'

`TermRelationship`.`object_id` が無いとのことなので、TermRelationship の定義が悪いのですが、ちょっとよくわかりません。TermRelationship モデル自体は、プライマリキーが object_id と term_taxonomy_id の 2 つになるので、これが原因でしょう。

後でもう少し調べてみますが、この手の複雑なテーブルの場合はクエリ文が楽かなと思ってしまいます。まあ、作業量を考えると、CakePHP に準拠してないテーブルを扱う時は(wordpress は、準拠していると思うのだけど)クエリのほうが作成が速いってのがありますね。

カテゴリー: CakePHP, Wordpress パーマリンク

CakePHP から wordpress のデータを扱う(3) への6件のフィードバック

  1. CakePHP初心者 のコメント:

    とても勉強になりました、ありがとうございます。

    私も色々試した結果…
    $joinsで各テーブルの連結を定義し、beforeFindで$queryDataとマージしました。

    こういう使い方は作法が悪いんでしょうか?

  2. Yako のコメント:

    すみません、カテゴリ内の記事一覧を取得する場合の「CategoriesController」部分の「function item()」の書き方を教えてもらえませんでしょうか?
    勝手なお願いですみません。宜しくお願い致します。

    • masuda のコメント:

      ちょっと前の記事で覚えていないのですが、
      function Item($slug) のように スラッグや ID 指定して1つだけ取ってきたい、ことですよね?

      CategoriesController クラスに function Item($slug) を作って、
      Category クラスの getCategoryName のように $Term->find(‘first’, … ) して、ひとつだけ取ってくる getCategory を自作したうえで、これで呼び出す。

      って感じでしょうか。

  3. Yako のコメント:

    お返事ありがとうございました。
    CakePHP から wordpress のデータを扱う(3)という記事を拝見しまして、その通りに動かしてみましたが、「カテゴリ内の記事一覧」が表示されています画像のように表示できませんでした。
    ひょっとしたら「CategoriesController」に「function item()」に関する記載がございませんでしたので、教えていただけたらと思っています。
    お忙しいところすみません、宜しければお願い致します。

    • masuda のコメント:

      ソースを確認してみました。
      なるほど、カテゴリ内の記事表示 item function が抜けておりました。…ので、追記しましたので参考にしてください。findPosts の解説は書いたのに Controller のコードの解説を直すのをわすれていたようです。

  4. Yako のコメント:

    お忙しい中ご連絡いただきましてありがとうございました。
    いろいろ勝手なことを言いましたすみませんでした。

コメントは停止中です。