Simple Tagging Behavior
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
/app/models/post.php
/app/models/post_tag.php
/app/models/tag.php
/app/models/behaviors/tag.php
Download code
/app/models/post.php (REVISION)
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.
Here is the implementation:
/app/views/posts/add.ctp
/app/views/posts/edit.ctp
Hope this helps someone.
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'), null, true);
} 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'), null, true);
}
if(!empty($this->data)) {
$this->cleanUpFields();
if($this->Post->save($this->data)) {
$this->Session->setFlash('The Post saved');
$this->redirect(array('action'=>'index'), null, true);
} 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
Comment
1 What does this code do
Comment
2 Thanks
Comment
3 What this code does
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.
Question
4 Working Example
Comment
5 Working Example
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.
Comment
6 thanks
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!
Comment
7 one enhancement
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.
Comment
8 typo
Comment
9 tags field
Comment
10 tags field
Question
11 doesnt seem to work
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', 3, 255)),
'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' => '',
),
);
}
?>
Question
12 Same problem.
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.
Question
13 How to display a Tag cloud
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.
Comment
14 I like the idea but it seems that this is against all normalization rules
It could also generate a fake 'tags' field in the data on find operations but it doesn't need to be a real field.
Comment
15 model name actas
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!
Question
16 How to use findall on associated model to search for tags
$this->Image->findall(x);
Comment
17 Credit
http://bakery.cakephp.org/articles/view/simple-tagging-component