TreeBehavior.php 38 KB

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