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. :)