12 min read

 

CakePHP 1.3 Application Development Cookbook

CakePHP 1.3 Application Development Cookbook

Over 70 great recipes for developing, maintaining, and deploying web applications

Introduction

This article deals with one of the most important aspects of a CakePHP application: the relationship between models, also known as model bindings or associations.

Being an integral part of any application’s logic, it is of crucial importance that we master all aspects of how model bindings can be manipulated to get the data we need, when we need it.

In order to do so, we will go through a series of recipes that will show us how to change the way bindings are fetched, what bindings and what information from a binding is returned, how to create new bindings, and how to build hierarchical data structures.

Adding Containable to all models

The Containable behavior is a part of the CakePHP core, and is probably one of the most important behaviors we have to help us deal with model bindings.

Almost all CakePHP applications will benefit from its functionalities, so in this recipe we see how to enable it for all models.

How to do it…

Create a file named app_model.php and place it in your app/ folder, with the following contents. If you already have one, make sure that either you add the actsAs property shown as follows, or that your actsAs property includes Containable.

<?php
class AppModel extends Model {
public $actsAs = array(‘Containable’);
}
?>


How it works…

The Containable behavior is nothing more and nothing less than a wrapper around the bindModel() and unbindModel() methods, defined in the CakePHP’s Model class. It is there to help us deal with the management of associations without having to go through a lengthy process of redefining all the associations when calling one of these methods, thus making our code much more readable and maintainable.

This is a very important point, because a common mistake CakePHP users make is to think that Containable is involved in the query-making process, that is, during the stage where CakePHP creates actual SQL queries to fetch data.

Containable saves us some unneeded queries, and optimizes the information that is fetched for each related model, but it will not serve as a way to change how queries are built in CakePHP.

Limiting the bindings returned in a find

This recipe shows how to use Containable to specify what related models are returned as a result of a find operation. It also shows us how to limit which fields are obtained for each association.

Getting ready

To go through this recipe we need some sample tables to work with.

  1. Create a table named families, using the following SQL statement:

    CREATE TABLE `families`(
    `id` INT UNSIGNED AUTO_INCREMENT NOT NULL,
    `name` VARCHAR(255) NOT NULL,
    PRIMARY KEY(`id`)
    );

    
    
  2. Create a table named people, using the following SQL statement:

    CREATE TABLE `people`(
    `id` INT UNSIGNED AUTO_INCREMENT NOT NULL,
    `family_id` INT UNSIGNED NOT NULL,
    `name` VARCHAR(255) NOT NULL,
    `email` VARCHAR(255) NOT NULL,
    PRIMARY KEY(`id`),
    KEY `family_id`(`family_id`),
    CONSTRAINT `people__families` FOREIGN KEY(`family_id`)
    REFERENCES `families`(`id`)
    );

    
    
  3. Create a table named profiles, using the following SQL statement:

    CREATE TABLE `profiles`(
    `id` INT UNSIGNED AUTO_INCREMENT NOT NULL,
    `person_id` INT UNSIGNED NOT NULL,
    `website` VARCHAR(255) default NULL,
    `birthdate` DATE default NULL,
    PRIMARY KEY(`id`),
    KEY `person_id`(`person_id`),
    CONSTRAINT `profiles__people` FOREIGN KEY(`person_id`)
    REFERENCES `people`(`id`)
    );

    
    
  4. Create a table named posts, using the following SQL statement:

    CREATE TABLE `posts`(
    `id` INT UNSIGNED AUTO_INCREMENT NOT NULL,
    `person_id` INT UNSIGNED NOT NULL,
    `title` VARCHAR(255) NOT NULL,
    `body` TEXT NOT NULL,
    `created` DATETIME NOT NULL,
    `modified` DATETIME NOT NULL,
    PRIMARY KEY(`id`),
    KEY `person_id`(`person_id`),
    CONSTRAINT `posts__people` FOREIGN KEY(`person_id`)
    REFERENCES `people`(`id`)
    );

    
    

    Even if you do not want to add foreign key constraints to your tables, make sure you use KEYs for each field that is a reference to a record in another table. By doing so, you will significantly improve the speed of your SQL queries when the referenced tables are joined.

  5. Add some sample data, using the following SQL statements:

    INSERT INTO `families`(`id`, `name`) VALUES
    (1, ‘The Does’);
    INSERT INTO `people`(`id`, `family_id`, `name`, `email`) VALUES
    (1, 1, ‘John Doe’, ‘[email protected]’),
    (2, 1, ‘Jane Doe’, ‘[email protected]’);
    INSERT INTO `profiles`(`person_id`,`website`,`birthdate`) VALUES
    (1, ‘http://john.example.com’, ‘1978-07-13’),
    (2, NULL, ‘1981-09-18’);
    INSERT INTO `posts`(`person_id`, `title`, `body`, `created`,
    `modified`) VALUES
    (1, ‘John’s Post 1’, ‘Body for John’s Post 1’, NOW(),
    NOW()),
    (1, ‘John’s Post 2’, ‘Body for John’s Post 2’, NOW(),
    NOW());

    
    
  6. We need Containable added to all our models.
  7. We proceed now to create the main model. Create a file named person.php and place it in your app/models folder with the following contents:

    <?php
    class Person extends AppModel {
    public $belongsTo = array(‘Family’);
    public $hasOne = array(‘Profile’);
    public $hasMany = array(‘Post’);
    }
    ?>

    
    
  8. Create the model Family in a file named family.php and place it in your app/models folder with the following contents:

    <?php
    class Family extends AppModel {
    public $hasMany = array(‘Person’);
    }
    ?>

    
    

How to do it…

When Containable is available for our models, we can add a setting to the find operation called contain. In that setting we specify, in an array-based hierarchy, the associated data we want returned. A special value contain can receive is false, or an empty array, which tells Containable not to return any associated data.

For example, to get the first Person record without associated data, we simply do:

$person = $this->Person->find(‘first’, array(
‘contain’ => false
));


Another way to tell CakePHP not to obtain related data is through the use of the recursive find setting. Setting recursive to -1 will have exactly the same effect as setting contain to false.

If we want to obtain the first Person record together with the Family they belong to, we do:

$person = $this->Person->find(‘first’, array(
‘contain’ => array(‘Family’)
));


Using our sample data, the above query will result in the following array structure:

array(
‘Person’ => array(
‘id’ => ‘1’,
‘family_id’ => ‘1’,
‘name’ => ‘John Doe’,
’email’ => ‘[email protected]
),
‘Family’ => array(
‘id’ => ‘1’,
‘name’ => ‘The Does’
)
)


Let’s say that now we also want to obtain all Post records for the person and all members in the family that Person belongs to. We would then have to do:

$person = $this->Person->find(‘first’, array(
‘contain’ => array(
‘Family.Person’
‘Post’
)
));


The above would result in the following array structure (the created and modified fields have been removed for readability):

array(
‘Person’ => array(
‘id’ => ‘1’,
‘family_id’ => ‘1’,
‘name’ => ‘John Doe’,
’email’ => ‘[email protected]
),
‘Family’ => array(
‘id’ => ‘1’,
‘name’ => ‘The Does’,
‘Person’ => array(
array(
‘id’ => ‘1’,
‘family_id’ => ‘1’,
‘name’ => ‘John Doe’,
’email’ => ‘[email protected]
),
array(
‘id’ => ‘2’,
‘family_id’ => ‘1’,
‘name’ => ‘Jane Doe’,
’email’ => ‘[email protected]
)
)
),
‘Post’ => array(
array(
‘id’ => ‘1’,
‘person_id’ => ‘1’,
‘title’ => ‘John’s Post 1’,
‘body’ => ‘Body for John’s Post 1’
),
array(
‘id’ => ‘2’,
‘person_id’ => ‘1’,
‘title’ => ‘John’s Post 2’,
‘body’ => ‘Body for John’s Post 2’
)
)
)


We can also use Containable to specify which fields from a related model we want to fetch. Using the preceding sample, let’s limit the Post fields so we only return the title and the Person records for the person’s Family, so we only return the name field. We do so by adding the name of the field to the associated model hierarchy:

$person = $this->Person->find(‘first’, array(
‘contain’ => array(
‘Family.Person.name’,
‘Post.title’
)
));


The returned data structure will then look like this:

array(
‘Person’ => array(
‘id’ => ‘1’,
‘family_id’ => ‘1’,
‘name’ => ‘John Doe’,
’email’ => ‘[email protected]
),
‘Family’ => array(
‘id’ => ‘1’,
‘name’ => ‘The Does’,
‘Person’ => array(
array(
‘name’ => ‘John Doe’,
‘family_id’ => ‘1’,
‘id’ => ‘1’
),
array(
‘name’ => ‘Jane Doe’,
‘family_id’ => ‘1’,
‘id’ => ‘2’
)
)
),
‘Post’ => array(
array(
‘title’ => ‘John’s Post 1’,
‘id’ => ‘1’,
‘person_id’ => ‘1’
),
array(
‘title’ => ‘John’s Post 2’,
‘id’ => ‘2’,
‘person_id’ => ‘1’
)
)
)


You may notice that even when we indicated specific fields for the Family => Person binding, and for the Post binding, there are some extra fields being returned. Those fields (such as family_id) are needed by CakePHP, and known as foreign key fields, to fetch the associated data, so Containable is smart enough to include them in the query.

Let us say that we also want a person’s e-mail. As there is more than a field needed, we will need to use the array notation, using the fields setting to specify the list of fields:

$person = $this->Person->find(‘first’, array(
‘contain’ => array(
‘Family’ => array(
‘Person’ => array(
‘fields’ => array(’email’, ‘name’)
)
),
‘Post.title’
)
));


How it works…

We use the contain find setting to specify what type of containment we want to use for the find operation. That containment is given as an array, where the array hierarchy mimics that of the model relationships. As the hierarchy can get deep enough to make array notation complex to deal with, the dot notation used throughout this recipe serves as an useful and more readable alternative.

If we want to refer to the model Person that belongs to the model Family, the proper contain syntax for that is Person => Family (we can also use Person.Family, which is more concise.)

We also use the fields setting to specify which fields we want fetched for a binding. We do that by specifying an array of field names as part of the binding Containable setting.

Containable looks for the contain find setting right before we issue a find operation on a model. If it finds one, it alters the model bindings to be returned by issuing unbindModel() calls on the appropriate models to unbind those relationships that are not specified in the contain find setting. It then sets the recursive find setting to the minimum value required to fetch the associated data.

Let us use a practical example to further understand this wrapping process. Using our Person model (which has a belongsTo relationship to Family, a hasOne relationship to Profile, and a hasMany relationship to Post), the following Containable based query:

$person = $this->Person->find(‘first’, array(
‘contain’ => array(‘Family.Person’)
));


or the same query using array notation:

$person = $this->Person->find(‘first’, array(
‘contain’ => array(‘Family’ => ‘Person’)
));


is equivalent to the following set of instructions, which do not use Containable, but the built in unbindModel() method available in CakePHP’s Model class:

$this->Person->unbindModel(array(
‘hasOne’ => array(‘Profile’),
‘hasMany’ => array(‘Post’)
));
$person = $this->Person->find(‘first’, array(
‘recursive’ => 2
));


Not using Containable is not only much more complicated, but can also pose a problem if we decide to alter some of our relationships. In the preceding example, if we decide to remove the Profile binding, or change its relationship type, we would have to modify the unbindModel() call. However, if we are using Containable, the same code applies, without us having to worry about such changes.

Format of the contain find parameter

We have seen how to use the contain find parameter to limit which bindings are returned after a find operation. Even when its format seems self-explanatory, let us go through another example to have a deeper understanding of Containable‘s array notation. Assume that we have the models and relationships shown in the following diagram:

CakePHP 1.3 Application Development

Transforming that diagram to something the Containable behavior understands is as simple as writing it using an array structure. For example, if we are issuing a find operation on the User model and we want to refer to the Profile relationship, a simple array(‘Profile’) expression would suffice, as the Profile model is directly related to the User model.

If we want to refer to the Comment relationship for the Article records the User is an owner of, which belongs to an Article that itself belongs to our User model, then we add another dimension to the structure, which is now represented as array(‘Article’ => ‘Comment’).

We can already deduce how the next example will look like. Assume we want to obtain the Comment together with the Profile of the User that commented on each Article. The structure will then look like: array(‘Article’ => array(‘Comment’ => array(‘User’ => ‘Profile’))).

Sometimes we want to simplify the readability, and fortunately the Containable behavior allows the above expression to be rewritten as array(‘Article.Comment.User.Profile’), which is known as dot notation. However, if you want to change other parameters to the binding, then this syntax would have to be changed to the full array-based expression.

Reset of binding changes

When you issue a find operation that uses the Containable behavior to change some of its bindings, CakePHP will reset all bindings’ changes to their original states, once the find is completed. This is what is normally wanted on most cases, but there are some scenarios where you want to keep your changes until you manually reset them, such as when you need to issue more than one find operation and have all those finds use the modified bindings.

To force our binding changes to be kept, we use the reset option in the contain find parameter, setting it to false. When we are ready to reset them, we issue a call to the resetBindings() method added by the Containable behavior to our model. The following sample code shows this procedure:

$person = $this->Person->find(‘first’, array(
‘contain’ => array(
‘reset’ => false,
‘Family’
)
));
// …
$this->Person->resetBindings();


Another way to achieve the same result is by calling the contain() method (setting its first argument to the contained bindings, and its second argument to false to indicate that we wish to keep these containments), available to all models that use Containable, issue the find (without, need to use the contain setting), and then reset the bindings:

$this->Person->contain(array(‘Family’), false);
$person = $this->Person->find(‘first’);

// …
$this->Person->resetBindings();


 

LEAVE A REPLY

Please enter your comment!
Please enter your name here