6.2 Containable
A new addition to the CakePHP core is the ContainableBehavior. This model behavior allows you to filter and limit model find operations. Using Containable will help you cut down on needless wear and tear on your database, increasing the speed and overall performance of your application. The class will also help you search and filter your data for your users in a clean and consistent way.
To use the new behavior, you can add it to the $actsAs property of your model:
class Post extends AppModel {
var $actsAs = array('Containable');
}
class Post extends AppModel {var $actsAs = array('Containable');}
You can also attach the behavior on the fly:
$this->Post->Behaviors->attach('Containable');
$this->Post->Behaviors->attach('Containable');
To see how Containable works, let's look at a few examples. First, we'll start off with a find() call on a model named Post. Let's say that Post hasMany Comment, and Post hasAndBelongsToMany Tag. The amount of data fetched in a normal find() call is rather extensive:
debug($this->Post->find('all'));
debug($this->Post->find('all'));
[0] => Array
(
[Post] => Array
(
[id] => 1
[title] => First article
[content] => aaa
[created] => 2008-05-18 00:00:00
)
[Comment] => Array
(
[0] => Array
(
[id] => 1
[post_id] => 1
[author] => Daniel
[email] => dan@example.com
[website] => http://example.com
[comment] => First comment
[created] => 2008-05-18 00:00:00
)
[1] => Array
(
[id] => 2
[post_id] => 1
[author] => Sam
[email] => sam@example.net
[website] => http://example.net
[comment] => Second comment
[created] => 2008-05-18 00:00:00
)
)
[Tag] => Array
(
[0] => Array
(
[id] => 1
[name] => A
)
[1] => Array
(
[id] => 2
[name] => B
)
)
)
[1] => Array
(
[Post] => Array
(...
For some interfaces in your application, you may not need that much information from the Post model. One thing the ContainableBehavior does is help you cut down on what find() returns.
For example, to get only the post-related information, you can do the following:
$this->Post->contain();
$this->Post->find('all');
$this->Post->contain();$this->Post->find('all');
$this->Post->find('all', array('contain' => false));
$this->Post->find('all', array('contain' => false));
Having done that, you end up with something a lot more concise:
[0] => Array
(
[Post] => Array
(
[id] => 1
[title] => First article
[content] => aaa
[created] => 2008-05-18 00:00:00
)
)
[1] => Array
(
[Post] => Array
(
[id] => 2
[title] => Second article
[content] => bbb
[created] => 2008-05-19 00:00:00
)
)
This sort of help isn't new: in fact, you can do that without the ContainableBehavior doing something like this:
$this->Post->recursive = -1;
$this->Post->find('all');
$this->Post->recursive = -1;$this->Post->find('all');
$this->Post->contain('Tag');
$this->Post->find('all');
$this->Post->contain('Tag');$this->Post->find('all');
$this->Post->find('all', array('contain' => 'Tag'));
$this->Post->find('all', array('contain' => 'Tag'));
Without Containable, you'd end up needing to use the unbindModel() method of the model, multiple times if you're paring off multiple models. Containable creates a cleaner way to accomplish this same task.
Containable also goes a step deeper: you can filter the data of the associated models. If you look at the results of the original find() call, notice the author field in the Comment model. If you are interested in the posts and the names of the comment authors—and nothing else—you could do something like the following:
$this->Post->contain('Comment.author');
$this->Post->find('all');
//or..
$this->Post->find('all', array('contain' => 'Comment.author'));
$this->Post->contain('Comment.author');$this->Post->find('all');//or..$this->Post->find('all', array('contain' => 'Comment.author'));
Here, we've told Containable to give us our post information, and just the author field of the associated Comment model. The output of the find call might look something like this:
[0] => Array
(
[Post] => Array
(
[id] => 1
[title] => First article
[content] => aaa
[created] => 2008-05-18 00:00:00
)
[Comment] => Array
(
[0] => Array
(
[author] => Daniel
[post_id] => 1
)
[1] => Array
(
[author] => Sam
[post_id] => 1
)
)
)
[1] => Array
(...
As you can see, the Comment arrays only contain the author field (plus the post_id which is needed by CakePHP to map the results).
You can also filter the associated Comment data by specifying a condition:
$this->Post->contain('Comment.author = "Daniel"');
$this->Post->find('all');
//or...
$this->Post->find('all', array('contain' => 'Comment.author = "Daniel"'));
$this->Post->contain('Comment.author = "Daniel"');$this->Post->find('all');//or...$this->Post->find('all', array('contain' => 'Comment.author = "Daniel"'));
This gives us a result that gives us posts with comments authored by Daniel:
[0] => Array
(
[Post] => Array
(
[id] => 1
[title] => First article
[content] => aaa
[created] => 2008-05-18 00:00:00
)
[Comment] => Array
(
[0] => Array
(
[id] => 1
[post_id] => 1
[author] => Daniel
[email] => dan@example.com
[website] => http://example.com
[comment] => First comment
[created] => 2008-05-18 00:00:00
)
)
)
Additional filtering can be performed by supplying the standard Model->find() options:
$this->Post->find('all', array('contain' => array(
'Comment' => array(
'conditions' => array('Comment.author =' => "Daniel"),
'order' => 'Comment.created DESC'
)
)));
$this->Post->find('all', array('contain' => array('Comment' => array('conditions' => array('Comment.author =' => "Daniel"),'order' => 'Comment.created DESC'))));
Here's an example of using the Containble behavior when you've got deep and complex model relationships.
Let's consider the following model associations:
User->Profile User->Account->AccountSummary User->Post->PostAttachment->PostAttachmentHistory->HistoryNotes User->Post->Tag
This is how we retrieve the above associations with Containable:
$this->User->find('all', array(
'contain'=>array(
'Profile',
'Account' => array(
'AccountSummary'
),
'Post' => array(
'PostAttachment' => array(
'fields' => array('id', 'name'),
'PostAttachmentHistory' => array(
'HistoryNotes' => array(
'fields' => array('id', 'note')
)
)
),
'Tag' => array(
'conditions' => array('Tag.name LIKE' => '%happy%')
)
)
)
));
$this->User->find('all', array('contain'=>array('Profile','Account' => array('AccountSummary'),'Post' => array('PostAttachment' => array('fields' => array('id', 'name'),'PostAttachmentHistory' => array('HistoryNotes' => array('fields' => array('id', 'note')))),'Tag' => array('conditions' => array('Tag.name LIKE' => '%happy%'))))));
Keep in mind that 'contain' key is only used once in the main model, you don't use 'contain' again for related models
When using 'fields' and 'contain' options - be careful to include all foreign keys that your query directly or indirectly requires. Please also note that because Containable must to be attached to all models used in containment, you may consider attaching it to your AppModel.
Here's an example of how to contain associations when paginating.
$this->paginate['User'] = array(
'contain' => array('Profile', 'Account'),
'order' => 'User.username'
);
$users = $this->paginate('User');
$this->paginate['User'] = array('contain' => array('Profile', 'Account'),'order' => 'User.username');$users = $this->paginate('User');
