Archive: September, 2008

Example of CakePHP's Containable for deep model bindings

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

It’s very possible that each of those models has other associations, so we need to use Containable to retrieve just the models we need (we’ll also specify fields and conditions for some models just to make it more “fun”). Remember, that Containable behavior has to be attached to all of the models in our relationship, therefore to make your life easier you should probably attach the behavior to all models in App Model…

Our find() call:

$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%'))
       )
    )
));

Crazy, huh?

Some things to keep in mind:

  • ‘contain’ key is only used once in the main model, you don’t use ‘contain’ again for related models
  • Deep binding is done by using the ‘contain’=>array(‘ModelA’=>array(‘ModelB’=>array(‘ModelC’…
  • Each model can have it’s own set of find() options, by using ‘contain’=>array(‘ModelA’=>array(‘fields’=>…, ‘conditions’=> …, ‘ModelB’=>array(‘fields’=>…etc.
  • As rafaelbandeira3 pointed out, ‘fields’ and ‘conditions’ are the only keys that will be of use. ‘limit’, ‘recursive’, ‘group’ and ‘order’ will not produce desired results for any of the “contained” models.
  • Of course you can still apply them to the main model (i.e. User). Well, except ‘recursive’, which is not needed since Containable handles the associations for you

Example of many nested conditions in CakePHP's find()

Just a quick example on how you can use deep, complex find conditions with OR, AND and NOT arrays in one shot…

We need to get a list of all companies, where:
Company.name is either ‘Future Holdings’ OR ‘Steel Mega Works’ AND we need to ensure that Company.status is either ‘active’ OR NOT ‘inactive’ OR ‘suspended’…

Here’s how you can accomplish this in cake:

$conditions = array(

   'OR' => array(
      array('Company.name' => 'Future Holdings'),
      array('Company.name' => 'Steel Mega Works')
   ),

   'AND' => array(
      array(

         'OR'=>array(
            array('Company.status' => 'active'),

            'NOT'=>array(
               array('Company.status'=> array('inactive', 'suspended'))
            )
         )
     )
   )
);

Which produces the following SQL:

SELECT `Company`.`id`, `Company`.`name`, `Company`.`description`, `Company`.`location`, `Company`.`created`, `Company`.`status`, `Company`.`size`

FROM
   `companies` AS `Company`
WHERE
   ((`Company`.`name` = 'Future Holdings')
   OR
   (`Company`.`name` = 'Steel Mega Works'))
AND
   ((`Company`.`status` = 'active')
   OR (NOT (`Company`.`status` IN ('inactive', 'suspended'))))

find(‘list’) with three (or combined) fields

Update 01/07/2010: Good news everyone, for those switching over to cake 1.3, there is a great new feature:
http://book.cakephp.org/view/1608/Virtual-fields
————–

How about a little trick to extend the find(‘list’) functionality?..

Let’s say we need to display a list of users, but instead of just User.id and User.name we need to have User.id, as well as User.name and User.email combined. Unfortunately find(‘list’) doesn’t have that type of functionality out-of-the-box. Well, that’s OK, we’ll make our own…

Before I continue, I’d like to thank grigri for a very creative way to use Set::combine() as well as cakebaker for his tips on extending the find() method functionality.

Alright, let’s add this to our app model:


 function find($type, $options = array()) {
        switch ($type) {
            case 'superlist':
                if(!isset($options['fields']) || count($options['fields']) < 3) {
                    return parent::find('list', $options);
                }

                if(!isset($options['separator'])) {
                    $options['separator'] = ' ';
                }

                $options['recursive'] = -1;              
                $list = parent::find('all', $options);

                for($i = 1; $i <= 2; $i++) {
                    $field[$i] = str_replace($this->alias.'.', '', $options['fields'][$i]);               
                }           

                return Set::combine($list, '{n}.'.$this->alias.'.'.$this->primaryKey,
                                 array('%s'.$options['separator'].'%s',
                                       '{n}.'.$this->alias.'.'.$field[1],
                                       '{n}.'.$this->alias.'.'.$field[2]));
            break;                      

            default:              
                return parent::find($type, $options);
            break;
        }
    }

Now all we have to do in our controller is:

$this->User->find('superlist', array('fields'=>array('User.id',
                                                                  'User.name',
                                                                  'User.email'),
                                              'separator'=>' * '));

Which ultimately gives us something like:

...
<option value="1">Bob * bob@hotmail.com</option>
...

Let’s quickly go over what we’ve done…

We overrode the default Model::find() method by extending it with a new find type called ‘superlist’. We also allow a new key ‘separator’ to specify the delimiter between the fields (for the sake of the example I chose a ‘ * ‘).

The code is not very complicated, so I hope you can figure out what’s going just by examining our custom find() function… but just a few points to help you along:

  • Specify at least 3 fields, or the method will default to the regular find(‘list’)
  • Field order is important
  • We need to specify the primary key field to extract the appropriate value
  • Model’s primary key field is always used for the option value
  • Fields can be specified as ‘fieldName’ or ‘Model.fieldName’
  • If you do not specify the ‘separator’ key, it will default to a space character

Keep in mind that this is more of an exercise, and you can achieve the same results without any overrides, but this certainly helps to keep your controllers nice, clean and skinny.