Working with Complex Associations using CakePHP

Step-by-step introduction to rapid web development using the open-source MVC CakePHP framework

Defining Many-To-Many Relationship in Models

In the previous article in this series on Working with Simple Associations using CakePHP, we assumed that a book can have only one author. But in real life scenario, a book may also have more than one author. In that case, the relation between authors and books is many-to-many. We are now going to see how to define associations for a many-to-many relation. We will modify our existing code-base that we were working on in the previous article to set up the associations needed to represent a many-to-many relation.

Time for Action: Defining Many-To-Many Relation

  1. Empty the database tables:
      TRUNCATE TABLE `authors`;
      TRUNCATE TABLE `books`;
  2. Remove the author_id field from the books table:
      ALTER TABLE `books` DROP `author_id`
  3. Create a new table, authors_books:;
      CREATE TABLE `authors_books` (
      `author_id` INT NOT NULL ,
      `book_id` INT NOT NULL
  4. Modify the Author (/app/models/author.php) model:
      <?php
      class Author extends AppModel
      {
      var $name = 'Author';
      var $hasAndBelongsToMany = 'Book';
      }
      ?>
  5. Modify the Book (/app/models/book.php) model:
      <?php
      class Book extends AppModel
      {
      var $name = 'Book';
      var $hasAndBelongsToMany = 'Author';
      }
      ?>
  6. Modify the AuthorsController (/app/controllers/authors_controller.php):
      <?php
      class AuthorsController extends AppController {
      var $name = 'Authors';
      var $scaffold;
      }
      ?>
  7. Modify the BooksController (/app/controllers/books_controller.php):
      <?php
      class BooksController extends AppController {
      var $name = 'Books';
      var $scaffold;
      }
      ?>
  8. Now, visit the following URLs and add some test data into the system:http://localhost/relationship/authors/
    and http://localhost/relationship/books/

What Just Happened?

We first emptied the database and then dropped the field author_id from the books table. Then we added a new join table authors_books that will be used to establish a many-to-many relation between authors and books. The following diagram shows how a join table relates two tables in many-to-many relation:

CakePHP Application Development

In a many-to-many relation, one record of any of the tables can be related to multiple records of the other table. To establish this link, a join table is used—a join table contains two fields to hold the primary-keys of both of the records in relation.

CakePHP has certain conventions for naming a join table—join tables should be named after the tables in relation, in alphabetical order, with underscores in between. The join table between authors and books tables should be named authors_books, not books_authors. Also by Cake convention, the default value for the foreign keys used in the join table must be underscored, singular name of the models in relation, suffixed with _id.

After creating the join table, we defined associations in the models, so that our models also know about the new relationship that they have. We added hasAndBelongsToMany (HABTM) associations in both of the models. HABTM is a special type of association used to define a many-to-many relation in models. Both the models have HABTM associations to define the many-to-many relationship from both ends. After defining the associations in the models, we created two controllers for these two models and put in scaffolding in them to see the association working.

We could also use an array to set up the HABTM association in the models. Following code segment shows how to use an array for setting up an HABTM association between authors and books in the Author model:

var $hasAndBelongsToMany = array(
'Book' =>
array(
'className' => 'Book',
'joinTable' => 'authors_books',
'foreignKey' => 'author_id',
'associationForeignKey' => 'book_id'
)
);

Like, simple relationships, we can also override default association characteristics by adding/modifying key/value pairs in the associative array. The foreignKey key/value pair holds the name of the foreign-key found in the current model—default is underscored, singular name of the current model suffixed with _id. Whereas, associationForeignKey key/value pair holds the foreign-key name found in the corresponding table of the other model—default is underscored, singular name of the associated model suffixed with _id. We can also have conditions, fields, and order key/value pairs to customize the relationship in more detail.

Retrieving Related Model Data in Many-To-Many Relation

Like one-to-one and one-to-many relations, once the associations are defined, CakePHP will automatically fetch the related data in many-to-many relation.

Time for Action: Retrieving Related Model Data

  1. Take out scaffolding from both of the controllers—AuthorsController (/app/controllers/authors_controller.php) and BooksController (/app/controllers/books_controller.php).
  2. Add an index() action inside the AuthorsController (/app/controllers/authors_controller.php), like the following:
      <?php
      class AuthorsController extends AppController {
      var $name = 'Authors';
      function index() {
      $this->Author->recursive = 1;
      $authors = $this->Author->find('all');
      $this->set('authors', $authors);
      }
      }
      ?>
  3. Create a view file for the /authors/index action (/app/views/authors/index.ctp):
      <?php foreach($authors as $author): ?>
      <h2><?php echo $author['Author']['name'] ?></h2>
      <hr />
      <h3>Book(s):</h3>
      <ul>
      <?php foreach($author['Book'] as $book): ?>
      <li><?php echo $book['title'] ?></li>
      <?php endforeach; ?>
      </ul>
      <?php endforeach; ?>
  4. Write down the following code inside the BooksController (/app/controllers/books_controller.php):
      <?php
      class BooksController extends AppController {
      var $name = 'Books';
      function index() {
      $this->Book->recursive = 1;
      $books = $this->Book->find('all');
      $this->set('books', $books);
      }
      }
      ?>
  5. Create a view file for the action /books/index (/app/views/books/index.ctp):
      <?php foreach($books as $book): ?>
      <h2><?php echo $book['Book']['title'] ?></h2>
      <hr />
      <h3>Author(s):</h3>
      <ul>
      <?php foreach($book['Author'] as $author): ?>
      <li><?php echo $author['name'] ?></li>
      <?php endforeach; ?>
      </ul>
      <?php endforeach; ?>
  6. Now, visit the following URLs:http://localhost/relationship/authors/
    http://localhost/relationship/books/

What Just Happened?

In both of the models, we first set the value of $recursive attributes to 1 and then we called the respective models find('all') functions. So, these subsequent find('all') operations return all associated model data that are related directly to the respective models. These returned results of the find('all') requests are then passed to the corresponding view files. In the view files, we looped through the returned results and printed out the models and their related data.

In the BooksController, this returned data from find('all') is stored in a variable $books. This find('all') returns an array of books and every element of that array contains information about one book and its related authors.

Array
(
[0] => Array
(
[Book] => Array
(
[id] => 1
[title] => Book Title
...
)
[Author] => Array
(
[0] => Array
(
[id] => 1
[name] => Author Name
...
)
[1] => Array
(
[id] => 3
... 54 54
...
...
)

Same for the Author model, the returned data is an array of authors. Every element of that array contains two arrays: one contains the author information and the other contains an array of books related to this author. These arrays are very much like what we got from a find('all') call in case of the hasMany association.

CakePHP Application Development

Saving Related Model Data in Many-To-Many Relation

Like the simple relations, saving related data in many-to-many relation is also fairly easy. We will now modify our code to add functionality to save a book and relate multiple authors with this book while saving.

Time for Action: Saving Related Model Data

  1. Add a controller action add() in BooksController (/app/controllers/books_controller.php),
      <?php
      class BooksController extends AppController {
      var $name = 'Books';
      var $helpers = array( 'Form' );
      function index() {
      $this->Book->recursive = 1;
      $books = $this->Book->find('all');
      $this->set('books', $books);
      }
      function add() {
      if (!empty($this->data)) {
      $this->Book->create();
      $this->Book->save($this->data);
      $this->redirect(array('action'=>'index'), null, true);
      }
      $authors = $this->Book->Author->generateList();
      $this->set('authors', $authors);
      }
      }
      ?>
  2. Create a view with a form, for the /books/add/ action (/app/views/books/add.ctp).
      <?php echo $form->create('Book');?>
      <fieldset>
      <legend>Add New Book</legend>
      <?php
      echo $form->input('isbn');
      echo $form->input('title');
      echo $form->input('description');
      echo $form->input('Author');
      ?>
      </fieldset>
  3. Point your browser to the following URL and add a book:
    http://localhost/relationship/books/add

What Just Happened?

Author model's generateList() function is used to get a list of all the authors. This list is passed to the view through $authors variable. A form is created in the view with input fields to take book information. In this form, we also created a select list to take all the related authors' names—multiple authors can be selected from this list.

Specifically, the line $form->input('Author'); creates the select list and automatically. In the input method of the form helper, the related model name: Author is supplied as parameter. It tells the method that it has to create a multiple select list from an array $authors.

When the form is submitted, the Book model's save() function automatically gets all the related authors' information along with the book information. The save() function first saves the book information and then creates entries in the join table to establish the relations between the book and the selected authors.

Deleting Associated Data

In case of one-to-one and one-to-many relation, to turn on the cascade delete option, we have to set the dependent key to true in the association definition array of the model. To enable the cascade delete option in Author model for its association with the Book model, we would add a dependent key in the association definition array and set its value to true like the following:

var $hasMany = array(  
      'Book' => array(  
          'className'     => 'Book',  
          'dependent'=> true  
      )  
  );

When the cascade delete is turned on, if we call the delete() method of the Author model to delete an author, then all the associated Book records related to that particular author are also deleted. To avoid cascade delete even when dependent is set to true, we can make use of the second parameter $cascade of the delete() method—which by default has a true value. To turn off the cascade delete on the fly, we just have to set this second parameter to false.

In case of many-to-many relation, the HABTM association logically does not have any such key/value pair to turn on/off the cascade delete. It is quite rational as in case of HABTM association, we will not like to delete a book if any of its related authors is deleted. Rather, we will want to delete the author and the records in the join table that relates some books with that author. We don't even have to think about that, Cake will automatically delete all such join table entries in delete() operations.

Changing Association On The Fly

Let's assume that we have two associations in the Author model at the same time:

  • hasMany association with Book model.
  • hasOne association with Profile model.

Now, to fetch out all authors and their profile information, we would write the following code:

$this->Author->recursive = 1;
$authors = $this->Author->find('all');

But this find('all') request will also return all associated book information as both Profile and Book models are on the first level of recursion. We can though ignore all Book model data and only use the User and Profile model data. But it can be costly to performance. We need to somehow destroy the association between the Author and Book model before the find('all') call, so that only the needed data are returned.

The unbindAll() method becomes handy in such situations. The following code snippet shows how we can temporarily remove the association between the Author and the Book model.

$this->Author->recursive = 1;
$this->Author->unbindModel(  
      array(
'hasMany' => array(  
              'Book' => array(
  
                  'className' => 'Book'
  
              )
  
          )
  
      )
  
  );
$authors = $this->Author->find('all');

Sometimes, we may want to temporarily add an association. If we want to temporarily add a hasMany association between the Author and the Tutorial model, we can do it using the model method bindModel() like the following:

$this->Author->bindModel(  
      array(
'hasMany' => array(  
              'Tutorial' => array(
  
                  'className' => 'Tutorial'  
              )  
          )  
      )  
 );

Creating or destroying associations using bindModel()and unbindModel() only works for the subsequent model operation unless the second parameter of these functions has been set to true. If the second parameter has been set to true, the bind remains in place for the remainder of the request.

Books to Consider

comments powered by Disqus