The following article has been coded, tested and submitted to the Cake 1.3 Book by Roxxor but has yet to be published.
It is sometimes desirable to store additional data with a many to many association. Consider the following
Student hasAndBelongsToMany Course Course hasAndBelongsToMany Student
In other words, a Student can take many Courses and a Course can be taken my many Students. This is a simple many to many association demanding a table such as this
id | student_id | course_id
Now what if we want to store the number of days that were attended by the student on the course and their final grade? The table we’d want would be
id | student_id | course_id | days_attended | grade
The trouble is, hasAndBelongsToMany will not support this type of scenario because when hasAndBelongsToMany associations are saved, the association is deleted first. You would lose the extra data in the columns as it is not replaced in the new insert.
The way to implement our requirement is to use a join model, otherwise known (in Rails) as a hasMany through association. That is, the association is a model itself. So, we can create a new model CourseMembership. Take a look at the following models.
student.php
class Student extends AppModel
{
public $hasMany = array(
'CourseMembership'
);
public $validate = array(
'first_name' => array(
'rule' => 'notEmpty',
'message' => 'A first name is required'
),
'last_name' => array(
'rule' => 'notEmpty',
'message' => 'A last name is required'
)
);
}
course.php
class Course extends AppModel
{
public $hasMany = array(
'CourseMembership'
);
public $validate = array(
'name' => array(
'rule' => 'notEmpty',
'message' => 'A course name is required'
)
);
}
course_membership.php
class CourseMembership extends AppModel
{
public $belongsTo = array(
'Student', 'Course'
);
public $validate = array(
'days_attended' => array(
'rule' => 'numeric',
'message' => 'Enter the number of days the student attended'
),
'grade' => array(
'rule' => 'notEmpty',
'message' => 'Select the grade the student received'
)
);
}
The CourseMembership join model uniquely identifies a given Student’s participation on a Course in addition to extra meta-information.
Working with join model data
Now that the models have been defined, let’s see how we can save all of this. Let’s say the Head of Cake School has asked us the developer to write an application that allows him to log a student’s attendance on a course with days attended and grade. Take a look at the following code.
controllers/course_membership_controller.php
class CourseMembershipsController extends AppController
{
public $uses = array('CourseMembership');
public function index() {
$this->set('course_memberships_list', $this->CourseMembership->find('all'));
}
public function add() {
if (! empty($this->data)) {
if ($this->CourseMembership->saveAll(
$this->data, array('validate' => 'first'))) {
$this->redirect(array('action' => 'index'));
}
}
}
}
views/course_memberships/add.ctp
echo $form->create('CourseMembership');
echo $form->input('Student.first_name');
echo $form->input('Student.last_name');
echo $form->input('Course.name');
echo $form->input('CourseMembership.days_attended');
echo $form->input('CourseMembership.grade');
echo '';
echo $form->end();
You can see that the form uses the form helper’s dot notation to build up the data array for the controller’s save which looks a bit like this when submitted.
Array ( [Student] => Array ( [first_name] => Joe [last_name] => Bloggs ) [Course] => Array ( [name] => Cake ) [CourseMembership] => Array ( [days_attended] => 5 [grade] => A ) )
Cake will happily be able to save the lot together and assigning the foreign keys of the Student and Course into CourseMembership with a saveAll call with this data structure. If we run the index action of our CourseMembershipsController the data structure received now from a find(‘all’) is:
Array ( [0] => Array ( [CourseMembership] => Array ( [id] => 1 [student_id] => 1 [course_id] => 1 [days_attended] => 5 [grade] => A ) [Student] => Array ( [id] => 1 [first_name] => Joe [last_name] => Bloggs ) [Course] => Array ( [id] => 1 [name] => Cake ) ) )
There are of course many ways to work with a join model. The version above assumes you want to save everything at-once. There will be cases where you want to create the Student and Course independently and at a later point associate the two together with a CourseMembership. So you might have a form that allows selection of existing students and courses from picklists or ID entry and then the two meta-fields for the CourseMembership, e.g.
views/course_memberships/add.ctp
echo $form->create('CourseMembership');
echo $form->input('Student.id', array('type' => 'text', 'label' => 'Student ID', 'default' => 1));
echo $form->input('Course.id', array('type' => 'text', 'label' => 'Course ID', 'default' => 1));
echo $form->input('CourseMembership.days_attended');
echo $form->input('CourseMembership.grade');
echo '';
echo $form->end();
$this->data when POSTed Array ( [Student] => Array ( [id] => 1 ) [Course] => Array ( [id] => 1 ) [CourseMembership] => Array ( [days_attended] => 10 [grade] => 5 ) )
Again Cake is good to us and pulls the Student id and Course id into the CourseMembership with the saveAll.
Join models are pretty useful things to be able to use and Cake makes it easy to do so with its built-in hasMany and belongsTo associations and saveAll feature.

