チュートリアルで作成したブログにタグ機能を実装するチュートリアル


以下はcakePHPのマニュアルの付録で作成したブログチュートリアルタグ機能を実装するチュートリアルです。
このチュートリアルで作成する機能は以下の機能です

作成する機能
  • 記事の登録、修正時にタグの登録&修正(カンマ区切りで指定(例 news,mysite,google,internet))
  • タグクラウドの実装
  • タグによる記事絞込み表示
ライセンス

タグ機能の実装処理についてはcakePHPで作成された、オープンソースであるcheesecake-photoblogから移植しています。
(cheesecake-photoblogのライセンスはGPLです)
なので実際のプロジェクトなどにコードを使用する場合はライセンスに注意してください。
cheeseCakerのライセンス部分は以下のようになっています。

 * @copyright Dr. Tarique Sani, tarique@sanisoft.com
 * @license http://opensource.org/licenses/gpl-license.php GNU General Public License V2
 * @package cheeseCake

1.テーブルの作成

・tagsテーブルの作成
 CREATE TABLE `tags` (
  `id` int(10) unsigned NOT NULL auto_increment,
  `tag` varchar(100) default NULL,
  PRIMARY KEY  (`id`)
)


・tagsテーブルとpostsテーブルの橋渡し用のテーブルを作成

ここで今回使用するテーブル間のアソシエーションについて確認しておきましょう。
アソシエーションについて、まだ理解が浅い場合はマニュアルの以下の項目をもう一度確認しなおしてみるといいでしょう。
・6.4. アソシエーション


TagとPostの関係は「複数フィールド-複数フィールド」の関係になります。
なので「1フィールド対1フィールド」の「hasOne」や
「1フィールド対複数フィールド」の「hasMany」では
今回作りたいタグ機能を実装することができません。
「hasAndBelongsToMany」を用いて「複数フィールド対複数フィールド」のリレーションを実現します。


hasAndBelongsToManyを用いるためには、2つのテーブル(tagsとposts)を橋渡しするためのテーブルを作成する必要があります。
作成するテーブルにはtags,postsそれぞれのidを格納するフィールドが必要です。
ということで作成するテーブルは以下のような内容になります。

CREATE TABLE `posts_tags` (
  `post_id` int(10) unsigned NOT NULL default '0',
  `tag_id` int(10) unsigned NOT NULL default '0',
  PRIMARY KEY  (`post_id`,`tag_id`)
) 

2.モデルの作成

  • cakePHPのhasAndBelongsToManyの規約に従ってモデルを定義します。
~/app/model/post.phpを以下のように追加
<?php
class Post extends AppModel
{
    var $name = 'Post';

    //ここから追加部
    var $hasAndBelongsToMany = array (
            'Tag' => array (
                'className' => 'Tag',
                'order' => 'tag'
             )
         );

    var $validate = array(

        'title' => VALID_NOT_EMPTY,
        'body' => VALID_NOT_EMPTY

    );

}
?>
~/app/model/tag.phpを作成
<?php
class Tag extends appmodel
{
    var $name = 'Tag';
    var $hasandbelongstomany = array ('Post' => array (
                                        'classname' => 'Post',
                                        'jointable'  => 'posts_tags',
                                        'foreignkey' => 'tag_id',
                                        'associationforeignkey'=> 'post_id'
                                     )
                                );
?>

3.Tag model内に登録・編集時に使用する関数を登録

  • /app/model/tag.phpのクラス内にタグ登録時に使用する関数を登録します
  • 関数の仕様は次のような仕様です
    • カンマ区切りで分けられた文字列を引数として受け取る(例 "news,social,medical")
    • カンマで分けられた要素を配列に格納
    • データベースからタグのidを取得して配列に入れて、それを返り値として返す
  • 関数の内容は以下の通りです(関数はcheesecakeより移植)
  • 関数の内容がよくわからないという人は次の章の関数を実際に使用している部分を見た後でもう一度みるとわかりやすいかもしれません
<?php
        function parseTags($tagString){                $ids = array();
                //カンマ区切りの文字列をパースして配列$tagsに格納
                $tags = explode(",",trim($tagString));

                //1つ1つのタグについてループ
                foreach($tags as $tag){
                        if(!empty($tag)){
                                //データベースからタグに対応する列を取得
                                $tag = trim($tag);
                                $this->unbindModel(array('hasAndBelongsToMany'=>array('Post')));
                                $tagRow = $this->findByTag($tag);

                                
                                if(is_array($tagRow)){
                                        //列が存在した=タグがすでにDBに登録済みだった場合の処理
                                        //DBから取得した値を$idsに格納
                                        if (in_array($tagRow['Tag']['id'], $ids)) {
                                                continue;
                                        }
                                        $ids[] = $tagRow['Tag']['id'];
                                }else{
                                        //列が存在しない=タグがDBに未登録だった場合の処理
                                        //タグを新規登録してIDを取得,$idsに格納
                                        $newTag['Tag']['tag']=$tag;
                                        $newTag['Tag']['id']="";
                                        $this->save($newTag);
                                        $ids[]=$this->getLastInsertId();
                                }
                        }
                }
                return $ids;
        }
?>

4.タグ登録機能の追加

フォームにタグ欄を追加
  • ~app/views/posts/add.thtmlとedit.htmlの適当なところに下の1行を追加
<p>Tags :   <?php echo $html->input('Post/Tags', array('size' => '40'))?></p>
コントローラにタグの処理を追加
  • ~app/controllers/posts_controller.php
  • add
<?php
    function add()
    {
        if (!empty($this->data))
        {
            //追加部
            if (!empty ($this->data['Post']['Tags'])){
                $this->data['Tag']['Tag'] = $this->Post->Tag->parseTags($this->data['Post']['Tags']);
            }

            if ($this->Post->save($this->data))
            {
                $this->flash('Your post has been saved.','/posts');
            }
        }
    }
?>
  • edit
<?php
    function edit($id = null)
    {
        if (empty($this->data))
        {
            $this->Post->id = $id;
            $this->data = $this->Post->read();

            //追加部1
            //DBから取得して配列に入っているタグをカンマ区切りの文字列に変換
            if (count($this->data['Tag']))
            {
                    $tags = '';
                    foreach ($this->data['Tag'] as $tag)
                    {
                            $tags .= $tag['tag'] . ",";
                    }
                    $this->data['Post']['Tags'] = substr($tags, 0, -1);
            }
        }
        else
        {

            //追加部2
            if (!empty ($this->data['Post']['Tags'])){
                $this->data['Tag']['Tag'] = $this->Post->Tag->parseTags($this->data['Post']['Tags']);
            }

            if ($this->Post->save($this->data['Post']))
            {
                $this->flash('投稿を更新しました。','/posts');
            }
        }
    }
  • ここまででタグの追加、編集ができるようになっているかどうか確認してください。
  • (タグの登録はカンマ区切りで行います "test,test2"等)

5.タグによる記事リストの絞込み表示

Postモデルにタグで検索するための関数を追加
  • findCountByTags,findAllByTagsの二つの関数を用意します(cheesecakeより移植)
  • タグの入った配列を引数にして返り値にDBから読み取ったデータを返します
  • 詳しい処理は実際の関数のSQLを見てください
  • ~/app/models/post.php
<?php
	function findCountByTags($tags = array(), $criteria = null) {
		if (count($tags) <= 0) {
			return 0;
		}

		if (!empty($criteria)) {
			$criteria = ' AND '.$criteria;
		}

		$prefix = $this->tablePrefix;
		$count = $this->query("SELECT COUNT('Post.id') AS count FROM
									(SELECT Post.id, COUNT(DISTINCT tags.tag) AS uniques
									FROM {$prefix}posts Post, {$prefix}posts_tags posts_tags, {$prefix}tags tags
									WHERE Post.id = posts_tags.Post_id
										  AND tags.id = posts_tags.tag_id
										  AND tags.tag IN ('".implode("', '", $tags)."') $criteria
									GROUP BY posts_tags.Post_id
									HAVING uniques = ".count($tags).") x" );

		return $count[0][0]['count'];
	}

	function findAllByTags($tags = array(), $limit = 50, $page = 1, $criteria = null) {
		if (count($tags) <= 0) {
			return false;
		}

		if (!empty($criteria)) {
			$criteria = ' AND '.$criteria;
		}

		$prefix = $this->tablePrefix;
		$offset = $limit * ($page-1);
		$posts = $this->query("SELECT
						Post.id,
						Post.title,
						Post.created,
						COUNT(DISTINCT tags.tag) AS uniques
								FROM
								 {$prefix}posts Post,
								 {$prefix}posts_tags posts_tags,
								 {$prefix}tags tags
								WHERE Post.id = posts_tags.Post_id
								  AND tags.id = posts_tags.tag_id
								  AND tags.tag IN ('".implode("', '", $tags)."') $criteria
								GROUP BY posts_tags.Post_id
								HAVING uniques = '".count($tags)."'
								ORDER BY Post.created DESC
								LIMIT $offset, $limit");
		return $posts;
	}

?>
Postコントローラにタグで表示するためのコントローラを作成
  • 仕様
    • tagを引数にして、複数指定できるようにする
    • 例 url/news/mysite/testだとnews,mysite,testという3つのタグが含まれている記事のリストを表示
  • リストの表示のviewにはindex.htmlを使用
  • ~app/controller/posts_controller.php
<?php
    function tag($tag = null)
    {        
        $tags = array ();
        uses('sanitize');
        $this->Sanitize = new Sanitize;
        if (isset ($this->params['pass']))
        {
            foreach ($this->params['pass'] as $tag)
            {
                $tag = $this->Sanitize->paranoid($tag, array (' '));
                $tags[] = $tag;       
            }
        }
        $paging['url'] = '/posts/tag/' . implode('/', $tags);
        $paging['total'] = $this->Post->findCountByTags($tags);
        if ($paging['total'] > 0)
        {       
            $posts = $this->Post>findAllByTags($tags);
            $this->set(compact('posts'));
            $this->render(null, null, 'views/posts/index.thtml');
        }   
        else
        {   
            //タグが存在しなかった場合のエラー処理をここに書いてください
            exit;
        }
        
    }
?>
  • url/posts/tag/タグ1/タグ2といったURLにアクセスしてタグ付けした記事が表示されることを確認してください

6.タグクラウドの作成

  • (時間がないため詳しい説明は後で追加します)
  • app/controllers/tags_controller.phpを作成
<?php

class TagsController extends AppController
{
    var $name = 'Tags';
   function tagcloudbox(){
	 $prefix = $this->Tag->tablePrefix;
	 $tagsData = $this->Tag->query("SELECT tags. * , count( posts_tags.tag_id ) PostCount
							FROM {$prefix}tags tags
							LEFT JOIN {$prefix}posts_tags posts_tags ON tags.id = posts_tags.tag_id
							GROUP BY tags.id
							ORDER BY tags.tag");

	 $this->set('tags', $tagsData);

	//初期化処理
	$PostCounts = array();
	// 各タグがつけられた数をPostCounts配列に入れる
   if (is_array($tagsData) && count($tagsData) > 0) {
	 foreach ($tagsData as $tagDetails) {
		$PostCounts[] =  $tagDetails[0]['PostCount'];
	 }
   }else {
		 $PostCounts[] = 0;
   }

	//postcountsの最大数を取得
	$maxQuantity = max($PostCounts);

	//postcountsの最小値を取得
	$minQuantity = min($PostCounts);

	$spread = ($maxQuantity - $minQuantity);


	// Spread は0以上である必要があるので以下の処理
	if ($spread == 0) {
		$spread = 1;
	}

	//viewののための値をセット 
	$this->set('spread', $spread);
	$this->set('minQuantity', $minQuantity);

	$this->set('browsing',"Tags");
   }
}
?>
  • app/views/tags/tagcloudbox.thtmlを作成
<div id="sidecontent">
<h2>タグ</h2>
<?php

/**
 * Calculate step
 */
$step = ((30 - 10) / $spread);

foreach ($tags as $tag) {
    /**
     * Calculate font size for this tag
     */
    $fontSize = (10 + ($tag[0]['PostCount'] - $minQuantity) * $step);
    /**
     * Link for tag
     */
    echo $html->link($tag['tags']['tag'], '/posts/tag/'.$tag['tags']['tag'], array('title'=>$tag[0]['PostCount'].' Photos','rel'=>'tag','style' => 'font-size: '.$fontSize.'px;')).' ';

}
?>
<br clear="all" />
</div>
  • urltags/tagcloudboxでタグクラウドが表示されれば完成です。お疲れ様でした。
  • 説明が欠けているところが多々ありますのであとで補強していきます。
  • cheesecakeのほうには関連タグ機能などもあるのでさらに詳しいソースを見たい場合はcheesecakeのソースをぜひダウンロードしてみてみてください。