A little while back, I create a tutorial on how to setup a HABTM relationship in CakePHP and gave a basic example. However, I never explained how to find the data. This short tutorial will explain how to find your data in a HABTM relationship. The original tutorial involves recipes and ingredients. So we have the following models:
- Recipe (hasAndBelongsToMany “Ingredient”)
- Ingredient (hasAndBelongsToMany “Recipe”)
If you have not seen the original tutorial, you can click here to view the original tutorial.
Step 1: Make your Models Containable
Any model that has an HABTM relationship should use the Containable Behavior. The conatinable behaior allows CakePHP to know that the model “contains” another model. In the case of Recipes, they contain Ingredients. You should make the following modification in Recipe.php and Ingredient.php
// this should done in both the Recipe and Ingredient models public $actsAs = array('Containable');
Step 2: Update your Find Statements
We are now ready to use the contain statement in our find statements. Whenever you use the contain() operator, you must set $recursive to -1 so that you are limited to only the models that we are interested in finding. Let’s go though an example. The following example assumes that we have a recipe with ID 9, lets imagine it’s a pizza. We can imagine that it has ingredients like pepperoni, cheese and onions. We can run a find statement to find all the ingredients that belong to this recipe.
$this->Ingredient->Recipe->recursive = -1; $ingredients = $this->Ingredient->Recipe->find('first', array( 'conditions' => array( 'Recipe.id' => 9 /* assume ID 9 is a pizza */ ), 'contain' => array( 'Ingredient' => array( 'order' => 'Ingredient.name ASC' ) ) ));
With this example, we will get all the ingredients that belong to the pizza, ordered by name. The use of the contain statement lets us know exactly what CakePHP should return. Within the contain statement, we can add the various statements. For example, let’s assume that we have certain conditions that we want for the returned ingredients, then we can add the condition in our contain statement. As a quick example, assume that we only want ingredients that are active and we only want the first 4, then we can modify our find statement to include that condition, like in the example below:
$this->Ingredient->Recipe->recursive = -1; $ingredients = $this->Ingredient->Recipe->find('all', array( 'conditions' => array( 'Recipe.id' => 9 /* assume ID 9 is a pizza */ ), 'contain' => array( 'Ingredient' => array( 'order' => 'Ingredient.name ASC', 'conditions' => array( "Ingredient.status" => "1", ), 'limit' => 4 ) ) ));
The following is a sample output
Array (  => Array ( [Ingredient] => Array (  => Array ( [id] => 1 [name] => Cheese )  => Array ( [id] => 2 [name] => Tomato )  => Array ( [id] => 4 [name] => Onion )  => Array ( [id] => 8 [name] => Pepperoni ) ) [Recipe] => Array ( [id] => 9 [name] => Pizza [created] => 2012-12-11 00:00:00 [modified] => 2012-12-11 00:00:00 ) ) )
What about pagination?
The examples above will work for regular find() statements in CakePHP. However, it will not work for pagination. Pagination must be treated separately for HABTM relationships. I honestly think that CakePHP’s documentation could do a better job of explaining how to do this… To paginate on a HABTM relationship, you need to temporarily bind the ‘hasOne’ join model to the model that you want to paginate. Lets keep working on our original example of the pizza with multiple ingredients. It is quite possible that the same ingredients are used for other recipes. Since you don’t want to display the same ingredient over and over, we will update our pagination query to group by ingredient. Then we will bind our Ingredient model to our IngredeintsRecipes model that was created through our join table. Here is the sample code for
$this->paginate = array( 'limit' => 4, 'group'=>'Ingredient.id', 'conditions' => array( "Ingredient.status" => "1", ) ); $this->Ingredient->bindModel(array('hasOne'=>array('IngredientsRecipes')), false); $ingredients = $this->paginate('Ingredient', array('IngredientsRecipes.recipe_id'=> 9 /* assume ID 9 is a pizza */));
And there you go. With this, you should be able to find anything with HABTM models like a boss!