One of CakePHP 1.2 coolest features are known as HABTM with models, which are particularly useful when you have a hasAndBelongsToMany binding between two models that may contain extra information (i.e: table fields) attached to the binding. They are also extremely useful when you need to make some model operations to the join table and feel too lazy to modelize it yourself, or when you have modelized the join table and need to tell CakePHP to use your own model as the join model.
Let’s start with a practical example, the classic Article -> hasAndBelongsToMany -> Tag binding. Set up the articles table in any way you like, and so the tags table. Now create the join table as follows:
CREATE TABLE `articles_tags`(
`id` INT NOT NULL AUTO_INCREMENT,
`article_id` INT NOT NULL,
`tag_id` INT NOT NULL,
PRIMARY KEY(`id`)
);
Yeah, I know, so much for extra fields: just the id. However feel free to add any extra fields yourself and the example here will still be valid. In fact, if there are more than two fields in the join table then CakePHP will assume that you are normally interested on those extra fields.
Assume you have set up some articles, some tags, and have joined articles with some tags. Let’s do a classic findAll on the Article model:
$records = $this->Article->findAll(); debug($records);
Bearing in mind the difference between the data you’ve set up, and the one we have on this example, the result may be something like this:
Array ( [0] => Array ( [Article] => Array ( [id] => 1 [title] => My First Article [body] => Hi there! [created] => 2007-03-19 15:01:16 [modified] => 2007-03-19 15:31:06 ) [Tag] => Array ( [0] => Array ( [id] => 1 [name] => tag1 [created] => 2007-09-19 03:04:33 [modified] => 2007-09-19 03:04:33 [ArticlesTag] => Array ( [id] => 1 [article_id] => 1 [tag_id] => 1 ) ) [1] => Array ( [id] => 3 [name] => tag3 [created] => 2007-09-19 03:04:33 [modified] => 2007-09-19 03:04:33 [ArticlesTag] => Array ( [id] => 2 [article_id] => 1 [tag_id] => 3 ) ) ) ) [1] => Array ( [Article] => Array ( [id] => 5 [title] => My Second Article [body] => Hi there, new article! [created] => 2007-03-19 15:06:02 [modified] => 2007-03-19 15:06:02 ) [Tag] => Array ( [0] => Array ( [id] => 2 [name] => tag2 [created] => 2007-09-19 03:04:33 [modified] => 2007-09-19 03:04:33 [ArticlesTag] => Array ( [id] => 3 [article_id] => 5 [tag_id] => 2 ) ) [1] => Array ( [id] => 3 [name] => tag3 [created] => 2007-09-19 03:04:33 [modified] => 2007-09-19 03:04:33 [ArticlesTag] => Array ( [id] => 4 [article_id] => 5 [tag_id] => 3 ) ) ) ) )
Ok now you may be asking: what is up with that ArticlesTag array that is showing up there? Well, you may have also guessed it: that’s the information coming from the HABTM join table. Let’s take a closer look at the SQL statements generated by CakePHP that yielded these results:
SELECT `Article`.`id`, `Article`.`title`, `Article`.`body`, `Article`.`created`, `Article`.`modified` FROM `articles` AS `Article` WHERE 1 = 1; SELECT `Tag`.`id`, `Tag`.`name`, `Tag`.`created`, `Tag`.`modified`, `ArticlesTag`.`id`, `ArticlesTag`.`article_id`, `ArticlesTag`.`tag_id` FROM `tags` AS `Tag` JOIN `articles_tags` AS `ArticlesTag` ON (`ArticlesTag`.`article_id` IN (1, 5) AND `ArticlesTag`.`tag_id` = `Tag`.`id`)
The first query does the normal Article search, this is where your findAll conditions would be inserted (so don’t think yet that you can attach conditions based on your join model). The next query finds all the tags linked through the join table to the set of Article IDs obtained during the first query. Yes, CakePHP 1.1 users: this query has also been optimized (CakePHP 1.1 runs one finder query per resulting row).
What if you want to only get those articles that are linked to a specific tag? You need then to query the join table. This is where auto-with tables can also help you out, since the join table has been auto-modelized for you (hence the auto part of the name). Try this:
$joinRecords = $this->Article->ArticlesTag->findAll(array('ArticlesTag.tag_id' => 3)); debug($joinRecords);
We now get:
Array ( [0] => Array ( [ArticlesTag] => Array ( [id] => 2 [article_id] => 1 [tag_id] => 3 ) ) [1] => Array ( [ArticlesTag] => Array ( [id] => 4 [article_id] => 5 [tag_id] => 3 ) ) )
So you can either play around and get the Article IDs using Set extract:
$ids = Set::extract($joinRecords, '{n}.ArticlesTag.article_id'); debug($ids);
After which $ids look like:
Array ( [0] => 1 [1] => 5 )
Or use bindModel to join the Article and Tag models to the join table, and then running the query:
$this->Article->ArticlesTag->bindModel(array('belongsTo' => array('Article', 'Tag'))); $joinRecords = $this->Article->ArticlesTag->findAll(array('ArticlesTag.tag_id' => 3)); debug($joinRecords);
The records now look like:
Array ( [0] => Array ( [ArticlesTag] => Array ( [id] => 2 [article_id] => 1 [tag_id] => 3 ) [Article] => Array ( [id] => 1 [title] => My First Article [body] => Hi there! [created] => 2007-03-19 15:01:16 [modified] => 2007-03-19 15:31:06 ) [Tag] => Array ( [id] => 3 [name] => tag3 [created] => 2007-09-19 03:04:33 [modified] => 2007-09-19 03:04:33 ) ) [1] => Array ( [ArticlesTag] => Array ( [id] => 4 [article_id] => 5 [tag_id] => 3 ) [Article] => Array ( [id] => 5 [title] => My Second Article [body] => Hi there, new article! [created] => 2007-03-19 15:06:02 [modified] => 2007-03-19 15:06:02 ) [Tag] => Array ( [id] => 3 [name] => tag3 [created] => 2007-09-19 03:04:33 [modified] => 2007-09-19 03:04:33 ) ) )
And if you need more information associated with each Article you can of course play with the $recursive level of the findAll query.
What if for some reason you have already modelized your join table? Let’s say you created a model called ArticlesTag (located on app/models/articles_tag.php) that looks like this:
class ArticlesTag extends AppModel { var $name = 'ArticlesTag'; var $belongsTo = array('Article', 'Tag'); }
How can we now tell CakePHP that the join table should be modelized using our model? Let’s go back to the Article model definition and make a small change where we define the with model:
class Article extends AppModel { var $name = 'Article'; var $hasAndBelongsToMany = array('Tag' => array('with' => 'ArticlesTag')); }
Remember how we had to bind the Article and Tag model to the auto-with model set up with CakePHP? Looking at our own version of the join model that shouldn’t be necessary anymore, so let’s try:
$joinRecords = $this->Article->ArticlesTag->findAll(array('ArticlesTag.tag_id' => 3)); debug($joinRecords);
You can now see that the yielded results are the same as those obtained when the auto-with model was previously binded.
Anyway, that’s a short introduction to CakePHP 1.2 with models. As usual with CakePHP, what seems to be a small addition gives us a new set of exciting possibilities.
Artículo original de Mariano Iglesias

