Thursday, January 31, 2013

HABTM

Part 1. The basics

HABTM seems to give a lot of people trouble, so I wanted to cover a few points that may or may not be in the manual. And I will assume here that you have basic understanding or some knowledge of HABTM, otherwise you should really go and read the manual first… (By the way, do read the manual if you are having trouble. It’s constantly evolving and some points you’ve missed before may very well be covered).
OK, so let’s begin with a good ol’ Post HABTM Tag.
Here’s the simplest definition of the Post model:


class Post extends AppModel {
     var $name = 'Post';
     var $hasAndBelongsToMany = array('Tag');
}

(In PHP5 you won’t even need $name, but let’s leave it in for a few years).
To make HABTM work you have to have a very similar definition of the Tag model:

class Tag extends AppModel {
     var $name = 'Tag';
     var $hasAndBelongsToMany = array('Post');
}

And the magic has happened already…
However, unless you know a thing or two about magic, it can be hard to figure out what’s going on because CakePHP makes a lot of assumptions if you do not manually override the defaults.
I’m going to cover here with and joinTable as I find these to be the most often overlooked/misunderstood HABTM basics.
with
The with key specifies an auto-model, which cake creates to represent the join table (joinTable in CakePHP syntax). If we follow the defaults the joinTable model is named PostsTag and you can use it like any other regular CakePHP model. Yes, it is automagically created for you.
joinTable
Still following conventions, CakePHP will assume that the joinTable is named posts_tags. Table name consists of plural model names involved in HABTM and is always in alphabetical order. So, P goes before T, therefore posts_tags.
Well, let’s say you don’t like the name posts_tags and instead would like to rename your joinTable to my_cool_join_table…
Off you go and modify your Post model like so:

class Post extends AppModel {
     var $name = 'Post';
     var $hasAndBelongsToMany = array('Tag'=>array(
                                               'joinTable'=>
                                               'my_cool_join_table'));
}

Guess what? You’ve just messed with the magic.
First of all CakePHP will now rename your auto-model to MyCoolJoinTable, so if you had some references in the code to PostsTag (the default with model) you have to go and change them.
Secondly, and maybe more importantly, you’ve probably forgot about your Tag model. If you haven’t made any changes, Tag will still expect all the defaults. This can create a lot of mess and bizarre, unexpected results. So the point here is that since HABTM goes in both directions any changes you apply to one model definition should most likely be manually applied to another.
Now, what if you don’t like PostsTag name and would like to rename your join (with) model to PostTagJoin?

class Post extends AppModel {
      var $name = 'Post';
      var $hasAndBelongsToMany = array('Tag'=>array('with'=>'PostTagJoin'));
}

Changing the with key will not affect the joinTable value (i.e. CakePHP will not assume posts_tags_joins or something) so if you don’t change the default, CakePHP will still expect posts_tags.
In other words, with only changes the name of the auto-model, so in reality it’s not something one would or should bother to do.
Now onto some good stuff…
Having this auto-model is quite handy if you are interested in the joinTable data and becomes even more powerful when you have some additional fields in the joinTable, which you need queried or saved.
In the Posts controller you could do:

$this->Post->PostsTag->find(‘all’);

Or you could even apply some conditions to find the most popular tag ID’s:

$this->Post->PostsTag->find('all', array('fields'=>array('tag_id','COUNT(tag_id) AS total'),
                                                      'group'=>'tag_id')));
One caveat to note here is that PostsTag is not automagically associated with neither Post nor Tag. So in order to fetch some associated data, you’ll have to manually do something like:

$this->Post->PostsTag->bindModel(array(‘belongsTo’=>array(‘Tag’)));

Now, using the above example, you can even get the most popular Tag names by using Containable in the above query or simply increasing the recursive of PostsTag.

Part 2. Saving data
The next topic I wanted to cover is how to go about saving the HABTM data. Actually it is really not that complicated, as most of the work is taken care of for you.
Probably the most important thing is to make sure that your data arrives in the correct format for the save() method. The rest is really pretty easily handled by cake.
So let’s take a look at a few examples (still using our Post HABTM Tag)…
First, we’ve got a case where we know a Post ID and we are creating a single, new tag for it. We’d like to save the tag name and have CakePHP automagically associate it with a given post.
The form would look something like this:

echo $form->create('Tag');
echo $form->input('tag');
echo $form->input('Post.id', array('type'=>'hidden', 'value'=>5));
echo $form->end('Add tag');

And the controller action would be:

function add() {
$this->Tag->save($this->data);
}

Let’s also take a look at our data:

Array
(
    [Tag] => Array
        (
            [tag] => new one
        )

    [Post] => Array
        (
            [id] => 5
        )

)
To clear things up quickly, CakePHP will insert the new tag, grab the ID of the last insert and save the ID of the known post plus the ID of the newly created tag into the join table.
Now, let’s say you need to associate that same tag with a few different posts. Well, the only thing you need to do is to, again, make sure that you have correct fields in your form… so:

echo $form->create('Tag');
echo $form->input('tag');
echo $form->input('Post.1', array('type'=>'hidden', 'value'=>5));
echo $form->input('Post.2', array('type'=>'hidden', 'value'=>6));
echo $form->input('Post.3', array('type'=>'hidden', 'value'=>7));
echo $form->end('Add tag');

Will produce a data array that looks like:

Array
(
    [Tag] => Array
        (
            [tag] => my brand new tag
        )

    [Post] => Array
        (
            [1] => 5
            [2] => 6
            [3] => 7
        )

)

Basically this will save a new tag and associate that newly created tag with posts, which happen to have ID’s 5,6 and 7. I should mention that in the previous example the field could be named Post.1 instead of Post.id, but I find that ‘id’ is more descriptive and works just as well.
Also, in the example I’m using a hidden field with a given value for the ID, in the real life it would probably be a few checkboxes, with values for ID’s.
And lastly it’s worth to note that the above can be reversed, just as well you could setup a quick form to create a new Post and associate some known tags with it.
P.S. Be sure to check out Habtamable behavior, which helps to avoid some of the pain when working with HABTM models.

No comments:

Post a Comment

Note: Only a member of this blog may post a comment.