Schneimi’s Dev Weblog


Behaviors on model associations

Posted in CakePHP by schneimi on September 6, 2009
Tags: , , , ,

I made a simple behavior that serializes (beforeSave) and deserializes (afterFind) data to and from the database.

The behavior works fine as long as I do a find directly over the model where my behavior is attached, but if I do a find through associated models using the containable behavior, my behavior is not triggered at all and I have to deserialize the data manually after the find, which is very uncomfortable.

Model behavior afterFind is triggered

$this->Model->find(...);

Model behavior afterFind isn’t triggered

$this->OtherModel->contain('Model');
$this->OtherModel->find(...);

I found a ticket and a thread concerning this topic, and it turns out that this is an existing problem with CakePHP for a longer time already.

Because I use the latest CakePHP 1.2.5 and couldn’t find any working patch for it, I adapted the patch found in the thread to this version. I am not very familiar with the core, so it’s still quick&dirty and I cannot tell if it works for every setup. Anyway I hope this still helps other people that run into the same problem.

Here are the changes that have to be done in the cake\libs\model\datasources\dbo_source.php, just place the code after the lines:

Line 877: $resultSet[$i][$association] = $linkModel->afterFind($resultSet[$i][$association]);

foreach ($linkModel->Behaviors->attached() as $behavior) {
  if ($behavior != 'Containable') {
    $data = array(array($association => $resultSet[$i][$association]));
    $filtered_data = $linkModel->Behaviors->{$behavior}->afterFind($linkModel, $data, false);
    
    if ($filtered_data) {
      $resultSet[$i][$association] = $filtered_data[0][$association];
    }
  }
}

Line 741: function queryAssociation(&$model, &$linkModel, $type, $association, $assocData, &$queryData, $external = false, &$resultSet, $recursive, $stack) {

foreach ($linkModel->Behaviors->attached() as $behavior) {
  if ($behavior != 'Containable') {
    $return = $linkModel->Behaviors->{$behavior}->beforeFind($linkModel, $assocData);
    $assocData = (is_array($return)) ? $return : $assocData;
  }
}

Line 716: $data = $model->{$className}->afterFind(array(array($className => $results[$i][$className])), false);

foreach ($model->{$className}->Behaviors->attached() as $behavior) {
  if ($behavior != 'Containable') {
    $filtered_data = $model->{$className}->Behaviors->{$behavior}->afterFind($model->{$className}, $data, false);
    
    if ($filtered_data) {
      $data = $filtered_data;
    }
  }
}

edit:
I added support for the beforeFind callback, because I needed this on a softdelete behavior to filter associated deleted data as well.

I also found out that you don’t have to really hack the core, instead just copy the cake\libs\model\datasources\dbo_source.php into your app app\models\datasources\dbo_source.php and change this file instead.