Polymorphic Behavior
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 model definition for the model for the Note model:
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:
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.
And that's all there is to it.
Bake on!
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 this, long 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!

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).
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;
}
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
http://bin.cakephp.org/view/906760296
This is where the magic happens:
$results[$key] = Set::merge($results[$key], $associated);http://groups.google.com/group/cake-php/browse_thread/thread/4072ab761b1565a6#
This is the fixed version:
http://bin.cakephp.org/view/1702492171
May be you should elaborate a bit more on the "display_field" it may confuse the CutNPaste artists ;)