Keeping bindModel and unbindModel out of your Controllers
Sometimes you need to fine-tune your associations: binding to other Models only when needed or unbinding exisiting relations to minimize the size of your result set. With a very simple method and a slight change in how you write some associations, this can be done cleanly and efficiently right from your controller.
Model::bindModel and Model::unbindModel are powerful tools that allow you to adjust associations on the fly. However, they are often misused, taking the association definition out of the Model and placing it in the Controller. Changing the requirements of a bind done in this way means going through your controllers and changing the bindModel call, often in multiple places. Using unbindModel in a controller also means every time a new association is added you may need to go back into your controllers and unbind the new associations in order to optimize your code.
Here I show you how to add a simple method to your Model classes that allows your controllers to specify binds directly, in a cleaner, more proper way.
Place the following code in app/app_model.php
Now, in your Models, define the associations in an array called $assocs. The only difference between this definition and the standard association definitions is the inclusion of a new key: 'type', which should be 'hasOne', 'hasMany', 'belongsTo' or 'hasAndBelongsToMany'.
Now, whenever you need to extend your results through an association, you can make a call to Model::expects() with the necessary values right before the query, and get the results you need.
That's it.
I still use the standard means of associations in most cases. For example, I will probably never need to show my Post without the User info, so my Post model has a $belongsTo = array('User') in its class definition. The above example of $this->Title->Post->expects(array('User')); would never be needed and was just included here to show you how the associations can work through recursion.
And now, with the way Models are loaded with CakePHP_1.1.11, this has an even greater affect. Models are only loaded when needed by a particular action.
Here I show you how to add a simple method to your Model classes that allows your controllers to specify binds directly, in a cleaner, more proper way.
Place the following code in app/app_model.php
Model Class:
<?php
class AppModel extends Model {
...
var $assocs = array();
...
function expects($array) {
foreach ($array as $assoc) {
$this->bindModel(
array($this->assocs[$assoc]['type'] =>
array($assoc => $this->assocs[$assoc])));
}
}
...
}
?>
Now, in your Models, define the associations in an array called $assocs. The only difference between this definition and the standard association definitions is the inclusion of a new key: 'type', which should be 'hasOne', 'hasMany', 'belongsTo' or 'hasAndBelongsToMany'.
Model Class:
<?php
class Title extends AppModel {
var $assocs = array(
'Book' => array(
'type' => 'belongsTo',
'className' => 'Book',
'foreignKey' => 'collection_id',
),
'Story' => array(
'type' => 'hasOne',
'className' => 'Story',
),
'Album' => array(
'type' => 'belongsTo',
'className' => 'Album',
'foreignKey' => 'collection_id',
),
'Photo' => array(
'type' => 'hasOne',
'className' => 'Photo',
),
'Post' => array(
'type' => 'hasMany',
'className' => 'Post',
'order' => 'Post.id DESC',
),
);
}
?>
Now, whenever you need to extend your results through an association, you can make a call to Model::expects() with the necessary values right before the query, and get the results you need.
Controller Class:
<?php
TitlesController extends AppController {
function list($id) {
// establish necessary associations
$this->Title->expects(array('Story', 'Post'));
$this->Title->Post->expects(array('User'));
$this->Title->recursive = 2;
$results = $this->Title->read(null, $id);
}
}
?>
That's it.
I still use the standard means of associations in most cases. For example, I will probably never need to show my Post without the User info, so my Post model has a $belongsTo = array('User') in its class definition. The above example of $this->Title->Post->expects(array('User')); would never be needed and was just included here to show you how the associations can work through recursion.
And now, with the way Models are loaded with CakePHP_1.1.11, this has an even greater affect. Models are only loaded when needed by a particular action.

To prevent this, simply change the key of the association type in your public assocs class variables to something else like "aType" or something.
Good luck
I tried adding some print_r's to fields() to see what was going on. It gets called a few times. The first call populates the model array, but a lot of the fields in my database table show up twice. A later call appears to remove the duplicates, but that leaves empty spaces in the array, which then leads to a bug later in fields() (the for loop will not work correctly).
I'd be happy to do some more debugging of this but I'm not sure where to start.
Update:
The problem is related to the way the model arrays are passed around, and the fact that in some cases, elements are removed without collapsing the array. This leads to a big problem in fields(), which uses a simple for() loop instead of a foreach.
Suggested Fix:
In dbo_source.php, about line 980, add the following just after the start of generateAssociationQuery():
$queryData['fields'] = array_merge($queryData['fields']);
JB, thanks for the note and the update. It is clear the problem is not in this little script but a bug in the Postgres dbo scripts. Did you file a bug report with the cake devs?
Okay so PhpNut suggested we build an array and pass it in. That fixed my issue described in comment #9. Essentially what I did was from action:edit I called setAction('view',$id) which calls expects a second time.
It seems ugly but:
Model Class:
<?phpfunction expects($array) {
$bind = array();
foreach ($array as $assoc) {
$bind[$this->assocs[$assoc]['type']][$assoc] = $this->assocs[$assoc];
}
$this->bindModel($bind);
}
?>
First of all, great work Tom! You hit the nail with this one as many people have to deal with limiting the information.
I've just changed the way you achieve this functionality, and came up with a solution that:
1. Doesn't require you to use a different variable to define your relations, you define them the CakePHP way.
2. You use expects() when you want to LIMIT the associated models that are returned. In fact, it is called the same way as Tom's.
3. If one doesn't specify expects(), you get CakePHP's normal response. If you call expects() with no parameters, it is the same as an ubindAll().
4. expects() makes use of Model::unbindModel() to unbind all non-specified models. This way it is CakePHP-upgrade safe.
I just wrote a tutorial right here on the bakery, still pending approval, but here's the link:
An improvement to unbindModel on model side
Model Class:
<?phpfunction expects($array) {
$this->unbindModelAll();
foreach ($array as $assoc) {
$this->bindModel(array($this->assocs[$assoc]['type'] => array($assoc => $this->assocs[$assoc])));
}
}
?>
Your suggestion is nice because it allows you to keep the associations written the way CakePHP expects. The problem I have here is that you end up with everything bound, then you unbindAll just to rebind the way you'd like, and on every call. Seems like overkill to me. Also requires now three new Model actions instead of one, and multiple copies of what is essentially the same association information.
Also, if you put unbindAll in expects() you need to be sure to pass all of your expectations in at the same time, as each call would result in unbinding the previous call.
So it's a personal choice: stick closer to conventions with more overhead and more model methods, or buck convention a little for a somewhat slicker process.
Model Class:
<?php
function __construct()
{
$this->assocs = array();
foreach ($this->belongsTo as $model=>$info)
{
$this->assocs[$model] = $info;
$this->assocs[$model]['type'] = 'belongsTo';
}
foreach ($this->hasOne as $model=>$info)
{
$this->assocs[$model] = $info;
$this->assocs[$model]['type'] = 'hasOne';
}
foreach ($this->hasMany as $model=>$info)
{
$this->assocs[$model] = $info;
$this->assocs[$model]['type'] = 'hasMany';
}
foreach ($this->hasAndBelongsToMany as $model=>$info)
{
$this->assocs[$model] = $info;
$this->assocs[$model]['type'] = 'hasAndBelongsToMany';
}
parent::__construct();
}
?>
http://bakery.cakephp.org/articles/view/183
Gives an "Invalid Article" error