tree.php

Go to the documentation of this file.
00001 <?php
00002 /* SVN FILE: $Id: tree_8php-source.html 580 2008-07-01 14:45:49Z gwoo $ */
00003 /**
00004  * Tree behavior class.
00005  *
00006  * Enables a model object to act as a node-based tree.
00007  *
00008  * PHP versions 4 and 5
00009  *
00010  * CakePHP :  Rapid Development Framework <http://www.cakephp.org/>
00011  * Copyright 2006-2008, Cake Software Foundation, Inc.
00012  *                              1785 E. Sahara Avenue, Suite 490-204
00013  *                              Las Vegas, Nevada 89104
00014  *
00015  * Licensed under The MIT License
00016  * Redistributions of files must retain the above copyright notice.
00017  *
00018  * @filesource
00019  * @copyright       Copyright 2006-2008, Cake Software Foundation, Inc.
00020  * @link                http://www.cakefoundation.org/projects/info/cakephp CakePHP Project
00021  * @package         cake
00022  * @subpackage      cake.cake.libs.model.behaviors
00023  * @since           CakePHP v 1.2.0.4487
00024  * @version         $Revision: 580 $
00025  * @modifiedby      $LastChangedBy: gwoo $
00026  * @lastmodified    $Date: 2008-07-01 09:45:49 -0500 (Tue, 01 Jul 2008) $
00027  * @license         http://www.opensource.org/licenses/mit-license.php The MIT License
00028  */
00029 /**
00030  * Short description for file
00031  *
00032  * Long description for file
00033  *
00034  * @package     cake
00035  * @subpackage  cake.cake.libs.model.behaviors
00036  */
00037 class TreeBehavior extends ModelBehavior {
00038 
00039     var $errors = array();
00040 
00041     var $_defaults = array(
00042         'parent' => 'parent_id', 'left' => 'lft', 'right' => 'rght',
00043         'scope' => '1 = 1', 'type' => 'nested', '__parentChange' => false, 'recursive' => -1
00044     );
00045 
00046     function setup(&$model, $config = array()) {
00047         if (!is_array($config)) {
00048             $config = array('type' => $config);
00049         }
00050         $settings = array_merge($this->_defaults, $config);
00051 
00052         if (in_array($settings['scope'], $model->getAssociated('belongsTo'))) {
00053             $data = $model->getAssociated($settings['scope']);
00054             $parent =& $model->{$settings['scope']};
00055             $settings['scope'] = $model->alias . '.' . $data['foreignKey'] . ' = ' . $parent->alias . '.' . $parent->primaryKey;
00056             $settings['recursive'] = 0;
00057         }
00058         $this->settings[$model->alias] = $settings;
00059     }
00060 /**
00061  * @deprecated
00062  */
00063     function setScope(&$model, $scope) {
00064         trigger_error(__('(TreeBehavior::setScope) Deprecated - Use BehaviorCollection::attach() to re-attach with new settings', true), E_USER_WARNING);
00065         $this->settings[$model->alias]['scope'] = $scope;
00066         if ($this->settings[$model->alias]['recursive'] < 0) {
00067             $this->settings[$model->alias]['recursive'] = 0;
00068         }
00069     }
00070 /**
00071  * After save method. Called after all saves
00072  *
00073  * Overriden to transparently manage setting the lft and rght fields if and only if the parent field is included in the
00074  * parameters to be saved.
00075  *
00076  * @param AppModel $model
00077  * @param boolean $created indicates whether the node just saved was created or updated
00078  * @return boolean true on success, false on failure
00079  */
00080     function afterSave(&$model, $created) {
00081         extract($this->settings[$model->alias]);
00082         if ($created) {
00083             if ((isset($model->data[$model->alias][$parent])) && $model->data[$model->alias][$parent]) {
00084                 return $this->_setParent($model, $model->data[$model->alias][$parent], $created);
00085             }
00086         } elseif ($__parentChange) {
00087             $this->settings[$model->alias]['__parentChange'] = false;
00088             return $this->_setParent($model, $model->data[$model->alias][$parent]);
00089         }
00090     }
00091 /**
00092  * Before delete method. Called before all deletes
00093  *
00094  * Will delete the current node and all children using the deleteAll method and sync the table
00095  *
00096  * @param AppModel $model
00097  * @return boolean true to continue, false to abort the delete
00098  */
00099     function beforeDelete(&$model) {
00100         extract($this->settings[$model->alias]);
00101         list($name, $data) = array($model->alias, $model->read());
00102         $data = $data[$name];
00103 
00104         if (!$data[$right] || !$data[$left]) {
00105             return true;
00106         }
00107         $diff = $data[$right] - $data[$left] + 1;
00108 
00109         if ($diff > 2) {
00110             if (is_string($scope)) {
00111                 $scope = array($scope);
00112             }
00113             $scope[]["{$model->alias}.{$left} BETWEEN ? AND ?"] = array($data[$left] + 1, $data[$right] - 1);
00114             $model->deleteAll($scope);
00115         }
00116         $this->__sync($model, $diff, '-', '> ' . $data[$right]);
00117         return true;
00118     }
00119 /**
00120  * Before save method. Called before all saves
00121  *
00122  * Overriden to transparently manage setting the lft and rght fields if and only if the parent field is included in the
00123  * parameters to be saved. For newly created nodes with NO parent the left and right field values are set directly by
00124  * this method bypassing the setParent logic.
00125  *
00126  * @since 1.2
00127  * @param AppModel $model
00128  * @return boolean true to continue, false to abort the save
00129  */
00130     function beforeSave(&$model) {
00131         extract($this->settings[$model->alias]);
00132 
00133         if (isset($model->data[$model->alias][$model->primaryKey])) {
00134             if ($model->data[$model->alias][$model->primaryKey]) {
00135                 if (!$model->id) {
00136                     $model->id = $model->data[$model->alias][$model->primaryKey];
00137                 }
00138             }
00139             unset($model->data[$model->alias][$model->primaryKey]);
00140         }
00141 
00142         $this->_addToWhitelist($model, array($left, $right));
00143         if (!$model->id) {
00144             if (array_key_exists($parent, $model->data[$model->alias]) && $model->data[$model->alias][$parent]) {
00145                 $parentNode = $model->find('first', array(
00146                     'conditions' => array($scope, $model->escapeField() => $model->data[$model->alias][$parent]),
00147                     'fields' => array($model->primaryKey, $right), 'recursive' => $recursive
00148                 ));
00149                 if (!$parentNode) {
00150                     return false;
00151                 }
00152                 list($parentNode) = array_values($parentNode);
00153                 $model->data[$model->alias][$left] = 0; //$parentNode[$right];
00154                 $model->data[$model->alias][$right] = 0; //$parentNode[$right] + 1;
00155             } else {
00156                 $edge = $this->__getMax($model, $scope, $right, $recursive);
00157                 $model->data[$model->alias][$left] = $edge + 1;
00158                 $model->data[$model->alias][$right] = $edge + 2;
00159             }
00160         } elseif (array_key_exists($parent, $model->data[$model->alias])) {
00161             if ($model->data[$model->alias][$parent] != $model->field($parent)) {
00162                 $this->settings[$model->alias]['__parentChange'] = true;
00163             }
00164             if (!$model->data[$model->alias][$parent]) {
00165                 $model->data[$model->alias][$parent] = null;
00166                 $this->_addToWhitelist($model, $parent);
00167             } else {
00168                 list($node) = array_values($model->find('first', array(
00169                     'conditions' => array($scope,$model->escapeField() => $model->id),
00170                     'fields' => array($model->primaryKey, $parent, $left, $right ), 'recursive' => $recursive)
00171                 ));
00172 
00173                 $parentNode = $model->find('first', array(
00174                     'conditions' => array($scope, $model->escapeField() => $model->data[$model->alias][$parent]),
00175                     'fields' => array($model->primaryKey, $left, $right), 'recursive' => $recursive
00176                 ));
00177                 if (!$parentNode) {
00178                     return false;
00179                 }
00180                 list($parentNode) = array_values($parentNode);
00181 
00182                 if (($node[$left] < $parentNode[$left]) && ($parentNode[$right] < $node[$right])) {
00183                     return false;
00184                 } elseif ($node[$model->primaryKey] == $parentNode[$model->primaryKey]) {
00185                     return false;
00186                 }
00187             }
00188         }
00189         return true;
00190     }
00191 /**
00192  * Get the number of child nodes
00193  *
00194  * If the direct parameter is set to true, only the direct children are counted (based upon the parent_id field)
00195  * If false is passed for the id parameter, all top level nodes are counted, or all nodes are counted.
00196  *
00197  * @param AppModel $model
00198  * @param mixed $id The ID of the record to read or false to read all top level nodes
00199  * @param boolean $direct whether to count direct, or all, children
00200  * @return integer number of child nodes
00201  * @access public
00202  */
00203     function childcount(&$model, $id = null, $direct = false) {
00204         if (is_array($id)) {
00205             extract (array_merge(array('id' => null), $id));
00206         }
00207         if ($id === null && $model->id) {
00208             $id = $model->id;
00209         } elseif (!$id) {
00210             $id = null;
00211         }
00212         extract($this->settings[$model->alias]);
00213 
00214         if ($direct) {
00215             return $model->find('count', array('conditions' => array($scope, $model->escapeField($parent) => $id)));
00216         }
00217 
00218         if ($id === null) {
00219             return $model->find('count', array('conditions' => $scope));
00220         } elseif (!empty ($model->data)) {
00221             $data = $model->data[$model->alias];
00222         } else {
00223             list($data) = array_values($model->find('first', array('conditions' => array($scope, $model->escapeField() => $id), 'recursive' => $recursive)));
00224         }
00225         return ($data[$right] - $data[$left] - 1) / 2;
00226     }
00227 /**
00228  * Get the child nodes of the current model
00229  *
00230  * If the direct parameter is set to true, only the direct children are returned (based upon the parent_id field)
00231  * If false is passed for the id parameter, top level, or all (depending on direct parameter appropriate) are counted.
00232  *
00233  * @param AppModel $model
00234  * @param mixed $id The ID of the record to read
00235  * @param boolean $direct whether to return only the direct, or all, children
00236  * @param mixed $fields Either a single string of a field name, or an array of field names
00237  * @param string $order SQL ORDER BY conditions (e.g. "price DESC" or "name ASC") defaults to the tree order
00238  * @param integer $limit SQL LIMIT clause, for calculating items per page.
00239  * @param integer $page Page number, for accessing paged data
00240  * @param integer $recursive The number of levels deep to fetch associated records
00241  * @return array Array of child nodes
00242  * @access public
00243  */
00244     function children(&$model, $id = null, $direct = false, $fields = null, $order = null, $limit = null, $page = 1, $recursive = null) {
00245         if (is_array($id)) {
00246             extract (array_merge(array('id' => null), $id));
00247         }
00248         $overrideRecursive = $recursive;
00249 
00250         if ($id === null && $model->id) {
00251             $id = $model->id;
00252         } elseif (!$id) {
00253             $id = null;
00254         }
00255         $name = $model->alias;
00256         extract($this->settings[$model->alias]);
00257 
00258         if (!is_null($overrideRecursive)) {
00259             $recursive = $overrideRecursive;
00260         }
00261         if (!$order) {
00262             $order = $model->alias . '.' . $left . ' asc';
00263         }
00264         if ($direct) {
00265             $conditions = array($scope, $model->escapeField($parent) => $id);
00266             return $model->find('all', compact('conditions', 'fields', 'order', 'limit', 'page', 'recursive'));
00267         }
00268 
00269         if (!$id) {
00270             $conditions = $scope;
00271         } else {
00272             $result = array_values($model->find('first', array(
00273                 'conditions' => array($scope, $model->escapeField() => $id),
00274                 'fields' => array($left, $right),
00275                 'recursive' => $recursive
00276             )));
00277 
00278             if (empty($result) || !isset($result[0])) {
00279                 return array();
00280             }
00281             $conditions = array($scope,
00282                 $model->escapeField($right) . ' <' => $result[0][$right],
00283                 $model->escapeField($left) . ' >' => $result[0][$left]
00284             );
00285         }
00286         return $model->find('all', compact('conditions', 'fields', 'order', 'limit', 'page', 'recursive'));
00287     }
00288 /**
00289  * A convenience method for returning a hierarchical array used for HTML select boxes
00290  *
00291  * @param AppModel $model
00292  * @param mixed $conditions SQL conditions as a string or as an array('field' =>'value',...)
00293  * @param string $keyPath A string path to the key, i.e. "{n}.Post.id"
00294  * @param string $valuePath A string path to the value, i.e. "{n}.Post.title"
00295  * @param string $spacer The character or characters which will be repeated
00296  * @param integer $recursive The number of levels deep to fetch associated records
00297  * @return array An associative array of records, where the id is the key, and the display field is the value
00298  * @access public
00299  */
00300     function generatetreelist(&$model, $conditions = null, $keyPath = null, $valuePath = null, $spacer = '_', $recursive = null) {
00301         $overrideRecursive = $recursive;
00302         extract($this->settings[$model->alias]);
00303         if (!is_null($overrideRecursive)) {
00304             $recursive = $overrideRecursive;
00305         }
00306 
00307         if ($keyPath == null && $valuePath == null && $model->hasField($model->displayField)) {
00308             $fields = array($model->primaryKey, $model->displayField, $left, $right);
00309         } else {
00310             $fields = null;
00311         }
00312 
00313         if ($keyPath == null) {
00314             $keyPath = '{n}.' . $model->alias . '.' . $model->primaryKey;
00315         }
00316 
00317         if ($valuePath == null) {
00318             $valuePath = array('{0}{1}', '{n}.tree_prefix', '{n}.' . $model->alias . '.' . $model->displayField);
00319 
00320         } elseif (is_string($valuePath)) {
00321             $valuePath = array('{0}{1}', '{n}.tree_prefix', $valuePath);
00322 
00323         } else {
00324             $valuePath[0] = '{' . (count($valuePath) - 1) . '}' . $valuePath[0];
00325             $valuePath[] = '{n}.tree_prefix';
00326         }
00327         $order = $model->alias . '.' . $left . ' asc';
00328         $results = $model->find('all', compact('conditions', 'fields', 'order', 'recursive'));
00329         $stack = array();
00330 
00331         foreach ($results as $i => $result) {
00332             while ($stack && ($stack[count($stack) - 1] < $result[$model->alias][$right])) {
00333                 array_pop($stack);
00334             }
00335             $results[$i]['tree_prefix'] = str_repeat($spacer,count($stack));
00336             $stack[] = $result[$model->alias][$right];
00337         }
00338         if (empty($results)) {
00339             return array();
00340         }
00341         return Set::combine($results, $keyPath, $valuePath);
00342     }
00343 /**
00344  * Get the parent node
00345  *
00346  * reads the parent id and returns this node
00347  *
00348  * @param AppModel $model
00349  * @param mixed $id The ID of the record to read
00350  * @param integer $recursive The number of levels deep to fetch associated records
00351  * @return array Array of data for the parent node
00352  * @access public
00353  */
00354     function getparentnode(&$model, $id = null, $fields = null, $recursive = null) {
00355         if (is_array($id)) {
00356             extract (array_merge(array('id' => null), $id));
00357         }
00358         $overrideRecursive = $recursive;
00359         if (empty ($id)) {
00360             $id = $model->id;
00361         }
00362         extract($this->settings[$model->alias]);
00363         if (!is_null($overrideRecursive)) {
00364             $recursive = $overrideRecursive;
00365         }
00366         $parentId = $model->read($parent, $id);
00367 
00368         if ($parentId) {
00369             $parentId = $parentId[$model->alias][$parent];
00370             $parent = $model->find('first', array('conditions' => array($model->escapeField() => $parentId), 'fields' => $fields, 'recursive' => $recursive));
00371 
00372             return $parent;
00373         } else {
00374             return false;
00375         }
00376     }
00377 /**
00378  * Get the path to the given node
00379  *
00380  * @param AppModel $model
00381  * @param mixed $id The ID of the record to read
00382  * @param mixed $fields Either a single string of a field name, or an array of field names
00383  * @param integer $recursive The number of levels deep to fetch associated records
00384  * @return array Array of nodes from top most parent to current node
00385  * @access public
00386  */
00387     function getpath(&$model, $id = null, $fields = null, $recursive = null) {
00388         if (is_array($id)) {
00389             extract (array_merge(array('id' => null), $id));
00390         }
00391         $overrideRecursive = $recursive;
00392         if (empty ($id)) {
00393             $id = $model->id;
00394         }
00395         extract($this->settings[$model->alias]);
00396         if (!is_null($overrideRecursive)) {
00397             $recursive = $overrideRecursive;
00398         }
00399         $result = $model->find('first', array('conditions' => array($model->escapeField() => $id), 'fields' => array($left, $right), 'recursive' => $recursive));
00400         if ($result) {
00401             $result = array_values($result);
00402         } else {
00403             return null;
00404         }
00405         $item = $result[0];
00406         $results = $model->find('all', array(
00407             'conditions' => array($scope, $model->escapeField($left) . ' <=' => $item[$left], $model->escapeField($right) . ' >=' => $item[$right]),
00408             'fields' => $fields, 'order' => array($model->escapeField($left) => 'asc'), 'recursive' => $recursive
00409         ));
00410         return $results;
00411     }
00412 /**
00413  * Reorder the node without changing the parent.
00414  *
00415  * If the node is the last child, or is a top level node with no subsequent node this method will return false
00416  *
00417  * @param AppModel $model
00418  * @param mixed $id The ID of the record to move
00419  * @param mixed $number how many places to move the node or true to move to last position
00420  * @return boolean true on success, false on failure
00421  * @access public
00422  */
00423     function movedown(&$model, $id = null, $number = 1) {
00424         if (is_array($id)) {
00425             extract (array_merge(array('id' => null), $id));
00426         }
00427         if (!$number) {
00428             return false;
00429         }
00430         if (empty ($id)) {
00431             $id = $model->id;
00432         }
00433         extract($this->settings[$model->alias]);
00434         list($node) = array_values($model->find('first', array(
00435             'conditions' => array($scope, $model->escapeField() => $id),
00436             'fields' => array($model->primaryKey, $left, $right, $parent), 'recursive' => $recursive
00437         )));
00438         if ($node[$parent]) {
00439             list($parentNode) = array_values($model->find('first', array(
00440                 'conditions' => array($scope, $model->escapeField() => $node[$parent]),
00441                 'fields' => array($model->primaryKey, $left, $right), 'recursive' => $recursive
00442             )));
00443             if (($node[$right] + 1) == $parentNode[$right]) {
00444                 return false;
00445             }
00446         }
00447         $nextNode = $model->find('first', array(
00448             'conditions' => array($scope, $model->escapeField($left) => ($node[$right] + 1)),
00449             'fields' => array($model->primaryKey, $left, $right), 'recursive' => $recursive)
00450         );
00451         if ($nextNode) {
00452             list($nextNode)= array_values($nextNode);
00453         } else {
00454             return false;
00455         }
00456         $edge = $this->__getMax($model, $scope, $right, $recursive);
00457         $this->__sync($model, $edge - $node[$left] + 1, '+', 'BETWEEN ' . $node[$left] . ' AND ' . $node[$right]);
00458         $this->__sync($model, $nextNode[$left] - $node[$left], '-', 'BETWEEN ' . $nextNode[$left] . ' AND ' . $nextNode[$right]);
00459         $this->__sync($model, $edge - $node[$left] - ($nextNode[$right] - $nextNode[$left]), '-', '> ' . $edge);
00460 
00461         if (is_int($number)) {
00462             $number--;
00463         }
00464         if ($number) {
00465             $this->moveDown($model, $id, $number);
00466         }
00467         return true;
00468     }
00469 /**
00470  * Reorder the node without changing the parent.
00471  *
00472  * If the node is the first child, or is a top level node with no previous node this method will return false
00473  *
00474  * @param AppModel $model
00475  * @param mixed $id The ID of the record to move
00476  * @param mixed $number how many places to move the node, or true to move to first position
00477  * @return boolean true on success, false on failure
00478  * @access public
00479  */
00480     function moveup(&$model, $id = null, $number = 1) {
00481         if (is_array($id)) {
00482             extract (array_merge(array('id' => null), $id));
00483         }
00484         if (!$number) {
00485             return false;
00486         }
00487         if (empty ($id)) {
00488             $id = $model->id;
00489         }
00490         extract($this->settings[$model->alias]);
00491         list($node) = array_values($model->find('first', array(
00492             'conditions' => array($scope, $model->escapeField() => $id),
00493             'fields' => array($model->primaryKey, $left, $right, $parent ), 'recursive' => $recursive
00494         )));
00495         if ($node[$parent]) {
00496             list($parentNode) = array_values($model->find('first', array(
00497                 'conditions' => array($scope, $model->escapeField() => $node[$parent]),
00498                 'fields' => array($model->primaryKey, $left, $right), 'recursive' => $recursive
00499             )));
00500             if (($node[$left] - 1) == $parentNode[$left]) {
00501                 return false;
00502             }
00503         }
00504         $previousNode = $model->find('first', array(
00505             'conditions' => array($scope, $model->escapeField($right) => ($node[$left] - 1)),
00506             'fields' => array($model->primaryKey, $left, $right),
00507             'recursive' => $recursive
00508         ));
00509 
00510         if ($previousNode) {
00511             list($previousNode) = array_values($previousNode);
00512         } else {
00513             return false;
00514         }
00515         $edge = $this->__getMax($model, $scope, $right, $recursive);
00516         $this->__sync($model, $edge - $previousNode[$left] +1, '+', 'BETWEEN ' . $previousNode[$left] . ' AND ' . $previousNode[$right]);
00517         $this->__sync($model, $node[$left] - $previousNode[$left], '-', 'BETWEEN ' .$node[$left] . ' AND ' . $node[$right]);
00518         $this->__sync($model, $edge - $previousNode[$left] - ($node[$right] - $node[$left]), '-', '> ' . $edge);
00519         if (is_int($number)) {
00520             $number--;
00521         }
00522         if ($number) {
00523             $this->moveUp($model, $id, $number);
00524         }
00525         return true;
00526     }
00527 /**
00528  * Recover a corrupted tree
00529  *
00530  * The mode parameter is used to specify the source of info that is valid/correct. The opposite source of data
00531  * will be populated based upon that source of info. E.g. if the MPTT fields are corrupt or empty, with the $mode
00532  * 'parent' the values of the parent_id field will be used to populate the left and right fields. The missingParentAction
00533  * parameter only applies to "parent" mode and determines what to do if the parent field contains an id that is not present.
00534  *
00535  * @todo Could be written to be faster, *maybe*. Ideally using a subquery and putting all the logic burden on the DB.
00536  * @param AppModel $model
00537  * @param string $mode parent or tree
00538  * @param mixed $missingParentAction 'return' to do nothing and return, 'delete' to
00539  * delete, or the id of the parent to set as the parent_id
00540  * @return boolean true on success, false on failure
00541  * @access public
00542  */
00543     function recover(&$model, $mode = 'parent', $missingParentAction = null) {
00544         if (is_array($mode)) {
00545             extract (array_merge(array('mode' => 'parent'), $mode));
00546         }
00547         extract($this->settings[$model->alias]);
00548         $model->recursive = $recursive;
00549         if ($mode == 'parent') {
00550             $model->bindModel(array('belongsTo' => array('VerifyParent' => array(
00551                 'className' => $model->alias,
00552                 'foreignKey' => $parent,
00553                 'fields' => array($model->primaryKey, $left, $right, $parent,
00554                 'actsAs' => '')
00555             ))));
00556             $missingParents = $model->find('list', array(
00557                 'recursive' => 0,
00558                 'conditions' => array($scope, array(
00559                     'NOT' => array($model->escapeField($parent) => null), $model->VerifyParent->escapeField() => null
00560                 ))
00561             ));
00562             $model->unbindModel(array('belongsTo' => array('VerifyParent')));
00563             if ($missingParents) {
00564                 if ($missingParentAction == 'return') {
00565                     foreach ($missingParents as $id => $display) {
00566                         $this->errors[] = 'cannot find the parent for ' . $model->alias . ' with id ' . $id . '(' . $display . ')';
00567 
00568                     }
00569                     return false;
00570                 } elseif ($missingParentAction == 'delete') {
00571                     $model->deleteAll(array($model->primaryKey => array_flip($missingParents)));
00572                 } else {
00573                     $model->updateAll(array($parent => $missingParentAction), array($model->escapeField($model->primaryKey) => array_flip($missingParents)));
00574                 }
00575             }
00576             $count = 1;
00577             foreach ($model->find('all', array('conditions' => $scope, 'fields' => array($model->primaryKey), 'order' => $left)) as $array) {
00578                 $model->{$model->primaryKey} = $array[$model->alias][$model->primaryKey];
00579                 $lft = $count++;
00580                 $rght = $count++;
00581                 $model->save(array($left => $lft, $right => $rght), array('callbacks' => false));
00582             }
00583             foreach ($model->find('all', array('conditions' => $scope, 'fields' => array($model->primaryKey, $parent), 'order' => $left)) as $array) {
00584                 $model->create();
00585                 $model->id = $array[$model->alias][$model->primaryKey];
00586                 $this->_setParent($model, $array[$model->alias][$parent]);
00587             }
00588         } else {
00589             foreach ($model->find('all', array('conditions' => $scope, 'fields' => array($model->primaryKey, $parent), 'order' => $left)) as $array) {
00590                 $path = $this->getpath($model, $array[$model->alias][$model->primaryKey]);
00591                 if ($path == null || count($path) < 2) {
00592                     $parentId = null;
00593                 } else {
00594                     $parentId = $path[count($path) - 2][$model->alias][$model->primaryKey];
00595                 }
00596                 $model->updateAll(array($parent => $parentId), array($model->escapeField() => $array[$model->alias][$model->primaryKey]));
00597             }
00598         }
00599         return true;
00600     }
00601 /**
00602  * Reorder method.
00603  *
00604  * Reorders the nodes (and child nodes) of the tree according to the field and direction specified in the parameters.
00605  * This method does not change the parent of any node.
00606  *
00607  * Requires a valid tree, by default it verifies the tree before beginning.
00608  *
00609  * @param AppModel $model
00610  * @param array $options
00611  * @return boolean true on success, false on failure
00612  */
00613     function reorder(&$model, $options = array()) {
00614         $options = array_merge(array('id' => null, 'field' => $model->displayField, 'order' => 'ASC', 'verify' => true), $options);
00615         extract($options);
00616         if ($verify && !$model->verify()) {
00617             return false;
00618         }
00619         $verify = false;
00620         extract($this->settings[$model->alias]);
00621         $fields = array($model->primaryKey, $field, $left, $right);
00622         $sort = $field . ' ' . $order;
00623         $nodes = $model->children($id, true, $fields, $sort, null, null, $recursive);
00624 
00625         if ($nodes) {
00626             foreach ($nodes as $node) {
00627                 $id = $node[$model->alias][$model->primaryKey];
00628                 $model->moveDown($id, true);
00629                 if ($node[$model->alias][$left] != $node[$model->alias][$right] - 1) {
00630                     $this->reorder($model, compact('id', 'field', 'order', 'verify'));
00631                 }
00632             }
00633         }
00634         return true;
00635     }
00636 /**
00637  * Remove the current node from the tree, and reparent all children up one level.
00638  *
00639  * If the parameter delete is false, the node will become a new top level node. Otherwise the node will be deleted
00640  * after the children are reparented.
00641  *
00642  * @param AppModel $model
00643  * @param mixed $id The ID of the record to remove
00644  * @param boolean $delete whether to delete the node after reparenting children (if any)
00645  * @return boolean true on success, false on failure
00646  * @access public
00647  */
00648     function removefromtree(&$model, $id = null, $delete = false) {
00649         if (is_array($id)) {
00650             extract (array_merge(array('id' => null), $id));
00651         }
00652         extract($this->settings[$model->alias]);
00653 
00654         list($node) = array_values($model->find('first', array(
00655             'conditions' => array($scope, $model->escapeField() => $id),
00656             'fields' => array($model->primaryKey, $left, $right, $parent),
00657             'recursive' => $recursive
00658         )));
00659 
00660         if ($node[$right] == $node[$left] + 1) {
00661             if ($delete) {
00662                 $model->delete();
00663             } else {
00664                 return false;
00665             }
00666         } elseif ($node[$parent]) {
00667             list($parentNode) = array_values($model->find('first', array(
00668                 'conditions' => array($scope, $model->escapeField() => $node[$parent]),
00669                 'fields' => array($model->primaryKey, $left, $right),
00670                 'recursive' => $recursive
00671             )));
00672         } else {
00673             $parentNode[$right] = $node[$right] + 1;
00674         }
00675 
00676         $model->updateAll(array($parent => $node[$parent]), array($parent => $node[$model->primaryKey]));
00677         $this->__sync($model, 1, '-', 'BETWEEN ' . ($node[$left] + 1) . ' AND ' . ($node[$right] - 1));
00678         $this->__sync($model, 2, '-', '> ' . ($node[$right]));
00679         $model->id = $id;
00680 
00681         if ($delete) {
00682             $model->updateAll(
00683                 array(
00684                     $model->escapeField($left) => 0,
00685                     $model->escapeField($right) => 0,
00686                     $model->escapeField($parent) => null
00687                 ),
00688                 array($model->escapeField() => $id)
00689             );
00690             return $model->delete($id);
00691         } else {
00692             $edge = $this->__getMax($model, $scope, $right, $recursive);
00693             if ($node[$right] == $edge) {
00694                 $edge = $edge - 2;
00695             }
00696             $model->id = $id;
00697             return $model->save(
00698                 array($left => $edge + 1, $right => $edge + 2, $parent => null),
00699                 array('callbacks' => false)
00700             );
00701         }
00702     }
00703 /**
00704  * Backward compatible method
00705  *
00706  * Returns true if the change is successful.
00707  *
00708  * @param AppModel $model
00709  * @param mixed $parentId The ID to set as the parent of the current node.
00710  * @return true on success
00711  * @access public
00712  * @deprecated
00713  */
00714     function setparent(&$model, $parentId = null , $created = null) {
00715         trigger_error(
00716             __('(TreeBehavior::setParent) Deprecated - save the record with a parent ID instead', true),
00717             E_USER_ERROR
00718         );
00719         extract($this->settings[$model->alias]);
00720 
00721         if ($created === false && $parentId == $model->field($parent)) {
00722             return true;
00723         }
00724         return $model->saveField($parent, $parentId, array('callbacks' => false));
00725     }
00726 /**
00727  * Check if the current tree is valid.
00728  *
00729  * Returns true if the tree is valid otherwise an array of (type, incorrect left/right index, message)
00730  *
00731  * @param AppModel $model
00732  * @return mixed true if the tree is valid or empty, otherwise an array of (error type [index, node],
00733  *  [incorrect left/right index,node id], message)
00734  * @access public
00735  */
00736     function verify(&$model) {
00737         extract($this->settings[$model->alias]);
00738         if (!$model->find('count', array('conditions' => $scope))) {
00739             return true;
00740         }
00741         $min = $this->__getMin($model, $scope, $left, $recursive);
00742         $edge = $this->__getMax($model, $scope, $right, $recursive);
00743         $errors =  array();
00744 
00745         for ($i = $min; $i <= $edge; $i++) {
00746             $count = $model->find('count', array('conditions' => array(
00747                 $scope, 'OR' => array($model->escapeField($left) => $i, $model->escapeField($right) => $i)
00748             )));
00749             if ($count != 1) {
00750                 if ($count == 0) {
00751                     $errors[] = array('index', $i, 'missing');
00752                 } else {
00753                     $errors[] = array('index', $i, 'duplicate');
00754                 }
00755             }
00756         }
00757         $node = $model->find('first', array('conditions' => array($scope, $model->escapeField($right) . '< ' . $model->escapeField($left)), 'recursive' => 0));
00758         if ($node) {
00759             $errors[] = array('node', $node[$model->alias][$model->primaryKey], 'left greater than right.');
00760         }
00761 
00762         $model->bindModel(array('belongsTo' => array('VerifyParent' => array(
00763             'className' => $model->alias,
00764             'foreignKey' => $parent,
00765             'fields' => array($model->primaryKey, $left, $right, $parent)
00766         ))));
00767 
00768         foreach ($model->find('all', array('conditions' => $scope, 'recursive' => 0)) as $instance) {
00769             if (is_null($instance[$model->alias][$left]) || is_null($instance[$model->alias][$right])) {
00770                 $errors[] = array('node', $instance[$model->alias][$model->primaryKey],
00771                     'has invalid left or right values');
00772             } elseif ($instance[$model->alias][$left] == $instance[$model->alias][$right]){
00773                 $errors[] = array('node', $instance[$model->alias][$model->primaryKey],
00774                     'left and right values identical');
00775             } elseif ($instance[$model->alias][$parent]) {
00776                 if (!$instance['VerifyParent'][$model->primaryKey]) {
00777                     $errors[] = array('node', $instance[$model->alias][$model->primaryKey],
00778                         'The parent node ' . $instance[$model->alias][$parent] . ' doesn\'t exist');
00779                 } elseif ($instance[$model->alias][$left] < $instance['VerifyParent'][$left]) {
00780                     $errors[] = array('node', $instance[$model->alias][$model->primaryKey],
00781                         'left less than parent (node ' . $instance['VerifyParent'][$model->primaryKey] . ').');
00782                 } elseif ($instance[$model->alias][$right] > $instance['VerifyParent'][$right]) {
00783                     $errors[] = array('node', $instance[$model->alias][$model->primaryKey],
00784                         'right greater than parent (node ' . $instance['VerifyParent'][$model->primaryKey] . ').');
00785                 }
00786             } elseif ($model->find('count', array('conditions' => array($scope, $model->escapeField($left) . '< ' . $instance[$model->alias][$left], $right . '> ' . $instance[$model->alias][$right]), 'recursive' => 0))) {
00787                 $errors[] = array('node', $instance[$model->alias][$model->primaryKey], 'The parent field is blank, but has a parent');
00788             }
00789         }
00790 
00791         if ($errors) {
00792             return $errors;
00793         } else {
00794             return true;
00795         }
00796     }
00797 /**
00798  * Sets the parent of the given node
00799  *
00800  * The force parameter is used to override the "don't change the parent to the current parent" logic in the event
00801  * of recovering a corrupted table, or creating new nodes. Otherwise it should always be false. In reality this
00802  * method could be private, since calling save with parent_id set also calls setParent
00803  *
00804  * @param AppModel $model
00805  * @param mixed $parentId
00806  * @return boolean true on success, false on failure
00807  * @access protected
00808  */
00809     function _setParent(&$model, $parentId = null, $created = false) {
00810         extract($this->settings[$model->alias]);
00811         list($node) = array_values($model->find('first', array(
00812             'conditions' => array($scope, $model->escapeField() => $model->id),
00813             'fields' => array($model->primaryKey, $parent, $left, $right),
00814             'recursive' => $recursive
00815         )));
00816         $edge = $this->__getMax($model, $scope, $right, $recursive, $created);
00817 
00818         if (empty ($parentId)) {
00819             $this->__sync($model, $edge - $node[$left] + 1, '+', 'BETWEEN ' . $node[$left] . ' AND ' . $node[$right], $created);
00820             $this->__sync($model, $node[$right] - $node[$left] + 1, '-', '> ' . $node[$left], $created);
00821         } else {
00822             $parentNode = array_values($model->find('first', array(
00823                 'conditions' => array($scope, $model->escapeField() => $parentId),
00824                 'fields' => array($model->primaryKey, $left, $right),
00825                 'recursive' => $recursive
00826             )));
00827 
00828             if (empty($parentNode) || empty($parentNode[0])) {
00829                 return false;
00830             }
00831             $parentNode = $parentNode[0];
00832 
00833             if (($model->id == $parentId)) {
00834                 return false;
00835 
00836             } elseif (($node[$left] < $parentNode[$left]) && ($parentNode[$right] < $node[$right])) {
00837                 return false;
00838             }
00839             if (empty ($node[$left]) && empty ($node[$right])) {
00840                 $this->__sync($model, 2, '+', '>= ' . $parentNode[$right], $created);
00841                 $model->save(
00842                     array($left => $parentNode[$right], $right => $parentNode[$right] + 1, $parent => $parentId),
00843                     array('validate' => false, 'callbacks' => false)
00844                 );
00845             } else {
00846                 $this->__sync($model, $edge - $node[$left] +1, '+', 'BETWEEN ' . $node[$left] . ' AND ' . $node[$right], $created);
00847                 $diff = $node[$right] - $node[$left] + 1;
00848 
00849                 if ($node[$left] > $parentNode[$left]) {
00850                     if ($node[$right] < $parentNode[$right]) {
00851                         $this->__sync($model, $diff, '-', 'BETWEEN ' . $node[$right] . ' AND ' . ($parentNode[$right] - 1), $created);
00852                         $this->__sync($model, $edge - $parentNode[$right] + $diff + 1, '-', '> ' . $edge, $created);
00853                     } else {
00854                         $this->__sync($model, $diff, '+', 'BETWEEN ' . $parentNode[$right] . ' AND ' . $node[$right], $created);
00855                         $this->__sync($model, $edge - $parentNode[$right] + 1, '-', '> ' . $edge, $created);
00856                     }
00857                 } else {
00858                     $this->__sync($model, $diff, '-', 'BETWEEN ' . $node[$right] . ' AND ' . ($parentNode[$right] - 1), $created);
00859                     $this->__sync($model, $edge - $parentNode[$right] + $diff + 1, '-', '> ' . $edge, $created);
00860                 }
00861             }
00862         }
00863         return true;
00864     }
00865 /**
00866  * get the maximum index value in the table.
00867  *
00868  * @param AppModel $model
00869  * @param string $scope
00870  * @param string $right
00871  * @return int
00872  * @access private
00873  */
00874     function __getMax($model, $scope, $right, $recursive = -1, $created = false) {
00875         $db =& ConnectionManager::getDataSource($model->useDbConfig);
00876         if ($created) {
00877             if (is_string($scope)) {
00878                 $scope .= " AND {$model->alias}.{$model->primaryKey} <> ";
00879                 $scope .= $db->value($model->id, $model->getColumnType($model->primaryKey));
00880             } else {
00881                 $scope['NOT'][$model->alias . '.' . $model->primaryKey] = $model->id;
00882             }
00883         }
00884         list($edge) = array_values($model->find('first', array(
00885             'conditions' => $scope,
00886             'fields' => $db->calculate($model, 'max', array($right)),
00887             'recursive' => $recursive
00888         )));
00889         return ife(empty ($edge[$right]), 0, $edge[$right]);
00890     }
00891 /**
00892  * get the minimum index value in the table.
00893  *
00894  * @param AppModel $model
00895  * @param string $scope
00896  * @param string $right
00897  * @return int
00898  * @access private
00899  */
00900     function __getMin($model, $scope, $left, $recursive = -1) {
00901         $db =& ConnectionManager::getDataSource($model->useDbConfig);
00902         list($edge) = array_values($model->find('first', array(
00903             'conditions' => $scope,
00904             'fields' => $db->calculate($model, 'min', array($left)),
00905             'recursive' => $recursive
00906         )));
00907         return ife(empty($edge[$left]), 0, $edge[$left]);
00908     }
00909 /**
00910  * Table sync method.
00911  *
00912  * Handles table sync operations, Taking account of the behavior scope.
00913  *
00914  * @param AppModel $model
00915  * @param integer $shift
00916  * @param string $direction
00917  * @param array $conditions
00918  * @param string $field
00919  * @access private
00920  */
00921     function __sync(&$model, $shift, $dir = '+', $conditions = array(), $created = false, $field = 'both') {
00922         $modelRecursive = $model->recursive;
00923         extract($this->settings[$model->alias]);
00924         $model->recursive = $recursive;
00925 
00926         if ($field == 'both') {
00927             $this->__sync($model, $shift, $dir, $conditions, $created, $left);
00928             $field = $right;
00929         }
00930         if (is_string($conditions)) {
00931             $conditions = array("{$model->alias}.{$field} {$conditions}");
00932         }
00933         if (($scope != '1 = 1' && $scope !== true) && $scope) {
00934             $conditions[] = $scope;
00935         }
00936         if ($created) {
00937             $conditions['NOT'][$model->alias . '.' . $model->primaryKey] = $model->id;
00938         }
00939         $model->updateAll(array($model->alias . '.' . $field => $model->alias . '.' . $field . ' ' . $dir . ' ' . $shift), $conditions);
00940         $model->recursive = $modelRecursive;
00941     }
00942 }
00943 
00944 ?>