Simple Tagging Behavior

By DW aka "dooltaz"
So far after looking at other tagging code in this site, I have not seen tags done properly when using normal database form. So this will be a basic, simple tagging system that allows you to use both a tags field in your table and a separate tagging table.
First create three tables.

The first table will be your tags table. This table will consist of the following fields id, tag.

The second table will be the table you would like to associate with tags. For this example let's use the table posts, with the required fields id, name, tags.

Third, we will need to build a connector table to link tags with posts. We will call this table posts_tags.

Please also build the proper hasAndBelongsToMany relationship for your model. The SQL and the Model Definition can be found in the Cake Manual for Models under Section 4. Look for the heading "HABTM Join Tables: Sample models and their join table names"

http://manual.cakephp.org/chapter/models

Models



/app/models/post.php

Model Class:

Download code <?php 
<?php
class Post extends AppModel
{
    var 
$name 'Post';
    var 
$hasAndBelongsToMany = array('Tag' =>
                               array(
'className'    => 'Tag',
                                     
'joinTable'    => 'posts_tags',
                                     
'foreignKey'   => 'post_id',
                                     
'associationForeignKey'=> 'tag_id',
                                     
'conditions'   => '',
                                     
'order'        => '',
                                     
'limit'        => '',
                                     
'unique'       => true,
                                     
'finderQuery'  => '',
                                     
'deleteQuery'  => '',
                               )
                               );
}
?>
?>


/app/models/post_tag.php

Model Class:

Download code <?php 
<?php
class PostTag extends AppModel
{
    var 
$name 'PostTag';
}
?>
?>


/app/models/tag.php

Model Class:

Download code <?php 
<?php
class Tag extends AppModel
{
    var 
$name 'Tag';
}
?>
?>


Tag Behavior



/app/models/behaviors/tag.php
Download code
<?php /**
 * Tag Behavior class file.
 *
 * Model Behavior to support tags.
 *
 * @filesource
 * @package    app
 * @subpackage    models.behaviors
 */
 
/**
 * Add tag behavior to a model.
 * 
 */
class TagBehavior extends ModelBehavior {
    
/**
     * Initiate behaviour for the model using specified settings.
     *
     * @param object $model    Model using the behaviour
     * @param array $settings    Settings to override for model.
     *
     * @access public
     */
    
function setup(&$model$settings = array()) {

    
        
$default = array( 'table_label' => 'tags''tag_label' => 'tag''separator' => ',');
        
        if (!isset(
$this->settings[$model->name])) {
            
$this->settings[$model->name] = $default;
        }
        
    
$this->settings[$model->name] = array_merge($this->settings[$model->name], ife(is_array($settings), $settings, array()));

    }
    
    
/**
     * Run before a model is saved, used to set up tag for model.
     *
     * @param object $model    Model about to be saved.
     *
     * @access public
     * @since 1.0
     */
    
function beforeSave(&$model) {
    
// Define the new tag model
    
$Tag =& new Tag;
        if (
$model->hasField($this->settings[$model->name]['table_label']) 
        && 
$Tag->hasField($this->settings[$model->name]['tag_label'])) {


        
// Parse out all of the 
        
$tag_list $this->_parseTag($model->data[$model->name][$this->settings[$model->name]['table_label']], $this->settings[$model->name]);
        
$tag_info = array(); // New tag array to store tag id and names from db
        
foreach($tag_list as $t) {
            if (
$res $Tag->find($this->settings[$model->name]['tag_label'] . " LIKE '" $t "'")) {
                
$tag_info[] = $res['Tag']['id'];
            } else {
                
$Tag->save(array('id'=>'',$this->settings[$model->name]['tag_label']=>$t));
                
$tag_info[] = sprintf($Tag->getLastInsertID());
            }
            unset(
$res);
        }

        
// This prepares the linking table data...
        
$model->data['Tag']['Tag'] = $tag_info;
        
// This formats the tags field before save...
        
$model->data[$model->name][$this->settings[$model->name]['table_label']] = implode(', '$tag_list);
    }
    return 
true;
    }


    
/**
     * Parse the tag string and return a properly formatted array
     *
     * @param string $string    String.
     * @param array $settings    Settings to use (looks for 'separator' and 'length')
     *
     * @return string    Tag for given string.
     *
     * @access private
     */
    
function _parseTag($string$settings) {
        
$string strtolower($string);
       
        
$string preg_replace('/[^a-z0-9' $settings['separator'] . ' ]/i'''$string);
        
$string preg_replace('/' $settings['separator'] . '[' $settings['separator'] . ']*/'$settings['separator'], $string);

    
$string_array preg_split('/' $settings['separator'] . '/'$string);
    
$return_array = array();

    foreach(
$string_array as $t) {
        
$t ucwords(trim($t));
        if (
strlen($t)>0) {
            
$return_array[] = $t;
        }
    }
    
        return 
$return_array;
    }
}

?>



Usage




/app/models/post.php (REVISION)

Model Class:

Download code <?php 
<?php
class Post extends AppModel
{
    var 
$name 'Post';

    var 
$actAs = array('Tag'=>array('table_label'=>'tags''tags_label'=>'tag''separator'=>',');

    var 
$hasAndBelongsToMany = array('Tag' =>
...
?>
?>


Telling the Post model to "act as" a tag behavior will automatically take a comma delimited tags field from the Posts table and when it is saved, it will parse out the tags, save them to the tags table, and save the associated links.

This can work in one table or multiple tables that want to use the same set of tags.

Views


Here is the implementation:
/app/views/posts/add.ctp

View Template:

Download code
<?php echo $form->create('Posts');?>
<?php 
echo $form->input('title');?>
<?php 
echo $form->input('tags');?>
<?php 
echo $form->input('body');?>
</form>


/app/views/posts/edit.ctp

View Template:

Download code
<?php echo $form->create('Posts');?>
<?php 
echo $form->input('id');?>
<?php 
echo $form->input('title');?>
<?php 
echo $form->input('tags');?>
<?php 
echo $form->input('body');?>
</form>


Controller



Controller Class:

Download code <?php 
<?php
class PostsController extends AppController {
    var 
$name 'Posts';
    var 
$helpers = array('Html''Form' );

    function 
index() {
        
$this->Post->recursive 0;
        
$this->set('posts'$this->paginate());
    }

    function 
add() {
        if(!empty(
$this->data)) {
            
$this->cleanUpFields();
            
$this->Post->create();
            if(
$this->Post->save($this->data)) {
                
$this->Session->setFlash('The Post has been saved');
                
$this->redirect(array('action'=>'index'), nulltrue);
            } else {
                
$this->Session->setFlash('The Post could not be saved. Please, try again.');
            }
        }
    }
    function 
edit($id null) {
        if(!
$id && empty($this->data)) {
            
$this->Session->setFlash('Invalid Post');
            
$this->redirect(array('action'=>'index'), nulltrue);
        }
        if(!empty(
$this->data)) {
            
$this->cleanUpFields();
            if(
$this->Post->save($this->data)) {
                
$this->Session->setFlash('The Post saved');
                
$this->redirect(array('action'=>'index'), nulltrue);
            } else {
                
$this->Session->setFlash('The Post could not be saved. Please, try again.');
            }
        }
        if(empty(
$this->data)) {
            
$this->data $this->Post->read(null$id);
        }
    }

}
?>



Hope this helps someone.

Comments 321

CakePHP team comments Author comments

Comment

1 What does this code do

Can you explain what tagging is. why would i need this
posted Sat, Apr 7th 2007, 12:43 by Luke Mackenzie

Comment

2 Thanks

I was halfway through writing similar thing for cheesecake-photoblog v2.0 - will use this instead :)
posted Sat, Apr 7th 2007, 22:47 by Dr. Tarique Sani

Comment

3 What this code does

Can you explain what tagging is. why would i need this

Tagging is basically a way to organize your content without having to use categories. It is similar to how search engines use keywords to find a webpage. With tagging, when you write an article, you simply add a number of keywords or "tags" along with the article describing it. The code on this page will insert each tag into a "tags" table so that you can do quick referencing on them. For more information you may want to google "understanding tags".

This code automatically grabs the "tag" field from your model/table (Post, in this example) and it will automatically update the tags table and the posts_tags table according to what you type into the tag field on your view. It also formats the tags properly.

This makes tag searching for multiple tables easier, it makes editing and adding tags seamless, and also makes building tag clouds much easier.
posted Mon, Apr 9th 2007, 12:07 by DW

Question

4 Working Example

Do you have a working example of this posted somewhere? I'm a little confused as to how to implement this in my views - is the entry field a text input? Would this Behavior conflict with any other Behaviors using a beforeSave function?
posted Mon, Apr 16th 2007, 12:04 by Bryan Buchs

Comment

5 Working Example

Do you have a working example of this posted somewhere? I'm a little confused as to how to implement this in my views - is the entry field a text input? Would this Behavior conflict with any other Behaviors using a beforeSave function?

First, all of my working examples are still in development. Also please note that behaviors are not functional in 1.1.x.x. I am using the trunk of 1.2.x.x for my development.

Second, this particular article does not explain how to display tags in views. It only deals with adding tags and editing tags so that they are stored in your "posts" table and in your "tags" table. This allows for the greatest amount of flexability when displaying tags in your views.

Third, beforeSave function calls are done in sequence, so you can have a beforeSave in your behavior and have a model with a beforeSave. As long as each one returns true, it will save properly, however I am not sure which beforeSave is executed first.
posted Fri, Apr 20th 2007, 18:33 by DW

Comment

6 thanks

First, all of my working examples are still in development. Also please note that behaviors are not functional in 1.1.x.x. I am using the trunk of 1.2.x.x for my development.

Second, this particular article does not explain how to display tags in views. It only deals with adding tags and editing tags so that they are stored in your "posts" table and in your "tags" table. This allows for the greatest amount of flexability when displaying tags in your views.

Third, beforeSave function calls are done in sequence, so you can have a beforeSave in your behavior and have a model with a beforeSave. As long as each one returns true, it will save properly, however I am not sure which beforeSave is executed first.


Yeah, I get that it's 1.2 only - all Behaviors are. I should have made my question clearer - of course I'm not trying to implement the Behavior in a view; my question was how does this interact with the view of my add/edit forms?

It looks like you've added some controller logic and sample views to the article; I'll give those a shot. Thanks!
posted Wed, May 2nd 2007, 08:09 by Bryan Buchs

Comment

7 one enhancement

First let me note, that this is the first example, that made me think and look more into behaviors and I like it a lot.

I have one enhancement to note. If you have some logic, that involves for example a $model->saveField() in the rest of your programm, then the _parseTag function can not work, because in $model->data is only one field. So just before the _parseTag call I have included this line of code:


if (array_key_exists($this->settings[$model->name]['table_label'], $model->data[$model->name])) {

So _parseTag will only be called if the table_label field is in the current data of the model.
posted Wed, May 9th 2007, 07:10 by Siegfried Hirsch

Comment

8 typo

it should be $actsAs not $actAs too hard to figure out when you copy paste ;)
posted Thu, Jun 21st 2007, 12:44 by bododo

Comment

9 tags field

I don't understand why the posts table needs a text field that duplicates the tags list since the posts already have that data via the tags associations.
posted Tue, Jul 10th 2007, 16:44 by Luke Sheridan

Comment

10 tags field

You keep a tags field to allow you to keep the order intact without complicated HABTM stuff going on. If your not worried about that, then i guess you could do away with it.
posted Tue, Jul 10th 2007, 18:54 by Alex McFadyen

Question

11 doesnt seem to work

It does not seem to add any relations with me.. i have setup the model?

Model Class:

<?php 
<?php
class Article extends AppModel
{
    
// Model name
    
var $name 'Article';

    
// Database table
    
var $useTable 'articles';

    
// Define the behaviours to use
    
var $actsAs = array('Slug',
                        
'ExtendAssociations',
                        
'Tag' => array('table_label'=>'tags''tags_label'=>'tag''separator'=>','));

    
// Validator
    
var $validate = array(
    
'title' => array('rule' => array('between'3255)),
    
'article' => VALID_NOT_EMPTY
    
);

    var 
$hasAndBelongsToMany = array(
    
'Tag' =>
        array(
'className'    => 'Tag',
        
'joinTable'    => 'articles_tags',
        
'foreignKey'   => 'article_id',
        
'associationForeignKey'=> 'tag_id',
        
'conditions'   => '',
        
'order'        => '',
        
'limit'        => '',
        
'unique'       => true,
        
'finderQuery'  => '',
        
'deleteQuery'  => '',
        ),
    
'Category' =>
        array(
'className'    => 'Category',
        
'joinTable'    => 'articles_categories',
        
'foreignKey'   => 'article_id',
        
'associationForeignKey'=> 'category_id',
        
'conditions'   => '',
        
'order'        => '',
        
'limit'        => '',
        
'unique'       => true,
        
'finderQuery'  => '',
        
'deleteQuery'  => '',
        ),
    );

}
?>
posted Sat, Jul 14th 2007, 09:06 by chris

Question

12 Same problem.

I have the same problem as you chris. I've setup everything correctly, but after a little debugging I see that the code is not getting passed this step in the beforeSave>


if ($model->hasField($this->settings[$model->name]['table_label']) && $Tag->hasField($this->settings[$model->name]['tag_label'])) {


it seems to fail this check and I'm not sure why. I've set everything else up correctly, and I know it's calling beforeSave because I can print text before that if statement is called but not after.

EDIT: I fixed it. Make sure you have the tags field present, in your Post (or equiv) db table. I overlooked this step.
posted Wed, Jul 18th 2007, 12:00 by Matt

Question

13 How to display a Tag cloud

First of all, really a nice behavior, works great 4 me and
gave me a slight insight of how to use behaviors.

What I'm missing now is how to make one of the famous
tag clouds out of all that data ? Is there an example how to
accomplish that in your special case with 3 DB tables ?


-S.
posted Mon, Oct 15th 2007, 07:36 by Sven

Comment

14 I like the idea but it seems that this is against all normalization rules

I understand the need to generate a 'tags' field in the view for which to enter tag data into the database but wouldn't it make more sense for the behavior to simply the the models data on beforeSave for the 'tags' field in the array and parse and save them that way?

It could also generate a fake 'tags' field in the data on find operations but it doesn't need to be a real field.
posted Sat, Nov 17th 2007, 23:34 by Abba Bryant

Comment

15 model name actas

1) model/posts_tag.php
2) var $actsAs = ...
3) Don't forget to put HABTM into tag.php - if you want to see all post relate to this tag ;-)

Today, I'm finishing to write tagcloud "helper". For this behaviors.

Together it's look too nice ;-)

Thanks, DW!
posted Mon, Mar 10th 2008, 15:27 by Vladislav

Question

16 How to use findall on associated model to search for tags

i have set up tagging for an Image model and it is working like a charm. i just have one question: how do i query the Image model for a given tag name? i am looking for x in this call:

$this->Image->findall(x);
posted Sun, Apr 20th 2008, 10:01 by gregor

Comment

17 Credit

It's good practice to give credit when you have based your code on somebody elses work:

http://bakery.cakephp.org/articles/view/simple-tagging-component
posted Fri, May 9th 2008, 08:59 by Ben Milleare

Login to Submit a Comment