Subversion Repositories SmartDukaan

Rev

Details | Last modification | View Log | RSS feed

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