Polymorphic Behavior

By Andy Dawson (AD7six)
A behavior which will allow a model to be associated with any other model.
In a couple of places, most notably the Generic Upload Behavior (http://cakeforge.org/frs/?group_id=152&release_id=355), I've made use of polymorphic associations to allow associating a model to any other model. Here's the polymorphic logic distilled into a dedicated behavior.

The behavior

For any find directly on the polymorphic model, the associated model data will also be returned.
Download code
<?php
/* SVN FILE: $Id: polymorphic.php 18 2008-03-07 12:56:09Z andy $ */
/**
 * Polymorphic Behavior.
 *
 * Allow the model to be associated with any other model object
 *
 * Copyright (c), Andy Dawson
 *
 * Licensed under The MIT License
 * Redistributions of files must retain the above copyright notice.
 *
 * @filesource
 * @author         Andy Dawson (AD7six)
 * @version        $Revision: 18 $
 * @modifiedby        $LastChangedBy: andy $
 * @lastmodified    $Date: 2008-03-07 13:56:09 +0100 (Fri, 07 Mar 2008) $
 * @license        http://www.opensource.org/licenses/mit-license.php The MIT License
 */
class PolymorphicBehavior extends ModelBehavior {
    
    function 
setup(&$model$config = array()) {
        
$this->settings[$model->name] = am (array('classField' => 'class''foreignKey' => 'foreign_id'),$config);
    }

    function 
afterFind (&$model$results$primary false) {
        
extract($this->settings[$model->name]);
        if (
$primary && isset($results[0][$model->alias][$classField]) && $model->recursive 0) {
            foreach (
$results as $key => $result) {
                
$associated = array();
                
$class $result[$model->alias][$classField];
                
$foreignId $result[$model->alias][$foreignKey];
                if (
$class && $foreignId) {
                    
$result $result[$model->alias];
                    if (!isset(
$model->$class)) {
                        
$model->bindModel(array('belongsTo' => array(
                            
$class => array(
                                
'conditions' => array($model->alias '.' $classField => $class),
                                
'foreignKey' => $foreignKey
                            
)
                        )));
                    }
                    
$associated $model->$class->find(array($class '.id' => $foreignId), 
                        array(
'id'$model->$class->displayField), null, -1);
                    
$associated[$class]['display_field'] = $associated[$class][$model->$class->displayField];
                    
$results[$key][$class] = $associated[$class];
                }
            }
        } elseif(isset(
$results[$model->alias][$classField])) {
            
$associated = array();
            
$class $results[$model->alias][$classField];
            
$foreignId $results[$model->alias][$foreignKey];
            if (
$class && $foreignId) {
                
$result $results[$model->alias];
                if (!isset(
$model->$class)) {
                    
$model->bindModel(array('belongsTo' => array(
                        
$class => array(
                            
'conditions' => array($model->alias '.' $classField => $class),
                            
'foreignKey' => $foreignKey
                        
)
                    )));
                }
                
$associated $model->$class->find(array($class.'.id' => $foreignId), array('id'$model->$class->displayField), null, -1);
                
$associated[$class]['display_field'] = $associated[$class][$model->$class->displayField];
                
$results[$class] = $associated[$class];
            }
        }
        return 
$results;
    }
}
?>

Setting up the model

The necessary sql to setup a polymorphic association, in this example "notes". The combination of class and foreign_id are used to find the associated model.
Download code
CREATE TABLE `notes` (
  `id` int(11) unsigned NOT NULL auto_increment,
  `class` varchar(30) NOT NULL,
  `foreign_id` int(11) unsigned NOT NULL,
  `title` varchar(100) NOT NULL,
  `content` text NOT NULL,
  `created` datetime default NULL,
  `modified` datetime default NULL,
  PRIMARY KEY  (`id`)
);

The model definition for the model for the Note model:

Model Class:

Download code <?php 
class Note extends AppModel {

    var 
$name 'Note';

    var 
$actsAs = array('Polymorphic');
}
?>

Setting up associations

Polymorphic conditions are not applied automatically and must be included in the association definition explicitly. For example:

Model Class:

Download code <?php 
class Thingy extends AppModel {

    var 
$name 'Thingy';

    var 
$hasMany = array(
        
'Note' => array(
            
'className' => 'Note',    
            
'foreignKey' => 'foreign_id',
            
'conditions' => array('Note.class' => 'Thingy'),
            
'dependent' => true
        
)
    );
}
?>

It's possible to avoid needing to explicitly state the conditions and foreignKey by adding some logic to your AppModel, e.g. if it's desired that all models have an association to Note:

Model Class:

Download code <?php 
class AppModel extends Model{

    var 
$hasMany => array('Note');

    function 
__construct($id false$table null$ds null) {
        
parent::__construct($id$table$ds);
        if (isset(
$this->hasMany['Note'])) {
            
$this->hasMany['Note']['conditions']['Note.class'] = $this->name;
            
$this->hasMany['Note']['foreignKey'] = 'foreign_id';
        }
    }
?>

If you do the above, for any model which does not required the Note model - override the var $hasMany and don't include Note in it.

Example Usage

Find all notes realted to this Thingy:
Download code
<?php
//...
$conditions['Note.class'] = 'Thingy';
$conditions['Note.foreign_id'] = $this->Thingy->id;
$notes $this->Thingy->Note->find('all'compact('conditions'));

// Or simply
$data $this->Thingy->read();
If you don't want to find all Notes for a particular object, but simply all notes in the system and whatever they are associated with - this is where the behavior actually does something. So:
Download code
<?php
//...
$notes $this->Note->find('all');
Would give you:
Download code
<?php
Array
(
    [
0] => Array
        (
            [
Note] => Array
                (
                    [
id] => 1
                    
[class] => Thingy
                    
[foreign_id] => 2
                    
[title] => Extremely important
                    
[content] => A note on something
                
)

            [
Thingy] => Array
                (
                    [
id] => 2
                    
[name] => Something // display field for this model
                    
[display_field] => Something
                
)

        )

    [
1] => Array
        (
            [
Note] => Array
                (
                    [
id] => 2
                    
[class] => Product
                    
[foreign_id] => 2
                    
[title] => Careful
                    
[content] => Be sure to speak to Gerald for ordering thislong lead time!
                )

            [
Product] => Array
                (
                    [
id] => 2
                    
[title] => Extra big comb // display field for this model
                    
[display_field] => Extra big comb
                
)

        )
...
etc.
Of interest in the above example:
  • The associated model data is present in the results
  • A virtual field "display_field" is added with the contents of the linked model's display field (to make admin listing logic easy - since the key "display_field" never changes whereas the model display field can)

And that's all there is to it.

Bake on!

 

Comments 637

CakePHP Team Comments Author Comments
 

Comment

1 Cool

Thanks this saved me some time on a task I was about to begin today.

May be you should elaborate a bit more on the "display_field" it may confuse the CutNPaste artists ;)

Posted Mar 13, 2008 by Dr. Tarique Sani
 

Comment

2 Nice

Yep, that would have helped me a lot a few months ago, but it's never too late...Good job!
Posted Mar 14, 2008 by Lucian Lature
 

Comment

3 Bug fix

The behavior doesn't fetch all the fields, Rob found it and I've fixed it.

http://groups.google.com/group/cake-php/browse_thread/thread/4072ab761b1565a6#
This is the fixed version:

http://bin.cakephp.org/view/1702492171
Posted Apr 1, 2008 by Dardo Sordi
 

Comment

4 Feature

I also needed the recursively associated models, therefore I altered the code a bit. It can be found here:

http://bin.cakephp.org/view/906760296
This is where the magic happens:

$results[$key] = Set::merge($results[$key], $associated);
Posted Sep 2, 2008 by Nemo Pohle
 

Comment

5 bug?

if you do a find('all',array('recursive'=>2)) directly on the polymorphic table "Notes" (from the example) it retrieves th polymorphic model data (Thingy and Product) as expected.

But if you have a Model (Book for example) that hasmany Notes, the Book->find('all',array('recursive'=>2)) only gathers the Notes data and none of the polymorphic models data (no Product, no Thingy).

It appears that the afterFind method in polymorphic.php is not executed in the latter scenario.

Does anyone know of a way to fix this? -b
Posted Nov 12, 2008 by Brandon Plasters
 

Comment

6 This field cannot be left blank

Thank you very much for this bit of code, really handy.
Posted Feb 2, 2009 by Travis Rowland
 

Comment

7 counterCache for your Polymorphic Parents.

Valuable is a cache of the number of Note/Comment/Vote/Etc your polymorphic parent model has. I wanted to effortlessly order "Notable" parent models by their Notability; literally. Thanks Andy for pointing me in the right direction...

For example in the model Note:


function beforeSave() {
    $this->bindModel(
        array(
            'belongsTo' => array(
                $this->data[$this->alias]['class'] => array(
                    'className' => $this->data[$this->alias]['class'],
                    'foreignKey' => 'foreign_id',
                    'counterCache' => 'note_count',
                    'counterScope' => array(
                        'class' => $this->data[$this->alias]['class']
                    )
                )
            )
        )
    );
    return true;
}
Posted Mar 10, 2009 by Aaron
 

Question

8 foreign keys?

any ideas how to implement polymorphic logic but not to lose database referential integrity?
Posted Dec 22, 2009 by Eugene