Keeping bindModel and unbindModel out of your Controllers

by TommyO
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

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.

Report

More on Tutorials

Advertising

Comments

  • coolkid posted on 05/25/11 07:40:47 AM
    this is an elegant way for removing some useless binding
  • Contrid posted on 07/21/08 05:07:26 PM
    I found a problem with the "type" key value pair in the assocs public class variable. I did not go into detail to find the actual cause, but there is a conflict between the query type (select, update, delete, etc...) and the association type in the DboSource::renderStatement, therefore it creates a MySQL error.

    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
  • simishag posted on 06/21/07 02:27:29 PM
    I'm using this code on my site, and it works fine for "hasOne", but "hasMany" generates bad SQL for Postgres. Specifically, the function fields() in dbo_postgres.php appears to miscount the actual number of fields, and has an off by 1 error that causes the bad SQL.

    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']);
    • TommyO posted on 08/07/07 10:38:09 AM
      I'm using this code on my site, and it works fine for "hasOne", but "hasMany" generates bad SQL for Postgres.
      ...
      Suggested Fix:

      In dbo_source.php, about line 980, add the following just after the start of generateAssociationQuery():...

      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?
      • pearcec posted on 11/01/07 09:46:32 PM
        I'm using this code on my site, and it works fine for "hasOne", but "hasMany" generates bad SQL for Postgres.
        ...
        Suggested Fix:

        In dbo_source.php, about line 980, add the following just after the start of generateAssociationQuery():...

        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:

        <?php 
            
        function expects($array) {
              
        $bind = array();
              foreach (
        $array as $assoc) { 
                
        $bind[$this->assocs[$assoc]['type']][$assoc] = $this->assocs[$assoc];
              }
              
        $this->bindModel($bind);
            }  
        ?>
  • mariano posted on 12/13/06 12:45:29 AM
    Hi there folks,

    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
  • cornernote posted on 12/10/06 07:09:00 PM
    Add the unbindModelAll() function to the expects() function so that you dont have any unwanted relations:

    Model Class:

    <?php 
      
    function expects($array) {
        
    $this->unbindModelAll();
        foreach (
    $array as $assoc) {
          
    $this->bindModel(array($this->assocs[$assoc]['type'] => array($assoc => $this->assocs[$assoc])));
        }
      }
    ?>
    • TommyO posted on 12/11/06 07:49:49 AM
      Bret,

      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.
  • cornernote posted on 12/10/06 07:04:10 PM
    You can define the $assocs automatically using something like this in the constructor of your app_model.php:

    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();
      }
    ?>
  • cornernote posted on 12/10/06 06:05:01 PM
    Here is what I am using for unbindAll:

    http://bakery.cakephp.org/articles/view/183
  • the_undefined posted on 12/09/06 07:42:21 AM
    I like this, it's pretty elegant ; ).
  • tariquesani posted on 12/08/06 11:54:06 PM
    This will indeed help the controller code to be much cleaner and model code to be modular now all we need is a simple unbindAll method to compliment this
login to post a comment.