pictcode / lib / Cake / Model / Behavior / TreeBehavior.php @ 9d2f0219
履歴 | 表示 | アノテート | ダウンロード (41.761 KB)
| 1 | 635eef61 | spyder1211 | <?php
|
|---|---|---|---|
| 2 | /**
|
||
| 3 | * Tree behavior class.
|
||
| 4 | *
|
||
| 5 | * Enables a model object to act as a node-based tree.
|
||
| 6 | *
|
||
| 7 | * CakePHP : Rapid Development Framework (http://cakephp.org)
|
||
| 8 | * Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
|
||
| 9 | *
|
||
| 10 | * Licensed under The MIT License
|
||
| 11 | * For full copyright and license information, please see the LICENSE.txt
|
||
| 12 | * Redistributions of files must retain the above copyright notice.
|
||
| 13 | *
|
||
| 14 | * @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
|
||
| 15 | * @link http://cakephp.org CakePHP Project
|
||
| 16 | * @package Cake.Model.Behavior
|
||
| 17 | * @since CakePHP v 1.2.0.4487
|
||
| 18 | * @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||
| 19 | */
|
||
| 20 | |||
| 21 | App::uses('ModelBehavior', 'Model'); |
||
| 22 | |||
| 23 | /**
|
||
| 24 | * Tree Behavior.
|
||
| 25 | *
|
||
| 26 | * Enables a model object to act as a node-based tree. Using Modified Preorder Tree Traversal
|
||
| 27 | *
|
||
| 28 | * @see http://en.wikipedia.org/wiki/Tree_traversal
|
||
| 29 | * @package Cake.Model.Behavior
|
||
| 30 | * @link http://book.cakephp.org/2.0/en/core-libraries/behaviors/tree.html
|
||
| 31 | */
|
||
| 32 | class TreeBehavior extends ModelBehavior { |
||
| 33 | |||
| 34 | /**
|
||
| 35 | * Errors
|
||
| 36 | *
|
||
| 37 | * @var array
|
||
| 38 | */
|
||
| 39 | public $errors = array(); |
||
| 40 | |||
| 41 | /**
|
||
| 42 | * Defaults
|
||
| 43 | *
|
||
| 44 | * @var array
|
||
| 45 | */
|
||
| 46 | protected $_defaults = array( |
||
| 47 | 'parent' => 'parent_id', 'left' => 'lft', 'right' => 'rght', 'level' => null, |
||
| 48 | 'scope' => '1 = 1', 'type' => 'nested', '__parentChange' => false, 'recursive' => -1 |
||
| 49 | ); |
||
| 50 | |||
| 51 | /**
|
||
| 52 | * Used to preserve state between delete callbacks.
|
||
| 53 | *
|
||
| 54 | * @var array
|
||
| 55 | */
|
||
| 56 | protected $_deletedRow = array(); |
||
| 57 | |||
| 58 | /**
|
||
| 59 | * Initiate Tree behavior
|
||
| 60 | *
|
||
| 61 | * @param Model $Model using this behavior of model
|
||
| 62 | * @param array $config array of configuration settings.
|
||
| 63 | * @return void
|
||
| 64 | */
|
||
| 65 | public function setup(Model $Model, $config = array()) { |
||
| 66 | if (isset($config[0])) { |
||
| 67 | $config['type'] = $config[0]; |
||
| 68 | unset($config[0]); |
||
| 69 | } |
||
| 70 | $settings = $config + $this->_defaults; |
||
| 71 | |||
| 72 | if (in_array($settings['scope'], $Model->getAssociated('belongsTo'))) { |
||
| 73 | $data = $Model->getAssociated($settings['scope']); |
||
| 74 | $Parent = $Model->{$settings['scope']}; |
||
| 75 | $settings['scope'] = $Model->escapeField($data['foreignKey']) . ' = ' . $Parent->escapeField(); |
||
| 76 | $settings['recursive'] = 0; |
||
| 77 | } |
||
| 78 | $this->settings[$Model->alias] = $settings; |
||
| 79 | } |
||
| 80 | |||
| 81 | /**
|
||
| 82 | * After save method. Called after all saves
|
||
| 83 | *
|
||
| 84 | * Overridden to transparently manage setting the lft and rght fields if and only if the parent field is included in the
|
||
| 85 | * parameters to be saved.
|
||
| 86 | *
|
||
| 87 | * @param Model $Model Model using this behavior.
|
||
| 88 | * @param bool $created indicates whether the node just saved was created or updated
|
||
| 89 | * @param array $options Options passed from Model::save().
|
||
| 90 | * @return bool true on success, false on failure
|
||
| 91 | */
|
||
| 92 | public function afterSave(Model $Model, $created, $options = array()) { |
||
| 93 | extract($this->settings[$Model->alias]); |
||
| 94 | if ($created) { |
||
| 95 | if ((isset($Model->data[$Model->alias][$parent])) && $Model->data[$Model->alias][$parent]) { |
||
| 96 | return $this->_setParent($Model, $Model->data[$Model->alias][$parent], $created); |
||
| 97 | } |
||
| 98 | } elseif ($this->settings[$Model->alias]['__parentChange']) { |
||
| 99 | $this->settings[$Model->alias]['__parentChange'] = false; |
||
| 100 | if ($level) { |
||
| 101 | $this->_setChildrenLevel($Model, $Model->id); |
||
| 102 | } |
||
| 103 | return $this->_setParent($Model, $Model->data[$Model->alias][$parent]); |
||
| 104 | } |
||
| 105 | } |
||
| 106 | |||
| 107 | /**
|
||
| 108 | * Set level for descendents.
|
||
| 109 | *
|
||
| 110 | * @param Model $Model Model using this behavior.
|
||
| 111 | * @param int|string $id Record ID
|
||
| 112 | * @return void
|
||
| 113 | */
|
||
| 114 | protected function _setChildrenLevel(Model $Model, $id) { |
||
| 115 | $settings = $Model->Behaviors->Tree->settings[$Model->alias]; |
||
| 116 | $primaryKey = $Model->primaryKey; |
||
| 117 | $depths = array($id => (int)$Model->data[$Model->alias][$settings['level']]); |
||
| 118 | |||
| 119 | $children = $Model->children( |
||
| 120 | $id,
|
||
| 121 | false,
|
||
| 122 | array($primaryKey, $settings['parent'], $settings['level']), |
||
| 123 | $settings['left'], |
||
| 124 | null,
|
||
| 125 | 1,
|
||
| 126 | -1
|
||
| 127 | ); |
||
| 128 | |||
| 129 | foreach ($children as $node) { |
||
| 130 | $parentIdValue = $node[$Model->alias][$settings['parent']]; |
||
| 131 | $depth = (int)$depths[$parentIdValue] + 1; |
||
| 132 | $depths[$node[$Model->alias][$primaryKey]] = $depth; |
||
| 133 | |||
| 134 | $Model->updateAll(
|
||
| 135 | array($Model->escapeField($settings['level']) => $depth), |
||
| 136 | array($Model->escapeField($primaryKey) => $node[$Model->alias][$primaryKey]) |
||
| 137 | ); |
||
| 138 | } |
||
| 139 | } |
||
| 140 | |||
| 141 | /**
|
||
| 142 | * Runs before a find() operation
|
||
| 143 | *
|
||
| 144 | * @param Model $Model Model using the behavior
|
||
| 145 | * @param array $query Query parameters as set by cake
|
||
| 146 | * @return array
|
||
| 147 | */
|
||
| 148 | public function beforeFind(Model $Model, $query) { |
||
| 149 | if ($Model->findQueryType === 'threaded' && !isset($query['parent'])) { |
||
| 150 | $query['parent'] = $this->settings[$Model->alias]['parent']; |
||
| 151 | } |
||
| 152 | return $query; |
||
| 153 | } |
||
| 154 | |||
| 155 | /**
|
||
| 156 | * Stores the record about to be deleted.
|
||
| 157 | *
|
||
| 158 | * This is used to delete child nodes in the afterDelete.
|
||
| 159 | *
|
||
| 160 | * @param Model $Model Model using this behavior.
|
||
| 161 | * @param bool $cascade If true records that depend on this record will also be deleted
|
||
| 162 | * @return bool
|
||
| 163 | */
|
||
| 164 | public function beforeDelete(Model $Model, $cascade = true) { |
||
| 165 | extract($this->settings[$Model->alias]); |
||
| 166 | $data = $Model->find('first', array( |
||
| 167 | 'conditions' => array($Model->escapeField($Model->primaryKey) => $Model->id), |
||
| 168 | 'fields' => array($Model->escapeField($left), $Model->escapeField($right)), |
||
| 169 | 'order' => false, |
||
| 170 | 'recursive' => -1)); |
||
| 171 | if ($data) { |
||
| 172 | $this->_deletedRow[$Model->alias] = current($data); |
||
| 173 | } |
||
| 174 | return true; |
||
| 175 | } |
||
| 176 | |||
| 177 | /**
|
||
| 178 | * After delete method.
|
||
| 179 | *
|
||
| 180 | * Will delete the current node and all children using the deleteAll method and sync the table
|
||
| 181 | *
|
||
| 182 | * @param Model $Model Model using this behavior
|
||
| 183 | * @return bool true to continue, false to abort the delete
|
||
| 184 | */
|
||
| 185 | public function afterDelete(Model $Model) { |
||
| 186 | extract($this->settings[$Model->alias]); |
||
| 187 | $data = $this->_deletedRow[$Model->alias]; |
||
| 188 | $this->_deletedRow[$Model->alias] = null; |
||
| 189 | |||
| 190 | if (!$data[$right] || !$data[$left]) { |
||
| 191 | return true; |
||
| 192 | } |
||
| 193 | $diff = $data[$right] - $data[$left] + 1; |
||
| 194 | |||
| 195 | if ($diff > 2) { |
||
| 196 | if (is_string($scope)) { |
||
| 197 | $scope = array($scope); |
||
| 198 | } |
||
| 199 | $scope[][$Model->escapeField($left) . " BETWEEN ? AND ?"] = array($data[$left] + 1, $data[$right] - 1); |
||
| 200 | $Model->deleteAll($scope); |
||
| 201 | } |
||
| 202 | $this->_sync($Model, $diff, '-', '> ' . $data[$right]); |
||
| 203 | return true; |
||
| 204 | } |
||
| 205 | |||
| 206 | /**
|
||
| 207 | * Before save method. Called before all saves
|
||
| 208 | *
|
||
| 209 | * Overridden to transparently manage setting the lft and rght fields if and only if the parent field is included in the
|
||
| 210 | * parameters to be saved. For newly created nodes with NO parent the left and right field values are set directly by
|
||
| 211 | * this method bypassing the setParent logic.
|
||
| 212 | *
|
||
| 213 | * @param Model $Model Model using this behavior
|
||
| 214 | * @param array $options Options passed from Model::save().
|
||
| 215 | * @return bool true to continue, false to abort the save
|
||
| 216 | * @see Model::save()
|
||
| 217 | */
|
||
| 218 | public function beforeSave(Model $Model, $options = array()) { |
||
| 219 | extract($this->settings[$Model->alias]); |
||
| 220 | |||
| 221 | $this->_addToWhitelist($Model, array($left, $right)); |
||
| 222 | if ($level) { |
||
| 223 | $this->_addToWhitelist($Model, $level); |
||
| 224 | } |
||
| 225 | $parentIsSet = array_key_exists($parent, $Model->data[$Model->alias]); |
||
| 226 | |||
| 227 | if (!$Model->id || !$Model->exists()) { |
||
| 228 | if ($parentIsSet && $Model->data[$Model->alias][$parent]) { |
||
| 229 | $parentNode = $this->_getNode($Model, $Model->data[$Model->alias][$parent]); |
||
| 230 | if (!$parentNode) { |
||
| 231 | return false; |
||
| 232 | } |
||
| 233 | |||
| 234 | $Model->data[$Model->alias][$left] = 0; |
||
| 235 | $Model->data[$Model->alias][$right] = 0; |
||
| 236 | if ($level) { |
||
| 237 | $Model->data[$Model->alias][$level] = (int)$parentNode[$Model->alias][$level] + 1; |
||
| 238 | } |
||
| 239 | return true; |
||
| 240 | } |
||
| 241 | |||
| 242 | $edge = $this->_getMax($Model, $scope, $right, $recursive); |
||
| 243 | $Model->data[$Model->alias][$left] = $edge + 1; |
||
| 244 | $Model->data[$Model->alias][$right] = $edge + 2; |
||
| 245 | if ($level) { |
||
| 246 | $Model->data[$Model->alias][$level] = 0; |
||
| 247 | } |
||
| 248 | return true; |
||
| 249 | } |
||
| 250 | |||
| 251 | if ($parentIsSet) { |
||
| 252 | if ($Model->data[$Model->alias][$parent] != $Model->field($parent)) { |
||
| 253 | $this->settings[$Model->alias]['__parentChange'] = true; |
||
| 254 | } |
||
| 255 | if (!$Model->data[$Model->alias][$parent]) { |
||
| 256 | $Model->data[$Model->alias][$parent] = null; |
||
| 257 | $this->_addToWhitelist($Model, $parent); |
||
| 258 | if ($level) { |
||
| 259 | $Model->data[$Model->alias][$level] = 0; |
||
| 260 | } |
||
| 261 | return true; |
||
| 262 | } |
||
| 263 | |||
| 264 | $values = $this->_getNode($Model, $Model->id); |
||
| 265 | if (empty($values)) { |
||
| 266 | return false; |
||
| 267 | } |
||
| 268 | list($node) = array_values($values); |
||
| 269 | |||
| 270 | $parentNode = $this->_getNode($Model, $Model->data[$Model->alias][$parent]); |
||
| 271 | if (!$parentNode) { |
||
| 272 | return false; |
||
| 273 | } |
||
| 274 | list($parentNode) = array_values($parentNode); |
||
| 275 | |||
| 276 | if (($node[$left] < $parentNode[$left]) && ($parentNode[$right] < $node[$right])) { |
||
| 277 | return false; |
||
| 278 | } |
||
| 279 | if ($node[$Model->primaryKey] === $parentNode[$Model->primaryKey]) { |
||
| 280 | return false; |
||
| 281 | } |
||
| 282 | if ($level) { |
||
| 283 | $Model->data[$Model->alias][$level] = (int)$parentNode[$level] + 1; |
||
| 284 | } |
||
| 285 | } |
||
| 286 | |||
| 287 | return true; |
||
| 288 | } |
||
| 289 | |||
| 290 | /**
|
||
| 291 | * Returns a single node from the tree from its primary key
|
||
| 292 | *
|
||
| 293 | * @param Model $Model Model using this behavior
|
||
| 294 | * @param int|string $id The ID of the record to read
|
||
| 295 | * @return array|bool The record read or false
|
||
| 296 | */
|
||
| 297 | protected function _getNode(Model $Model, $id) { |
||
| 298 | $settings = $this->settings[$Model->alias]; |
||
| 299 | $fields = array($Model->primaryKey, $settings['parent'], $settings['left'], $settings['right']); |
||
| 300 | if ($settings['level']) { |
||
| 301 | $fields[] = $settings['level']; |
||
| 302 | } |
||
| 303 | |||
| 304 | return $Model->find('first', array( |
||
| 305 | 'conditions' => array($Model->escapeField() => $id), |
||
| 306 | 'fields' => $fields, |
||
| 307 | 'recursive' => $settings['recursive'], |
||
| 308 | 'order' => false, |
||
| 309 | )); |
||
| 310 | } |
||
| 311 | |||
| 312 | /**
|
||
| 313 | * Get the number of child nodes
|
||
| 314 | *
|
||
| 315 | * If the direct parameter is set to true, only the direct children are counted (based upon the parent_id field)
|
||
| 316 | * If false is passed for the id parameter, all top level nodes are counted, or all nodes are counted.
|
||
| 317 | *
|
||
| 318 | * @param Model $Model Model using this behavior
|
||
| 319 | * @param int|string|bool $id The ID of the record to read or false to read all top level nodes
|
||
| 320 | * @param bool $direct whether to count direct, or all, children
|
||
| 321 | * @return int number of child nodes
|
||
| 322 | * @link http://book.cakephp.org/2.0/en/core-libraries/behaviors/tree.html#TreeBehavior::childCount
|
||
| 323 | */
|
||
| 324 | public function childCount(Model $Model, $id = null, $direct = false) { |
||
| 325 | if (is_array($id)) { |
||
| 326 | extract(array_merge(array('id' => null), $id)); |
||
| 327 | } |
||
| 328 | if ($id === null && $Model->id) { |
||
| 329 | $id = $Model->id; |
||
| 330 | } elseif (!$id) { |
||
| 331 | $id = null; |
||
| 332 | } |
||
| 333 | extract($this->settings[$Model->alias]); |
||
| 334 | |||
| 335 | if ($direct) { |
||
| 336 | return $Model->find('count', array('conditions' => array($scope, $Model->escapeField($parent) => $id))); |
||
| 337 | } |
||
| 338 | |||
| 339 | if ($id === null) { |
||
| 340 | return $Model->find('count', array('conditions' => $scope)); |
||
| 341 | } elseif ($Model->id === $id && isset($Model->data[$Model->alias][$left]) && isset($Model->data[$Model->alias][$right])) { |
||
| 342 | $data = $Model->data[$Model->alias]; |
||
| 343 | } else {
|
||
| 344 | $data = $this->_getNode($Model, $id); |
||
| 345 | if (!$data) { |
||
| 346 | return 0; |
||
| 347 | } |
||
| 348 | $data = $data[$Model->alias]; |
||
| 349 | } |
||
| 350 | return ($data[$right] - $data[$left] - 1) / 2; |
||
| 351 | } |
||
| 352 | |||
| 353 | /**
|
||
| 354 | * Get the child nodes of the current model
|
||
| 355 | *
|
||
| 356 | * If the direct parameter is set to true, only the direct children are returned (based upon the parent_id field)
|
||
| 357 | * If false is passed for the id parameter, top level, or all (depending on direct parameter appropriate) are counted.
|
||
| 358 | *
|
||
| 359 | * @param Model $Model Model using this behavior
|
||
| 360 | * @param int|string $id The ID of the record to read
|
||
| 361 | * @param bool $direct whether to return only the direct, or all, children
|
||
| 362 | * @param string|array $fields Either a single string of a field name, or an array of field names
|
||
| 363 | * @param string $order SQL ORDER BY conditions (e.g. "price DESC" or "name ASC") defaults to the tree order
|
||
| 364 | * @param int $limit SQL LIMIT clause, for calculating items per page.
|
||
| 365 | * @param int $page Page number, for accessing paged data
|
||
| 366 | * @param int $recursive The number of levels deep to fetch associated records
|
||
| 367 | * @return array Array of child nodes
|
||
| 368 | * @link http://book.cakephp.org/2.0/en/core-libraries/behaviors/tree.html#TreeBehavior::children
|
||
| 369 | */
|
||
| 370 | public function children(Model $Model, $id = null, $direct = false, $fields = null, $order = null, $limit = null, $page = 1, $recursive = null) { |
||
| 371 | $options = array(); |
||
| 372 | if (is_array($id)) { |
||
| 373 | $options = $this->_getOptions($id); |
||
| 374 | extract(array_merge(array('id' => null), $id)); |
||
| 375 | } |
||
| 376 | $overrideRecursive = $recursive; |
||
| 377 | |||
| 378 | if ($id === null && $Model->id) { |
||
| 379 | $id = $Model->id; |
||
| 380 | } elseif (!$id) { |
||
| 381 | $id = null; |
||
| 382 | } |
||
| 383 | |||
| 384 | extract($this->settings[$Model->alias]); |
||
| 385 | |||
| 386 | if ($overrideRecursive !== null) { |
||
| 387 | $recursive = $overrideRecursive; |
||
| 388 | } |
||
| 389 | if (!$order) { |
||
| 390 | $order = $Model->escapeField($left) . " asc"; |
||
| 391 | } |
||
| 392 | if ($direct) { |
||
| 393 | $conditions = array($scope, $Model->escapeField($parent) => $id); |
||
| 394 | return $Model->find('all', compact('conditions', 'fields', 'order', 'limit', 'page', 'recursive')); |
||
| 395 | } |
||
| 396 | |||
| 397 | if (!$id) { |
||
| 398 | $conditions = $scope; |
||
| 399 | } else {
|
||
| 400 | $result = array_values((array)$Model->find('first', array( |
||
| 401 | 'conditions' => array($scope, $Model->escapeField() => $id), |
||
| 402 | 'fields' => array($left, $right), |
||
| 403 | 'recursive' => $recursive, |
||
| 404 | 'order' => false, |
||
| 405 | ))); |
||
| 406 | |||
| 407 | if (empty($result) || !isset($result[0])) { |
||
| 408 | return array(); |
||
| 409 | } |
||
| 410 | $conditions = array($scope, |
||
| 411 | $Model->escapeField($right) . ' <' => $result[0][$right], |
||
| 412 | $Model->escapeField($left) . ' >' => $result[0][$left] |
||
| 413 | ); |
||
| 414 | } |
||
| 415 | $options = array_merge(compact( |
||
| 416 | 'conditions', 'fields', 'order', 'limit', 'page', 'recursive' |
||
| 417 | ), $options);
|
||
| 418 | return $Model->find('all', $options); |
||
| 419 | } |
||
| 420 | |||
| 421 | /**
|
||
| 422 | * A convenience method for returning a hierarchical array used for HTML select boxes
|
||
| 423 | *
|
||
| 424 | * @param Model $Model Model using this behavior
|
||
| 425 | * @param string|array $conditions SQL conditions as a string or as an array('field' =>'value',...)
|
||
| 426 | * @param string $keyPath A string path to the key, i.e. "{n}.Post.id"
|
||
| 427 | * @param string $valuePath A string path to the value, i.e. "{n}.Post.title"
|
||
| 428 | * @param string $spacer The character or characters which will be repeated
|
||
| 429 | * @param int $recursive The number of levels deep to fetch associated records
|
||
| 430 | * @return array An associative array of records, where the id is the key, and the display field is the value
|
||
| 431 | * @link http://book.cakephp.org/2.0/en/core-libraries/behaviors/tree.html#TreeBehavior::generateTreeList
|
||
| 432 | */
|
||
| 433 | public function generateTreeList(Model $Model, $conditions = null, $keyPath = null, $valuePath = null, $spacer = '_', $recursive = null) { |
||
| 434 | $overrideRecursive = $recursive; |
||
| 435 | extract($this->settings[$Model->alias]); |
||
| 436 | if ($overrideRecursive !== null) { |
||
| 437 | $recursive = $overrideRecursive; |
||
| 438 | } |
||
| 439 | |||
| 440 | $fields = null; |
||
| 441 | if (!$keyPath && !$valuePath && $Model->hasField($Model->displayField)) { |
||
| 442 | $fields = array($Model->primaryKey, $Model->displayField, $left, $right); |
||
| 443 | } |
||
| 444 | |||
| 445 | $conditions = (array)$conditions; |
||
| 446 | if ($scope) { |
||
| 447 | $conditions[] = $scope; |
||
| 448 | } |
||
| 449 | |||
| 450 | $order = $Model->escapeField($left) . ' asc'; |
||
| 451 | $results = $Model->find('all', compact('conditions', 'fields', 'order', 'recursive')); |
||
| 452 | |||
| 453 | return $this->formatTreeList($Model, $results, compact('keyPath', 'valuePath', 'spacer')); |
||
| 454 | } |
||
| 455 | |||
| 456 | /**
|
||
| 457 | * Formats result of a find() call to a hierarchical array used for HTML select boxes.
|
||
| 458 | *
|
||
| 459 | * Note that when using your own find() call this expects the order to be "left" field asc in order
|
||
| 460 | * to generate the same result as using generateTreeList() directly.
|
||
| 461 | *
|
||
| 462 | * Options:
|
||
| 463 | *
|
||
| 464 | * - 'keyPath': A string path to the key, i.e. "{n}.Post.id"
|
||
| 465 | * - 'valuePath': A string path to the value, i.e. "{n}.Post.title"
|
||
| 466 | * - 'spacer': The character or characters which will be repeated
|
||
| 467 | *
|
||
| 468 | * @param Model $Model Model using this behavior
|
||
| 469 | * @param array $results Result array of a find() call
|
||
| 470 | * @param array $options Options
|
||
| 471 | * @return array An associative array of records, where the id is the key, and the display field is the value
|
||
| 472 | */
|
||
| 473 | public function formatTreeList(Model $Model, array $results, array $options = array()) { |
||
| 474 | if (empty($results)) { |
||
| 475 | return array(); |
||
| 476 | } |
||
| 477 | $defaults = array( |
||
| 478 | 'keyPath' => null, |
||
| 479 | 'valuePath' => null, |
||
| 480 | 'spacer' => '_' |
||
| 481 | ); |
||
| 482 | $options += $defaults; |
||
| 483 | |||
| 484 | extract($this->settings[$Model->alias]); |
||
| 485 | |||
| 486 | if (!$options['keyPath']) { |
||
| 487 | $options['keyPath'] = '{n}.' . $Model->alias . '.' . $Model->primaryKey; |
||
| 488 | } |
||
| 489 | |||
| 490 | if (!$options['valuePath']) { |
||
| 491 | $options['valuePath'] = array('%s%s', '{n}.tree_prefix', '{n}.' . $Model->alias . '.' . $Model->displayField); |
||
| 492 | |||
| 493 | } elseif (is_string($options['valuePath'])) { |
||
| 494 | $options['valuePath'] = array('%s%s', '{n}.tree_prefix', $options['valuePath']); |
||
| 495 | |||
| 496 | } else {
|
||
| 497 | array_unshift($options['valuePath'], '%s' . $options['valuePath'][0], '{n}.tree_prefix'); |
||
| 498 | } |
||
| 499 | |||
| 500 | $stack = array(); |
||
| 501 | |||
| 502 | foreach ($results as $i => $result) { |
||
| 503 | $count = count($stack); |
||
| 504 | while ($stack && ($stack[$count - 1] < $result[$Model->alias][$right])) { |
||
| 505 | array_pop($stack); |
||
| 506 | $count--;
|
||
| 507 | } |
||
| 508 | $results[$i]['tree_prefix'] = str_repeat($options['spacer'], $count); |
||
| 509 | $stack[] = $result[$Model->alias][$right]; |
||
| 510 | } |
||
| 511 | |||
| 512 | return Hash::combine($results, $options['keyPath'], $options['valuePath']); |
||
| 513 | } |
||
| 514 | |||
| 515 | /**
|
||
| 516 | * Get the parent node
|
||
| 517 | *
|
||
| 518 | * reads the parent id and returns this node
|
||
| 519 | *
|
||
| 520 | * @param Model $Model Model using this behavior
|
||
| 521 | * @param int|string $id The ID of the record to read
|
||
| 522 | * @param string|array $fields Fields to get
|
||
| 523 | * @param int $recursive The number of levels deep to fetch associated records
|
||
| 524 | * @return array|bool Array of data for the parent node
|
||
| 525 | * @link http://book.cakephp.org/2.0/en/core-libraries/behaviors/tree.html#TreeBehavior::getParentNode
|
||
| 526 | */
|
||
| 527 | public function getParentNode(Model $Model, $id = null, $fields = null, $recursive = null) { |
||
| 528 | $options = array(); |
||
| 529 | if (is_array($id)) { |
||
| 530 | $options = $this->_getOptions($id); |
||
| 531 | extract(array_merge(array('id' => null), $id)); |
||
| 532 | } |
||
| 533 | $overrideRecursive = $recursive; |
||
| 534 | if (empty($id)) { |
||
| 535 | $id = $Model->id; |
||
| 536 | } |
||
| 537 | extract($this->settings[$Model->alias]); |
||
| 538 | if ($overrideRecursive !== null) { |
||
| 539 | $recursive = $overrideRecursive; |
||
| 540 | } |
||
| 541 | $parentId = $Model->find('first', array( |
||
| 542 | 'conditions' => array($Model->primaryKey => $id), |
||
| 543 | 'fields' => array($parent), |
||
| 544 | 'order' => false, |
||
| 545 | 'recursive' => -1 |
||
| 546 | )); |
||
| 547 | |||
| 548 | if ($parentId) { |
||
| 549 | $parentId = $parentId[$Model->alias][$parent]; |
||
| 550 | $options = array_merge(array( |
||
| 551 | 'conditions' => array($Model->escapeField() => $parentId), |
||
| 552 | 'fields' => $fields, |
||
| 553 | 'order' => false, |
||
| 554 | 'recursive' => $recursive |
||
| 555 | ), $options);
|
||
| 556 | $parent = $Model->find('first', $options); |
||
| 557 | |||
| 558 | return $parent; |
||
| 559 | } |
||
| 560 | return false; |
||
| 561 | } |
||
| 562 | |||
| 563 | /**
|
||
| 564 | * Convenience method to create default find() options from $arg when it is an
|
||
| 565 | * associative array.
|
||
| 566 | *
|
||
| 567 | * @param array $arg Array
|
||
| 568 | * @return array Options array
|
||
| 569 | */
|
||
| 570 | protected function _getOptions($arg) { |
||
| 571 | return count(array_filter(array_keys($arg), 'is_string') > 0) ? |
||
| 572 | $arg :
|
||
| 573 | array();
|
||
| 574 | } |
||
| 575 | |||
| 576 | /**
|
||
| 577 | * Get the path to the given node
|
||
| 578 | *
|
||
| 579 | * @param Model $Model Model using this behavior
|
||
| 580 | * @param int|string $id The ID of the record to read
|
||
| 581 | * @param string|array $fields Either a single string of a field name, or an array of field names
|
||
| 582 | * @param int $recursive The number of levels deep to fetch associated records
|
||
| 583 | * @return array Array of nodes from top most parent to current node
|
||
| 584 | * @link http://book.cakephp.org/2.0/en/core-libraries/behaviors/tree.html#TreeBehavior::getPath
|
||
| 585 | */
|
||
| 586 | public function getPath(Model $Model, $id = null, $fields = null, $recursive = null) { |
||
| 587 | $options = array(); |
||
| 588 | if (is_array($id)) { |
||
| 589 | $options = $this->_getOptions($id); |
||
| 590 | extract(array_merge(array('id' => null), $id)); |
||
| 591 | } |
||
| 592 | |||
| 593 | if (!empty($options)) { |
||
| 594 | $fields = null; |
||
| 595 | if (!empty($options['fields'])) { |
||
| 596 | $fields = $options['fields']; |
||
| 597 | } |
||
| 598 | if (!empty($options['recursive'])) { |
||
| 599 | $recursive = $options['recursive']; |
||
| 600 | } |
||
| 601 | } |
||
| 602 | $overrideRecursive = $recursive; |
||
| 603 | if (empty($id)) { |
||
| 604 | $id = $Model->id; |
||
| 605 | } |
||
| 606 | extract($this->settings[$Model->alias]); |
||
| 607 | if ($overrideRecursive !== null) { |
||
| 608 | $recursive = $overrideRecursive; |
||
| 609 | } |
||
| 610 | $result = $Model->find('first', array( |
||
| 611 | 'conditions' => array($Model->escapeField() => $id), |
||
| 612 | 'fields' => array($left, $right), |
||
| 613 | 'order' => false, |
||
| 614 | 'recursive' => $recursive |
||
| 615 | )); |
||
| 616 | if ($result) { |
||
| 617 | $result = array_values($result); |
||
| 618 | } else {
|
||
| 619 | return array(); |
||
| 620 | } |
||
| 621 | $item = $result[0]; |
||
| 622 | $options = array_merge(array( |
||
| 623 | 'conditions' => array( |
||
| 624 | $scope,
|
||
| 625 | $Model->escapeField($left) . ' <=' => $item[$left], |
||
| 626 | $Model->escapeField($right) . ' >=' => $item[$right], |
||
| 627 | ), |
||
| 628 | 'fields' => $fields, |
||
| 629 | 'order' => array($Model->escapeField($left) => 'asc'), |
||
| 630 | 'recursive' => $recursive |
||
| 631 | ), $options);
|
||
| 632 | $results = $Model->find('all', $options); |
||
| 633 | return $results; |
||
| 634 | } |
||
| 635 | |||
| 636 | /**
|
||
| 637 | * Reorder the node without changing the parent.
|
||
| 638 | *
|
||
| 639 | * If the node is the last child, or is a top level node with no subsequent node this method will return false
|
||
| 640 | *
|
||
| 641 | * @param Model $Model Model using this behavior
|
||
| 642 | * @param int|string $id The ID of the record to move
|
||
| 643 | * @param int|bool $number how many places to move the node or true to move to last position
|
||
| 644 | * @return bool true on success, false on failure
|
||
| 645 | * @link http://book.cakephp.org/2.0/en/core-libraries/behaviors/tree.html#TreeBehavior::moveDown
|
||
| 646 | */
|
||
| 647 | public function moveDown(Model $Model, $id = null, $number = 1) { |
||
| 648 | if (is_array($id)) { |
||
| 649 | extract(array_merge(array('id' => null), $id)); |
||
| 650 | } |
||
| 651 | if (!$number) { |
||
| 652 | return false; |
||
| 653 | } |
||
| 654 | if (empty($id)) { |
||
| 655 | $id = $Model->id; |
||
| 656 | } |
||
| 657 | extract($this->settings[$Model->alias]); |
||
| 658 | list($node) = array_values($this->_getNode($Model, $id)); |
||
| 659 | if ($node[$parent]) { |
||
| 660 | list($parentNode) = array_values($this->_getNode($Model, $node[$parent])); |
||
| 661 | if (($node[$right] + 1) == $parentNode[$right]) { |
||
| 662 | return false; |
||
| 663 | } |
||
| 664 | } |
||
| 665 | $nextNode = $Model->find('first', array( |
||
| 666 | 'conditions' => array($scope, $Model->escapeField($left) => ($node[$right] + 1)), |
||
| 667 | 'fields' => array($Model->primaryKey, $left, $right), |
||
| 668 | 'order' => false, |
||
| 669 | 'recursive' => $recursive) |
||
| 670 | ); |
||
| 671 | if ($nextNode) { |
||
| 672 | list($nextNode) = array_values($nextNode); |
||
| 673 | } else {
|
||
| 674 | return false; |
||
| 675 | } |
||
| 676 | $edge = $this->_getMax($Model, $scope, $right, $recursive); |
||
| 677 | $this->_sync($Model, $edge - $node[$left] + 1, '+', 'BETWEEN ' . $node[$left] . ' AND ' . $node[$right]); |
||
| 678 | $this->_sync($Model, $nextNode[$left] - $node[$left], '-', 'BETWEEN ' . $nextNode[$left] . ' AND ' . $nextNode[$right]); |
||
| 679 | $this->_sync($Model, $edge - $node[$left] - ($nextNode[$right] - $nextNode[$left]), '-', '> ' . $edge); |
||
| 680 | |||
| 681 | if (is_int($number)) { |
||
| 682 | $number--;
|
||
| 683 | } |
||
| 684 | if ($number) { |
||
| 685 | $this->moveDown($Model, $id, $number); |
||
| 686 | } |
||
| 687 | return true; |
||
| 688 | } |
||
| 689 | |||
| 690 | /**
|
||
| 691 | * Reorder the node without changing the parent.
|
||
| 692 | *
|
||
| 693 | * If the node is the first child, or is a top level node with no previous node this method will return false
|
||
| 694 | *
|
||
| 695 | * @param Model $Model Model using this behavior
|
||
| 696 | * @param int|string $id The ID of the record to move
|
||
| 697 | * @param int|bool $number how many places to move the node, or true to move to first position
|
||
| 698 | * @return bool true on success, false on failure
|
||
| 699 | * @link http://book.cakephp.org/2.0/en/core-libraries/behaviors/tree.html#TreeBehavior::moveUp
|
||
| 700 | */
|
||
| 701 | public function moveUp(Model $Model, $id = null, $number = 1) { |
||
| 702 | if (is_array($id)) { |
||
| 703 | extract(array_merge(array('id' => null), $id)); |
||
| 704 | } |
||
| 705 | if (!$number) { |
||
| 706 | return false; |
||
| 707 | } |
||
| 708 | if (empty($id)) { |
||
| 709 | $id = $Model->id; |
||
| 710 | } |
||
| 711 | extract($this->settings[$Model->alias]); |
||
| 712 | list($node) = array_values($this->_getNode($Model, $id)); |
||
| 713 | if ($node[$parent]) { |
||
| 714 | list($parentNode) = array_values($this->_getNode($Model, $node[$parent])); |
||
| 715 | if (($node[$left] - 1) == $parentNode[$left]) { |
||
| 716 | return false; |
||
| 717 | } |
||
| 718 | } |
||
| 719 | $previousNode = $Model->find('first', array( |
||
| 720 | 'conditions' => array($scope, $Model->escapeField($right) => ($node[$left] - 1)), |
||
| 721 | 'fields' => array($Model->primaryKey, $left, $right), |
||
| 722 | 'order' => false, |
||
| 723 | 'recursive' => $recursive |
||
| 724 | )); |
||
| 725 | |||
| 726 | if ($previousNode) { |
||
| 727 | list($previousNode) = array_values($previousNode); |
||
| 728 | } else {
|
||
| 729 | return false; |
||
| 730 | } |
||
| 731 | $edge = $this->_getMax($Model, $scope, $right, $recursive); |
||
| 732 | $this->_sync($Model, $edge - $previousNode[$left] + 1, '+', 'BETWEEN ' . $previousNode[$left] . ' AND ' . $previousNode[$right]); |
||
| 733 | $this->_sync($Model, $node[$left] - $previousNode[$left], '-', 'BETWEEN ' . $node[$left] . ' AND ' . $node[$right]); |
||
| 734 | $this->_sync($Model, $edge - $previousNode[$left] - ($node[$right] - $node[$left]), '-', '> ' . $edge); |
||
| 735 | if (is_int($number)) { |
||
| 736 | $number--;
|
||
| 737 | } |
||
| 738 | if ($number) { |
||
| 739 | $this->moveUp($Model, $id, $number); |
||
| 740 | } |
||
| 741 | return true; |
||
| 742 | } |
||
| 743 | |||
| 744 | /**
|
||
| 745 | * Recover a corrupted tree
|
||
| 746 | *
|
||
| 747 | * The mode parameter is used to specify the source of info that is valid/correct. The opposite source of data
|
||
| 748 | * will be populated based upon that source of info. E.g. if the MPTT fields are corrupt or empty, with the $mode
|
||
| 749 | * 'parent' the values of the parent_id field will be used to populate the left and right fields. The missingParentAction
|
||
| 750 | * parameter only applies to "parent" mode and determines what to do if the parent field contains an id that is not present.
|
||
| 751 | *
|
||
| 752 | * @param Model $Model Model using this behavior
|
||
| 753 | * @param string $mode parent or tree
|
||
| 754 | * @param string|int $missingParentAction 'return' to do nothing and return, 'delete' to
|
||
| 755 | * delete, or the id of the parent to set as the parent_id
|
||
| 756 | * @return bool true on success, false on failure
|
||
| 757 | * @link http://book.cakephp.org/2.0/en/core-libraries/behaviors/tree.html#TreeBehavior::recover
|
||
| 758 | */
|
||
| 759 | public function recover(Model $Model, $mode = 'parent', $missingParentAction = null) { |
||
| 760 | if (is_array($mode)) { |
||
| 761 | extract(array_merge(array('mode' => 'parent'), $mode)); |
||
| 762 | } |
||
| 763 | extract($this->settings[$Model->alias]); |
||
| 764 | $Model->recursive = $recursive; |
||
| 765 | if ($mode === 'parent') { |
||
| 766 | $Model->bindModel(array('belongsTo' => array('VerifyParent' => array( |
||
| 767 | 'className' => $Model->name, |
||
| 768 | 'foreignKey' => $parent, |
||
| 769 | 'fields' => array($Model->primaryKey, $left, $right, $parent), |
||
| 770 | )))); |
||
| 771 | $missingParents = $Model->find('list', array( |
||
| 772 | 'recursive' => 0, |
||
| 773 | 'conditions' => array($scope, array( |
||
| 774 | 'NOT' => array($Model->escapeField($parent) => null), $Model->VerifyParent->escapeField() => null |
||
| 775 | )), |
||
| 776 | 'order' => false, |
||
| 777 | )); |
||
| 778 | $Model->unbindModel(array('belongsTo' => array('VerifyParent'))); |
||
| 779 | if ($missingParents) { |
||
| 780 | if ($missingParentAction === 'return') { |
||
| 781 | foreach ($missingParents as $id => $display) { |
||
| 782 | $this->errors[] = 'cannot find the parent for ' . $Model->alias . ' with id ' . $id . '(' . $display . ')'; |
||
| 783 | } |
||
| 784 | return false; |
||
| 785 | } elseif ($missingParentAction === 'delete') { |
||
| 786 | $Model->deleteAll(array($Model->escapeField($Model->primaryKey) => array_flip($missingParents)), false); |
||
| 787 | } else {
|
||
| 788 | $Model->updateAll(array($Model->escapeField($parent) => $missingParentAction), array($Model->escapeField($Model->primaryKey) => array_flip($missingParents))); |
||
| 789 | } |
||
| 790 | } |
||
| 791 | |||
| 792 | $this->_recoverByParentId($Model); |
||
| 793 | } else {
|
||
| 794 | $db = ConnectionManager::getDataSource($Model->useDbConfig); |
||
| 795 | foreach ($Model->find('all', array('conditions' => $scope, 'fields' => array($Model->primaryKey, $parent), 'order' => $left)) as $array) { |
||
| 796 | $path = $this->getPath($Model, $array[$Model->alias][$Model->primaryKey]); |
||
| 797 | $parentId = null; |
||
| 798 | if (count($path) > 1) { |
||
| 799 | $parentId = $path[count($path) - 2][$Model->alias][$Model->primaryKey]; |
||
| 800 | } |
||
| 801 | $Model->updateAll(array($parent => $db->value($parentId, $parent)), array($Model->escapeField() => $array[$Model->alias][$Model->primaryKey])); |
||
| 802 | } |
||
| 803 | } |
||
| 804 | return true; |
||
| 805 | } |
||
| 806 | |||
| 807 | /**
|
||
| 808 | * _recoverByParentId
|
||
| 809 | *
|
||
| 810 | * Recursive helper function used by recover
|
||
| 811 | *
|
||
| 812 | * @param Model $Model Model instance.
|
||
| 813 | * @param int $counter Counter
|
||
| 814 | * @param mixed $parentId Parent record Id
|
||
| 815 | * @return int counter
|
||
| 816 | */
|
||
| 817 | protected function _recoverByParentId(Model $Model, $counter = 1, $parentId = null) { |
||
| 818 | $params = array( |
||
| 819 | 'conditions' => array( |
||
| 820 | $this->settings[$Model->alias]['parent'] => $parentId |
||
| 821 | ), |
||
| 822 | 'fields' => array($Model->primaryKey), |
||
| 823 | 'page' => 1, |
||
| 824 | 'limit' => 100, |
||
| 825 | 'order' => array($Model->primaryKey) |
||
| 826 | ); |
||
| 827 | |||
| 828 | $scope = $this->settings[$Model->alias]['scope']; |
||
| 829 | if ($scope && ($scope !== '1 = 1' && $scope !== true)) { |
||
| 830 | $params['conditions'][] = $scope; |
||
| 831 | } |
||
| 832 | |||
| 833 | $children = $Model->find('all', $params); |
||
| 834 | $hasChildren = (bool)$children; |
||
| 835 | |||
| 836 | if ($parentId !== null) { |
||
| 837 | if ($hasChildren) { |
||
| 838 | $Model->updateAll(
|
||
| 839 | array($this->settings[$Model->alias]['left'] => $counter), |
||
| 840 | array($Model->escapeField() => $parentId) |
||
| 841 | ); |
||
| 842 | $counter++;
|
||
| 843 | } else {
|
||
| 844 | $Model->updateAll(
|
||
| 845 | array(
|
||
| 846 | $this->settings[$Model->alias]['left'] => $counter, |
||
| 847 | $this->settings[$Model->alias]['right'] => $counter + 1 |
||
| 848 | ), |
||
| 849 | array($Model->escapeField() => $parentId) |
||
| 850 | ); |
||
| 851 | $counter += 2; |
||
| 852 | } |
||
| 853 | } |
||
| 854 | |||
| 855 | while ($children) { |
||
| 856 | foreach ($children as $row) { |
||
| 857 | $counter = $this->_recoverByParentId($Model, $counter, $row[$Model->alias][$Model->primaryKey]); |
||
| 858 | } |
||
| 859 | |||
| 860 | if (count($children) !== $params['limit']) { |
||
| 861 | break;
|
||
| 862 | } |
||
| 863 | $params['page']++; |
||
| 864 | $children = $Model->find('all', $params); |
||
| 865 | } |
||
| 866 | |||
| 867 | if ($parentId !== null && $hasChildren) { |
||
| 868 | $Model->updateAll(
|
||
| 869 | array($this->settings[$Model->alias]['right'] => $counter), |
||
| 870 | array($Model->escapeField() => $parentId) |
||
| 871 | ); |
||
| 872 | $counter++;
|
||
| 873 | } |
||
| 874 | |||
| 875 | return $counter; |
||
| 876 | } |
||
| 877 | |||
| 878 | /**
|
||
| 879 | * Reorder method.
|
||
| 880 | *
|
||
| 881 | * Reorders the nodes (and child nodes) of the tree according to the field and direction specified in the parameters.
|
||
| 882 | * This method does not change the parent of any node.
|
||
| 883 | *
|
||
| 884 | * Requires a valid tree, by default it verifies the tree before beginning.
|
||
| 885 | *
|
||
| 886 | * Options:
|
||
| 887 | *
|
||
| 888 | * - 'id' id of record to use as top node for reordering
|
||
| 889 | * - 'field' Which field to use in reordering defaults to displayField
|
||
| 890 | * - 'order' Direction to order either DESC or ASC (defaults to ASC)
|
||
| 891 | * - 'verify' Whether or not to verify the tree before reorder. defaults to true.
|
||
| 892 | *
|
||
| 893 | * @param Model $Model Model using this behavior
|
||
| 894 | * @param array $options array of options to use in reordering.
|
||
| 895 | * @return bool true on success, false on failure
|
||
| 896 | * @link http://book.cakephp.org/2.0/en/core-libraries/behaviors/tree.html#TreeBehavior::reorder
|
||
| 897 | */
|
||
| 898 | public function reorder(Model $Model, $options = array()) { |
||
| 899 | $options += array('id' => null, 'field' => $Model->displayField, 'order' => 'ASC', 'verify' => true); |
||
| 900 | extract($options); |
||
| 901 | if ($verify && !$this->verify($Model)) { |
||
| 902 | return false; |
||
| 903 | } |
||
| 904 | $verify = false; |
||
| 905 | extract($this->settings[$Model->alias]); |
||
| 906 | $fields = array($Model->primaryKey, $field, $left, $right); |
||
| 907 | $sort = $field . ' ' . $order; |
||
| 908 | $nodes = $this->children($Model, $id, true, $fields, $sort, null, null, $recursive); |
||
| 909 | |||
| 910 | $cacheQueries = $Model->cacheQueries; |
||
| 911 | $Model->cacheQueries = false; |
||
| 912 | if ($nodes) { |
||
| 913 | foreach ($nodes as $node) { |
||
| 914 | $id = $node[$Model->alias][$Model->primaryKey]; |
||
| 915 | $this->moveDown($Model, $id, true); |
||
| 916 | if ($node[$Model->alias][$left] != $node[$Model->alias][$right] - 1) { |
||
| 917 | $this->reorder($Model, compact('id', 'field', 'order', 'verify')); |
||
| 918 | } |
||
| 919 | } |
||
| 920 | } |
||
| 921 | $Model->cacheQueries = $cacheQueries; |
||
| 922 | return true; |
||
| 923 | } |
||
| 924 | |||
| 925 | /**
|
||
| 926 | * Remove the current node from the tree, and reparent all children up one level.
|
||
| 927 | *
|
||
| 928 | * If the parameter delete is false, the node will become a new top level node. Otherwise the node will be deleted
|
||
| 929 | * after the children are reparented.
|
||
| 930 | *
|
||
| 931 | * @param Model $Model Model using this behavior
|
||
| 932 | * @param int|string $id The ID of the record to remove
|
||
| 933 | * @param bool $delete whether to delete the node after reparenting children (if any)
|
||
| 934 | * @return bool true on success, false on failure
|
||
| 935 | * @link http://book.cakephp.org/2.0/en/core-libraries/behaviors/tree.html#TreeBehavior::removeFromTree
|
||
| 936 | */
|
||
| 937 | public function removeFromTree(Model $Model, $id = null, $delete = false) { |
||
| 938 | if (is_array($id)) { |
||
| 939 | extract(array_merge(array('id' => null), $id)); |
||
| 940 | } |
||
| 941 | extract($this->settings[$Model->alias]); |
||
| 942 | |||
| 943 | list($node) = array_values($this->_getNode($Model, $id)); |
||
| 944 | |||
| 945 | if ($node[$right] == $node[$left] + 1) { |
||
| 946 | if ($delete) { |
||
| 947 | return $Model->delete($id); |
||
| 948 | } |
||
| 949 | $Model->id = $id; |
||
| 950 | return $Model->saveField($parent, null); |
||
| 951 | } elseif ($node[$parent]) { |
||
| 952 | list($parentNode) = array_values($this->_getNode($Model, $node[$parent])); |
||
| 953 | } else {
|
||
| 954 | $parentNode[$right] = $node[$right] + 1; |
||
| 955 | } |
||
| 956 | |||
| 957 | $db = ConnectionManager::getDataSource($Model->useDbConfig); |
||
| 958 | $Model->updateAll(
|
||
| 959 | array($parent => $db->value($node[$parent], $parent)), |
||
| 960 | array($Model->escapeField($parent) => $node[$Model->primaryKey]) |
||
| 961 | ); |
||
| 962 | $this->_sync($Model, 1, '-', 'BETWEEN ' . ($node[$left] + 1) . ' AND ' . ($node[$right] - 1)); |
||
| 963 | $this->_sync($Model, 2, '-', '> ' . ($node[$right])); |
||
| 964 | $Model->id = $id; |
||
| 965 | |||
| 966 | if ($delete) { |
||
| 967 | $Model->updateAll(
|
||
| 968 | array(
|
||
| 969 | $Model->escapeField($left) => 0, |
||
| 970 | $Model->escapeField($right) => 0, |
||
| 971 | $Model->escapeField($parent) => null |
||
| 972 | ), |
||
| 973 | array($Model->escapeField() => $id) |
||
| 974 | ); |
||
| 975 | return $Model->delete($id); |
||
| 976 | } |
||
| 977 | $edge = $this->_getMax($Model, $scope, $right, $recursive); |
||
| 978 | if ($node[$right] == $edge) { |
||
| 979 | $edge = $edge - 2; |
||
| 980 | } |
||
| 981 | $Model->id = $id; |
||
| 982 | return $Model->save( |
||
| 983 | array($left => $edge + 1, $right => $edge + 2, $parent => null), |
||
| 984 | array('callbacks' => false, 'validate' => false) |
||
| 985 | ); |
||
| 986 | } |
||
| 987 | |||
| 988 | /**
|
||
| 989 | * Check if the current tree is valid.
|
||
| 990 | *
|
||
| 991 | * Returns true if the tree is valid otherwise an array of (type, incorrect left/right index, message)
|
||
| 992 | *
|
||
| 993 | * @param Model $Model Model using this behavior
|
||
| 994 | * @return mixed true if the tree is valid or empty, otherwise an array of (error type [index, node],
|
||
| 995 | * [incorrect left/right index,node id], message)
|
||
| 996 | * @link http://book.cakephp.org/2.0/en/core-libraries/behaviors/tree.html#TreeBehavior::verify
|
||
| 997 | */
|
||
| 998 | public function verify(Model $Model) { |
||
| 999 | extract($this->settings[$Model->alias]); |
||
| 1000 | if (!$Model->find('count', array('conditions' => $scope))) { |
||
| 1001 | return true; |
||
| 1002 | } |
||
| 1003 | $min = $this->_getMin($Model, $scope, $left, $recursive); |
||
| 1004 | $edge = $this->_getMax($Model, $scope, $right, $recursive); |
||
| 1005 | $errors = array(); |
||
| 1006 | |||
| 1007 | for ($i = $min; $i <= $edge; $i++) { |
||
| 1008 | $count = $Model->find('count', array('conditions' => array( |
||
| 1009 | $scope, 'OR' => array($Model->escapeField($left) => $i, $Model->escapeField($right) => $i) |
||
| 1010 | ))); |
||
| 1011 | if ($count != 1) { |
||
| 1012 | if (!$count) { |
||
| 1013 | $errors[] = array('index', $i, 'missing'); |
||
| 1014 | } else {
|
||
| 1015 | $errors[] = array('index', $i, 'duplicate'); |
||
| 1016 | } |
||
| 1017 | } |
||
| 1018 | } |
||
| 1019 | $node = $Model->find('first', array( |
||
| 1020 | 'conditions' => array($scope, $Model->escapeField($right) . '< ' . $Model->escapeField($left)), |
||
| 1021 | 'order' => false, |
||
| 1022 | 'recursive' => 0 |
||
| 1023 | )); |
||
| 1024 | if ($node) { |
||
| 1025 | $errors[] = array('node', $node[$Model->alias][$Model->primaryKey], 'left greater than right.'); |
||
| 1026 | } |
||
| 1027 | |||
| 1028 | $Model->bindModel(array('belongsTo' => array('VerifyParent' => array( |
||
| 1029 | 'className' => $Model->name, |
||
| 1030 | 'foreignKey' => $parent, |
||
| 1031 | 'fields' => array($Model->primaryKey, $left, $right, $parent) |
||
| 1032 | )))); |
||
| 1033 | |||
| 1034 | $rows = $Model->find('all', array('conditions' => $scope, 'recursive' => 0)); |
||
| 1035 | foreach ($rows as $instance) { |
||
| 1036 | if ($instance[$Model->alias][$left] === null || $instance[$Model->alias][$right] === null) { |
||
| 1037 | $errors[] = array('node', $instance[$Model->alias][$Model->primaryKey], |
||
| 1038 | 'has invalid left or right values');
|
||
| 1039 | } elseif ($instance[$Model->alias][$left] == $instance[$Model->alias][$right]) { |
||
| 1040 | $errors[] = array('node', $instance[$Model->alias][$Model->primaryKey], |
||
| 1041 | 'left and right values identical');
|
||
| 1042 | } elseif ($instance[$Model->alias][$parent]) { |
||
| 1043 | if (!$instance['VerifyParent'][$Model->primaryKey]) { |
||
| 1044 | $errors[] = array('node', $instance[$Model->alias][$Model->primaryKey], |
||
| 1045 | 'The parent node ' . $instance[$Model->alias][$parent] . ' doesn\'t exist'); |
||
| 1046 | } elseif ($instance[$Model->alias][$left] < $instance['VerifyParent'][$left]) { |
||
| 1047 | $errors[] = array('node', $instance[$Model->alias][$Model->primaryKey], |
||
| 1048 | 'left less than parent (node ' . $instance['VerifyParent'][$Model->primaryKey] . ').'); |
||
| 1049 | } elseif ($instance[$Model->alias][$right] > $instance['VerifyParent'][$right]) { |
||
| 1050 | $errors[] = array('node', $instance[$Model->alias][$Model->primaryKey], |
||
| 1051 | 'right greater than parent (node ' . $instance['VerifyParent'][$Model->primaryKey] . ').'); |
||
| 1052 | } |
||
| 1053 | } elseif ($Model->find('count', array('conditions' => array($scope, $Model->escapeField($left) . ' <' => $instance[$Model->alias][$left], $Model->escapeField($right) . ' >' => $instance[$Model->alias][$right]), 'recursive' => 0))) { |
||
| 1054 | $errors[] = array('node', $instance[$Model->alias][$Model->primaryKey], 'The parent field is blank, but has a parent'); |
||
| 1055 | } |
||
| 1056 | } |
||
| 1057 | if ($errors) { |
||
| 1058 | return $errors; |
||
| 1059 | } |
||
| 1060 | return true; |
||
| 1061 | } |
||
| 1062 | |||
| 1063 | /**
|
||
| 1064 | * Returns the depth level of a node in the tree.
|
||
| 1065 | *
|
||
| 1066 | * @param Model $Model Model using this behavior
|
||
| 1067 | * @param int|string $id The primary key for record to get the level of.
|
||
| 1068 | * @return int|bool Integer of the level or false if the node does not exist.
|
||
| 1069 | */
|
||
| 1070 | public function getLevel(Model $Model, $id = null) { |
||
| 1071 | if ($id === null) { |
||
| 1072 | $id = $Model->id; |
||
| 1073 | } |
||
| 1074 | |||
| 1075 | $node = $Model->find('first', array( |
||
| 1076 | 'conditions' => array($Model->escapeField() => $id), |
||
| 1077 | 'order' => false, |
||
| 1078 | 'recursive' => -1 |
||
| 1079 | )); |
||
| 1080 | |||
| 1081 | if (empty($node)) { |
||
| 1082 | return false; |
||
| 1083 | } |
||
| 1084 | |||
| 1085 | extract($this->settings[$Model->alias]); |
||
| 1086 | |||
| 1087 | return $Model->find('count', array( |
||
| 1088 | 'conditions' => array( |
||
| 1089 | $scope,
|
||
| 1090 | $left . ' <' => $node[$Model->alias][$left], |
||
| 1091 | $right . ' >' => $node[$Model->alias][$right] |
||
| 1092 | ), |
||
| 1093 | 'order' => false, |
||
| 1094 | 'recursive' => -1 |
||
| 1095 | )); |
||
| 1096 | } |
||
| 1097 | |||
| 1098 | /**
|
||
| 1099 | * Sets the parent of the given node
|
||
| 1100 | *
|
||
| 1101 | * The force parameter is used to override the "don't change the parent to the current parent" logic in the event
|
||
| 1102 | * of recovering a corrupted table, or creating new nodes. Otherwise it should always be false. In reality this
|
||
| 1103 | * method could be private, since calling save with parent_id set also calls setParent
|
||
| 1104 | *
|
||
| 1105 | * @param Model $Model Model using this behavior
|
||
| 1106 | * @param int|string $parentId Parent record Id
|
||
| 1107 | * @param bool $created True if newly created record else false.
|
||
| 1108 | * @return bool true on success, false on failure
|
||
| 1109 | */
|
||
| 1110 | protected function _setParent(Model $Model, $parentId = null, $created = false) { |
||
| 1111 | extract($this->settings[$Model->alias]); |
||
| 1112 | list($node) = array_values($this->_getNode($Model, $Model->id)); |
||
| 1113 | $edge = $this->_getMax($Model, $scope, $right, $recursive, $created); |
||
| 1114 | |||
| 1115 | if (empty($parentId)) { |
||
| 1116 | $this->_sync($Model, $edge - $node[$left] + 1, '+', 'BETWEEN ' . $node[$left] . ' AND ' . $node[$right], $created); |
||
| 1117 | $this->_sync($Model, $node[$right] - $node[$left] + 1, '-', '> ' . $node[$left], $created); |
||
| 1118 | } else {
|
||
| 1119 | $values = $this->_getNode($Model, $parentId); |
||
| 1120 | |||
| 1121 | if ($values === false) { |
||
| 1122 | return false; |
||
| 1123 | } |
||
| 1124 | $parentNode = array_values($values); |
||
| 1125 | |||
| 1126 | if (empty($parentNode) || empty($parentNode[0])) { |
||
| 1127 | return false; |
||
| 1128 | } |
||
| 1129 | $parentNode = $parentNode[0]; |
||
| 1130 | |||
| 1131 | if (($Model->id === $parentId)) { |
||
| 1132 | return false; |
||
| 1133 | } elseif (($node[$left] < $parentNode[$left]) && ($parentNode[$right] < $node[$right])) { |
||
| 1134 | return false; |
||
| 1135 | } |
||
| 1136 | if (empty($node[$left]) && empty($node[$right])) { |
||
| 1137 | $this->_sync($Model, 2, '+', '>= ' . $parentNode[$right], $created); |
||
| 1138 | $result = $Model->save( |
||
| 1139 | array($left => $parentNode[$right], $right => $parentNode[$right] + 1, $parent => $parentId), |
||
| 1140 | array('validate' => false, 'callbacks' => false) |
||
| 1141 | ); |
||
| 1142 | $Model->data = $result; |
||
| 1143 | } else {
|
||
| 1144 | $this->_sync($Model, $edge - $node[$left] + 1, '+', 'BETWEEN ' . $node[$left] . ' AND ' . $node[$right], $created); |
||
| 1145 | $diff = $node[$right] - $node[$left] + 1; |
||
| 1146 | |||
| 1147 | if ($node[$left] > $parentNode[$left]) { |
||
| 1148 | if ($node[$right] < $parentNode[$right]) { |
||
| 1149 | $this->_sync($Model, $diff, '-', 'BETWEEN ' . $node[$right] . ' AND ' . ($parentNode[$right] - 1), $created); |
||
| 1150 | $this->_sync($Model, $edge - $parentNode[$right] + $diff + 1, '-', '> ' . $edge, $created); |
||
| 1151 | } else {
|
||
| 1152 | $this->_sync($Model, $diff, '+', 'BETWEEN ' . $parentNode[$right] . ' AND ' . $node[$right], $created); |
||
| 1153 | $this->_sync($Model, $edge - $parentNode[$right] + 1, '-', '> ' . $edge, $created); |
||
| 1154 | } |
||
| 1155 | } else {
|
||
| 1156 | $this->_sync($Model, $diff, '-', 'BETWEEN ' . $node[$right] . ' AND ' . ($parentNode[$right] - 1), $created); |
||
| 1157 | $this->_sync($Model, $edge - $parentNode[$right] + $diff + 1, '-', '> ' . $edge, $created); |
||
| 1158 | } |
||
| 1159 | } |
||
| 1160 | } |
||
| 1161 | return true; |
||
| 1162 | } |
||
| 1163 | |||
| 1164 | /**
|
||
| 1165 | * get the maximum index value in the table.
|
||
| 1166 | *
|
||
| 1167 | * @param Model $Model Model Instance.
|
||
| 1168 | * @param string $scope Scoping conditions.
|
||
| 1169 | * @param string $right Right value
|
||
| 1170 | * @param int $recursive Recursive find value.
|
||
| 1171 | * @param bool $created Whether it's a new record.
|
||
| 1172 | * @return int
|
||
| 1173 | */
|
||
| 1174 | protected function _getMax(Model $Model, $scope, $right, $recursive = -1, $created = false) { |
||
| 1175 | $db = ConnectionManager::getDataSource($Model->useDbConfig); |
||
| 1176 | if ($created) { |
||
| 1177 | if (is_string($scope)) { |
||
| 1178 | $scope .= " AND " . $Model->escapeField() . " <> "; |
||
| 1179 | $scope .= $db->value($Model->id, $Model->getColumnType($Model->primaryKey)); |
||
| 1180 | } else {
|
||
| 1181 | $scope['NOT'][$Model->alias . '.' . $Model->primaryKey] = $Model->id; |
||
| 1182 | } |
||
| 1183 | } |
||
| 1184 | $name = $Model->escapeField($right); |
||
| 1185 | list($edge) = array_values($Model->find('first', array( |
||
| 1186 | 'conditions' => $scope, |
||
| 1187 | 'fields' => $db->calculate($Model, 'max', array($name, $right)), |
||
| 1188 | 'recursive' => $recursive, |
||
| 1189 | 'order' => false, |
||
| 1190 | 'callbacks' => false |
||
| 1191 | ))); |
||
| 1192 | return (empty($edge[$right])) ? 0 : $edge[$right]; |
||
| 1193 | } |
||
| 1194 | |||
| 1195 | /**
|
||
| 1196 | * get the minimum index value in the table.
|
||
| 1197 | *
|
||
| 1198 | * @param Model $Model Model instance.
|
||
| 1199 | * @param string $scope Scoping conditions.
|
||
| 1200 | * @param string $left Left value.
|
||
| 1201 | * @param int $recursive Recurursive find value.
|
||
| 1202 | * @return int
|
||
| 1203 | */
|
||
| 1204 | protected function _getMin(Model $Model, $scope, $left, $recursive = -1) { |
||
| 1205 | $db = ConnectionManager::getDataSource($Model->useDbConfig); |
||
| 1206 | $name = $Model->escapeField($left); |
||
| 1207 | list($edge) = array_values($Model->find('first', array( |
||
| 1208 | 'conditions' => $scope, |
||
| 1209 | 'fields' => $db->calculate($Model, 'min', array($name, $left)), |
||
| 1210 | 'recursive' => $recursive, |
||
| 1211 | 'order' => false, |
||
| 1212 | 'callbacks' => false |
||
| 1213 | ))); |
||
| 1214 | return (empty($edge[$left])) ? 0 : $edge[$left]; |
||
| 1215 | } |
||
| 1216 | |||
| 1217 | /**
|
||
| 1218 | * Table sync method.
|
||
| 1219 | *
|
||
| 1220 | * Handles table sync operations, Taking account of the behavior scope.
|
||
| 1221 | *
|
||
| 1222 | * @param Model $Model Model instance.
|
||
| 1223 | * @param int $shift Shift by.
|
||
| 1224 | * @param string $dir Direction.
|
||
| 1225 | * @param array $conditions Conditions.
|
||
| 1226 | * @param bool $created Whether it's a new record.
|
||
| 1227 | * @param string $field Field type.
|
||
| 1228 | * @return void
|
||
| 1229 | */
|
||
| 1230 | protected function _sync(Model $Model, $shift, $dir = '+', $conditions = array(), $created = false, $field = 'both') { |
||
| 1231 | $ModelRecursive = $Model->recursive; |
||
| 1232 | extract($this->settings[$Model->alias]); |
||
| 1233 | $Model->recursive = $recursive; |
||
| 1234 | |||
| 1235 | if ($field === 'both') { |
||
| 1236 | $this->_sync($Model, $shift, $dir, $conditions, $created, $left); |
||
| 1237 | $field = $right; |
||
| 1238 | } |
||
| 1239 | if (is_string($conditions)) { |
||
| 1240 | $conditions = array($Model->escapeField($field) . " {$conditions}"); |
||
| 1241 | } |
||
| 1242 | if (($scope !== '1 = 1' && $scope !== true) && $scope) { |
||
| 1243 | $conditions[] = $scope; |
||
| 1244 | } |
||
| 1245 | if ($created) { |
||
| 1246 | $conditions['NOT'][$Model->escapeField()] = $Model->id; |
||
| 1247 | } |
||
| 1248 | $Model->updateAll(array($Model->escapeField($field) => $Model->escapeField($field) . ' ' . $dir . ' ' . $shift), $conditions); |
||
| 1249 | $Model->recursive = $ModelRecursive; |
||
| 1250 | } |
||
| 1251 | |||
| 1252 | } |