THIS CONTENT IS OUT OF DATE! IF YOU’RE USING CAKE 2, YOU SHOULDN’T READ THIS.
OK, so I’ve been working a lot with Cake’s Tree Behavior for the past six or seven months, because a lot of the plugins I’m building at work require complex category structure (i.e. more complex than simple parent -> child). Tree Behavior’s implementation of MPTT is good, but – unless I’m using the behavior incorrectly or can’t find the proper syntax – the ‘scope ‘ setting doesn’t really work in the way that I would expect.
Basically, Tree Behavior is great when all of the records in a table fall are part of the same tree. But what if you want different trees within the same table? For example, if you’re making a navigation system, you might have this relationship:
NavigationList hasMany NavigationItem NavigationItem belongsTo NavigationList
Since navigation can get pretty complex, we might want to use the Tree Behavior to keep track of the ordering of the NavigationItem records. But, we want a separate tree for each NavigationList, so we don’t get our main menu, footer menu, member’s only menu, etc. mixed up. The “scope” setting already exists in Tree Behavior, but it’s basically undocumented, so I had to guess at its syntax (after looking at the Tree Behavior code of course). I tried this:
class NavigationItem extends AppModel {
var $actsAs = array('Tree' => array('scope' => 'NavList'));
}
… hoping that the behavior would figure out that, when saving the NavigationItem, it should only make changes to the left/right values for NavigationItems with the same nav_list_id as the NavigationItem that’s being saved. But, no such luck; no limit is placed during the save operation, meaning that the entire table’s tree structure gets updated, rather than just those with nav_list_id = the nav_list_id of the NavigationItem we’re saving. So, I just decided to modify the TreeBehavior a bit. Some might whine about not modifying the core, but it’s a fine thing to do if you want more functionality. Just move tree.php out of /cake/lib/models/behaviors and into /app/models/behaviors.
I modified two methods, setup() and beforeSave(), and added my own _addScopeForeignKey() method. The full code is available on Github, but for the lazy the changes I made are:
TreeBehavior::setup() :
function setup(&$Model, $config = array()) {
if (!is_array($config)) {
$config = array('type' => $config);
}
$settings = array_merge($this->_defaults, $config);
if (in_array($settings['scope'], $Model->getAssociated('belongsTo'))) {
$data = $Model->getAssociated($settings['scope']);
$parent =& $Model->{$settings['scope']};
$settings['scope'] = $Model->alias . '.' . $data['foreignKey'] . ' = ' . $parent->alias . '.' . $parent->primaryKey;
$settings['recursive'] = 0;
$settings['scopeForeignKey'] = $data['foreignKey'];
}
$this->settings[$Model->alias] = $settings;
}
Add this line to the top of TreeBehavior::beforeSave() :
function beforeSave(&$Model) {
$this->_addScopeForeignKey(&$Model);
....
}
Add this new function:
/**
* add a foreign ID scope to the model settings.
*
* @param AppModel $Model Model instance
* @return true on success, false on failure
* @access protected
*/
function _addScopeForeignKey(&$Model) {
if (!isset($this->settings[$Model->alias]['scopeForeignKey'])) {
return false;
}
if (!isset($Model->data[$Model->alias][$this->settings[$Model->alias]['scopeForeignKey']])) {
return false;
}
$this->settings[$Model->alias]['scope'] = $Model->alias . '.' . $this->settings[$Model->alias]['scopeForeignKey'] . ' = ' . $Model->data[$Model->alias][$this->settings[$Model->alias]['scopeForeignKey']];
return true;
}
And that’s it. I haven’t tested it all that much but it’s doing the job right now. Comments, suggestions, improvements, polite criticism welcome.

Does the scope option not work like counterScope for counterCache? I.e. an array like you’d pass to find conditions?
Also, this just got posted on the Bakery : http://bakery.cakephp.org/articles/view/btree-behavior ?
Thanks for posting this. Seems to work great for me.
Neil: Interesting code on the bakery – I hadn’t seen it. I don’t check the bakery all too often these days since I’m still waiting for one that I posted months ago to be approved. Yes, I’m bitter.
As for the scope option – yes and no. Scope for TreeBehavior can be used to add simple find conditions, just like counterScope for counterCache. But you can also pass the name of an associated belongsTo model, as TreeBehavior::setup() shows. Since this feature is undocumented, I had thought (naively perhaps) that it would add a find condition to reordering operations so that only other records in the table with the same belongsTo foreign model would be affected.
So, you pass a “NavList” as the scope and TreeBehavior ensures that only NavItems with the same nav_list_id as the record you’re manipulating get included in tree-restructuring. But obviously that didn’t turn out to be the case. Instead placing a condition like this on reordering operations:
NavItem.nav_list_id = 7 // assuming “7″ is the nav_list_id for the current record
The stock TreeBehavior just inserts this:
NavItem.nav_list_id = NavList.id
which is useful in some cases but not when you want to maintain separate trees within the same table.
Oops, looks like my bakery article (which I wrote months ago and don’t even like anymore!) finally got posted a couple of days ago. I take back my bitterness.
This is exactly what I needed!
I found another behavior for this on the Bakery but decided to use yours because it seems to deviate less from the core.
(other behavior:
http://bakery.cakephp.org/articles/view/scopedtreebehavior
)
I knew I had seen something that addressed this issue in the past, without resorting to rewriting parts of the behavior – not that I am against that if it is needed.
Basically, the scope adds a condition to the queries, but since you don’t know the foreign_key id ahead of time for the related Model’s record you are limited when using $actsAs to setting either a static value..
i.e. ‘scope’ => array( ‘RelatedModel.field’ => 23 )
*OR* you can attach and detach on the fly ala
http://www.mcfarren.org/CodeCakePHPStoringMultipleTreesWithTreeBehavior
I extracted your modifications into a separate behavior so that changes to the Tree core doesn’t get lost.
Have a look at http://pastebin.com/FHwTtPvV.
Cheers
Arno
I am having problems using moveUp() and moveDown() with your scoped tree behaviour. I am not getting any error messages and the page refreshes as if it was working fine, but there are nothing has been moved. Do you have any idea what might cause this?
There are also some issues with saving a category as a child – somehow it just simply doesn’t work.