sfPropelActAsNestedSetBehavior.class.php 35 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150
  1. <?php
  2. /*
  3. * This file is part of the sfPropelActAsNestedSetBehavior package.
  4. *
  5. * (c) 2006-2007 Tristan Rivoallan <tristan@rivoallan.net>
  6. *
  7. * For the full copyright and license information, please view the LICENSE
  8. * file that was distributed with this source code.
  9. */
  10. /**
  11. * This behavior adds necessary logic for a propel model to behave as a nested set.
  12. *
  13. * To enable this behavior add this after Propel stub class declaration :
  14. *
  15. * <code>
  16. * $columns_map = array('left' => MyClassPeer::TREE_LEFT,
  17. * 'right' => MyClassPeer::TREE_RIGHT,
  18. * 'parent' => MyClassPeer::TREE_PARENT,
  19. * 'scope' => MyClassPeer::TOPIC_ID);
  20. *
  21. * sfPropelBehavior::add('MyClass', array('actasnestedset' => array('columns' => $columns_map)));
  22. * </code>
  23. *
  24. * Column map values signification :
  25. *
  26. * - left : Model column holding nested set left value for a row
  27. * - right : Model column holding nested set right value for a row
  28. * - parent : Model column holding row's parent id
  29. * (this is necessary because we use adjacency list tree traversal for some methods)
  30. * - scope : Model column holding row's scope id. The scope is used to differenciate trees stored in
  31. * the same table
  32. *
  33. * @author Tristan Rivoallan <tristan@rivoallan.net>
  34. * @author Heltem (http://propel.phpdb.org/trac/ticket/312)
  35. * @author Joe Simms (http://www.symfony-project.com/forum/index.php/m/20657/)
  36. *
  37. * @see http://www.symfony-project.com/trac/wiki/sfPropelActAsNestedSetBehaviorPlugin
  38. */
  39. class sfPropelActAsNestedSetBehavior
  40. {
  41. # -- PROPERTIES
  42. /**
  43. * Nested set related columns.
  44. *
  45. * @var array
  46. */
  47. private static $columns = array();
  48. /**
  49. * Holds SQL queries propel objects that need to be processed before a node is saved.
  50. * It acts as a FIFO stack.
  51. *
  52. * @var array
  53. */
  54. private $presave_stack = array();
  55. /**
  56. * Holds SQL queries propel objects that need to be processed before a node is deleted.
  57. * It acts as a FIFO stack.
  58. *
  59. * @var array
  60. */
  61. private $predelete_stack = array();
  62. # -- HOOKS
  63. /**
  64. * Runs necessary queries before node is saved.
  65. *
  66. * @param BaseObject $node
  67. */
  68. public function preSave(BaseObject $node)
  69. {
  70. $this->processPreSaveStack();
  71. }
  72. /**
  73. * Runs necessary queries before node is deleted.
  74. *
  75. * @param BaseObject $node
  76. */
  77. public function preDelete(BaseObject $node)
  78. {
  79. $peer_name = get_class($node->getPeer());
  80. $node = $node->reload(); // load the current node state
  81. $this->addPreDeleteStackEntries(self::shiftRLRange($peer_name,
  82. $node->getLeftValue(),
  83. $node->getRightValue(),
  84. -1,
  85. $node->getScopeIdValue()));
  86. $this->addPreDeleteStackEntries(self::shiftRLValues($peer_name,
  87. $node->getRightValue() + 1,
  88. -2,
  89. $node->getScopeIdValue()));
  90. // Take care of adjacency parameters
  91. if ($node->hasChildren())
  92. {
  93. $peer_name = get_class($node->getPeer());
  94. $node_class = get_class($node);
  95. $query = sprintf('UPDATE %s SET %s = %s WHERE %s = %s',
  96. constant("$peer_name::TABLE_NAME"),
  97. self::getColumnConstant($node_class, 'parent', true),
  98. $node->getParentIdValue(),
  99. self::getColumnConstant($node_class, 'parent'),
  100. $node->getPrimaryKey());
  101. $con = Propel::getConnection();
  102. $statement = $con->createStatement();
  103. $statement->execute($query);
  104. }
  105. $this->processPreDeleteStack();
  106. }
  107. # -- GETTERS AND SETTERS
  108. /**
  109. * Proxy method used to access column holding nested set's left value getter.
  110. *
  111. * @param BaseObject $node
  112. */
  113. public function getLeftValue(BaseObject $node)
  114. {
  115. $getter = self::forgeMethodName($node, 'get', 'left');
  116. return $node->$getter();
  117. }
  118. /**
  119. * Proxy method used to access column holding nested set's left value setter.
  120. *
  121. * @param BaseObject $node
  122. * @param integer $value
  123. */
  124. public function setLeftValue(BaseObject $node, $value)
  125. {
  126. $setter = self::forgeMethodName($node, 'set', 'left');
  127. return $node->$setter($value);
  128. }
  129. /**
  130. * Proxy method used to access column holding nested set's right value setter.
  131. *
  132. * @param BaseObject $node
  133. * @param integer $value
  134. */
  135. public function setRightValue(BaseObject $node, $value)
  136. {
  137. $setter = self::forgeMethodName($node, 'set', 'right');
  138. return $node->$setter($value);
  139. }
  140. /**
  141. * Proxy method used to access column holding nested set's right value getter.
  142. *
  143. * @param BaseObject $node
  144. */
  145. public function getRightValue(BaseObject $node)
  146. {
  147. $getter = self::forgeMethodName($node, 'get', 'right');
  148. return $node->$getter();
  149. }
  150. /**
  151. * Proxy method used to access column holding nested set's scope value setter.
  152. *
  153. * @param BaseObject $node
  154. * @param integer $value
  155. */
  156. public function setScopeIdValue(BaseObject $node, $value)
  157. {
  158. $setter = self::forgeMethodName($node, 'set', 'scope');
  159. return $node->$setter($value);
  160. }
  161. /**
  162. * Proxy method used to access column holding nested set's scope value getter.
  163. *
  164. * @param BaseObject $node
  165. */
  166. public function getScopeIdValue(BaseObject $node)
  167. {
  168. $getter = self::forgeMethodName($node, 'get', 'scope');
  169. return $node->$getter();
  170. }
  171. /**
  172. * Proxy method used to access column holding nested set's parent value setter.
  173. *
  174. * @param BaseObject $node
  175. * @param integer $value
  176. */
  177. public function setParentIdValue(BaseObject $node, $value)
  178. {
  179. $setter = self::forgeMethodName($node, 'set', 'parent');
  180. return $node->$setter($value);
  181. }
  182. /**
  183. * Proxy method used to access column holding nested set's parent value getter.
  184. *
  185. * @param BaseObject $node
  186. */
  187. public function getParentIdValue(BaseObject $node)
  188. {
  189. $getter = self::forgeMethodName($node, 'get', 'parent');
  190. return $node->$getter();
  191. }
  192. # -- NESTED SETS PUBLIC API
  193. /**
  194. * Sets node properties to make it a root node.
  195. *
  196. * @param BaseObject $node
  197. * @throws Exception When trying to turn an existing non-root node into a root node
  198. */
  199. public function makeRoot(BaseObject $node)
  200. {
  201. if ((bool)$node->getLeftValue())
  202. {
  203. throw new Exception('Cannot turn an existing node into a root node.');
  204. }
  205. $node->setLeftValue(1);
  206. $node->setRightValue(2);
  207. }
  208. # ---- INSERTION METHODS
  209. /**
  210. * Inserts node as first child of given node.
  211. *
  212. * @param BaseObject $node
  213. * @param BaseObject $dest_node
  214. */
  215. public function insertAsFirstChildOf(BaseObject $node, BaseObject $dest_node)
  216. {
  217. $node->setLeftValue($dest_node->getLeftValue() + 1);
  218. $node->setRightValue($dest_node->getLeftValue() + 2);
  219. $node->setScopeIdValue($dest_node->getScopeIdValue());
  220. $node->setParentIdValue($dest_node->getPrimaryKey());
  221. $this->addPreSaveStackEntries($this->shiftRLValues(get_class($node->getPeer()),
  222. $node->getLeftValue(),
  223. 2,
  224. $dest_node->getScopeIdValue()));
  225. $dest_node->setRightValue($dest_node->getRightValue() + 2);
  226. $this->addPreSaveStackEntries(array($dest_node));
  227. }
  228. /**
  229. * Inserts node as last child of given node.
  230. *
  231. * @param BaseObject $node
  232. * @param BaseObject $dest_node
  233. */
  234. public function insertAsLastChildOf(BaseObject $node, BaseObject $dest_node)
  235. {
  236. $node->setLeftValue($dest_node->getRightValue());
  237. $node->setRightValue($dest_node->getRightValue() + 1);
  238. $node->setScopeIdValue($dest_node->getScopeIdValue());
  239. $node->setParentIdValue($dest_node->getPrimaryKey());
  240. $this->addPreSaveStackEntries($this->shiftRLValues(get_class($node->getPeer()),
  241. $node->getLeftValue(),
  242. 2,
  243. $dest_node->getScopeIdValue()));
  244. $dest_node->setRightValue($dest_node->getRightValue() + 2);
  245. $this->addPreSaveStackEntries(array($dest_node));
  246. }
  247. /**
  248. * Inserts node as next sibling of given node.
  249. *
  250. * @param BaseObject $node
  251. * @param BaseObject $dest_node
  252. * @throws Exception When trying to create a sibling to a root node
  253. */
  254. public function insertAsNextSiblingOf(BaseObject $node, BaseObject $dest_node)
  255. {
  256. if ($dest_node->isRoot())
  257. {
  258. $msg = 'Root nodes cannot have siblings';
  259. throw new Exception($msg);
  260. }
  261. $node->setLeftValue($dest_node->getRightValue() + 1);
  262. $node->setRightValue($dest_node->getRightValue() + 2);
  263. $node->setScopeIdValue($dest_node->getScopeIdValue());
  264. $node->setParentIdValue($dest_node->getParentIdValue());
  265. $this->addPreSaveStackEntries($this->shiftRLValues(get_class($node->getPeer()),
  266. $node->getLeftValue(),
  267. 2,
  268. $dest_node->getScopeIdValue()));
  269. }
  270. /**
  271. * Inserts node as previous sibling of given node.
  272. *
  273. * @param BaseObject $node
  274. * @param BaseObject $dest_node
  275. * @throws Exception When trying to create a sibling to a root node
  276. */
  277. public function insertAsPrevSiblingOf(BaseObject $node, BaseObject $dest_node)
  278. {
  279. if ($dest_node->isRoot())
  280. {
  281. $msg = 'Root nodes cannot have siblings';
  282. throw new Exception($msg);
  283. }
  284. $node->setLeftValue($dest_node->getLeftValue());
  285. $node->setRightValue($dest_node->getLeftValue() + 1);
  286. $node->setScopeIdValue($dest_node->getScopeIdValue());
  287. $node->setParentIdValue($dest_node->getParentIdValue());
  288. $this->addPreSaveStackEntries($this->shiftRLValues(get_class($node->getPeer()),
  289. $node->getLeftValue(),
  290. 2,
  291. $dest_node->getScopeIdValue()));
  292. $dest_node->setLeftValue($dest_node->getLeftValue() + 2);
  293. $dest_node->setRightValue($dest_node->getRightValue() + 2);
  294. $this->addPreSaveStackEntries(array($dest_node));
  295. }
  296. /**
  297. * Inserts node as parent of given node.
  298. *
  299. * @param BaseObject $node
  300. * @param BaseObject $dest_node
  301. * @throws Exception When trying to insert node as parent of a root node
  302. */
  303. public function insertAsParentOf(BaseObject $node, BaseObject $dest_node)
  304. {
  305. if ($dest_node->isRoot())
  306. {
  307. $msg = 'Impossible to insert a node as parent of a root node';
  308. throw new Exception($msg);
  309. }
  310. $peer_name = get_class($node->getPeer());
  311. $this->addPreSaveStackEntries(self::shiftRLValues($peer_name, $dest_node->getLeftValue(), 1, $dest_node->getScopeIdValue()));
  312. $this->addPreSaveStackEntries(self::shiftRLValues($peer_name, $dest_node->getRightValue() + 2, -1, $dest_node->getScopeIdValue()));
  313. $node->setLeftValue($dest_node->getLeftValue());
  314. $node->setRightValue($dest_node->getRightValue() + 2);
  315. $previous_parent = $dest_node->getParentIdValue();
  316. $node->setParentIdValue($previous_parent);
  317. $dest_node->setParentIdValue($node->getPrimaryKey());
  318. $this->addPreSaveStackEntries(array($dest_node));
  319. }
  320. # ---- INFORMATIONAL METHODS
  321. /**
  322. * Returns true if given node as one or several children.
  323. *
  324. * @param BaseObject $node
  325. * @return bool
  326. */
  327. public function hasChildren(BaseObject $node)
  328. {
  329. return (bool)($node->getRightValue() - $node->getLeftValue() > 1);
  330. }
  331. /**
  332. * Returns true if given node is a root node.
  333. *
  334. * @param BaseObject $node
  335. * @return bool
  336. */
  337. public function isRoot(BaseObject $node)
  338. {
  339. return $node->getLeftValue() == 1;
  340. }
  341. /**
  342. * Returns true if given node has a parent node.
  343. *
  344. * @param BaseObject $node
  345. * @return bool
  346. */
  347. public function hasParent(BaseObject $node)
  348. {
  349. return (bool)$node->getParentIdValue();
  350. }
  351. /**
  352. * Returns true if given node has a next sibling.
  353. *
  354. * @param BaseObject $node
  355. * @return bool
  356. */
  357. public function hasNextSibling(BaseObject $node)
  358. {
  359. return (bool)$node->retrieveNextSibling();
  360. }
  361. /**
  362. * Returns true if given node has a previous sibling.
  363. *
  364. * @param BaseObject $node
  365. * @return bool
  366. */
  367. public function hasPrevSibling(BaseObject $node)
  368. {
  369. return (bool)$node->retrievePrevSibling();
  370. }
  371. /**
  372. * Returns true if given node does not have children.
  373. *
  374. * @param BaseObject $node
  375. * @return bool
  376. */
  377. public function isLeaf(BaseObject $node)
  378. {
  379. return $node->getRightValue() - $node->getLeftValue() == 1;
  380. }
  381. /**
  382. * Returns true if given node is identical to node.
  383. *
  384. * @param BaseObject $node
  385. * @param BaseObject $compared_node
  386. * @return bool
  387. */
  388. public function isEqualTo(BaseObject $node, BaseObject $compared_node)
  389. {
  390. return ($node->getLeftValue() === $compared_node->getLeftValue()
  391. && $node->getRightValue() === $compared_node->getRightValue()
  392. && $node->getScopeIdValue() === $compared_node->getScopeIdValue()
  393. && $node->getParentIdValue() === $compared_node->getParentIdValue());
  394. }
  395. /**
  396. * Returns true if given node is parent of node.
  397. *
  398. * @param BaseObject $node
  399. * @param BaseObject $parent_node
  400. */
  401. public function isChildOf(BaseObject $node, BaseObject $parent_node)
  402. {
  403. return ($node->getParentIdValue() === $parent_node->getPrimaryKey()
  404. && $node->getScopeIdValue() === $parent_node->getScopeIdValue());
  405. }
  406. /**
  407. * Returns true if given node is descendant of node.
  408. *
  409. * @param BaseObject $descendant_node
  410. * @param BaseObject $node
  411. */
  412. public function isDescendantOf(BaseObject $descendant_node, BaseObject $node)
  413. {
  414. return (
  415. $node->getLeftValue() <= $descendant_node->getLeftValue()
  416. && $node->getRightValue() >= $descendant_node->getRightValue()
  417. && $node->getScopeIdValue() === $descendant_node->getScopeIdValue()
  418. );
  419. }
  420. /**
  421. * Returns given node number of direct children.
  422. *
  423. * @param BaseObject $node
  424. * @return integer
  425. */
  426. public function getNumberOfChildren(BaseObject $node)
  427. {
  428. $peer_name = get_class($node->getPeer());
  429. $con = Propel::getConnection();
  430. $scope_sql = '';
  431. if (!is_null($node->getScopeIdValue()))
  432. {
  433. $scope_sql = sprintf(' AND %s = \'%s\'',
  434. self::getColumnConstant(get_class($node), 'scope'),
  435. $node->getScopeIdValue());
  436. }
  437. $sql = sprintf('SELECT COUNT(*) AS num_children FROM %s WHERE %s = %s %s',
  438. constant("$peer_name::TABLE_NAME"),
  439. self::getColumnConstant(get_class($node), 'parent'),
  440. $node->getPrimaryKey(),
  441. $scope_sql
  442. );
  443. $stmt = $con->createStatement();
  444. $rs = $stmt->executeQuery($sql);
  445. $rs->next();
  446. return $rs->getInt('num_children');
  447. }
  448. /**
  449. * Returns given node number of descendants (n level).
  450. *
  451. * @param BaseObject $node
  452. * @return integer
  453. */
  454. public function getNumberOfDescendants(BaseObject $node)
  455. {
  456. $right = $node->getRightValue();
  457. $left = $node->getLeftValue();
  458. $num = ($right - $left - 1) / 2;
  459. return $num;
  460. }
  461. /**
  462. * Returns given node level.
  463. *
  464. * @param BaseObject $node
  465. * @return integer
  466. */
  467. public function getLevel(BaseObject $node)
  468. {
  469. if (!isset($node->level))
  470. {
  471. $peer_name = get_class($node->getPeer());
  472. $con = Propel::getConnection();
  473. $stmt = $con->createStatement();
  474. $scope_sql = '';
  475. if (!is_null($node->getScopeIdValue())) {
  476. $scope_sql = sprintf(' AND %s = \'%s\'', self::getColumnConstant(get_class($node), 'scope'), $node->getScopeIdValue());
  477. }
  478. $sql = sprintf('SELECT COUNT(*) AS level FROM %s WHERE (%s < %d AND %s > %d) %s',
  479. constant("$peer_name::TABLE_NAME"),
  480. self::getColumnConstant(get_class($node), 'left'),
  481. $node->getLeftValue(),
  482. self::getColumnConstant(get_class($node), 'right'),
  483. $node->getRightValue(),
  484. $scope_sql
  485. );
  486. $rs = $stmt->executeQuery($sql);
  487. $rs->next();
  488. $level = $rs->getInt('level');
  489. $node->level = $level;
  490. }
  491. else
  492. {
  493. $level = $node->level;
  494. }
  495. return $level;
  496. }
  497. /**
  498. * Sets node level.
  499. *
  500. * @param BaseObject $node
  501. * @param int $level
  502. */
  503. public function setLevel(BaseObject $node, $level)
  504. {
  505. $node->level = $level;
  506. }
  507. # ---- NODE RETRIEVAL METHODS
  508. /**
  509. * Returns node's parent.
  510. *
  511. * @param BaseObject $node
  512. * @param string $peer_method (optional) Method used for selecting node
  513. * @return BaseObject or null if node does not have a parent.
  514. */
  515. public function getParent(BaseObject $node, $peer_method = 'retrieveByPk')
  516. {
  517. return $node->isRoot() ? null : call_user_func(array($node->getPeer(), $peer_method), $node->getParentIdValue());
  518. }
  519. /**
  520. * Returns given node direct children.
  521. *
  522. * @param BaseObject $node
  523. * @param string $peer_method (optional) Method used for selecting nodes
  524. * @param Criteria $c (optional) Criteria object for restricting lookup
  525. * @return array Node children
  526. */
  527. public function getChildren(BaseObject $node, $peer_method = 'doSelect', Criteria $c = null)
  528. {
  529. if(!$c)
  530. {
  531. $c = new Criteria();
  532. }
  533. $c->addAnd(self::getColumnConstant(get_class($node), 'parent'), $node->getPrimaryKey(), Criteria::EQUAL);
  534. $c->addAnd(self::getColumnConstant(get_class($node), 'scope'), $node->getScopeIdValue(), Criteria::EQUAL);
  535. $c->addAscendingOrderByColumn(self::getColumnConstant(get_class($node), 'left'));
  536. $children = call_user_func(array(get_class($node->getPeer()), $peer_method), $c);
  537. /*
  538. * Set children level depending on node's.
  539. * This prevents many further queries for getting children levels.
  540. */
  541. if (is_array($children))
  542. {
  543. $child_level = $node->getLevel() + 1;
  544. for ($i = 0; $i < count($children); $i++)
  545. {
  546. $children[$i]->setLevel($child_level);
  547. }
  548. }
  549. return $children;
  550. }
  551. /**
  552. * Returns given node descendants (n level) in pre-order.
  553. *
  554. * @param BaseObject $node
  555. * @param string $peer_method (optional) Method used for selecting nodes
  556. * @param Criteria $c (optional) Criteria object for restricting lookup
  557. * @return array Node descendants, pre-order
  558. */
  559. public function getDescendants(BaseObject $node, $peer_method = 'doSelect', Criteria $c = null)
  560. {
  561. $descendants = array();
  562. if (!$node->isLeaf())
  563. {
  564. if(!$c)
  565. {
  566. $c = new Criteria();
  567. }
  568. $c->addAnd(self::getColumnConstant(get_class($node), 'left'), $node->getLeftValue(), Criteria::GREATER_THAN);
  569. $c->addAnd(self::getColumnConstant(get_class($node), 'right'), $node->getRightValue(), Criteria::LESS_THAN);
  570. $c->addAnd(self::getColumnConstant(get_class($node), 'scope'), $node->getScopeIdValue(), Criteria::EQUAL);
  571. $c->addAscendingOrderByColumn(self::getColumnConstant(get_class($node), 'left'));
  572. $descendants = call_user_func(array(get_class($node->getPeer()), $peer_method), $c);
  573. }
  574. /*
  575. * Set node levels to prevent further queries to database
  576. */
  577. $prev = array($node->getRightValue());
  578. $i = 0;
  579. if (count($descendants))
  580. {
  581. $initial_level = $descendants[0]->getLevel() - 1;
  582. }
  583. foreach ($descendants as $cur)
  584. {
  585. // get back to the parent
  586. while ($cur->getRightValue() > $prev[$i])
  587. {
  588. $i--;
  589. }
  590. $cur->setLevel(++$i + $initial_level);
  591. $prev[$i] = $cur->getRightValue();
  592. }
  593. return $descendants;
  594. }
  595. /**
  596. * Returns given node next sibling.
  597. *
  598. * @param BaseObject $node
  599. * @return BaseObject
  600. */
  601. public function retrieveNextSibling(BaseObject $node)
  602. {
  603. $c = new Criteria();
  604. $c->add(self::getColumnConstant(get_class($node), 'left'), $node->getRightValue() + 1, Criteria::EQUAL);
  605. $c->add(self::getColumnConstant(get_class($node), 'scope'), $node->getScopeIdValue(), Criteria::EQUAL);
  606. return call_user_func(array(get_class($node->getPeer()), 'doSelectOne'), $c);
  607. }
  608. /**
  609. * Returns given node previous sibling.
  610. *
  611. * @param BaseObject $node
  612. * @return BaseObject
  613. */
  614. public function retrievePrevSibling(BaseObject $node)
  615. {
  616. $c = new Criteria();
  617. $c->add(self::getColumnConstant(get_class($node), 'right'), $node->getLeftValue() - 1, Criteria::EQUAL);
  618. $c->add(self::getColumnConstant(get_class($node), 'scope'), $node->getScopeIdValue(), Criteria::EQUAL);
  619. return call_user_func(array(get_class($node->getPeer()), 'doSelectOne'), $c);
  620. }
  621. /**
  622. * Returns given node first child.
  623. *
  624. * @param BaseObject $node
  625. * @return BaseObject
  626. */
  627. public function retrieveFirstChild(BaseObject $node)
  628. {
  629. $c = new Criteria();
  630. $c->add(self::getColumnConstant(get_class($node), 'left'), $node->getLeftValue() + 1, Criteria::EQUAL);
  631. $c->add(self::getColumnConstant(get_class($node), 'scope'), $node->getScopeIdValue(), Criteria::EQUAL);
  632. return call_user_func(array(get_class($node->getPeer()), 'doSelectOne'), $c);
  633. }
  634. /**
  635. * Returns given node last child.
  636. *
  637. * @param BaseObject $node
  638. * @return BaseObject
  639. */
  640. public function retrieveLastChild(BaseObject $node)
  641. {
  642. $c = new Criteria();
  643. $c->add(self::getColumnConstant(get_class($node), 'right'), $node->getRightValue() - 1, Criteria::EQUAL);
  644. $c->add(self::getColumnConstant(get_class($node), 'scope'), $node->getScopeIdValue(), Criteria::EQUAL);
  645. return call_user_func(array(get_class($node->getPeer()), 'doSelectOne'), $c);
  646. }
  647. /**
  648. * Returns given node parent.
  649. *
  650. * @param BaseObject $node
  651. * @return BaseObject
  652. */
  653. public function retrieveParent(BaseObject $node, $peer_method = 'doSelectOne')
  654. {
  655. if ($node->isRoot())
  656. {
  657. return false;
  658. }
  659. // Trick to get proper criteria
  660. $clone = clone $node;
  661. $clone->setId($node->getParentIdValue());
  662. $c = $clone->buildPKeyCriteria();
  663. return call_user_func(array(get_class($node->getPeer()), $peer_method), $c);
  664. }
  665. /**
  666. * Returns node siblings.
  667. *
  668. * @param BaseObject $node
  669. * @param string $peer_method (optional) defaults to "doSelect"
  670. * @param Criteria $c (optional) Criteria object for restricting lookup
  671. * @return array
  672. */
  673. public function retrieveSiblings(BaseObject $node, $peer_method = 'doSelect', Criteria $c = null)
  674. {
  675. if(!$c)
  676. {
  677. $c = new Criteria();
  678. }
  679. $c->add(self::getColumnConstant(get_class($node), 'parent'), $node->getParentIdValue());
  680. $c->add(self::getColumnConstant(get_class($node), 'scope'), $node->getScopeIdValue());
  681. $results = call_user_func(array($node->getPeer(), $peer_method), $c);
  682. $final_results = array();
  683. if (is_array($results) && count($results))
  684. {
  685. $level = $results[0]->getLevel();
  686. foreach ($results as $r)
  687. {
  688. if ($node->isEqualTo($r))
  689. {
  690. continue;
  691. }
  692. $r->setLevel($level);
  693. $final_results[] = $r;
  694. }
  695. }
  696. return $final_results;
  697. }
  698. /**
  699. * Returns path to a specific node as an array, useful to create breadcrumbs.
  700. *
  701. * @param BaseObject $node
  702. * @param string $peer_method (optional) defaults to "doSelect"
  703. * @param Criteria $c (optional) Criteria object for restricting lookup
  704. * @return array
  705. */
  706. public function getPath(BaseObject $node, $peer_method = 'doSelect', Criteria $c = null)
  707. {
  708. if(!$c)
  709. {
  710. $c = new Criteria();
  711. }
  712. $c->add(self::getColumnConstant(get_class($node), 'left'), $node->getLeftValue(), Criteria::LESS_THAN);
  713. $c->add(self::getColumnConstant(get_class($node), 'right'), $node->getRightValue(), Criteria::GREATER_THAN);
  714. if ($node->getScopeIdValue())
  715. {
  716. $c->add(self::getColumnConstant(get_class($node), 'scope'), $node->getScopeIdValue());
  717. }
  718. $c->addAscendingOrderByColumn(self::getColumnConstant(get_class($node), 'left'));
  719. return call_user_func(array($node->getPeer(), $peer_method), $c);
  720. }
  721. # ---- TREE MODIFICATIONS METHODS
  722. /**
  723. * Moves node to first child of given node.
  724. *
  725. * @param BaseObject $node
  726. * @param BaseObject $dest_node
  727. */
  728. public function moveToFirstChildOf(BaseObject $node, BaseObject $dest_node)
  729. {
  730. $node->setParentIdValue($dest_node->getPrimaryKey());
  731. $node->setScopeIdValue($dest_node->getScopeIdValue());
  732. $this->addPreSaveStackEntries(self::getUpdateTreeQueries($node, $dest_node->getLeftValue() + 1));
  733. }
  734. /**
  735. * Moves node to last child of given node.
  736. *
  737. * @param BaseObject $node
  738. * @param BaseObject $dest_node
  739. */
  740. public function moveToLastChildOf(BaseObject $node, BaseObject $dest_node)
  741. {
  742. $node->setParentIdValue($dest_node->getPrimaryKey());
  743. $node->setScopeIdValue($dest_node->getScopeIdValue());
  744. $this->addPreSaveStackEntries(self::getUpdateTreeQueries($node, $dest_node->getRightValue()));
  745. }
  746. /**
  747. * Moves node to next sibling of given node.
  748. *
  749. * @param BaseObject $node
  750. * @param BaseObject $dest_node
  751. */
  752. public function moveToNextSiblingOf(BaseObject $node, BaseObject $dest_node)
  753. {
  754. $node->setParentIdValue($dest_node->getParentIdValue());
  755. $node->setScopeIdValue($dest_node->getScopeIdValue());
  756. $this->addPreSaveStackEntries(self::getUpdateTreeQueries($node, $dest_node->getRightValue() + 1));
  757. }
  758. /**
  759. * Moves node to previous sibling of given node.
  760. *
  761. * @param BaseObject $node
  762. * @param BaseObject $dest_node
  763. */
  764. public function moveToPrevSiblingOf(BaseObject $node, BaseObject $dest_node)
  765. {
  766. $node->setParentIdValue($dest_node->getParentIdValue());
  767. $node->setScopeIdValue($dest_node->getScopeIdValue());
  768. $this->addPreSaveStackEntries(self::getUpdateTreeQueries($node, $dest_node->getLeftValue()));
  769. }
  770. /**
  771. * Deletes given node direct children.
  772. *
  773. * @param BaseObject $node
  774. */
  775. public function deleteChildren(BaseObject $node)
  776. {
  777. // array_reverse() call is necessary for root node properties to be correctly updated
  778. foreach (array_reverse($node->getChildren()) as $child)
  779. {
  780. $child->delete();
  781. }
  782. }
  783. /**
  784. * Deletes all nodes below given node (n level).
  785. *
  786. * @param BaseObject $node
  787. */
  788. public function deleteDescendants(BaseObject $node, $peer_method='doSelect')
  789. {
  790. $peer_name = get_class($node->getPeer());
  791. $stub_name = get_class($node);
  792. $c = new Criteria();
  793. $c1 = $c->getNewCriterion(self::getColumnConstant($stub_name, 'left'), $node->getLeftValue(), Criteria::GREATER_THAN);
  794. $c2 = $c->getNewCriterion(self::getColumnConstant($stub_name, 'right'), $node->getRightValue(), Criteria::LESS_THAN);
  795. $c1->addAnd($c2);
  796. $c->add($c1);
  797. $c->add(self::getColumnConstant($stub_name, 'scope'), $node->getScopeIdValue());
  798. // order the nodes ascending (deletes leafes only in the foreach loop)
  799. $c->addAscendingOrderByColumn(self::getColumnConstant($stub_name, 'right'));
  800. // Nodes are not directly deleted because we need to maintain adjacency list properties
  801. $descendants = call_user_func(array($peer_name, $peer_method), $c);
  802. foreach ($descendants as $descendant)
  803. {
  804. $descendant->delete();
  805. }
  806. }
  807. # -- HELPER METHODS
  808. /**
  809. * Returns an up to date version of node.
  810. *
  811. * @param BaseObject $node
  812. * @return BaseObject
  813. */
  814. public function reload(BaseObject $node)
  815. {
  816. return call_user_func(array($node->getPeer(), 'retrieveByPk'), $node->getPrimaryKey());
  817. }
  818. private static function shiftRLValues($peer_name, $first, $delta, $scopeId = null)
  819. {
  820. $statements = array();
  821. $stub_name = self::getStubFromPeer($peer_name);
  822. $scope_sql = '';
  823. if (!is_null($scopeId))
  824. {
  825. $scope_sql = sprintf(' AND %s = \'%s\'', self::getColumnConstant($stub_name, 'scope'), $scopeId);
  826. }
  827. $sql = sprintf('UPDATE %s SET %s = %s + %d WHERE %s >= %d %s',
  828. constant("$peer_name::TABLE_NAME"),
  829. self::getColumnConstant($stub_name, 'left', true),
  830. self::getColumnConstant($stub_name, 'left'),
  831. $delta,
  832. self::getColumnConstant($stub_name, 'left'),
  833. $first,
  834. $scope_sql);
  835. $statements[] = $sql;
  836. $sql = sprintf('UPDATE %s SET %s = %s + %d WHERE %s >= %d %s',
  837. constant("$peer_name::TABLE_NAME"),
  838. self::getColumnConstant($stub_name, 'right', true),
  839. self::getColumnConstant($stub_name, 'right'),
  840. $delta,
  841. self::getColumnConstant($stub_name, 'right'),
  842. $first,
  843. $scope_sql);
  844. $statements[] = $sql;
  845. return $statements;
  846. }
  847. private static function shiftRLRange($peer_name, $first, $last, $delta, $scopeId = null)
  848. {
  849. $statements = array();
  850. $stub_name = self::getStubFromPeer($peer_name);
  851. $scope_sql = '';
  852. if (!is_null($scopeId))
  853. {
  854. $scope_sql = sprintf(' AND %s = \'%s\'', self::getColumnConstant($stub_name, 'scope'), $scopeId);
  855. }
  856. $sql = sprintf('UPDATE %s SET %s = %s + %d WHERE %s >= %d AND %s <= %d %s',
  857. constant("$peer_name::TABLE_NAME"),
  858. self::getColumnConstant($stub_name, 'left', true),
  859. self::getColumnConstant($stub_name, 'left'),
  860. $delta,
  861. self::getColumnConstant($stub_name, 'left'),
  862. $first,
  863. self::getColumnConstant($stub_name, 'left'),
  864. $last,
  865. $scope_sql);
  866. $statements[] = $sql;
  867. $sql = sprintf('UPDATE %s SET %s = %s + %d WHERE %s >= %d AND %s <= %d %s',
  868. constant("$peer_name::TABLE_NAME"),
  869. self::getColumnConstant($stub_name, 'right', true),
  870. self::getColumnConstant($stub_name, 'right'),
  871. $delta,
  872. self::getColumnConstant($stub_name, 'right'),
  873. $first,
  874. self::getColumnConstant($stub_name, 'right'),
  875. $last,
  876. $scope_sql);
  877. $statements[] = $sql;
  878. return $statements;
  879. }
  880. /**
  881. * Returns getter / setter name for requested column.
  882. *
  883. * @param BaseObject $node
  884. * @param string $prefix Usually 'get' or 'set'
  885. * @param string $column left|right|parent|scope
  886. */
  887. private static function forgeMethodName($node, $prefix, $column)
  888. {
  889. $method_name = sprintf('%s%s', $prefix,
  890. $node->getPeer()->translateFieldName(self::getColumnConstant(get_class($node), $column),
  891. BasePeer::TYPE_COLNAME,
  892. BasePeer::TYPE_PHPNAME));
  893. return $method_name;
  894. }
  895. /**
  896. * Returns the appropriate column name.
  897. *
  898. * @param string $node_class Propel model class
  899. * @param string $column "generic" column name (either parent, left, right, scope)
  900. * @param bool $skip_table_name_prefix Removes table name from column name if true (defaults to false)
  901. *
  902. * @return string Column's name
  903. */
  904. private static function getColumnConstant($node_class, $column, $skip_table_name_prefix = false)
  905. {
  906. $conf_directive = sprintf('propel_behavior_'.sfConfig::get('app_actasnestedset_behavior_name', 'actasnestedset').'_%s_columns', $node_class);
  907. $columns = sfConfig::get($conf_directive);
  908. return $skip_table_name_prefix ? substr($columns[$column], strpos($columns[$column], '.') + 1) : $columns[$column];
  909. }
  910. /**
  911. * Adds entries to given stack.
  912. *
  913. * @param string $stack_name
  914. * @param array $entries
  915. */
  916. private function addStackEntries($stack_name, $entries = array())
  917. {
  918. $stack = $this->$stack_name;
  919. foreach ($entries as $entry)
  920. {
  921. $stack[] = $entry;
  922. }
  923. $this->$stack_name = $stack;
  924. }
  925. private function addPreDeleteStackEntries($entries = array())
  926. {
  927. $this->addStackEntries('predelete_stack', $entries);
  928. }
  929. private function addPreSaveStackEntries($entries = array())
  930. {
  931. $this->addStackEntries('presave_stack', $entries);
  932. }
  933. /**
  934. * Processes presave stack : runs stacked queries and saves stackes objects.
  935. */
  936. private function processStack($stack_name)
  937. {
  938. foreach ($this->$stack_name as $action)
  939. {
  940. array_shift($this->$stack_name);
  941. // stack entry is an object, let's save it
  942. if (is_object($action) && $action instanceof BaseObject)
  943. {
  944. $action->save();
  945. }
  946. // stack entry is an sql query, let's execute it
  947. elseif (is_string($action))
  948. {
  949. $con = Propel::getConnection();
  950. $statement = $con->createStatement();
  951. $result = $statement->executeQuery($action);
  952. }
  953. else
  954. {
  955. $msg = sprintf('Unable to process %s stack entry: %s', $stack_name, serialize($action));
  956. throw new Exception($msg);
  957. }
  958. }
  959. }
  960. private function processPreDeleteStack()
  961. {
  962. $this->processStack('predelete_stack');
  963. }
  964. private function processPreSaveStack()
  965. {
  966. $this->processStack('presave_stack');
  967. }
  968. /**
  969. * Returns queries needed to update tree.
  970. *
  971. * @param BaseObject $node
  972. * @param integer $dest_left
  973. *
  974. * @return array
  975. */
  976. private static function getUpdateTreeQueries(BaseObject $node, $dest_left)
  977. {
  978. $statements = array();
  979. $peer_name = get_class($node->getPeer());
  980. $left = $node->getLeftValue();
  981. $right = $node->getRightValue();
  982. $tree_size = $right - $left +1;
  983. $statements = array_merge($statements, self::shiftRLValues($peer_name, $dest_left, $tree_size, $node->getScopeIdValue()));
  984. if ($left >= $dest_left)
  985. {
  986. $left += $tree_size;
  987. $right += $tree_size;
  988. }
  989. $statements = array_merge($statements, self::shiftRLRange($peer_name, $left, $right, $dest_left - $left, $node->getScopeIdValue()));
  990. $statements = array_merge($statements, self::shiftRLValues($peer_name, $right + 1, -$tree_size, $node->getScopeIdValue()));
  991. return $statements;
  992. }
  993. /**
  994. * Returns peer's stub name.
  995. *
  996. * @param string $peer_name
  997. * @return string
  998. */
  999. public static function getStubFromPeer($peer_name)
  1000. {
  1001. return preg_replace('/^(\w+)Peer$/', '$1', $peer_name);
  1002. }
  1003. }