TreeBehavior.php 38 KB

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