Adding Tags to a CakePHP App (hasAndBelongsToMany)
In this article I’m going to concentrate on the ‘hasAndBelongsToMany’ (HABM) model association by integrating Tags to the Blog application that we have experimenting with. As you are probably aware Tags have become a popular method of organising posts and are really just categories. This type of relationship is different from the others you have been using because a single post can have multiple Tags and a single Tag can have multiple posts. This is different from the comments example that I looked at previously where a comment can only belong to a single Post.
As usual I suggest reading the models chapter of the online CakePHP manual, taking special attention to the ‘Defining and Querying with hasAndBelongsToMany’ section near the bottom. At first I had problems with choosing an association to use but this will come with practise and experience building different applications. If you’re having trouble just drop me an email and I’ll try to help you out.
Designing the Database
When using this type of association (HABM) we need an extra table in the database to keep track of all the associated data. Once this has been setup we don’t need to worry about it in our application because CakePHP will take of the associated data when getting and saving our information.
I’m going to create a simple ‘Tags’ table in the database that will hold the name of the tag along with when it was created and modified. (Just a side note, if you have a ‘created’ and ‘modified’ fields in your table CakePHP will automatically use these when adding and editing data).
CREATE TABLE `tags` ( `id` int(11) NOT NULL auto_increment, `tag` varchar(100) NOT NULL, `created` datetime NOT NULL, `modified` datetime NOT NULL, PRIMARY KEY (`id`) );
Next I’m going to create the table that will link the ‘posts’ with their ‘tags’, this is a simple table with just the primary keys of the two tables. In this case ‘post_id’ and ‘tag_id’, the name of the table is also very important and allows CakePHP to automatically build the relationship, we need to call the table the plural versions of the two tables in alphabetical order. In the case of ‘posts’ and ‘tags’ we simply need to join them together with an underscore in alphabetical order and we get ‘posts_tags’.
CREATE TABLE `posts_tags` ( `post_id` int(11) NOT NULL, `tag_id` int(11) NOT NULL );
Creating the Models and Relationships
Like in all of my previous articles I’m going to start by creating the model files, create a file called ‘tag.php’, define the class and the HABM relationship like any other model you would create and do the same for the ‘post.php’. I didn’t have to modify the comment model so your ‘post’ and ‘tag’ model should look like the ones I have below.
class Tag extends AppModel {
var $name = 'Tag';
var $hasAndBelongsToMany = array('Post'=>array('className'=>'Post'));
}
class Post extends AppModel {
var $name = 'Post';
var $hasMany = array('Comment'=>array('className'=>'Comment'));
var $hasAndBelongsToMany = array('Tag'=>array('className'=>'Tag'));
}
The relationship has been defined using the absolute bare minimum of configuration and this is enough for the time being. This only works however if you are using the naming conventions set out by CakePHP (which we have done in this example) so everything will work just fine.
Baking the Application
Hopefully by now you should be able to bake the controller files along with all of the view files for each of the models in the application, if not then please go through my previous article on ‘baking with CakePHP’. Now that you done that you can test your application in a browser.
If we go to our application (http://cakephp/tags/) just like our posts, we can start adding, editing and deleting tags. Start by adding a few Tags, the first thing you will notice is that you can choose posts that may be related to your Tag and this is all done automatically because we have followed the conventions set out by CakePHP.
If we try to add a new post (http://cakephp/posts/add/) you will notice that the ‘related tags’ are not displaying correctly. To fix that we need to make a small change in the ‘posts_controller.php’, in the add function CakePHP uses a method called ‘generateList()’ which is extremely handy for creating a html select tag. At the moment the method is returning just the id of the tags:
Array
(
[1] => 1
[2] => 2
[3] => 3
)
This isn’t very helpful and we don’t know the name of the tag without delving into the database, to fix this we need to know how the ‘generateList()’ method works. To find this out we need to use the highly useful API. Go to the API page and go a search for the method name, it should return a few results, click on the top item in the list. This will take you to a page that shows all the arguments that the method can take. All we are interested in is the $keyPath and $valuePath which correspond to the key and values of the array that it returns. To get the tag name we simply insert “{n}.Tag.name” into the fifth argument leaving everything else null so we have:
$this->set('tags',
$this->Post->Tag->generateList(null,null,null,null,"{n}.Tag.tag"));
Now instead of a number this will return the name of the tag because we have specified which column in the database we should select in this case its ‘tag’ which is just the name of the tag.
Array
(
[1] => General
[2] => Programming
[3] => Films
)
In our add post page the tag names should be properly displayed, please note that this change needs to be made throughout your ‘posts_controller.php’ file. If you also want to remove the empty space in the select tag, open up the add post view file and in your $html->selectTag simply change the final argument from ‘true’ to ‘false’.
$html->selectTag('Tag/Tag',
$tags, $selectedTags,
array('multiple' => 'multiple', 'class' => 'selectMultiple'),
array(), false);
This problem has been encountered many times before especially in the CakePHP Google group, however I’m trying to push the use of the API and the manual before you concede and ask a question online. Obviously if you’re really stuck then head online and try searching for a solution before asking a question, no doubt someone else has come across the same problem and it’s been answered already. A few resources for help include the CakePHP Google group and the unofficial forum which I’ve used a few times before with great results.
Wrapping Up
One of the biggest problems that you’ll encounter when starting with CakePHP is understanding how your models are related and which associations to use. The HABM relationship is unique as it requires an extra table in the database to keep track of everything, this table is necessary because each ‘tag’ can have multiple ‘posts’ and vice versa. This is unlike the ‘posts’ and ‘comments’ association where we had to include the ‘post_id’ as a foreign key in the ‘comments’ table, notice that this is not the case in the ‘tags’ table.
The most important part of building any application in CakePHP are your model files, if they are setup correctly you should have no problems baking your controller and view files. If you do encounter problems with bake, then check and double check your model files. Again I’m going to emphasise reading the ‘Models’ section of the manual especially the associations chapters, if you are having trouble with something then drop me an email and I’ll try and help you out. In my next CakePHP article I’m going to dealing with file uploads so check back soon.
Comments
Vinz (27/02/2008 - 05:00)
Thank, your article help me a lot !
khanou (26/05/2008 - 13:29)
Thank!
John (15/06/2008 - 05:31)
Excellent summary, cheers!
techiguy (18/09/2008 - 23:20)
$html->selectTag('Tag/Tag',$tags, $selectedTags,array('multiple' => 'multiple', 'class' => 'selectMultiple'),array(), false);
in the above code we have to write code for $tags and $selectedTags in the controller.
where is the code?
James (19/09/2008 - 03:18)
@techiguy: If you go through the bake process the code will be written for you. Otherwise you can use the "$this->Post->Tag->generateList()" function in CakePHP 1.1 or the "$this->Post->Tag->find('list')" function in 1.2 to get an array to use in your lists.
Al (21/11/2008 - 08:02)
I was going a little crazy trying to get this to work in 1.2. I've been all over the interwebs looking at various tutorials. Then after reading this article again, and just trying to rebake my view and conroller it's working.
The only semi tough thing (for a noob) is that everyone references the deprecated generateList(). One needs to understand the find('list') to get the select looking correct and you should be home free.
Thank you for the info
James (21/11/2008 - 08:23)
@Al: Hi, thanks for commenting. Yeah it is a little tricky if your updating to 1.2 - quite a lot of functions have been depreciated and others have been modified quite a bit. Although after working with 1.2 for a while I've realised its for the best.
Sean (12/06/2009 - 06:03)
Thanks for the tutorial, I was having trouble figuring out the HABTM relationship. Quick question: when displaying the tags in something like a tag cloud, how would you get the number of posts associated with each tag? Cheers
Sean (12/06/2009 - 08:25)
It appears I found the answer to my own question on how to count the number of posts associated with a tag, if anyone else is interested. This works well: Counter Cache behaviour for HABTM relations - http://bakery.cakephp.org/articles/view/counter-cache-behavior-for-habtm-relations
RP (02/03/2010 - 12:25)
Hi James, this is a great walkthrough and it has helped me loads. The only problem that I am having is that for one reason or another I am not able to use the bake functionality so I am left without knowing what the MVCs should be like.
Can anybody out there point me in the direction of the MVCs that are created for this tutorial?
James (03/03/2010 - 23:50)
@RP: not too sure where you could get those without actually running the bake script with your specific Models etc. Have a search online and try getting it to work as it's quick & easy to get setup baking an application.
Frank (21/08/2010 - 07:28)
Hi, generateList does not produce anything in the API page.
James (01/09/2010 - 22:48)
@Frank: the "generateList" method is really old and is probably depreciated. Try getting a list by using the find method:
$this->User->find('list');