Polymorphic Behavior

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

<?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.

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:

<?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:

<?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:

<?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:

<?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:

<?php
//...
$notes $this->Note->find('all');
Would give you:

<?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!

Report

More on Behaviors

Advertising

Comments

  • keymaster posted on 09/22/10 02:33:02 AM
    For situations where it is not possible to know at development time which model(s) your polymorphic model will belongTo, or for some reason you absolutely need to have a totally decoupled model codewise so it can easily associate with any model, this behavior is a great approach.

    However, if at compile time you know what association(s) this polymorphic model will belongTo, and especially if you have a large number of records in your polymorphic model, you will be much better off going the standard cake route and hardcoding your associations (where possible) as a regular $belongsTo variable in the model, specifying the 'foreignKey' and 'conditions' keys in the association as appropriate to make it work.

    The reason is this behavior will issue a separate db query for every record in the polymorphic model's result set (to obtain the associated model record), and then, in php, manually merge it into the results.

    On the other hand, if you hardcode the $belongsTo between the polymorphic models and those it is associated with, even when it belongsTo multiple models, and you do a find on the polymorphic model, cake will fetch all the belongsTo associated data in a single join query.

    For large data sets, or highly traffic'ed sites, this can have a tremendous performance difference.

    Also, fyi, see: http://github.com/Theaxiom/Polymorphic2.0 for a forked enhancement of this behavior (never tried it).
  • ok32 posted on 12/22/09 04:54:16 AM
    any ideas how to implement polymorphic logic but not to lose database referential integrity?
  • AaronP posted on 03/10/09 05:16:36 PM
    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;
    }
  • Theaxiom posted on 02/02/09 04:21:35 AM
    Thank you very much for this bit of code, really handy.
  • bMilesp posted on 11/12/08 03:34:28 PM
    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
  • sid6581 posted on 09/02/08 06:03:57 AM
    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);
  • dardosordi posted on 04/01/08 07:29:35 PM
    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
  • lucian posted on 03/14/08 02:00:52 AM
    Yep, that would have helped me a lot a few months ago, but it's never too late...Good job!
  • tariquesani posted on 03/13/08 09:43:31 PM
    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 ;)

login to post a comment.