タグ: cakePHP

  • 「チュートリアルで作成したブログにタグ機能を実装するチュートリアル」をcakePHP2.0でやってみた。

    元ネタはこれ。チュートリアルで作成したブログにタグ機能を実装するチュートリアル – 「最果て」の支部

    cakePHPを最近触りだしたのですが、巷にcakePHP1系の情報があふれているのでなかなか大変な今日この頃です。

    とりあえず、cakePHP2.0のブログチュートリアルにタグ機能を実装してみました。

    やっていることは元ネタと変わらないので、そちらを読みつつやっていければと思います。

    テーブルの作成

    タグ機能みたいな、複数の値と複数の値を関連づけるような機能(この場合だと複数の記事が一つのタグに所属する&複数のタグが一つの記事に所属する)は、一筋縄ではいかないようです。投稿を管理するテーブルにタグを管理するフィールドを作って、タグのテーブルにもその逆を作って、そこにカンマ区切りとかjsonとかで・・・・とか、どう考えても無茶が過ぎるし、SQLで対処できなくなりますよね。

    なので、タグと投稿を関連づけるための別のテーブルを用意します。WordPressだと、term_relationshipsっていうテーブルが有るのですが、それと同じ事をやろうというわけ。

    それをcakePHPではhasAndBelongsToMany通称HABTMと言う機能で実装できるようです。

    なので、まずその二つのテーブルを作成します。

    [sql]
    CREATE TABLE `tags` (
    `id` int(10) unsigned NOT NULL auto_increment,
    `tag` varchar(100) default NULL,
    PRIMARY KEY (`id`)
    )
    [/sql]

    [sql]
    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`)
    )
    [/sql]

    HABTMのテーブル名は、関連づけるテーブル1_テーブル2、フィールド名も、テーブル1の単数形_idで作成するようにと規約にあるようなので、その方がベターだと思われます。

    モデルの作成

    Tag.phpをModelディレクトリ内に作成します。
    この中にタグの追加、編集などを行うメソッド(parseTags)を定義します。これをコントローラーで呼ぶ訳ですね。

    [php]
    <?php
    class Tag extends AppModel {
    public $name = ‘Tag’;

    public $hasAndBelongsToMany = array(
    ‘Post’ => array(
    ‘className’ => ‘Post’,
    ‘joinTable’ => ‘posts_tags’,
    ‘foreignKey’ => ‘tag_id’,
    ‘associationForeignKey’ => ‘post_id’
    )
    );

    public function parseTags($tagString) {
    $ids = array();

    $tags = explode(‘,’,trim($tagString));

    foreach ($tags as $tag):
    if(!empty($tag)){
    //DBからタグに対応する列を取得
    $tag = trim($tag);
    $this->unbindModel(array(‘hasAndBelongsToMany’ => array(‘Post’)));
    $tagRow = $this->findByTag($tag);

    if(is_array($tagRow)){
    //タグが登録済の処理
    if(in_array($tagRow[‘Tag’][‘id’], $ids)){
    continue;
    }
    $ids[] = $tagRow[‘Tag’][‘id’];
    }else {
    $newTag[‘Tag’][‘tag’] = $tag;
    $newTag[‘Tag’][‘id’] = "";
    $this->save($newTag);
    $ids[] = $this->getLastInsertID();
    }
    }

    endforeach;

    return $ids;
    }
    }

    [/php]

    $hasAndBelongsToMany繋ぐテーブルを定義します。
    結合とかはこれだけで勝手にやってくれる模様。

    次いで、Post.php。
    $hasAndBelongsToManyを定義するのは一緒。
    Tag.phpでは書いてあった、’foreignKey’と’associationForeignKey’が書いてません。ただ、デフォルト値でforeignKeyは現在のモデル名_id,associationForteignKeyは外部モデル名_idになるようですので問題ないようです。
    ここで、findAllByTagsというデータベースからあるタグがついた記事のデータを持ってくるメソッドと、それの数を数えるfindCountByTagsというメソッドの二つを用意します。

    [php]
    class Post extends AppModel {
    public $name = ‘Post’;

    public $validate = array(
    ‘title’ => array(
    ‘rule’ => ‘notEmpty’
    ),
    ‘body’ => array(
    ‘rule’ => ‘notEmpty’
    )
    );

    public $hasAndBelongsToMany = array(
    ‘Tag’ => array(
    ‘className’ => ‘Tag’,
    ‘order’ => ‘tag’
    )
    );

    public 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’];
    }

    public function findAllByTags($tags = array(), $limit = 50, $page = 1, $criteria = null){
    if(count($tags) <= 0){
    return 0;
    }
    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;
    }
    }
    [/php]

    コントローラーの作成、編集

    PostController.phpでは、
    まず、add,editにタグを追加、編集する機能を追加。元記事では、$this->dataとなっていますが、cakePHP2.0の場合は$this->request->dataとしないとエラーになります。なりました。

    そして、tagアクションの追加。ここで、先ほどPostモデルで作ったメソッドが使われるわけですね。

    [php]
    <?php
    class PostsController extends AppController {
    public $name = ‘Posts’;
    public $uses = array(‘Post’,’Tag’);
    public $helper = array(‘Html’,’Form’);
    public $components = array(‘Session’);

    public function index() {
    //全てのデータの読み出し
    $posts = $this->Post->find(‘all’);
    $this->set(‘posts’,$posts);
    }

    public function view($id = null) {
    //単一投稿の表示
    $this->Post->id = $id;
    $post = $this->Post->read();
    $this->set(‘post’,$post);
    }

    public function add() {
    if($this->request->is(‘post’)){//$_POSTの判定

    //タグのパース
    if(!empty($this->request->data[‘Post’][‘Tags’])){
    $this->request->data[‘Tag’][‘Tag’] = $this->Post->Tag->parseTags($this->data[‘Post’][‘Tags’]);
    }

    if($this->Post->save($this->request->data)){
    $this->Session->setFlash(‘投稿が保存されました’);
    $this->redirect(array(‘action’=>’index’));//indexへリダイレクト
    }
    else {
    $this->Session->setFlash(‘投稿が保存できませんでした’);
    }
    }
    }

    public function edit($id = null) {
    $this->Post->id = $id;
    if($this->request->is(‘get’)){
    //読み出し
    $this->request->data = $this->Post->read();//データの取得

    //タグの取得
    if(count($this->request->data[‘Tag’])){
    $tags = ”;
    foreach ($this->request->data[‘Tag’] as $tag) {//一行ずつ取り出す
    $tags .= $tag[‘tag’].",";
    }
    $this->request->data[‘Post’][‘Tags’] = substr($tags, 0 ,-1);//最後のカンマを取り除いてViewにデータを投げる
    }
    }
    else {

    if(!empty($this->data[‘Post’][‘Tags’])){
    $this->request->data[‘Tag’][‘Tag’] = $this->Post->Tag->parseTags($this->data[‘Post’][‘Tags’]);
    }

    if($this->Post->save($this->request->data)){
    $this->Session->setFlash(‘投稿を更新しました’);
    $this->redirect(array(‘action’=> ‘index’));
    }else{
    $this->Session->setFlash(‘更新できませんでした’);
    }
    }
    }

    public function delete($id) {
    if($this->request->is(‘get’)){
    //GETならエラー
    throw new MethodNotAllowedException();
    }

    if($this->Post->delete($id)){
    $this->Session->setFlash(‘ID:’.$id.’の投稿を削除しました’);
    $this->redirect(array(‘action’=>’index’));
    }
    }

    public function tag($tag = null) {
    $tags = array();
    App::uses(‘Sanitize’, ‘Utility’);
    $this->Sanitize = new Sanitize;

    if(isset($this->params[‘pass’])){

    foreach($this->params[‘pass’] as $tag):
    $this->Sanitize->paranoid($tag, array(‘ ‘));
    $tags[] = $tag;
    endforeach;

    }
    $paging[‘url’] = ‘/posts/tag’. implode(‘/’, $tags);
    $paging[‘total’] = $this->Post->findCountByTags($tags);
    if($paging[‘total’] > 0){
    $posts = $this->Post->findAllByTags($tags);
    $this->set(‘posts’,$this->Post->findAllByTags($tags));
    $this->render(‘index’);
    }
    else {
    //タグの記事が無い場合の処理。記事が見つかりませんでしたみたいな。
    //exit;
    }
    }

    }

    [/php]

    Viewの編集

    とりあえず、add.ctpとedit.ctpにタグ用のテキストボックスを追加。
    [php]
    echo $this->Form->input(‘Tags’, array(‘size’ => ’40’));
    [/php]

    edit.ctpはこんな感じ。add.ctpは省略。
    [php]
    <h1>Edit Post</h1>
    <?php
    echo $this->Form->create(‘Post’, array(‘action’ => ‘edit’));
    echo $this->Form->input(‘title’);
    echo $this->Form->input(‘body’, array(‘rows’ => ‘3’));
    echo $this->Form->input(‘Tags’, array(‘size’ => ’40’));
    echo $this->Form->input(‘id’, array(‘type’ => ‘hidden’));
    echo $this->Form->end(‘Save Post’);
    ?>
    [/php]

    ここまででちゃんと動くはず!

    タグクラウドの作成

    ここではタグクラウドをTagsController.phpに作成です。タグクラウド(゚⊿゚)イラネって人は別に作らないでいいと思います。
    [php]

    class TagsController extends AppController {

    public $name = "Tags";

    public 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();

    if(is_array($tagsData) && count($tagsData) > 0) {
    foreach($tagsData as $tagDetails):
    $PostCounts[] = $tagDetails[0][‘PostCount’];
    endforeach;
    }
    else {
    $PostCounts[] = 0;
    }

    $maxQuantity = max($PostCounts);
    $minQuantity = min($PostCounts);

    $spread = $maxQuantity – $minQuantity;

    if($spread == 0){
    $spread = 1;
    }

    $this->set(‘spread’, $spread);
    $this->set(‘minQuantity’, $minQuantity);
    $this->set(‘browsing’, ‘Tags’);

    }
    }
    [/php]

    んで、View/Tags/tagcloudbox.ctpを作成
    [php]
    <div id="sidecontent">
    <h2>Tag</h2>
    <?php
    $step = (30-10) /$spread;

    foreach($tags as $tag):
    $fontSize = 10 + ($tag[0][‘PostCount’] – $minQuantity) * $step;
    endforeach;

    echo $this->Html->Link($tag[‘tags’][‘tag’], array(‘action’ => ‘tag’,’controller’ => ‘posts’,$tag[‘tags’][‘tag’]), array(‘title’ => $tag[0][‘PostCount’].’Photos’,’rel’ => ‘tag’, ‘style’ => ‘font-size:’.$fontSize.’px’)).’ ‘;

    ?>
    </div>
    [/php]

    ここでは普通にViewにしてありますが、エレメントにしてあげて、他のViewから呼んであげるとかの使い方が良さそうです。

    とりあえず何とかcakePHP2.0で実装できました。
    ちょっとできる人になった気分!笑
    まぁ、タグって結構需要はあると思うので、こんな感じで実装できました!ってだけのエントリーなんですけどね。

    posts/tag/tag1/tag2/tag3
    みたいな絞り込みもできるようです。

    ただ、この元ネタのcheesecake-photoblogのライセンスはGPLv2ですので、当然このエントリーのコードもGPLv2です。使う際はご注意下さい。

  • hetemlでCakePHPをインストールするときは、PHP5.3で動かすべし。

    hetemlでcakePHPを使おうと思って色々一日試行錯誤していたのですが、ちゃんとインストールできていると、

    http://example.org/cake_dir/

    が、

    という画面になるらしいんですが、CSSの効いていない画面で、“Missing Controller”

    また、アプリケーション自体にはアクセスできるのですが、アプリケーションのアクションにはアクセスできない。つまり、

    http://example.org/cake_dir/appname/にはアクセスできるけど、http://example.org/cake_dir/appname/actionnameや、http://example.org/cake_dir/appname/indexにはアクセスできないといった症状が見られた場合、PHP5.3で動作させるようにすると解決するようです。

    PHP5.3で動作させるには、動作させたい、ディレクトリのか、その親の階層の.htaccessに、

    [plain]
    AddHandler php5.3-script .php
    [/plain]

    に記述すれば動作します。

    あんまり日本語の情報が無いようですが、頑張っていきたいです。