zen of coding

Notes on CakePHP HABTM (Part 1, the basics)

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 HABTM data

  • Thanks for this article, it helps me to understand few things, but also messed up another things in my head :)

    Okay, here comes my question: Few days ago I was doing HABTM association with themes and tags (probably the same as with posts and tags, except the alphabetical order :) )
    One thing I created was a TagsThemes associative model, as I read this article it should be called TagsTheme, but if I remember it doesn’t worked for me. Also from this article I understand that there is no need for a model like this as TagsTheme should be automatically created, am I right? Now I’m little messed with this, however my code is working, but maybe in not very clear cake way :-/

  • Pingback: This week in Cake (Part 3) | Personal weblog of Robert Beekman()

  • teknoid

    @depi

    Technically there is no need to actually create a model. (i.e. make the model_name.php file). There is nothing wrong with doing it, however. The auto-model will not have any relationships by default, so you might define those in the file if needed. As well, you can add validation and maybe some custom methods. The reason this is not done often is because the join (auto-model) generally only contains foreign keys and nothing else, so there is rarely a need to do anything complex with it.

    A general advice, if you are not sure about what’s going on with your models (naming, associations, defaults, data, etc.)… simply debug them. Remember that you can always do pr($this->Post), for example, in your controller to see all the information about your model object. Give it a shot.

    Hope that clears things up a bit ;)

  • Okay so the main thing I should do is to rename TagsThemes to TagThemes and create tags_theme.php model to be it all right (cake way)

  • Very nice!!

  • teknoid

    @Ropsronrern

    Thanks

  • Webguy

    What is missing from every single habtm example I have seen (and maybe what I still haven’t figured out) is complete code including controllers and views (in addition to the model info everyone is keen on sharing). Specifically, I have not seen the scaffold display a join table with extra fields properly from the posts or the tags view. Additionally, what if your join table has extra attribute tables hanging off of it? Let’s say, in a poor example, that a given post’s tag can have many authors? Schema looks like this (I believe):

    authors
    authors_posts_tags
    posts
    posts_tags
    tags

    Firstly, I am guessing the scaffold is incapable of displaying these relationships on its own, which is fine. If that’s the case what does a complete example look like (again, focusing on the controllers and the views) if you want to be able to see both a posts tags and their respective authors in the posts view? What about editing this data in the Post edit view? Any pointers _greatly_ appreciated.

  • @Webguy

    Retrieving data for models is extensively covered in the manual. $this->Post->find(‘all’) (depending on the recursion level, or if you use the Containable behavior) will get you all the relevant information that you require, i.e. Authors and Tags.

    authors_posts_tags is probably not going to fly, it might be authors_posts (if you have Author HABTM Post) meaning that you can have more than one author for a given post.

    Editing multiple models is accomplished via saveAll() (it is a bit trickier for HABTM), but there are examples here on my blog and in the manual as well.

  • Webguy

    Appreciate your willingness to provide insight, but unfortunately you missed my point/questions.

    1. There aren’t any complete examples as I describe which include models, views, and controllers (I did, basically figure out how to use the containable behavior in a find in my view method of my controller, but no idea if that’s a best practice).
    2. Imagine the join table itself has attributes — the posts don’t have authors per se, in my poor example, but let’s just say it’s votes for or against a tag of an article. Have not seen that example covered with any level of detail. And it can’t be that uncommon for the join table to have attributes. The real life example I am working is essentially server HABTM applications. And, in this case, one install of Apache on one server has a ports table. Since it’s an indeterminant number of ports that have been configured for an “instance(read: applications_servers)”, the join table has an attributes table – in this case ports.

    Can you point me to a _complete_ example? I’d just assume follow a best practice rather than guess that the sloppy code I’m putting together is right.

    Thanks again for the help and the info on your site, which is quite informative.

  • @Webguy

    1. It is the best practice, so you did the right thing. As far as complete example… you are essentially looking for an application that does something specific to what you need. There are quite a few open source applications that you can study to see how things are done. There are even some tutorials out there, the IBM series come to mind, that show how to build a complete app.
    Here’s another one: http://cakebaker.42dh.com/2008/09/07/building-a-dvd-catalog-application-with-cakephp/

    I don’t know of any more “complete” examples.

    2. That’s just part of the learning curve, first you have to grasp the concepts and then apply them to real-world scenarios. I, unfortunately, do not know of any complete app that is specific to your needs. But there are countless hints out there, which should guide you on how to approach your situation. You can jump on the IRC channel or ask at the google group, but I would not expect an answer the solves your problem end-to-end.

    Thanks, and I hope you find the relevant info that helps you to accomplish your goals. Best of luck ;)

  • Pingback: Notes on CakePHP HABTM (Part 1, the basics) " nuts and bolts of cakephp()

  • Pingback: Notes on CakePHP HABTM (Part 1, the basics) " nuts and bolts of cakephp()

  • Jr

    Need Help

    I mistakenly renamed the app_model.php file in my development system, but renamed it to its appropriate filename, yet, I cant access any methods I previously had in that file. Is this a caching issue or what??

  • @Jr

    Sounds very strange. Hard to guess….

  • Chrono

    var $hasAndBelongsToMany = array(‘Project’ => array(‘with’ => ‘my_cool_join_table’, ‘joinTable’ => ‘projects_states’));

    prompts out: Error: Database table my_cool_join_tables for model my_cool_join_table was not found.

    I don’t understand why he isn’t looking for the correct joinTable.

    Any suggestions?

  • @Chrono

    Don’t specify the ‘with’ key.
    If you must, then create a model and add a $useTable property.

  • Pingback: Creating social network friendships with CakePHP | dConstructing - PHP()

  • i have theses tables
    article , article_currency, currency in database
    and i made HABTM between article and currency like this

    var $hasAndBelongsToMany = array(‘currency’=>array(‘className’ => ‘currency’,’joinTable’=>’article_currency’,’foreignKey’ => ‘articleid’,’associationForeignKey’ => ‘CurrencyID’));

    var $hasAndBelongsToMany = array(‘articlemodel’=>array(‘className’ => ‘articlemodel’,’joinTable’=>’article_currency’,’foreignKey’ => ‘CurrencyID’,’associationForeignKey’ => ‘ArticleID’));

    and here cake genrate the model **ArticleCurrency** for me
    when i try to change its name using **with** to ‘article_currency’
    is give me this error
    ***Database table article_currencies for model article_currency was not found.***

    and you say that if i change with value will not affect

    how come ?

  • @islam khalil
    ArticleCurrency expects the table name to be article_currencies (plural).
    To me it seems like everything works as expected.

    Plus this article was written a while ago, and it is possible that there were changes since then.

  • mohsin

    How can fetch data of other tables while making query to joinTable ?
    Please elaborate with example…

  • teknoid

    @mohsin

    Please read the manual about find(‘all’); … it works the same for join tables, like any other ones.

%d bloggers like this: