CakePHP join models – or hasMany through

Filed under: CakePHP — Allistair @ 8:41 pm

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.

Wrapping text in FPDF table cells

Filed under: PHP — Allistair @ 11:37 am

This article aims to help those who like myself wondered how to make text wrap within a table cell using FPDF to generate a the PDF. A few posts around the web seem to have missed the point that wrapping comes out of the box in FPDF using the MultiCell function. However, it’s just a tiny bit tricky but not prohibitively so. So, this is what we are aiming for – an invoice.

FPDF Invoice

There are 2 examples of wrapping here. Firstly the client address is wrapped, and secondly the first row of the line item data has a wrapped description.

View the PDF here

In both cases the MultiCell in-built function is used. Before I had this solution working I was using the Cell function. In that case, long text simply continued through to the next cell ‘Quantity’. In the case of Cell, you can simply output a series of calls to the function with a cell width and so fourth and the cells will line up against each other on the same line.

When I switched to MultiCell for the first cell of each row only, I found that it would cause all subsequent cells in the same row to wrap to the next line. Therefore my solution was to use the SetXY function to reposition the ‘cursor’ back to where the 2nd cell of the current row would normally be, and then proceed to use Cell calls.

In order to do that it’s a case of marking the current X and Y coordinates of the cursor prior to entering the line item loop and calling SetXY just after the 1st cell MultiCell function call. That solved the line wrapping issue and my text wrapped inside the first cell – great.

The second issue however is that the MultiCell is arbitrarily high depending on the amount of text wrapping going on. I had been provided a static height to my Cell calls as as such they were now out of line with the first column, that is, the text and border lines were all out of sync with the 1st column.

To solve that problem it was a case of grabbing the Y coordinate of the cursor before and immediately after the MultiCell call, finding the difference to ascertain the height of the cell, and then using this to size the height of the remaining Cell calls on the row.

Source Code

Please feel free to download the source code for this article which builds the invoice PDF as described.

The source code is provided as a ZIP file with index.php and InvoicePDF.class.php. You will need to obtain the FPDF library yourself and modify the index.php include file locations.

Best of luck!

Populate PHP Object Attributes from Database Column Names Dynamically

Filed under: OOP,PHP,Web Development — Allistair @ 9:27 pm

If you like coding PHP the OOP way then you will often find yourself in the arduous position of needing to populate your object attributes (member variables) from database row data. And you do this again and again and again, and you find it all rather repetitive, which it is. And you don’t fancy looking at an object relational mapping solution for now, but promise yourself you will “Another Time”.

Here it is;

class MyObject {
  function __construct($data = NULL) {
    if ($data) {
      foreach ($data as $ak => $av) {
        eval("\$this->" . $this->db2Camel($ak) . " = '{$av}';");
      }
    }
  }

  function db2Camel($str) {
    $str = strtolower($str);
    $parts = explode('_', $str);
    for ($i = 0; $i < count($parts); $i++) {
      if ($i == 0) {
        $parts[$i] = strtolower($parts[$i]);
      } else {
        $parts[$i] = ucfirst($parts[$i]);
      }
    }
    return implode('', $parts);
  }
}

This code relies on a number of assumptions. These are, but are probably not limited to;

  1. The data parameter to the constructor is an associative array, depicting a row from the database.
  2. That the database column names are lowercase, and separated by underscores, e.g. the_column_name
  3. Your attributes will be in camel casing as governed by the db2Camel function
  4. Your attributes will be public as they do not benefit from an explicit declaration of access scope such as private or protected.
  5. You will access the attributes direct on the object, not through get accessors, e.g. $obj->theAttribute
  6. A programmer would not know the attributes that an object contained from reading the code, they would need to understand the constructor's function and consult the database column names to infer the attribute names.

If you are happy with all those assumptions, then this code might just prove quite handy in expediating the process of population of PHP object attributes dynamically from a database.