CakePHP
  • Documentation
    • Book
    • API
    • Videos
    • Reporting Security Issues
    • Privacy Policy
    • Logos & Trademarks
  • Business Solutions
  • Swag
  • Road Trip
  • Team
  • Community
    • Community
    • Get Involved
    • Issues (GitHub)
    • Bakery
    • Featured Resources
    • Training
    • Meetups
    • My CakePHP
    • CakeFest
    • Newsletter
    • Linkedin
    • YouTube
    • Facebook
    • Twitter
    • Mastodon
    • Help & Support
    • Forum
    • Stack Overflow
    • Slack
    • Paid Support
CakePHP

C CakePHP 2.2 API

  • Overview
  • Tree
  • Deprecated
  • Version:
    • 2.2
      • 4.2
      • 4.1
      • 4.0
      • 3.9
      • 3.8
      • 3.7
      • 3.6
      • 3.5
      • 3.4
      • 3.3
      • 3.2
      • 3.1
      • 3.0
      • 2.10
      • 2.9
      • 2.8
      • 2.7
      • 2.6
      • 2.5
      • 2.4
      • 2.3
      • 2.2
      • 2.1
      • 2.0
      • 1.3
      • 1.2

Packages

  • Cake
    • Cache
      • Engine
    • Configure
    • Console
      • Command
        • Task
    • Controller
      • Component
        • Acl
        • Auth
    • Core
    • Error
    • Event
    • I18n
    • Log
      • Engine
    • Model
      • Behavior
      • Datasource
        • Database
        • Session
      • Validator
    • Network
      • Email
      • Http
    • Routing
      • Filter
      • Route
    • TestSuite
      • Coverage
      • Fixture
      • Reporter
    • Utility
    • View
      • Helper

Classes

  • AclBehavior
  • ContainableBehavior
  • TranslateBehavior
  • TreeBehavior
   1: <?php
   2: /**
   3:  * Tree behavior class.
   4:  *
   5:  * Enables a model object to act as a node-based tree.
   6:  *
   7:  * PHP 5
   8:  *
   9:  * CakePHP :  Rapid Development Framework (http://cakephp.org)
  10:  * Copyright 2005-2012, Cake Software Foundation, Inc.
  11:  *
  12:  * Licensed under The MIT License
  13:  * Redistributions of files must retain the above copyright notice.
  14:  *
  15:  * @copyright     Copyright 2005-2012, Cake Software Foundation, Inc. (http://cakefoundation.org)
  16:  * @link          http://cakephp.org CakePHP Project
  17:  * @package       Cake.Model.Behavior
  18:  * @since         CakePHP v 1.2.0.4487
  19:  * @license       MIT License (http://www.opensource.org/licenses/mit-license.php)
  20:  */
  21: 
  22: /**
  23:  * Tree Behavior.
  24:  *
  25:  * Enables a model object to act as a node-based tree. Using Modified Preorder Tree Traversal
  26:  *
  27:  * @see http://en.wikipedia.org/wiki/Tree_traversal
  28:  * @package       Cake.Model.Behavior
  29:  * @link http://book.cakephp.org/2.0/en/core-libraries/behaviors/tree.html
  30:  */
  31: class TreeBehavior extends ModelBehavior {
  32: 
  33: /**
  34:  * Errors
  35:  *
  36:  * @var array
  37:  */
  38:     public $errors = array();
  39: 
  40: /**
  41:  * Defaults
  42:  *
  43:  * @var array
  44:  */
  45:     protected $_defaults = array(
  46:         'parent' => 'parent_id', 'left' => 'lft', 'right' => 'rght',
  47:         'scope' => '1 = 1', 'type' => 'nested', '__parentChange' => false, 'recursive' => -1
  48:     );
  49: 
  50: /**
  51:  * Used to preserve state between delete callbacks.
  52:  *
  53:  * @var array
  54:  */
  55:     protected $_deletedRow = array();
  56: 
  57: /**
  58:  * Initiate Tree behavior
  59:  *
  60:  * @param Model $Model instance of model
  61:  * @param array $config array of configuration settings.
  62:  * @return void
  63:  */
  64:     public function setup(Model $Model, $config = array()) {
  65:         if (isset($config[0])) {
  66:             $config['type'] = $config[0];
  67:             unset($config[0]);
  68:         }
  69:         $settings = array_merge($this->_defaults, $config);
  70: 
  71:         if (in_array($settings['scope'], $Model->getAssociated('belongsTo'))) {
  72:             $data = $Model->getAssociated($settings['scope']);
  73:             $Parent = $Model->{$settings['scope']};
  74:             $settings['scope'] = $Model->escapeField($data['foreignKey']) . ' = ' . $Parent->escapeField();
  75:             $settings['recursive'] = 0;
  76:         }
  77:         $this->settings[$Model->alias] = $settings;
  78:     }
  79: 
  80: /**
  81:  * After save method. Called after all saves
  82:  *
  83:  * Overridden to transparently manage setting the lft and rght fields if and only if the parent field is included in the
  84:  * parameters to be saved.
  85:  *
  86:  * @param Model $Model Model instance.
  87:  * @param boolean $created indicates whether the node just saved was created or updated
  88:  * @return boolean true on success, false on failure
  89:  */
  90:     public function afterSave(Model $Model, $created) {
  91:         extract($this->settings[$Model->alias]);
  92:         if ($created) {
  93:             if ((isset($Model->data[$Model->alias][$parent])) && $Model->data[$Model->alias][$parent]) {
  94:                 return $this->_setParent($Model, $Model->data[$Model->alias][$parent], $created);
  95:             }
  96:         } elseif ($this->settings[$Model->alias]['__parentChange']) {
  97:             $this->settings[$Model->alias]['__parentChange'] = false;
  98:             return $this->_setParent($Model, $Model->data[$Model->alias][$parent]);
  99:         }
 100:     }
 101: 
 102: /**
 103:  * Runs before a find() operation
 104:  *
 105:  * @param Model $Model  Model using the behavior
 106:  * @param array $query Query parameters as set by cake
 107:  * @return array
 108:  */
 109:     public function beforeFind(Model $Model, $query) {
 110:         if ($Model->findQueryType === 'threaded' && !isset($query['parent'])) {
 111:             $query['parent'] = $this->settings[$Model->alias]['parent'];
 112:         }
 113:         return $query;
 114:     }
 115: 
 116: /**
 117:  * Stores the record about to be deleted.
 118:  *
 119:  * This is used to delete child nodes in the afterDelete.
 120:  *
 121:  * @param Model $Model Model instance
 122:  * @param boolean $cascade
 123:  * @return boolean
 124:  */
 125:     public function beforeDelete(Model $Model, $cascade = true) {
 126:         extract($this->settings[$Model->alias]);
 127:         $data = $Model->find('first', array(
 128:             'conditions' => array($Model->escapeField($Model->primaryKey) => $Model->id),
 129:             'fields' => array($Model->escapeField($left), $Model->escapeField($right)),
 130:             'recursive' => -1));
 131:         if ($data) {
 132:             $this->_deletedRow[$Model->alias] = current($data);
 133:         }
 134:         return true;
 135:     }
 136: 
 137: /**
 138:  * After delete method.
 139:  *
 140:  * Will delete the current node and all children using the deleteAll method and sync the table
 141:  *
 142:  * @param Model $Model Model instance
 143:  * @return boolean true to continue, false to abort the delete
 144:  */
 145:     public function afterDelete(Model $Model) {
 146:         extract($this->settings[$Model->alias]);
 147:         $data = $this->_deletedRow[$Model->alias];
 148:         $this->_deletedRow[$Model->alias] = null;
 149: 
 150:         if (!$data[$right] || !$data[$left]) {
 151:             return true;
 152:         }
 153:         $diff = $data[$right] - $data[$left] + 1;
 154: 
 155:         if ($diff > 2) {
 156:             if (is_string($scope)) {
 157:                 $scope = array($scope);
 158:             }
 159:             $scope[][$Model->escapeField($left) . " BETWEEN ? AND ?"] = array($data[$left] + 1, $data[$right] - 1);
 160:             $Model->deleteAll($scope);
 161:         }
 162:         $this->_sync($Model, $diff, '-', '> ' . $data[$right]);
 163:         return true;
 164:     }
 165: 
 166: /**
 167:  * Before save method. Called before all saves
 168:  *
 169:  * Overridden to transparently manage setting the lft and rght fields if and only if the parent field is included in the
 170:  * parameters to be saved. For newly created nodes with NO parent the left and right field values are set directly by
 171:  * this method bypassing the setParent logic.
 172:  *
 173:  * @since         1.2
 174:  * @param Model $Model Model instance
 175:  * @return boolean true to continue, false to abort the save
 176:  */
 177:     public function beforeSave(Model $Model) {
 178:         extract($this->settings[$Model->alias]);
 179: 
 180:         $this->_addToWhitelist($Model, array($left, $right));
 181:         if (!$Model->id || !$Model->exists()) {
 182:             if (array_key_exists($parent, $Model->data[$Model->alias]) && $Model->data[$Model->alias][$parent]) {
 183:                 $parentNode = $Model->find('first', array(
 184:                     'conditions' => array($scope, $Model->escapeField() => $Model->data[$Model->alias][$parent]),
 185:                     'fields' => array($Model->primaryKey, $right), 'recursive' => $recursive
 186:                 ));
 187:                 if (!$parentNode) {
 188:                     return false;
 189:                 }
 190:                 list($parentNode) = array_values($parentNode);
 191:                 $Model->data[$Model->alias][$left] = 0;
 192:                 $Model->data[$Model->alias][$right] = 0;
 193:             } else {
 194:                 $edge = $this->_getMax($Model, $scope, $right, $recursive);
 195:                 $Model->data[$Model->alias][$left] = $edge + 1;
 196:                 $Model->data[$Model->alias][$right] = $edge + 2;
 197:             }
 198:         } elseif (array_key_exists($parent, $Model->data[$Model->alias])) {
 199:             if ($Model->data[$Model->alias][$parent] != $Model->field($parent)) {
 200:                 $this->settings[$Model->alias]['__parentChange'] = true;
 201:             }
 202:             if (!$Model->data[$Model->alias][$parent]) {
 203:                 $Model->data[$Model->alias][$parent] = null;
 204:                 $this->_addToWhitelist($Model, $parent);
 205:             } else {
 206:                 $values = $Model->find('first', array(
 207:                     'conditions' => array($scope, $Model->escapeField() => $Model->id),
 208:                     'fields' => array($Model->primaryKey, $parent, $left, $right), 'recursive' => $recursive)
 209:                 );
 210: 
 211:                 if ($values === false) {
 212:                     return false;
 213:                 }
 214:                 list($node) = array_values($values);
 215: 
 216:                 $parentNode = $Model->find('first', array(
 217:                     'conditions' => array($scope, $Model->escapeField() => $Model->data[$Model->alias][$parent]),
 218:                     'fields' => array($Model->primaryKey, $left, $right), 'recursive' => $recursive
 219:                 ));
 220:                 if (!$parentNode) {
 221:                     return false;
 222:                 }
 223:                 list($parentNode) = array_values($parentNode);
 224: 
 225:                 if (($node[$left] < $parentNode[$left]) && ($parentNode[$right] < $node[$right])) {
 226:                     return false;
 227:                 } elseif ($node[$Model->primaryKey] == $parentNode[$Model->primaryKey]) {
 228:                     return false;
 229:                 }
 230:             }
 231:         }
 232:         return true;
 233:     }
 234: 
 235: /**
 236:  * Get the number of child nodes
 237:  *
 238:  * If the direct parameter is set to true, only the direct children are counted (based upon the parent_id field)
 239:  * If false is passed for the id parameter, all top level nodes are counted, or all nodes are counted.
 240:  *
 241:  * @param Model $Model Model instance
 242:  * @param integer|string|boolean $id The ID of the record to read or false to read all top level nodes
 243:  * @param boolean $direct whether to count direct, or all, children
 244:  * @return integer number of child nodes
 245:  * @link http://book.cakephp.org/2.0/en/core-libraries/behaviors/tree.html#TreeBehavior::childCount
 246:  */
 247:     public function childCount(Model $Model, $id = null, $direct = false) {
 248:         if (is_array($id)) {
 249:             extract(array_merge(array('id' => null), $id));
 250:         }
 251:         if ($id === null && $Model->id) {
 252:             $id = $Model->id;
 253:         } elseif (!$id) {
 254:             $id = null;
 255:         }
 256:         extract($this->settings[$Model->alias]);
 257: 
 258:         if ($direct) {
 259:             return $Model->find('count', array('conditions' => array($scope, $Model->escapeField($parent) => $id)));
 260:         }
 261: 
 262:         if ($id === null) {
 263:             return $Model->find('count', array('conditions' => $scope));
 264:         } elseif ($Model->id === $id && isset($Model->data[$Model->alias][$left]) && isset($Model->data[$Model->alias][$right])) {
 265:             $data = $Model->data[$Model->alias];
 266:         } else {
 267:             $data = $Model->find('first', array('conditions' => array($scope, $Model->escapeField() => $id), 'recursive' => $recursive));
 268:             if (!$data) {
 269:                 return 0;
 270:             }
 271:             $data = $data[$Model->alias];
 272:         }
 273:         return ($data[$right] - $data[$left] - 1) / 2;
 274:     }
 275: 
 276: /**
 277:  * Get the child nodes of the current model
 278:  *
 279:  * If the direct parameter is set to true, only the direct children are returned (based upon the parent_id field)
 280:  * If false is passed for the id parameter, top level, or all (depending on direct parameter appropriate) are counted.
 281:  *
 282:  * @param Model $Model Model instance
 283:  * @param integer|string $id The ID of the record to read
 284:  * @param boolean $direct whether to return only the direct, or all, children
 285:  * @param string|array $fields Either a single string of a field name, or an array of field names
 286:  * @param string $order SQL ORDER BY conditions (e.g. "price DESC" or "name ASC") defaults to the tree order
 287:  * @param integer $limit SQL LIMIT clause, for calculating items per page.
 288:  * @param integer $page Page number, for accessing paged data
 289:  * @param integer $recursive The number of levels deep to fetch associated records
 290:  * @return array Array of child nodes
 291:  * @link http://book.cakephp.org/2.0/en/core-libraries/behaviors/tree.html#TreeBehavior::children
 292:  */
 293:     public function children(Model $Model, $id = null, $direct = false, $fields = null, $order = null, $limit = null, $page = 1, $recursive = null) {
 294:         if (is_array($id)) {
 295:             extract(array_merge(array('id' => null), $id));
 296:         }
 297:         $overrideRecursive = $recursive;
 298: 
 299:         if ($id === null && $Model->id) {
 300:             $id = $Model->id;
 301:         } elseif (!$id) {
 302:             $id = null;
 303:         }
 304: 
 305:         extract($this->settings[$Model->alias]);
 306: 
 307:         if (!is_null($overrideRecursive)) {
 308:             $recursive = $overrideRecursive;
 309:         }
 310:         if (!$order) {
 311:             $order = $Model->escapeField($left) . " asc";
 312:         }
 313:         if ($direct) {
 314:             $conditions = array($scope, $Model->escapeField($parent) => $id);
 315:             return $Model->find('all', compact('conditions', 'fields', 'order', 'limit', 'page', 'recursive'));
 316:         }
 317: 
 318:         if (!$id) {
 319:             $conditions = $scope;
 320:         } else {
 321:             $result = array_values((array)$Model->find('first', array(
 322:                 'conditions' => array($scope, $Model->escapeField() => $id),
 323:                 'fields' => array($left, $right),
 324:                 'recursive' => $recursive
 325:             )));
 326: 
 327:             if (empty($result) || !isset($result[0])) {
 328:                 return array();
 329:             }
 330:             $conditions = array($scope,
 331:                 $Model->escapeField($right) . ' <' => $result[0][$right],
 332:                 $Model->escapeField($left) . ' >' => $result[0][$left]
 333:             );
 334:         }
 335:         return $Model->find('all', compact('conditions', 'fields', 'order', 'limit', 'page', 'recursive'));
 336:     }
 337: 
 338: /**
 339:  * A convenience method for returning a hierarchical array used for HTML select boxes
 340:  *
 341:  * @param Model $Model Model instance
 342:  * @param string|array $conditions SQL conditions as a string or as an array('field' =>'value',...)
 343:  * @param string $keyPath A string path to the key, i.e. "{n}.Post.id"
 344:  * @param string $valuePath A string path to the value, i.e. "{n}.Post.title"
 345:  * @param string $spacer The character or characters which will be repeated
 346:  * @param integer $recursive The number of levels deep to fetch associated records
 347:  * @return array An associative array of records, where the id is the key, and the display field is the value
 348:  * @link http://book.cakephp.org/2.0/en/core-libraries/behaviors/tree.html#TreeBehavior::generateTreeList
 349:  */
 350:     public function generateTreeList(Model $Model, $conditions = null, $keyPath = null, $valuePath = null, $spacer = '_', $recursive = null) {
 351:         $overrideRecursive = $recursive;
 352:         extract($this->settings[$Model->alias]);
 353:         if (!is_null($overrideRecursive)) {
 354:             $recursive = $overrideRecursive;
 355:         }
 356: 
 357:         if ($keyPath == null && $valuePath == null && $Model->hasField($Model->displayField)) {
 358:             $fields = array($Model->primaryKey, $Model->displayField, $left, $right);
 359:         } else {
 360:             $fields = null;
 361:         }
 362: 
 363:         if ($keyPath == null) {
 364:             $keyPath = '{n}.' . $Model->alias . '.' . $Model->primaryKey;
 365:         }
 366: 
 367:         if ($valuePath == null) {
 368:             $valuePath = array('%s%s', '{n}.tree_prefix', '{n}.' . $Model->alias . '.' . $Model->displayField);
 369: 
 370:         } elseif (is_string($valuePath)) {
 371:             $valuePath = array('%s%s', '{n}.tree_prefix', $valuePath);
 372: 
 373:         } else {
 374:             array_unshift($valuePath, '%s' . $valuePath[0], '{n}.tree_prefix');
 375:         }
 376:         $order = $Model->escapeField($left) . " asc";
 377:         $results = $Model->find('all', compact('conditions', 'fields', 'order', 'recursive'));
 378:         $stack = array();
 379: 
 380:         foreach ($results as $i => $result) {
 381:             $count = count($stack);
 382:             while ($stack && ($stack[$count - 1] < $result[$Model->alias][$right])) {
 383:                 array_pop($stack);
 384:                 $count--;
 385:             }
 386:             $results[$i]['tree_prefix'] = str_repeat($spacer, $count);
 387:             $stack[] = $result[$Model->alias][$right];
 388:         }
 389:         if (empty($results)) {
 390:             return array();
 391:         }
 392:         return Hash::combine($results, $keyPath, $valuePath);
 393:     }
 394: 
 395: /**
 396:  * Get the parent node
 397:  *
 398:  * reads the parent id and returns this node
 399:  *
 400:  * @param Model $Model Model instance
 401:  * @param integer|string $id The ID of the record to read
 402:  * @param string|array $fields
 403:  * @param integer $recursive The number of levels deep to fetch associated records
 404:  * @return array|boolean Array of data for the parent node
 405:  * @link http://book.cakephp.org/2.0/en/core-libraries/behaviors/tree.html#TreeBehavior::getParentNode
 406:  */
 407:     public function getParentNode(Model $Model, $id = null, $fields = null, $recursive = null) {
 408:         if (is_array($id)) {
 409:             extract(array_merge(array('id' => null), $id));
 410:         }
 411:         $overrideRecursive = $recursive;
 412:         if (empty ($id)) {
 413:             $id = $Model->id;
 414:         }
 415:         extract($this->settings[$Model->alias]);
 416:         if (!is_null($overrideRecursive)) {
 417:             $recursive = $overrideRecursive;
 418:         }
 419:         $parentId = $Model->find('first', array('conditions' => array($Model->primaryKey => $id), 'fields' => array($parent), 'recursive' => -1));
 420: 
 421:         if ($parentId) {
 422:             $parentId = $parentId[$Model->alias][$parent];
 423:             $parent = $Model->find('first', array('conditions' => array($Model->escapeField() => $parentId), 'fields' => $fields, 'recursive' => $recursive));
 424: 
 425:             return $parent;
 426:         }
 427:         return false;
 428:     }
 429: 
 430: /**
 431:  * Get the path to the given node
 432:  *
 433:  * @param Model $Model Model instance
 434:  * @param integer|string $id The ID of the record to read
 435:  * @param string|array $fields Either a single string of a field name, or an array of field names
 436:  * @param integer $recursive The number of levels deep to fetch associated records
 437:  * @return array Array of nodes from top most parent to current node
 438:  * @link http://book.cakephp.org/2.0/en/core-libraries/behaviors/tree.html#TreeBehavior::getPath
 439:  */
 440:     public function getPath(Model $Model, $id = null, $fields = null, $recursive = null) {
 441:         if (is_array($id)) {
 442:             extract(array_merge(array('id' => null), $id));
 443:         }
 444:         $overrideRecursive = $recursive;
 445:         if (empty ($id)) {
 446:             $id = $Model->id;
 447:         }
 448:         extract($this->settings[$Model->alias]);
 449:         if (!is_null($overrideRecursive)) {
 450:             $recursive = $overrideRecursive;
 451:         }
 452:         $result = $Model->find('first', array('conditions' => array($Model->escapeField() => $id), 'fields' => array($left, $right), 'recursive' => $recursive));
 453:         if ($result) {
 454:             $result = array_values($result);
 455:         } else {
 456:             return null;
 457:         }
 458:         $item = $result[0];
 459:         $results = $Model->find('all', array(
 460:             'conditions' => array($scope, $Model->escapeField($left) . ' <=' => $item[$left], $Model->escapeField($right) . ' >=' => $item[$right]),
 461:             'fields' => $fields, 'order' => array($Model->escapeField($left) => 'asc'), 'recursive' => $recursive
 462:         ));
 463:         return $results;
 464:     }
 465: 
 466: /**
 467:  * Reorder the node without changing the parent.
 468:  *
 469:  * If the node is the last child, or is a top level node with no subsequent node this method will return false
 470:  *
 471:  * @param Model $Model Model instance
 472:  * @param integer|string $id The ID of the record to move
 473:  * @param integer|boolean $number how many places to move the node or true to move to last position
 474:  * @return boolean true on success, false on failure
 475:  * @link http://book.cakephp.org/2.0/en/core-libraries/behaviors/tree.html#TreeBehavior::moveDown
 476:  */
 477:     public function moveDown(Model $Model, $id = null, $number = 1) {
 478:         if (is_array($id)) {
 479:             extract(array_merge(array('id' => null), $id));
 480:         }
 481:         if (!$number) {
 482:             return false;
 483:         }
 484:         if (empty ($id)) {
 485:             $id = $Model->id;
 486:         }
 487:         extract($this->settings[$Model->alias]);
 488:         list($node) = array_values($Model->find('first', array(
 489:             'conditions' => array($scope, $Model->escapeField() => $id),
 490:             'fields' => array($Model->primaryKey, $left, $right, $parent), 'recursive' => $recursive
 491:         )));
 492:         if ($node[$parent]) {
 493:             list($parentNode) = array_values($Model->find('first', array(
 494:                 'conditions' => array($scope, $Model->escapeField() => $node[$parent]),
 495:                 'fields' => array($Model->primaryKey, $left, $right), 'recursive' => $recursive
 496:             )));
 497:             if (($node[$right] + 1) == $parentNode[$right]) {
 498:                 return false;
 499:             }
 500:         }
 501:         $nextNode = $Model->find('first', array(
 502:             'conditions' => array($scope, $Model->escapeField($left) => ($node[$right] + 1)),
 503:             'fields' => array($Model->primaryKey, $left, $right), 'recursive' => $recursive)
 504:         );
 505:         if ($nextNode) {
 506:             list($nextNode) = array_values($nextNode);
 507:         } else {
 508:             return false;
 509:         }
 510:         $edge = $this->_getMax($Model, $scope, $right, $recursive);
 511:         $this->_sync($Model, $edge - $node[$left] + 1, '+', 'BETWEEN ' . $node[$left] . ' AND ' . $node[$right]);
 512:         $this->_sync($Model, $nextNode[$left] - $node[$left], '-', 'BETWEEN ' . $nextNode[$left] . ' AND ' . $nextNode[$right]);
 513:         $this->_sync($Model, $edge - $node[$left] - ($nextNode[$right] - $nextNode[$left]), '-', '> ' . $edge);
 514: 
 515:         if (is_int($number)) {
 516:             $number--;
 517:         }
 518:         if ($number) {
 519:             $this->moveDown($Model, $id, $number);
 520:         }
 521:         return true;
 522:     }
 523: 
 524: /**
 525:  * Reorder the node without changing the parent.
 526:  *
 527:  * If the node is the first child, or is a top level node with no previous node this method will return false
 528:  *
 529:  * @param Model $Model Model instance
 530:  * @param integer|string $id The ID of the record to move
 531:  * @param integer|boolean $number how many places to move the node, or true to move to first position
 532:  * @return boolean true on success, false on failure
 533:  * @link http://book.cakephp.org/2.0/en/core-libraries/behaviors/tree.html#TreeBehavior::moveUp
 534:  */
 535:     public function moveUp(Model $Model, $id = null, $number = 1) {
 536:         if (is_array($id)) {
 537:             extract(array_merge(array('id' => null), $id));
 538:         }
 539:         if (!$number) {
 540:             return false;
 541:         }
 542:         if (empty ($id)) {
 543:             $id = $Model->id;
 544:         }
 545:         extract($this->settings[$Model->alias]);
 546:         list($node) = array_values($Model->find('first', array(
 547:             'conditions' => array($scope, $Model->escapeField() => $id),
 548:             'fields' => array($Model->primaryKey, $left, $right, $parent), 'recursive' => $recursive
 549:         )));
 550:         if ($node[$parent]) {
 551:             list($parentNode) = array_values($Model->find('first', array(
 552:                 'conditions' => array($scope, $Model->escapeField() => $node[$parent]),
 553:                 'fields' => array($Model->primaryKey, $left, $right), 'recursive' => $recursive
 554:             )));
 555:             if (($node[$left] - 1) == $parentNode[$left]) {
 556:                 return false;
 557:             }
 558:         }
 559:         $previousNode = $Model->find('first', array(
 560:             'conditions' => array($scope, $Model->escapeField($right) => ($node[$left] - 1)),
 561:             'fields' => array($Model->primaryKey, $left, $right),
 562:             'recursive' => $recursive
 563:         ));
 564: 
 565:         if ($previousNode) {
 566:             list($previousNode) = array_values($previousNode);
 567:         } else {
 568:             return false;
 569:         }
 570:         $edge = $this->_getMax($Model, $scope, $right, $recursive);
 571:         $this->_sync($Model, $edge - $previousNode[$left] + 1, '+', 'BETWEEN ' . $previousNode[$left] . ' AND ' . $previousNode[$right]);
 572:         $this->_sync($Model, $node[$left] - $previousNode[$left], '-', 'BETWEEN ' . $node[$left] . ' AND ' . $node[$right]);
 573:         $this->_sync($Model, $edge - $previousNode[$left] - ($node[$right] - $node[$left]), '-', '> ' . $edge);
 574:         if (is_int($number)) {
 575:             $number--;
 576:         }
 577:         if ($number) {
 578:             $this->moveUp($Model, $id, $number);
 579:         }
 580:         return true;
 581:     }
 582: 
 583: /**
 584:  * Recover a corrupted tree
 585:  *
 586:  * The mode parameter is used to specify the source of info that is valid/correct. The opposite source of data
 587:  * will be populated based upon that source of info. E.g. if the MPTT fields are corrupt or empty, with the $mode
 588:  * 'parent' the values of the parent_id field will be used to populate the left and right fields. The missingParentAction
 589:  * parameter only applies to "parent" mode and determines what to do if the parent field contains an id that is not present.
 590:  *
 591:  * @param Model $Model Model instance
 592:  * @param string $mode parent or tree
 593:  * @param string|integer $missingParentAction 'return' to do nothing and return, 'delete' to
 594:  * delete, or the id of the parent to set as the parent_id
 595:  * @return boolean true on success, false on failure
 596:  * @link http://book.cakephp.org/2.0/en/core-libraries/behaviors/tree.html#TreeBehavior::recover
 597:  */
 598:     public function recover(Model $Model, $mode = 'parent', $missingParentAction = null) {
 599:         if (is_array($mode)) {
 600:             extract(array_merge(array('mode' => 'parent'), $mode));
 601:         }
 602:         extract($this->settings[$Model->alias]);
 603:         $Model->recursive = $recursive;
 604:         if ($mode === 'parent') {
 605:             $Model->bindModel(array('belongsTo' => array('VerifyParent' => array(
 606:                 'className' => $Model->name,
 607:                 'foreignKey' => $parent,
 608:                 'fields' => array($Model->primaryKey, $left, $right, $parent),
 609:             ))));
 610:             $missingParents = $Model->find('list', array(
 611:                 'recursive' => 0,
 612:                 'conditions' => array($scope, array(
 613:                     'NOT' => array($Model->escapeField($parent) => null), $Model->VerifyParent->escapeField() => null
 614:                 ))
 615:             ));
 616:             $Model->unbindModel(array('belongsTo' => array('VerifyParent')));
 617:             if ($missingParents) {
 618:                 if ($missingParentAction === 'return') {
 619:                     foreach ($missingParents as $id => $display) {
 620:                         $this->errors[] = 'cannot find the parent for ' . $Model->alias . ' with id ' . $id . '(' . $display . ')';
 621:                     }
 622:                     return false;
 623:                 } elseif ($missingParentAction === 'delete') {
 624:                     $Model->deleteAll(array($Model->escapeField($Model->primaryKey) => array_flip($missingParents)), false);
 625:                 } else {
 626:                     $Model->updateAll(array($Model->escapeField($parent) => $missingParentAction), array($Model->escapeField($Model->primaryKey) => array_flip($missingParents)));
 627:                 }
 628:             }
 629:             $count = 1;
 630:             foreach ($Model->find('all', array('conditions' => $scope, 'fields' => array($Model->primaryKey), 'order' => $left)) as $array) {
 631:                 $lft = $count++;
 632:                 $rght = $count++;
 633:                 $Model->create(false);
 634:                 $Model->id = $array[$Model->alias][$Model->primaryKey];
 635:                 $Model->save(array($left => $lft, $right => $rght), array('callbacks' => false, 'validate' => false));
 636:             }
 637:             foreach ($Model->find('all', array('conditions' => $scope, 'fields' => array($Model->primaryKey, $parent), 'order' => $left)) as $array) {
 638:                 $Model->create(false);
 639:                 $Model->id = $array[$Model->alias][$Model->primaryKey];
 640:                 $this->_setParent($Model, $array[$Model->alias][$parent]);
 641:             }
 642:         } else {
 643:             $db = ConnectionManager::getDataSource($Model->useDbConfig);
 644:             foreach ($Model->find('all', array('conditions' => $scope, 'fields' => array($Model->primaryKey, $parent), 'order' => $left)) as $array) {
 645:                 $path = $this->getPath($Model, $array[$Model->alias][$Model->primaryKey]);
 646:                 if ($path == null || count($path) < 2) {
 647:                     $parentId = null;
 648:                 } else {
 649:                     $parentId = $path[count($path) - 2][$Model->alias][$Model->primaryKey];
 650:                 }
 651:                 $Model->updateAll(array($parent => $db->value($parentId, $parent)), array($Model->escapeField() => $array[$Model->alias][$Model->primaryKey]));
 652:             }
 653:         }
 654:         return true;
 655:     }
 656: 
 657: /**
 658:  * Reorder method.
 659:  *
 660:  * Reorders the nodes (and child nodes) of the tree according to the field and direction specified in the parameters.
 661:  * This method does not change the parent of any node.
 662:  *
 663:  * Requires a valid tree, by default it verifies the tree before beginning.
 664:  *
 665:  * Options:
 666:  *
 667:  * - 'id' id of record to use as top node for reordering
 668:  * - 'field' Which field to use in reordering defaults to displayField
 669:  * - 'order' Direction to order either DESC or ASC (defaults to ASC)
 670:  * - 'verify' Whether or not to verify the tree before reorder. defaults to true.
 671:  *
 672:  * @param Model $Model Model instance
 673:  * @param array $options array of options to use in reordering.
 674:  * @return boolean true on success, false on failure
 675:  * @link http://book.cakephp.org/2.0/en/core-libraries/behaviors/tree.html#TreeBehavior::reorder
 676:  */
 677:     public function reorder(Model $Model, $options = array()) {
 678:         $options = array_merge(array('id' => null, 'field' => $Model->displayField, 'order' => 'ASC', 'verify' => true), $options);
 679:         extract($options);
 680:         if ($verify && !$this->verify($Model)) {
 681:             return false;
 682:         }
 683:         $verify = false;
 684:         extract($this->settings[$Model->alias]);
 685:         $fields = array($Model->primaryKey, $field, $left, $right);
 686:         $sort = $field . ' ' . $order;
 687:         $nodes = $this->children($Model, $id, true, $fields, $sort, null, null, $recursive);
 688: 
 689:         $cacheQueries = $Model->cacheQueries;
 690:         $Model->cacheQueries = false;
 691:         if ($nodes) {
 692:             foreach ($nodes as $node) {
 693:                 $id = $node[$Model->alias][$Model->primaryKey];
 694:                 $this->moveDown($Model, $id, true);
 695:                 if ($node[$Model->alias][$left] != $node[$Model->alias][$right] - 1) {
 696:                     $this->reorder($Model, compact('id', 'field', 'order', 'verify'));
 697:                 }
 698:             }
 699:         }
 700:         $Model->cacheQueries = $cacheQueries;
 701:         return true;
 702:     }
 703: 
 704: /**
 705:  * Remove the current node from the tree, and reparent all children up one level.
 706:  *
 707:  * If the parameter delete is false, the node will become a new top level node. Otherwise the node will be deleted
 708:  * after the children are reparented.
 709:  *
 710:  * @param Model $Model Model instance
 711:  * @param integer|string $id The ID of the record to remove
 712:  * @param boolean $delete whether to delete the node after reparenting children (if any)
 713:  * @return boolean true on success, false on failure
 714:  * @link http://book.cakephp.org/2.0/en/core-libraries/behaviors/tree.html#TreeBehavior::removeFromTree
 715:  */
 716:     public function removeFromTree(Model $Model, $id = null, $delete = false) {
 717:         if (is_array($id)) {
 718:             extract(array_merge(array('id' => null), $id));
 719:         }
 720:         extract($this->settings[$Model->alias]);
 721: 
 722:         list($node) = array_values($Model->find('first', array(
 723:             'conditions' => array($scope, $Model->escapeField() => $id),
 724:             'fields' => array($Model->primaryKey, $left, $right, $parent),
 725:             'recursive' => $recursive
 726:         )));
 727: 
 728:         if ($node[$right] == $node[$left] + 1) {
 729:             if ($delete) {
 730:                 return $Model->delete($id);
 731:             } else {
 732:                 $Model->id = $id;
 733:                 return $Model->saveField($parent, null);
 734:             }
 735:         } elseif ($node[$parent]) {
 736:             list($parentNode) = array_values($Model->find('first', array(
 737:                 'conditions' => array($scope, $Model->escapeField() => $node[$parent]),
 738:                 'fields' => array($Model->primaryKey, $left, $right),
 739:                 'recursive' => $recursive
 740:             )));
 741:         } else {
 742:             $parentNode[$right] = $node[$right] + 1;
 743:         }
 744: 
 745:         $db = ConnectionManager::getDataSource($Model->useDbConfig);
 746:         $Model->updateAll(
 747:             array($parent => $db->value($node[$parent], $parent)),
 748:             array($Model->escapeField($parent) => $node[$Model->primaryKey])
 749:         );
 750:         $this->_sync($Model, 1, '-', 'BETWEEN ' . ($node[$left] + 1) . ' AND ' . ($node[$right] - 1));
 751:         $this->_sync($Model, 2, '-', '> ' . ($node[$right]));
 752:         $Model->id = $id;
 753: 
 754:         if ($delete) {
 755:             $Model->updateAll(
 756:                 array(
 757:                     $Model->escapeField($left) => 0,
 758:                     $Model->escapeField($right) => 0,
 759:                     $Model->escapeField($parent) => null
 760:                 ),
 761:                 array($Model->escapeField() => $id)
 762:             );
 763:             return $Model->delete($id);
 764:         } else {
 765:             $edge = $this->_getMax($Model, $scope, $right, $recursive);
 766:             if ($node[$right] == $edge) {
 767:                 $edge = $edge - 2;
 768:             }
 769:             $Model->id = $id;
 770:             return $Model->save(
 771:                 array($left => $edge + 1, $right => $edge + 2, $parent => null),
 772:                 array('callbacks' => false, 'validate' => false)
 773:             );
 774:         }
 775:     }
 776: 
 777: /**
 778:  * Check if the current tree is valid.
 779:  *
 780:  * Returns true if the tree is valid otherwise an array of (type, incorrect left/right index, message)
 781:  *
 782:  * @param Model $Model Model instance
 783:  * @return mixed true if the tree is valid or empty, otherwise an array of (error type [index, node],
 784:  *  [incorrect left/right index,node id], message)
 785:  * @link http://book.cakephp.org/2.0/en/core-libraries/behaviors/tree.html#TreeBehavior::verify
 786:  */
 787:     public function verify(Model $Model) {
 788:         extract($this->settings[$Model->alias]);
 789:         if (!$Model->find('count', array('conditions' => $scope))) {
 790:             return true;
 791:         }
 792:         $min = $this->_getMin($Model, $scope, $left, $recursive);
 793:         $edge = $this->_getMax($Model, $scope, $right, $recursive);
 794:         $errors = array();
 795: 
 796:         for ($i = $min; $i <= $edge; $i++) {
 797:             $count = $Model->find('count', array('conditions' => array(
 798:                 $scope, 'OR' => array($Model->escapeField($left) => $i, $Model->escapeField($right) => $i)
 799:             )));
 800:             if ($count != 1) {
 801:                 if ($count == 0) {
 802:                     $errors[] = array('index', $i, 'missing');
 803:                 } else {
 804:                     $errors[] = array('index', $i, 'duplicate');
 805:                 }
 806:             }
 807:         }
 808:         $node = $Model->find('first', array('conditions' => array($scope, $Model->escapeField($right) . '< ' . $Model->escapeField($left)), 'recursive' => 0));
 809:         if ($node) {
 810:             $errors[] = array('node', $node[$Model->alias][$Model->primaryKey], 'left greater than right.');
 811:         }
 812: 
 813:         $Model->bindModel(array('belongsTo' => array('VerifyParent' => array(
 814:             'className' => $Model->name,
 815:             'foreignKey' => $parent,
 816:             'fields' => array($Model->primaryKey, $left, $right, $parent)
 817:         ))));
 818: 
 819:         foreach ($Model->find('all', array('conditions' => $scope, 'recursive' => 0)) as $instance) {
 820:             if (is_null($instance[$Model->alias][$left]) || is_null($instance[$Model->alias][$right])) {
 821:                 $errors[] = array('node', $instance[$Model->alias][$Model->primaryKey],
 822:                     'has invalid left or right values');
 823:             } elseif ($instance[$Model->alias][$left] == $instance[$Model->alias][$right]) {
 824:                 $errors[] = array('node', $instance[$Model->alias][$Model->primaryKey],
 825:                     'left and right values identical');
 826:             } elseif ($instance[$Model->alias][$parent]) {
 827:                 if (!$instance['VerifyParent'][$Model->primaryKey]) {
 828:                     $errors[] = array('node', $instance[$Model->alias][$Model->primaryKey],
 829:                         'The parent node ' . $instance[$Model->alias][$parent] . ' doesn\'t exist');
 830:                 } elseif ($instance[$Model->alias][$left] < $instance['VerifyParent'][$left]) {
 831:                     $errors[] = array('node', $instance[$Model->alias][$Model->primaryKey],
 832:                         'left less than parent (node ' . $instance['VerifyParent'][$Model->primaryKey] . ').');
 833:                 } elseif ($instance[$Model->alias][$right] > $instance['VerifyParent'][$right]) {
 834:                     $errors[] = array('node', $instance[$Model->alias][$Model->primaryKey],
 835:                         'right greater than parent (node ' . $instance['VerifyParent'][$Model->primaryKey] . ').');
 836:                 }
 837:             } elseif ($Model->find('count', array('conditions' => array($scope, $Model->escapeField($left) . ' <' => $instance[$Model->alias][$left], $Model->escapeField($right) . ' >' => $instance[$Model->alias][$right]), 'recursive' => 0))) {
 838:                 $errors[] = array('node', $instance[$Model->alias][$Model->primaryKey], 'The parent field is blank, but has a parent');
 839:             }
 840:         }
 841:         if ($errors) {
 842:             return $errors;
 843:         }
 844:         return true;
 845:     }
 846: 
 847: /**
 848:  * Sets the parent of the given node
 849:  *
 850:  * The force parameter is used to override the "don't change the parent to the current parent" logic in the event
 851:  * of recovering a corrupted table, or creating new nodes. Otherwise it should always be false. In reality this
 852:  * method could be private, since calling save with parent_id set also calls setParent
 853:  *
 854:  * @param Model $Model Model instance
 855:  * @param integer|string $parentId
 856:  * @param boolean $created
 857:  * @return boolean true on success, false on failure
 858:  */
 859:     protected function _setParent(Model $Model, $parentId = null, $created = false) {
 860:         extract($this->settings[$Model->alias]);
 861:         list($node) = array_values($Model->find('first', array(
 862:             'conditions' => array($scope, $Model->escapeField() => $Model->id),
 863:             'fields' => array($Model->primaryKey, $parent, $left, $right),
 864:             'recursive' => $recursive
 865:         )));
 866:         $edge = $this->_getMax($Model, $scope, $right, $recursive, $created);
 867: 
 868:         if (empty ($parentId)) {
 869:             $this->_sync($Model, $edge - $node[$left] + 1, '+', 'BETWEEN ' . $node[$left] . ' AND ' . $node[$right], $created);
 870:             $this->_sync($Model, $node[$right] - $node[$left] + 1, '-', '> ' . $node[$left], $created);
 871:         } else {
 872:             $values = $Model->find('first', array(
 873:                 'conditions' => array($scope, $Model->escapeField() => $parentId),
 874:                 'fields' => array($Model->primaryKey, $left, $right),
 875:                 'recursive' => $recursive
 876:             ));
 877: 
 878:             if ($values === false) {
 879:                 return false;
 880:             }
 881:             $parentNode = array_values($values);
 882: 
 883:             if (empty($parentNode) || empty($parentNode[0])) {
 884:                 return false;
 885:             }
 886:             $parentNode = $parentNode[0];
 887: 
 888:             if (($Model->id == $parentId)) {
 889:                 return false;
 890:             } elseif (($node[$left] < $parentNode[$left]) && ($parentNode[$right] < $node[$right])) {
 891:                 return false;
 892:             }
 893:             if (empty($node[$left]) && empty($node[$right])) {
 894:                 $this->_sync($Model, 2, '+', '>= ' . $parentNode[$right], $created);
 895:                 $result = $Model->save(
 896:                     array($left => $parentNode[$right], $right => $parentNode[$right] + 1, $parent => $parentId),
 897:                     array('validate' => false, 'callbacks' => false)
 898:                 );
 899:                 $Model->data = $result;
 900:             } else {
 901:                 $this->_sync($Model, $edge - $node[$left] + 1, '+', 'BETWEEN ' . $node[$left] . ' AND ' . $node[$right], $created);
 902:                 $diff = $node[$right] - $node[$left] + 1;
 903: 
 904:                 if ($node[$left] > $parentNode[$left]) {
 905:                     if ($node[$right] < $parentNode[$right]) {
 906:                         $this->_sync($Model, $diff, '-', 'BETWEEN ' . $node[$right] . ' AND ' . ($parentNode[$right] - 1), $created);
 907:                         $this->_sync($Model, $edge - $parentNode[$right] + $diff + 1, '-', '> ' . $edge, $created);
 908:                     } else {
 909:                         $this->_sync($Model, $diff, '+', 'BETWEEN ' . $parentNode[$right] . ' AND ' . $node[$right], $created);
 910:                         $this->_sync($Model, $edge - $parentNode[$right] + 1, '-', '> ' . $edge, $created);
 911:                     }
 912:                 } else {
 913:                     $this->_sync($Model, $diff, '-', 'BETWEEN ' . $node[$right] . ' AND ' . ($parentNode[$right] - 1), $created);
 914:                     $this->_sync($Model, $edge - $parentNode[$right] + $diff + 1, '-', '> ' . $edge, $created);
 915:                 }
 916:             }
 917:         }
 918:         return true;
 919:     }
 920: 
 921: /**
 922:  * get the maximum index value in the table.
 923:  *
 924:  * @param Model $Model
 925:  * @param string $scope
 926:  * @param string $right
 927:  * @param integer $recursive
 928:  * @param boolean $created
 929:  * @return integer
 930:  */
 931:     protected function _getMax(Model $Model, $scope, $right, $recursive = -1, $created = false) {
 932:         $db = ConnectionManager::getDataSource($Model->useDbConfig);
 933:         if ($created) {
 934:             if (is_string($scope)) {
 935:                 $scope .= " AND " . $Model->escapeField() . " <> ";
 936:                 $scope .= $db->value($Model->id, $Model->getColumnType($Model->primaryKey));
 937:             } else {
 938:                 $scope['NOT'][$Model->alias . '.' . $Model->primaryKey] = $Model->id;
 939:             }
 940:         }
 941:         $name = $Model->escapeField($right);
 942:         list($edge) = array_values($Model->find('first', array(
 943:             'conditions' => $scope,
 944:             'fields' => $db->calculate($Model, 'max', array($name, $right)),
 945:             'recursive' => $recursive
 946:         )));
 947:         return (empty($edge[$right])) ? 0 : $edge[$right];
 948:     }
 949: 
 950: /**
 951:  * get the minimum index value in the table.
 952:  *
 953:  * @param Model $Model
 954:  * @param string $scope
 955:  * @param string $left
 956:  * @param integer $recursive
 957:  * @return integer
 958:  */
 959:     protected function _getMin(Model $Model, $scope, $left, $recursive = -1) {
 960:         $db = ConnectionManager::getDataSource($Model->useDbConfig);
 961:         $name = $Model->escapeField($left);
 962:         list($edge) = array_values($Model->find('first', array(
 963:             'conditions' => $scope,
 964:             'fields' => $db->calculate($Model, 'min', array($name, $left)),
 965:             'recursive' => $recursive
 966:         )));
 967:         return (empty($edge[$left])) ? 0 : $edge[$left];
 968:     }
 969: 
 970: /**
 971:  * Table sync method.
 972:  *
 973:  * Handles table sync operations, Taking account of the behavior scope.
 974:  *
 975:  * @param Model $Model
 976:  * @param integer $shift
 977:  * @param string $dir
 978:  * @param array $conditions
 979:  * @param boolean $created
 980:  * @param string $field
 981:  * @return void
 982:  */
 983:     protected function _sync(Model $Model, $shift, $dir = '+', $conditions = array(), $created = false, $field = 'both') {
 984:         $ModelRecursive = $Model->recursive;
 985:         extract($this->settings[$Model->alias]);
 986:         $Model->recursive = $recursive;
 987: 
 988:         if ($field === 'both') {
 989:             $this->_sync($Model, $shift, $dir, $conditions, $created, $left);
 990:             $field = $right;
 991:         }
 992:         if (is_string($conditions)) {
 993:             $conditions = array($Model->escapeField($field) . " {$conditions}");
 994:         }
 995:         if (($scope !== '1 = 1' && $scope !== true) && $scope) {
 996:             $conditions[] = $scope;
 997:         }
 998:         if ($created) {
 999:             $conditions['NOT'][$Model->escapeField()] = $Model->id;
1000:         }
1001:         $Model->updateAll(array($Model->escapeField($field) => $Model->escapeField($field) . ' ' . $dir . ' ' . $shift), $conditions);
1002:         $Model->recursive = $ModelRecursive;
1003:     }
1004: 
1005: }
1006: 
OpenHub
Rackspace
Rackspace
  • Business Solutions
  • Showcase
  • Documentation
  • Book
  • API
  • Videos
  • Reporting Security Issues
  • Privacy Policy
  • Logos & Trademarks
  • Community
  • Get Involved
  • Issues (GitHub)
  • Bakery
  • Featured Resources
  • Training
  • Meetups
  • My CakePHP
  • CakeFest
  • Newsletter
  • Linkedin
  • YouTube
  • Facebook
  • Twitter
  • Mastodon
  • Help & Support
  • Forum
  • Stack Overflow
  • Slack
  • Paid Support

Generated using CakePHP API Docs