PageRenderTime 52ms CodeModel.GetById 25ms RepoModel.GetById 1ms app.codeStats 0ms

/fuel/packages/orm/classes/model/nestedset.php

https://bitbucket.org/trujka/codegrounds
PHP | 1746 lines | 903 code | 257 blank | 586 comment | 82 complexity | 90adc591e720cb86902ba438e39e35c1 MD5 | raw file
Possible License(s): MIT, BSD-3-Clause, LGPL-2.1
  1. <?php
  2. /**
  3. * Fuel
  4. *
  5. * Fuel is a fast, lightweight, community driven PHP5 framework.
  6. *
  7. * @package Fuel
  8. * @version 1.6
  9. * @author Fuel Development Team
  10. * @license MIT License
  11. * @copyright 2010 - 2013 Fuel Development Team
  12. * @link http://fuelphp.com
  13. */
  14. namespace Orm;
  15. /**
  16. * Implements NestedSets (http://en.wikipedia.org/wiki/Nested_set_model)
  17. *
  18. * Some design ideas borrowed from https://github.com/hubspace/fuel-nestedset
  19. * (see http://fuelphp.com/forums/discussion/12206/fuel-nested-sets)
  20. */
  21. class Model_Nestedset extends Model
  22. {
  23. /*
  24. * @var array tree configuration for this model
  25. */
  26. // protected static $_tree = array();
  27. /**
  28. * @var array cached tree configurations
  29. */
  30. protected static $_tree_cached = array();
  31. /*
  32. * @var array nestedset tree configuration defaults
  33. */
  34. protected static $_defaults = array(
  35. 'left_field' => 'left_id', // name of the tree node left index field
  36. 'right_field' => 'right_id', // name of the tree node right index field
  37. 'tree_field' => null, // name of the tree node tree index field
  38. 'title_field' => null, // value of the tree node title field
  39. 'read-only' => array(), // list of properties to protect against direct updates
  40. );
  41. // -------------------------------------------------------------------------
  42. // tree configuration
  43. // -------------------------------------------------------------------------
  44. /**
  45. * Get a tree configuration parameter
  46. *
  47. * @param string name of the parameter to get
  48. * @return mixed parameter value, or null if the parameter does not exist
  49. */
  50. public static function tree_config($name = null)
  51. {
  52. $class = get_called_class();
  53. // configuration not loaded yet
  54. if ( ! array_key_exists($class, static::$_tree_cached))
  55. {
  56. // do we have a custom config for this model?
  57. if (property_exists($class, '_tree'))
  58. {
  59. static::$_tree_cached[$class] = array_merge(static::$_defaults, static::$_tree);
  60. }
  61. else
  62. {
  63. static::$_tree_cached[$class] = static::$_defaults;
  64. }
  65. // array of read-only column names, the can not be set manually
  66. foreach(array('left_field','right_field','tree_field') as $field)
  67. {
  68. $column = static::tree_config($field) and static::$_tree_cached[$class]['read-only'][] = $column;
  69. }
  70. }
  71. if (func_num_args() == 0)
  72. {
  73. return static::$_tree_cached[$class];
  74. }
  75. else
  76. {
  77. return array_key_exists($name, static::$_tree_cached[$class]) ? static::$_tree_cached[$class][$name] : null;
  78. }
  79. }
  80. // -------------------------------------------------------------------------
  81. // updated constructor, capture un-supported compound PK's
  82. // -------------------------------------------------------------------------
  83. /**
  84. * @var array store the node operation we need to execute on save() or get()
  85. */
  86. protected $_node_operation = array();
  87. /**
  88. * @var mixed id value of the current tree in multi-tree models
  89. */
  90. protected $_current_tree_id = null;
  91. /*
  92. * Initialize the nestedset model instance
  93. *
  94. * @param array any data passed to this model
  95. * @param bool whether or not this is a new model instance
  96. * @param string name of a database view to use instead of a table
  97. * @param bool whether or not to cache this object
  98. *
  99. * @throws OutOfBoundsException if the model has a compound primary key defined
  100. */
  101. public function __construct(array $data = array(), $new = true, $view = null, $cache = true)
  102. {
  103. // check for a compound key, we don't do that (yet)
  104. if (count(static::$_primary_key) > 1)
  105. {
  106. throw new \OutOfBoundsException('The Nestedset ORM model doesn\'t support compound primary keys.');
  107. }
  108. // call the ORM base model constructor
  109. parent::__construct($data, $new, $view, $cache);
  110. }
  111. // -------------------------------------------------------------------------
  112. // multi-tree select
  113. // -------------------------------------------------------------------------
  114. /**
  115. * Select a specific tree if the table contains multiple trees
  116. *
  117. * @param mixed type depends on the field type of the tree_field
  118. * @return Model_Nestedset this object, for chaining
  119. *
  120. * @throws BadMethodCallException if the model is not multi-tree
  121. */
  122. public function set_tree_id($tree = null)
  123. {
  124. // is this a multi-tree model?
  125. if (static::tree_config('tree_field') === null)
  126. {
  127. throw new \BadMethodCallException('This is not a multi-tree model, set_tree_id() can not be used.');
  128. }
  129. // set the tree filter value to select a specific tree
  130. $this->_current_tree_id = $tree;
  131. // return the object for chaining
  132. return $this;
  133. }
  134. // -------------------------------------------------------------------------
  135. /**
  136. * Select a specific tree if the table contains multiple trees
  137. *
  138. * @return mixed current tree id value
  139. *
  140. * @throws OutOfRangeException if no tree id has been set
  141. */
  142. public function get_tree_id()
  143. {
  144. // check if the current object is part of a tree
  145. if (($value = $this->{static::tree_config('tree_field')}) !== null)
  146. {
  147. return $value;
  148. }
  149. // check if there is a default tree id value set
  150. if ($this->_current_tree_id !== null)
  151. {
  152. return $this->_current_tree_id;
  153. }
  154. // we needed a tree id, but there isn't one defined
  155. throw new \OutOfRangeException('tree id required, but none is defined.');
  156. }
  157. // -------------------------------------------------------------------------
  158. // tree queries
  159. // -------------------------------------------------------------------------
  160. /**
  161. * Returns a query object on the selected tree
  162. *
  163. * @return Query the constructed query object
  164. */
  165. public function build_query()
  166. {
  167. // create a new query object
  168. $query = $this->query();
  169. // get params to avoid excessive method calls
  170. $tree_field = static::tree_config('tree_field');
  171. // add the tree id if needed
  172. if ( ! is_null($tree_field))
  173. {
  174. $query->where($tree_field, $this->get_tree_id());
  175. }
  176. // return the query object
  177. return $query;
  178. }
  179. // -------------------------------------------------------------------------
  180. /**
  181. * Returns the root of the tree the current node belongs to
  182. *
  183. * @return Model_Nestedset this object, for chaining
  184. */
  185. public function root()
  186. {
  187. $this->_node_operation = array(
  188. 'single' => true,
  189. 'action' => 'root',
  190. 'to' => null,
  191. );
  192. // return the object for chaining
  193. return $this;
  194. }
  195. // -------------------------------------------------------------------------
  196. /**
  197. * Returns the roots of all trees
  198. *
  199. * @return Model_Nestedset this object, for chaining
  200. */
  201. public function roots()
  202. {
  203. $this->_node_operation = array(
  204. 'single' => false,
  205. 'action' => 'roots',
  206. 'to' => null,
  207. );
  208. // return the object for chaining
  209. return $this;
  210. }
  211. // -------------------------------------------------------------------------
  212. /**
  213. * Returns the parent of the current node
  214. *
  215. * @return Model_Nestedset this object, for chaining
  216. */
  217. public function parent()
  218. {
  219. $this->_node_operation = array(
  220. 'single' => true,
  221. 'action' => 'parent',
  222. 'to' => null,
  223. );
  224. // return the object for chaining
  225. return $this;
  226. }
  227. // -------------------------------------------------------------------------
  228. /**
  229. * Returns the children of the current node
  230. *
  231. * @return Model_Nestedset this object, for chaining
  232. */
  233. public function children()
  234. {
  235. $this->_node_operation = array(
  236. 'single' => false,
  237. 'action' => 'children',
  238. 'to' => null,
  239. );
  240. // return the object for chaining
  241. return $this;
  242. }
  243. // -------------------------------------------------------------------------
  244. /**
  245. * Returns all ancestors of the current node
  246. *
  247. * @return Model_Nestedset this object, for chaining
  248. */
  249. public function ancestors()
  250. {
  251. $this->_node_operation = array(
  252. 'single' => false,
  253. 'action' => 'ancestors',
  254. 'to' => null,
  255. );
  256. // return the object for chaining
  257. return $this;
  258. }
  259. // -------------------------------------------------------------------------
  260. /**
  261. * Returns all descendants of the current node
  262. *
  263. * @return Model_Nestedset this object, for chaining
  264. */
  265. public function descendants()
  266. {
  267. $this->_node_operation = array(
  268. 'single' => false,
  269. 'action' => 'descendants',
  270. 'to' => null,
  271. );
  272. // return the object for chaining
  273. return $this;
  274. }
  275. // -------------------------------------------------------------------------
  276. /**
  277. * Returns all leafs of the current node
  278. *
  279. * @return Model_Nestedset this object, for chaining
  280. */
  281. public function leaf_descendants()
  282. {
  283. $this->_node_operation = array(
  284. 'single' => false,
  285. 'action' => 'leaf_descendants',
  286. 'to' => null,
  287. );
  288. // return the object for chaining
  289. return $this;
  290. }
  291. // -------------------------------------------------------------------------
  292. /**
  293. * Returns the siblings of the current node (includes the node itself!)
  294. *
  295. * @return Model_Nestedset this object, for chaining
  296. */
  297. public function siblings()
  298. {
  299. $this->_node_operation = array(
  300. 'single' => false,
  301. 'action' => 'siblings',
  302. 'to' => null,
  303. );
  304. // return the object for chaining
  305. return $this;
  306. }
  307. // -------------------------------------------------------------------------
  308. /**
  309. * Returns the path to the current node
  310. *
  311. * @return Model_Nestedset this object, for chaining
  312. */
  313. public function path()
  314. {
  315. $this->_node_operation = array(
  316. 'single' => false,
  317. 'action' => 'path',
  318. 'to' => null,
  319. );
  320. // return the object for chaining
  321. return $this;
  322. }
  323. // -------------------------------------------------------------------------
  324. // node manipulation methods
  325. // -------------------------------------------------------------------------
  326. /**
  327. * Alias for last_child()
  328. *
  329. * @param Model_Nestedset, or PK of the parent object, or null
  330. * @return Model_Nestedset this object, for chaining
  331. */
  332. public function child($to = null)
  333. {
  334. return $this->last_child($to);
  335. }
  336. // -------------------------------------------------------------------------
  337. /**
  338. * Gets or sets the first child of a node
  339. *
  340. * @param Model_Nestedset, or PK of the parent object, or null
  341. * @return Model_Nestedset this object, for chaining
  342. */
  343. public function first_child($to = null)
  344. {
  345. $this->_node_operation = array(
  346. 'single' => true,
  347. 'action' => 'first_child',
  348. 'to' => $to,
  349. );
  350. // return the object for chaining
  351. return $this;
  352. }
  353. // -------------------------------------------------------------------------
  354. /**
  355. * Gets or sets the last child of a node
  356. *
  357. * @param Model_Nestedset, or PK of the parent object, or null
  358. * @return Model_Nestedset this object, for chaining
  359. */
  360. public function last_child($to = null)
  361. {
  362. $this->_node_operation = array(
  363. 'single' => true,
  364. 'action' => 'last_child',
  365. 'to' => $to,
  366. );
  367. // return the object for chaining
  368. return $this;
  369. }
  370. // -------------------------------------------------------------------------
  371. /**
  372. * Alias for next_sibling()
  373. *
  374. * @param Model_Nestedset, or PK of the parent object, or null
  375. * @return Model_Nestedset this object, for chaining
  376. */
  377. public function sibling($to = null)
  378. {
  379. return $this->next_sibling($to);
  380. }
  381. // -------------------------------------------------------------------------
  382. /**
  383. * Gets or sets the previous sibling of a node
  384. *
  385. * @param Model_Nestedset, or PK of the parent object, or null
  386. * @return Model_Nestedset this object, for chaining
  387. */
  388. public function previous_sibling($to = null)
  389. {
  390. $this->_node_operation = array(
  391. 'single' => true,
  392. 'action' => 'previous_sibling',
  393. 'to' => $to,
  394. );
  395. // return the object for chaining
  396. return $this;
  397. }
  398. // -------------------------------------------------------------------------
  399. /**
  400. * Gets or sets the next sibling of a node
  401. *
  402. * @param Model_Nestedset, or PK of the parent object, or null
  403. * @return Model_Nestedset this object, for chaining
  404. */
  405. public function next_sibling($to = null)
  406. {
  407. $this->_node_operation = array(
  408. 'single' => true,
  409. 'action' => 'next_sibling',
  410. 'to' => $to,
  411. );
  412. // return the object for chaining
  413. return $this;
  414. }
  415. // -------------------------------------------------------------------------
  416. // boolean tree functions
  417. // -------------------------------------------------------------------------
  418. /**
  419. * Check if the object is a tree root
  420. *
  421. * @return bool
  422. */
  423. public function is_root()
  424. {
  425. return $this->{static::tree_config('left_field')} == 1;
  426. }
  427. // -------------------------------------------------------------------------
  428. /**
  429. * Check if the object is a tree leaf (node with no children)
  430. *
  431. * @return bool
  432. */
  433. public function is_leaf()
  434. {
  435. return $this->{static::tree_config('right_field')} - $this->{static::tree_config('left_field')} == 1;
  436. }
  437. // -------------------------------------------------------------------------
  438. /**
  439. * Check if the object is a child node (not a root node)
  440. *
  441. * @return bool
  442. */
  443. public function is_child()
  444. {
  445. return ! $this->is_root($this);
  446. }
  447. // -------------------------------------------------------------------------
  448. /**
  449. * Check if the object is a child of node
  450. *
  451. * @param Model_Nestedset of the parent to check
  452. * @return bool
  453. */
  454. public function is_child_of(Model_Nestedset $parent)
  455. {
  456. // get our parent
  457. $our_parent = $this->parent()->get_one();
  458. // and check if the parents match
  459. return $parent == $our_parent;
  460. }
  461. // -------------------------------------------------------------------------
  462. /**
  463. * Check if the object is a direct descendant of node
  464. *
  465. * @param Model_Nestedset of the parent to check
  466. * @return bool
  467. */
  468. public function is_descendant_of(Model_Nestedset $parent)
  469. {
  470. // get params to avoid excessive method calls
  471. $left_field = static::tree_config('left_field');
  472. $right_field = static::tree_config('right_field');
  473. return $this->{$left_field} > $parent->{$left_field} and
  474. $this->{$right_field} < $parent->{$right_field};
  475. }
  476. // -------------------------------------------------------------------------
  477. /**
  478. * Check if the object is the parent of node
  479. *
  480. * @param Model_Nestedset of the child to check
  481. * @return bool
  482. */
  483. public function is_parent_of(Model_Nestedset $child)
  484. {
  485. return $this == $child->parent()->get_one();
  486. }
  487. // -------------------------------------------------------------------------
  488. /**
  489. * Check if the object is the ancestor of node
  490. *
  491. * @param Model_Nestedset of the child to check
  492. * @return bool
  493. */
  494. public function is_ancestor_of(Model_Nestedset $child)
  495. {
  496. // get params to avoid excessive method calls
  497. $left_field = static::tree_config('left_field');
  498. $right_field = static::tree_config('right_field');
  499. return $child->{$left_field} > $this->{$left_field} and
  500. $child->{$right_field} < $this->{$right_field};
  501. }
  502. // -------------------------------------------------------------------------
  503. /**
  504. * Check if the object is the same model
  505. *
  506. * @param Model_Nestedset object to verify against
  507. * @return bool
  508. */
  509. public function is_same_model_as(Model_Nestedset $object)
  510. {
  511. return (get_class($object) == get_class($this));
  512. }
  513. // -------------------------------------------------------------------------
  514. /**
  515. * Check if the object is the same model and the same tree
  516. *
  517. * @param Model_Nestedset object to verify against
  518. * @return bool
  519. */
  520. public function is_same_tree_as(Model_Nestedset $object)
  521. {
  522. // make sure they're the same model
  523. if ($this->is_same_model_as($object))
  524. {
  525. // get params to avoid excessive method calls
  526. $tree_field = static::tree_config('tree_field');
  527. if (empty($this->{$tree_field}) or $this->{$tree_field} === $object->{$tree_field})
  528. {
  529. // same tree, or not a multi-tree model
  530. return true;
  531. }
  532. }
  533. // not the same tree
  534. return false;
  535. }
  536. // -------------------------------------------------------------------------
  537. /**
  538. * Check if the object has a parent
  539. *
  540. * Note: this is an alias for is_child()
  541. *
  542. * @return bool
  543. */
  544. public function has_parent()
  545. {
  546. return $this->is_child($this);
  547. }
  548. // -------------------------------------------------------------------------
  549. /**
  550. * Check if the object has children
  551. *
  552. * @return bool
  553. */
  554. public function has_children()
  555. {
  556. return $this->is_leaf($this) ? false : true;
  557. }
  558. // -------------------------------------------------------------------------
  559. /**
  560. * Check if the object has a previous sibling
  561. *
  562. * @return bool
  563. */
  564. public function has_previous_sibling()
  565. {
  566. return ! is_null($this->previous_sibling()->get_one());
  567. }
  568. // -------------------------------------------------------------------------
  569. /**
  570. * Check if the object has a next sibling
  571. *
  572. * @return bool
  573. */
  574. public function has_next_sibling()
  575. {
  576. return ! is_null($this->next_sibling()->get_one());
  577. }
  578. // -------------------------------------------------------------------------
  579. // integer tree methods
  580. // -------------------------------------------------------------------------
  581. /**
  582. * Return the count of the objects children
  583. *
  584. * @return mixed integer, or false in case no valid object was passed
  585. */
  586. public function count_children()
  587. {
  588. $result = $this->children()->get();
  589. return $result ? count($result) : 0;
  590. }
  591. // -------------------------------------------------------------------------
  592. /**
  593. * Return the count of the objects descendants
  594. *
  595. * @return mixed integer, or false in case no valid object was passed
  596. */
  597. public function count_descendants()
  598. {
  599. return ($this->{static::tree_config('right_field')} - $this->{static::tree_config('left_field')} - 1) / 2;
  600. }
  601. // -------------------------------------------------------------------------
  602. /**
  603. * Return the depth of the object in the tree, where the root = 0
  604. *
  605. * @return mixed integer, of false in case no valid object was found
  606. */
  607. public function depth()
  608. {
  609. // get params to avoid excessive method calls
  610. $left_field = static::tree_config('left_field');
  611. $right_field = static::tree_config('right_field');
  612. // we need a valid object for this to work
  613. if ($this->is_new())
  614. {
  615. return false;
  616. }
  617. else
  618. {
  619. // if we have a valid object, run the query to calculate the depth
  620. $query = $this->build_query()
  621. ->where($left_field, '<', $this->{$left_field})
  622. ->where($right_field, '>', $this->{$right_field});
  623. // return the result count
  624. return $query->count();
  625. }
  626. }
  627. // -------------------------------------------------------------------------
  628. /**
  629. * Return the tree, with the current node as root, as a nested array structure
  630. *
  631. * @param bool whether or not to return an array of objects
  632. * @param string property name to store the node's children
  633. * @return array
  634. */
  635. public function dump_tree($as_object = false, $children = 'children')
  636. {
  637. // get the PK
  638. $pk = reset(static::$_primary_key);
  639. // and the tree pointers
  640. $left_field = static::tree_config('left_field');
  641. $right_field = static::tree_config('right_field');
  642. // storage for the result, start with the current node
  643. $this[$children] = array();
  644. $tree = $as_object ? array($this->{$pk} => $this) : array($this->{$pk} => $this->to_array(true));
  645. // parent tracker
  646. $tracker = array();
  647. $index = 0;
  648. $tracker[$index] =& $tree[$this->{$pk}];
  649. // loop over the descendants
  650. foreach ($this->descendants()->get() as $treenode)
  651. {
  652. // get the data for this node
  653. $node = $as_object ? $treenode : $treenode->to_array(true);
  654. // make sure we have a place to store child information
  655. $node[$children] = array();
  656. // is this node a child of the current parent?
  657. if ($treenode->{$left_field} > $tracker[$index][$right_field])
  658. {
  659. // no, so pop the last parent and move a level back up
  660. $index--;
  661. }
  662. // add it as a child to the current parent
  663. if ($as_object)
  664. {
  665. $tracker[$index]->{$children}[$treenode->{$pk}] = $node;
  666. }
  667. else
  668. {
  669. $tracker[$index][$children][$treenode->{$pk}] = $node;
  670. }
  671. // does this node have children?
  672. if ($treenode->{$right_field} - $treenode->{$left_field} > 1)
  673. {
  674. // create a new parent level
  675. if ($as_object)
  676. {
  677. $tracker[$index+1] =& $tracker[$index]->{$children}[$treenode->{$pk}];
  678. }
  679. else
  680. {
  681. $tracker[$index+1] =& $tracker[$index][$children][$treenode->{$pk}];
  682. }
  683. $index++;
  684. }
  685. }
  686. return $as_object ? $this : $tree;
  687. }
  688. // -------------------------------------------------------------------------
  689. /**
  690. * Capture __unset() to make sure no read-only properties are erased
  691. *
  692. * @param string $property
  693. */
  694. public function __unset($property)
  695. {
  696. // make sure we're not unsetting a read-only value
  697. if (in_array($property, static::tree_config('read-only')))
  698. {
  699. throw new \InvalidArgumentException('Property "'.$property.'" is read-only and can not be changed');
  700. }
  701. parent::__unset($property);
  702. }
  703. // -------------------------------------------------------------------------
  704. /**
  705. * Capture set() to make sure no read-only properties are overwritten
  706. *
  707. * @param string|array $property
  708. * @param string $value in case $property is a string
  709. * @return Model
  710. */
  711. public function set($property, $value = null)
  712. {
  713. // check if we're in a frozen state
  714. if ($this->_frozen)
  715. {
  716. throw new FrozenObject('No changes allowed.');
  717. }
  718. // make sure we're not setting a read-only value
  719. if (in_array($property, static::tree_config('read-only')) and $this->{$property} !== $value)
  720. {
  721. throw new \InvalidArgumentException('Property "'.$property.'" is read-only and can not be changed');
  722. }
  723. return parent::set($property, $value);
  724. }
  725. // -------------------------------------------------------------------------
  726. /**
  727. * Capture calls to save(), to make sure no new record is inserted
  728. * directly which would seriously break the tree...
  729. */
  730. public function save($cascade = null, $use_transaction = false)
  731. {
  732. // get params to avoid excessive method calls
  733. $tree_field = static::tree_config('tree_field');
  734. $left_field = static::tree_config('left_field');
  735. $right_field = static::tree_config('right_field');
  736. // deal with new objects
  737. if ($this->_is_new)
  738. {
  739. // was a relocation of this node asked?
  740. if ( ! empty($this->_node_operation))
  741. {
  742. if ($this->_node_operation['to'])
  743. {
  744. // do we have a model? if not, try to autoload it
  745. if ( ! $this->_node_operation['to'] instanceOf Model_Nestedset)
  746. {
  747. $this->_node_operation['to'] = static::find($this->_node_operation['to']);
  748. }
  749. // verify that both objects are from the same model
  750. $this->_same_model_as($this->_node_operation['to'], __METHOD__);
  751. // set the tree id if needed
  752. if ( ! is_null($tree_field))
  753. {
  754. $this->_data[$tree_field] = $this->_node_operation['to']->get_tree_id();
  755. }
  756. }
  757. // add the left- and right pointers to the current object, and make room for it
  758. if ($use_transaction)
  759. {
  760. $db = \Database_Connection::instance(static::connection(true));
  761. $db->start_transaction();
  762. }
  763. try
  764. {
  765. switch ($this->_node_operation['action'])
  766. {
  767. case 'next_sibling':
  768. // set the left- and right pointers for the new node
  769. $this->_data[$left_field] = $this->_node_operation['to']->{$right_field} + 1;
  770. $this->_data[$right_field] = $this->_node_operation['to']->{$right_field} + 2;
  771. // create room for this new node
  772. $this->_shift_rl_values($this->{$left_field}, 2);
  773. break;
  774. case 'previous_sibling':
  775. // set the left- and right pointers for the new node
  776. $this->_data[$left_field] = $this->_node_operation['to']->{$left_field};
  777. $this->_data[$right_field] = $this->_node_operation['to']->{$left_field} + 1;
  778. // create room for this new node
  779. $this->_shift_rl_values($this->{$left_field}, 2);
  780. break;
  781. case 'first_child':
  782. // set the left- and right pointers for the new node
  783. $this->_data[$left_field] = $this->_node_operation['to']->{$left_field} + 1;
  784. $this->_data[$right_field] = $this->_node_operation['to']->{$left_field} + 2;
  785. // create room for this new node
  786. $this->_shift_rl_values($this->{$left_field}, 2);
  787. break;
  788. case 'last_child':
  789. // set the left- and right pointers for the new node
  790. $this->_data[$left_field] = $this->_node_operation['to']->{$right_field};
  791. $this->_data[$right_field] = $this->_node_operation['to']->{$right_field} + 1;
  792. // create room for this new node
  793. $this->_shift_rl_values($this->{$left_field}, 2);
  794. break;
  795. default:
  796. throw new \OutOfBoundsException('You can not define a '.$this->_node_operation['action'].'() action before a save().');
  797. break;
  798. }
  799. }
  800. catch (\Exception $e)
  801. {
  802. $use_transaction and $db->rollback_transaction();
  803. throw $e;
  804. }
  805. }
  806. // assume we want a new root node
  807. else
  808. {
  809. // set the left- and right pointers for the new root
  810. $this->_data[$left_field] = 1;
  811. $this->_data[$right_field] = 2;
  812. // we need to check if we don't already have this root
  813. $query = \DB::select('id')
  814. ->from(static::table())
  815. ->where($left_field, '=', 1);
  816. // multi-root tree? And no new tree id defined?
  817. if ( ! is_null($tree_field) and empty($this->{$tree_field}))
  818. {
  819. // get the next free tree id, and hope it's numeric...
  820. $this->_data[$tree_field] = $this->max($tree_field) + 1;
  821. // and set it as the default tree id for this node
  822. $this->set_tree_id($this->_data[$tree_field]);
  823. }
  824. // add the tree_id to the query if present
  825. if ( ! empty($this->{$tree_field}))
  826. {
  827. $query->where($tree_field, '=', $this->_data[$tree_field]);
  828. }
  829. // run the check
  830. $result = $query->execute(static::connection());
  831. // any hits?
  832. if (count($result))
  833. {
  834. throw new \OutOfBoundsException('You can not add this new tree root, it already exists.');
  835. }
  836. }
  837. }
  838. // and with existing objects
  839. else
  840. {
  841. // get the classname of this model
  842. $class = get_called_class();
  843. // readonly fields may not be changed
  844. foreach (static::$_tree_cached[$class]['read-only'] as $column)
  845. {
  846. // so reset them if they were changed
  847. $this->_data[$column] = $this->_original[$column];
  848. }
  849. // was a relocation of this node asked
  850. if ( ! empty($this->_node_operation))
  851. {
  852. if ($this->_node_operation['to'])
  853. {
  854. // do we have a model? if not, try to autoload it
  855. if ( ! $this->_node_operation['to'] instanceOf Model_Nestedset)
  856. {
  857. $this->_node_operation['to'] = static::find($this->_node_operation['to']);
  858. }
  859. // verify that both objects are from the same model
  860. $this->_same_model_as($this->_node_operation['to'], __METHOD__);
  861. // and from the same tree (if we have multi-tree support for this object)
  862. if ( ! is_null($tree_field))
  863. {
  864. if ($this->{$tree_field} !== $this->_node_operation['to']->{$tree_field})
  865. {
  866. throw new \OutOfBoundsException('When moving nodes, nodes must be part of the same tree.');
  867. }
  868. }
  869. }
  870. // move the node
  871. if ($use_transaction)
  872. {
  873. $db = \Database_Connection::instance(static::connection(true));
  874. $db->start_transaction();
  875. }
  876. try
  877. {
  878. switch ($this->_node_operation['action'])
  879. {
  880. case 'next_sibling':
  881. $this->_move_subtree($this->_node_operation['to']->{static::tree_config('right_field')} + 1);
  882. break;
  883. case 'previous_sibling':
  884. $this->_move_subtree($this->_node_operation['to']->{static::tree_config('left_field')});
  885. break;
  886. case 'first_child':
  887. $this->_move_subtree($this->_node_operation['to']->{static::tree_config('left_field')} + 1);
  888. break;
  889. case 'last_child':
  890. $this->_move_subtree($this->_node_operation['to']->{static::tree_config('right_field')});
  891. break;
  892. default:
  893. throw new \OutOfBoundsException('You can not define a '.$this->_node_operation['action'].'() action before a save().');
  894. break;
  895. }
  896. }
  897. catch (\Exception $e)
  898. {
  899. $use_transaction and $db->rollback_transaction();
  900. throw $e;
  901. }
  902. }
  903. }
  904. // reset the node operation store to make sure nothings pending...
  905. $this->_node_operation = array();
  906. // save the current node and return the result
  907. return parent::save($cascade, $use_transaction);
  908. }
  909. // -------------------------------------------------------------------------
  910. /**
  911. * Capture calls to delete(), to make sure no delete happens without reindexing
  912. *
  913. * @param mixed $cascade
  914. * null = use default config,
  915. * bool = force/prevent cascade,
  916. * array cascades only the relations that are in the array
  917. * @return Model this instance as a new object without primary key(s)
  918. *
  919. * @throws DomainException if you try to delete a root node with multiple children
  920. */
  921. public function delete($cascade = null, $use_transaction = false)
  922. {
  923. if ($use_transaction)
  924. {
  925. $db = \Database_Connection::instance(static::connection(true));
  926. $db->start_transaction();
  927. }
  928. // get params to avoid excessive method calls
  929. $left_field = static::tree_config('left_field');
  930. $right_field = static::tree_config('right_field');
  931. // if this is a root node with multiple children, bail out
  932. if ($this->is_root() and $this->count_children() > 1)
  933. {
  934. throw new \DomainException('You can not delete a tree root with multiple children.');
  935. }
  936. // put the entire operation in a try/catch, so we can rollback if needed
  937. try
  938. {
  939. // delete the node itself
  940. $result = parent::delete($cascade);
  941. // check if the delete was succesful
  942. if ($result !== false)
  943. {
  944. // re-index the tree
  945. $this->_shift_rl_range($this->{$left_field} + 1, $this->{$right_field} - 1, -1);
  946. $this->_shift_rl_values($this->{$right_field} + 1, -2);
  947. }
  948. }
  949. catch (\Exception $e)
  950. {
  951. $use_transaction and $db->rollback_transaction();
  952. throw $e;
  953. }
  954. // reset the node operation store to make sure nothings pending...
  955. $this->_node_operation = array();
  956. // and return the result
  957. return $result;
  958. }
  959. // -------------------------------------------------------------------------
  960. // tree destructors
  961. // -------------------------------------------------------------------------
  962. /**
  963. * Deletes the entire tree structure using the current node as starting point
  964. *
  965. * @param mixed $cascade
  966. * null = use default config,
  967. * bool = force/prevent cascade,
  968. * array cascades only the relations that are in the array
  969. * @return Model this instance as a new object without primary key(s)
  970. */
  971. public function delete_tree($cascade = null, $use_transaction = false)
  972. {
  973. if ($use_transaction)
  974. {
  975. $db = \Database_Connection::instance(static::connection(true));
  976. $db->start_transaction();
  977. }
  978. // get params to avoid excessive method calls
  979. $left_field = static::tree_config('left_field');
  980. $right_field = static::tree_config('right_field');
  981. $pk = reset(static::$_primary_key);
  982. // put the entire operation in a try/catch, so we can rollback if needed
  983. try
  984. {
  985. // check if the node has children
  986. if ($this->has_children())
  987. {
  988. // get them
  989. $children = $this->children()->get();
  990. // and delete them to
  991. foreach ($children as $child)
  992. {
  993. if ($child->delete($cascade) === false)
  994. {
  995. throw new \UnexpectedValueException('delete of child node with PK "'.$child->{$pk}.'" failed.');
  996. }
  997. }
  998. }
  999. // delete the node itself
  1000. $result = parent::delete($cascade);
  1001. // check if the delete was succesful
  1002. if ($result !== false)
  1003. {
  1004. // re-index the tree
  1005. $this->_shift_rl_values($this->{$right_field} + 1, $this->{$left_field} - $this->{$right_field} - 1);
  1006. }
  1007. }
  1008. catch (\Exception $e)
  1009. {
  1010. $use_transaction and $db->rollback_transaction();
  1011. throw $e;
  1012. }
  1013. // reset the node operation store to make sure nothings pending...
  1014. $this->_node_operation = array();
  1015. // and return the result
  1016. return $result;
  1017. }
  1018. // -------------------------------------------------------------------------
  1019. // get methods
  1020. // -------------------------------------------------------------------------
  1021. /**
  1022. * Creates a new query with optional settings up front, or return a pre-build
  1023. * query to further chain upon
  1024. *
  1025. * @param array
  1026. * @return Query
  1027. */
  1028. public function get_query()
  1029. {
  1030. // make sure there's a node operation defined
  1031. if (empty($this->_node_operation))
  1032. {
  1033. // assume a get-all operation
  1034. $this->_node_operation = array(
  1035. 'single' => false,
  1036. 'action' => 'all',
  1037. 'to' => null,
  1038. );
  1039. }
  1040. return $this->_fetch_nodes('query');
  1041. }
  1042. // -------------------------------------------------------------------------
  1043. /**
  1044. * Get one or more tree nodes, and provide fallback for
  1045. * the original model getter
  1046. *
  1047. * @param mixed
  1048. *
  1049. * @returns mixed
  1050. * @throws BadMethodCallException if called without a parameter and without a node to fetch
  1051. */
  1052. public function & get($query = null)
  1053. {
  1054. // do we have any parameters passed?
  1055. if (func_num_args())
  1056. {
  1057. // capture normal getter calls
  1058. if ($query instanceOf Query)
  1059. {
  1060. // run a get() on the query
  1061. return $query->get();
  1062. }
  1063. else
  1064. {
  1065. // assume it's a model getter call
  1066. return parent::get($query);
  1067. }
  1068. }
  1069. // make sure there's a node operation defined
  1070. if (empty($this->_node_operation))
  1071. {
  1072. // assume a get-all operation
  1073. $this->_node_operation = array(
  1074. 'single' => false,
  1075. 'action' => 'all',
  1076. 'to' => null,
  1077. );
  1078. }
  1079. // no parameters, so we need to fetch something
  1080. $result = $this->_fetch_nodes('multiple');
  1081. return $result;
  1082. }
  1083. // -------------------------------------------------------------------------
  1084. /*
  1085. * Get a single tree node
  1086. *
  1087. * @param Query
  1088. *
  1089. * @returns mixed
  1090. * @throws BadMethodCallException if called without a parameter and without a node to fetch
  1091. */
  1092. public function get_one(Query $query = null)
  1093. {
  1094. // do we have a query object passed?
  1095. if (func_num_args())
  1096. {
  1097. // return the query result
  1098. return $query->get_one();
  1099. }
  1100. // make sure there's a node operation defined
  1101. if (empty($this->_node_operation))
  1102. {
  1103. // assume a get-all operation
  1104. $this->_node_operation = array(
  1105. 'single' => true,
  1106. 'action' => 'all',
  1107. 'to' => null,
  1108. );
  1109. }
  1110. // so we need to fetch something
  1111. return $this->_fetch_nodes('single');
  1112. }
  1113. // -------------------------------------------------------------------------
  1114. // protected class functions
  1115. // -------------------------------------------------------------------------
  1116. /**
  1117. * Check if the object passed is an instance of the current model
  1118. *
  1119. * @param Model_Nestedset
  1120. * @param string optional method name to display in the exception message
  1121. * @return bool
  1122. *
  1123. * @throws OutOfBoundsException in case the two objects are not part of the same model
  1124. */
  1125. protected function _same_model_as($object, $method = 'unknown')
  1126. {
  1127. if ( ! $this->is_same_model_as($object))
  1128. {
  1129. throw new \OutOfBoundsException('Model object passed to '.$method.'() is not an instance of '.get_class($this).'.');
  1130. }
  1131. }
  1132. // -------------------------------------------------------------------------
  1133. /*
  1134. * Fetch a node or nodes, and return the result
  1135. *
  1136. * @param string action, either 'single' or 'multiple'
  1137. * @return mixed Model_Nestedset or an array of Model_Nestedset, or null if none found
  1138. */
  1139. protected function _fetch_nodes($action)
  1140. {
  1141. // get params to avoid excessive method calls
  1142. $left_field = static::tree_config('left_field');
  1143. $right_field = static::tree_config('right_field');
  1144. $tree_field = static::tree_config('tree_field');
  1145. // construct the query
  1146. switch ($this->_node_operation['action'])
  1147. {
  1148. case 'all':
  1149. $query = $this->build_query();
  1150. break;
  1151. case 'root':
  1152. $query = $this->build_query()
  1153. ->where($left_field, '=', 1);
  1154. break;
  1155. case 'roots':
  1156. $query = $this->query()
  1157. ->where($left_field, '=', 1);
  1158. break;
  1159. case 'parent':
  1160. $query = $this->build_query()
  1161. ->where($left_field, '<', $this->{$left_field})
  1162. ->where($right_field, '>', $this->{$right_field})
  1163. ->order_by($right_field, 'ASC');
  1164. break;
  1165. case 'first_child':
  1166. $query = $this->build_query()
  1167. ->where($left_field, $this->{$left_field} + 1);
  1168. break;
  1169. case 'last_child':
  1170. $query = $this->build_query()
  1171. ->where($right_field, $this->{$right_field} - 1);
  1172. break;
  1173. case 'children':
  1174. // get the PK's of all child objects
  1175. $pk = reset(static::$_primary_key);
  1176. $left = $this->{$left_field};
  1177. $right = $this->{$right_field};
  1178. // if we're multitree, add the tree filter to the query
  1179. if (is_null($tree_field))
  1180. {
  1181. $query = \DB::select('child.'.$pk)
  1182. ->from(array(static::table(), 'child'))
  1183. ->join(array(static::table(), 'ancestor'), 'left')
  1184. ->on(\DB::identifier('ancestor.' . $left_field), 'BETWEEN', \DB::expr(($left + 1) . ' AND ' . ($right - 1)))
  1185. ->on(\DB::identifier('child.' . $left_field), 'BETWEEN', \DB::expr(\DB::identifier('ancestor.'.$left_field).' + 1 AND '.\DB::identifier('ancestor.'.$right_field).' - 1'))
  1186. ->where(\DB::identifier('child.' . $left_field), 'BETWEEN', \DB::expr(($left + 1) . ' AND ' . ($right - 1)))
  1187. ->and_where('ancestor.'.$pk, null);
  1188. }
  1189. else
  1190. {
  1191. $query = \DB::select('child.'.$pk)
  1192. ->from(array(static::table(), 'child'))
  1193. ->join(array(static::table(), 'ancestor'), 'left')
  1194. ->on(\DB::identifier('ancestor.' . $left_field), 'BETWEEN', \DB::expr(($left + 1) . ' AND ' . ($right - 1) . ' AND '.\DB::identifier('ancestor.'.$tree_field).' = '.$this->get_tree_id()))
  1195. ->on(\DB::identifier('child.' . $left_field), 'BETWEEN', \DB::expr(\DB::identifier('ancestor.'.$left_field).' + 1 AND '.\DB::identifier('ancestor.'.$right_field).' - 1'))
  1196. ->where(\DB::identifier('child.' . $left_field), 'BETWEEN', \DB::expr(($left + 1) . ' AND ' . ($right - 1)))
  1197. ->and_where('ancestor.'.$pk, null)
  1198. ->and_where('child.'.$tree_field, '=', $this->get_tree_id());
  1199. }
  1200. // extract the PK's, and bail out if no children found
  1201. if ( ! $pks = $query->execute(static::connection())->as_array())
  1202. {
  1203. return null;
  1204. }
  1205. // construct the query to find all child objects
  1206. $query = $this->build_query()
  1207. ->where($pk, 'IN', $pks)
  1208. ->order_by($left_field, 'ASC');
  1209. break;
  1210. case 'ancestors':
  1211. // storage for the result
  1212. $result = array();
  1213. // new objects don't have a parent
  1214. if ( ! $this->is_new())
  1215. {
  1216. $parent = $this;
  1217. while (($parent = $parent->parent()->get_one()) !== null)
  1218. {
  1219. $result[$parent->id] = $parent;
  1220. }
  1221. }
  1222. // reverse the result
  1223. $result = array_reverse($result, true);
  1224. // return the result
  1225. return $result;
  1226. break;
  1227. case 'descendants':
  1228. $query = $this->build_query()
  1229. ->where($left_field, '>', $this->{$left_field})
  1230. ->where($right_field, '<', $this->{$right_field})
  1231. ->order_by($left_field, 'ASC');
  1232. break;
  1233. case 'leaf_descendants':
  1234. $query = $this->build_query()
  1235. ->where($left_field, '>', $this->{$left_field})
  1236. ->where($right_field, '<', $this->{$right_field})
  1237. ->where(\DB::expr(\DB::quote_identifier($right_field) . ' - ' . \DB::quote_identifier($left_field)), '=', 1)
  1238. ->order_by($left_field, 'ASC');
  1239. break;
  1240. case 'previous_sibling':
  1241. $query = $this->build_query()
  1242. ->where(static::tree_config('right_field'), $this->{static::tree_config('left_field')} - 1);
  1243. break;
  1244. case 'next_sibling':
  1245. $query = $this->build_query()
  1246. ->where(static::tree_config('left_field'), $this->{static::tree_config('right_field')} + 1);
  1247. break;
  1248. case 'siblings':
  1249. // if we have a parent object
  1250. if ($parent = $this->parent()->get_one())
  1251. {
  1252. // get the children of that parent
  1253. return $parent->children()->get();
  1254. }
  1255. else
  1256. {
  1257. // no siblings
  1258. return null;
  1259. }
  1260. break;
  1261. case 'path':
  1262. // do we have a title field defined?
  1263. if ($title_field = static::tree_config('title_field'))
  1264. {
  1265. // storage for the path
  1266. $path = '';
  1267. // get all parents
  1268. $result = $this->ancestors()->get();
  1269. // construct the path
  1270. foreach($result as $object)
  1271. {
  1272. $path .= $object->{$title_field}.'/';
  1273. }
  1274. $path .= $this->{$title_field}.'/';
  1275. // and return it
  1276. return $path;
  1277. }
  1278. else
  1279. {
  1280. throw new \OutOfBoundsException('You can call path(), the "'.get_class($this).'" model does not define a title field.');
  1281. }
  1282. break;
  1283. default:
  1284. throw new \OutOfBoundsException('You can not set a '.$this->_node_operation['action'].'() operation on a get() or get_one().');
  1285. break;
  1286. }
  1287. // reset the node operation store to make sure nothings pending...
  1288. $this->_node_operation = array();
  1289. if ($action == 'query')
  1290. {
  1291. // return the query object for further chaining
  1292. return $query;
  1293. }
  1294. else
  1295. {
  1296. // return the query result based on the action type
  1297. return $action == 'single' ? $query->get_one() : $query->get();
  1298. }
  1299. }
  1300. // -------------------------------------------------------------------------
  1301. /**
  1302. * Interal tree operation. Shift left-right pointers to make room for
  1303. * one or mode nodes, or to re-order the pointers after a delete
  1304. * operation.
  1305. *
  1306. * @param integer left pointer of the first node to shift
  1307. * @param integer number of positions to shift (if negative the shift will be to the left)
  1308. */
  1309. protected function _shift_rl_values($first, $delta)
  1310. {
  1311. // get params to avoid excessive method calls
  1312. $tree_field = static::tree_config('tree_field');
  1313. $left_field = static::tree_config('left_field');
  1314. $right_field = static::tree_config('right_field');
  1315. $query = \DB::update(static::table());
  1316. // if we have multiple roots
  1317. if ( ! is_null($tree_field))
  1318. {
  1319. $query->where($tree_field, $this->get_tree_id());
  1320. }
  1321. $query->where($left_field, '>=', $first);
  1322. // correct the delta
  1323. $sqldelta = ($delta < 0) ? (' - '.abs($delta)) : (' + '.$delta);
  1324. // set clause
  1325. $query->set(array(
  1326. $left_field => \DB::expr(\DB::quote_identifier($left_field).$sqldelta),
  1327. ));
  1328. // update in the correct order to avoid constraint conflicts
  1329. $query->order_by($left_field, ($delta < 0 ? 'ASC' : 'DESC'));
  1330. // execute it
  1331. $query->execute(static::connection(true));
  1332. $query = \DB::update(static::table());
  1333. // if we have multiple roots
  1334. if ( ! is_null($tree_field))
  1335. {
  1336. $query->where($tree_field, $this->get_tree_id());
  1337. }
  1338. $query->where($right_field, '>=', $first);
  1339. // set clause
  1340. $query->set(array(
  1341. $right_field => \DB::expr(\DB::quote_identifier($right_field).$sqldelta),
  1342. ));
  1343. // update in the correct order to avoid constraint conflicts
  1344. $query->order_by($right_field, ($delta < 0 ? 'ASC' : 'DESC'));
  1345. // execute it
  1346. $query->execute(static::connection(true));
  1347. // update cached objects, we've modified pointers
  1348. $class = get_called_class();
  1349. if (array_key_exists($class, static::$_cached_objects))
  1350. {
  1351. foreach (static::$_cached_objects[$class] as $object)
  1352. {
  1353. if (is_null($tree_field) or $object->{$tree_field} == $this->{$tree_field})
  1354. {
  1355. if ($object->{$left_field} >= $first)
  1356. {
  1357. if ($delta < 0)
  1358. {
  1359. $object->_data[$left_field] -= abs($delta);
  1360. $object->_original[$left_field] -= abs($delta);
  1361. }
  1362. else
  1363. {
  1364. $object->_data[$left_field] += $delta;
  1365. $object->_original[$left_field] += $delta;
  1366. }
  1367. }
  1368. if ($object->{$right_field} >= $first)
  1369. {
  1370. if ($delta < 0)
  1371. {
  1372. $object->_data[$right_field] -= abs($delta);
  1373. $object->_original[$right_field] -= abs($delta);
  1374. }
  1375. else
  1376. {
  1377. $object->_data[$right_field] += $delta;
  1378. $object->_original[$right_field] += $delta;
  1379. }
  1380. }
  1381. }
  1382. }
  1383. }
  1384. }
  1385. // -------------------------------------------------------------------------
  1386. /**
  1387. * Interal tree operation. Shift left-right pointers to make room for
  1388. * one or mode nodes, or to re-order the pointers after a delete
  1389. * operation, in the given range
  1390. *
  1391. * @param integer left pointer of the first node to shift
  1392. * @param integer right pointer of the last node to shift
  1393. * @param integer number of positions to shift (if negative the shift will be to the left)
  1394. */
  1395. protected function _shift_rl_range($first, $last, $delta)
  1396. {
  1397. // get params to avoid excessive method calls
  1398. $tree_field = static::tree_config('tree_field');
  1399. $left_field = static::tree_config('left_field');
  1400. $right_field = static::tree_config('right_field');
  1401. $query = \DB::update(static::table());
  1402. // if we have multiple roots
  1403. if ( ! is_null($tree_field))
  1404. {
  1405. $query->where($tree_field, $this->get_tree_id());
  1406. }
  1407. // select the range
  1408. $query->where($left_field, '>=', $first);
  1409. $query->where($right_field, '<=', $last);
  1410. // correct the delta
  1411. $sqldelta = ($delta < 0) ? (' - '.abs($delta)) : (' + '.$delta);
  1412. // set clause
  1413. $query->set(array(
  1414. $left_field => \DB::expr(\DB::quote_identifier($left_field).$sqldelta),
  1415. $right_field => \DB::expr(\DB::quote_identifier($right_field).$sqldelta),
  1416. ));
  1417. // update in the correct order to avoid constraint conflicts
  1418. $query->order_by($right_field, ($delta < 0 ? 'ASC' : 'DESC'));
  1419. // execute it
  1420. $query->execute(static::connection(true));
  1421. // update cached objects, we've modified pointers
  1422. $class = get_called_class();
  1423. if (array_key_exists($class, static::$_cached_objects))
  1424. {
  1425. foreach (static::$_cached_objects[$class] as $object)
  1426. {
  1427. if (is_null($tree_field) or $object->{$tree_field} == $this->{$tree_field})
  1428. {
  1429. if ($object->{$left_field} >= $first and $object->{$right_field} <= $last)
  1430. {
  1431. if ($delta < 0)
  1432. {
  1433. $object->_data[$left_field] -= abs($delta);
  1434. $object->_data[$right_field] -= abs($delta);
  1435. $object->_original[$left_field] -= abs($delta);
  1436. $object->_original[$right_field] -= abs($delta);
  1437. }
  1438. else
  1439. {
  1440. $object->_data[$left_field] += $delta;
  1441. $object->_data[$right_field] += $delta;
  1442. $object->_original[$left_field] += $delta;
  1443. $object->_original[$right_field] += $delta;
  1444. }
  1445. }
  1446. }
  1447. }
  1448. }
  1449. }
  1450. // -------------------------------------------------------------------------
  1451. /**
  1452. * Interal tree operation. Move the current node and all children
  1453. * to a new position in the tree
  1454. *
  1455. * @param integer new left pointer location to move to
  1456. */
  1457. protected function _move_subtree($destination_id)
  1458. {
  1459. // get params to avoid excessive method calls
  1460. $tree_field = static::tree_config('tree_field');
  1461. $left_field = static::tree_config('left_field');
  1462. $right_field = static::tree_config('right_field');
  1463. // catch a move into the subtree
  1464. if ( $destination_id >= $this->{$left_field} and $destination_id <= $this->{$right_field} )
  1465. {
  1466. // it would make no change to the tree
  1467. return $this;
  1468. }
  1469. // determine the size of the tree to move
  1470. $treesize = $this->{$right_field} - $this->{$left_field} + 1;
  1471. // get the objects left- and right pointers
  1472. $left_id = $this->{$left_field};
  1473. $right_id = $this->{$right_field};
  1474. // shift to make some space
  1475. $this->_shift_rl_values($destination_id, $treesize);
  1476. // correct pointers if there were shifted to
  1477. if ($this->{$left_field} >= $destination_id)
  1478. {
  1479. $left_id += $treesize;
  1480. $right_id += $treesize;
  1481. }
  1482. // enough room now, start the move
  1483. $this->_shift_rl_range($left_id, $right_id, $destination_id - $left_id);
  1484. // and correct index values after the source
  1485. $this->_shift_rl_values(++$right_id, (-1 * $treesize));
  1486. // return the moved object
  1487. return $this;
  1488. }
  1489. }