チュートリアルで作成したブログにタグ機能を実装するチュートリアル
以下はcakePHPのマニュアルの付録で作成したブログチュートリアルにタグ機能を実装するチュートリアルです。
このチュートリアルで作成する機能は以下の機能です
ライセンス
タグ機能の実装処理については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>