TreeNodeRepository.php 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681
  1. <?php
  2. namespace Gedmo\Tree\Repository;
  3. use Doctrine\ORM\EntityRepository,
  4. Doctrine\ORM\Query,
  5. Gedmo\Tree\Node;
  6. /**
  7. * The TreeNodeRepository has some useful functions
  8. * to interact with tree.
  9. *
  10. * Some Tree logic is copied from -
  11. * CakePHP: Rapid Development Framework (http://cakephp.org)
  12. *
  13. * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com>
  14. * @package Gedmo.Tree.Repository
  15. * @subpackage TreeNodeRepository
  16. * @link http://www.gediminasm.org
  17. * @license MIT License (http://www.opensource.org/licenses/mit-license.php)
  18. */
  19. class TreeNodeRepository extends EntityRepository
  20. {
  21. /**
  22. * List of cached entity configurations
  23. *
  24. * @var array
  25. */
  26. protected $_configurations = array();
  27. /**
  28. * Get the Tree path of Nodes by given $node
  29. *
  30. * @param object $node
  31. * @return array - list of Nodes in path
  32. */
  33. public function getPath($node)
  34. {
  35. $result = array();
  36. $meta = $this->getClassMetadata();
  37. $config = $this->getConfiguration();
  38. $left = $meta->getReflectionProperty($config['left'])->getValue($node);
  39. $right = $meta->getReflectionProperty($config['right'])->getValue($node);
  40. if ($left && $right) {
  41. $qb = $this->_em->createQueryBuilder();
  42. $qb->select('node')
  43. ->from($this->_entityName, 'node')
  44. ->where('node.' . $config['left'] . " <= :left")
  45. ->andWhere('node.' . $config['right'] . " >= :right")
  46. ->orderBy('node.' . $config['left'], 'ASC');
  47. $q = $qb->getQuery();
  48. $result = $q->execute(
  49. compact('left', 'right'),
  50. Query::HYDRATE_OBJECT
  51. );
  52. }
  53. return $result;
  54. }
  55. /**
  56. * Counts the children of given TreeNode
  57. *
  58. * @param object $node - if null counts all records in tree
  59. * @param boolean $direct - true to count only direct children
  60. * @return integer
  61. */
  62. public function childCount($node = null, $direct = false)
  63. {
  64. $count = 0;
  65. $meta = $this->getClassMetadata();
  66. $nodeId = $meta->getSingleIdentifierFieldName();
  67. $config = $this->getConfiguration();
  68. if (null !== $node) {
  69. if ($direct) {
  70. $id = $meta->getReflectionProperty($nodeId)->getValue($node);
  71. $qb = $this->_em->createQueryBuilder();
  72. $qb->select('COUNT(node.' . $nodeId . ')')
  73. ->from($this->_entityName, 'node')
  74. ->where('node.' . $config['parent'] . ' = ' . $id);
  75. $q = $qb->getQuery();
  76. $count = intval($q->getSingleScalarResult());
  77. } else {
  78. $left = $meta->getReflectionProperty($config['left'])->getValue($node);
  79. $right = $meta->getReflectionProperty($config['right'])->getValue($node);
  80. if ($left && $right) {
  81. $count = ($right - $left - 1) / 2;
  82. }
  83. }
  84. } else {
  85. $dql = "SELECT COUNT(node.{$nodeId}) FROM {$this->_entityName} node";
  86. if ($direct) {
  87. $dql .= ' WHERE node.' . $config['parent'] . ' IS NULL';
  88. }
  89. $q = $this->_em->createQuery($dql);
  90. $count = intval($q->getSingleScalarResult());
  91. }
  92. return $count;
  93. }
  94. /**
  95. * Get list of children followed by given $node
  96. *
  97. * @param object $node - if null, all tree nodes will be taken
  98. * @param boolean $direct - true to take only direct children
  99. * @param string $sortByField - field name to sort by
  100. * @param string $direction - sort direction : "ASC" or "DESC"
  101. * @return array - list of given $node children, null on failure
  102. */
  103. public function children($node = null, $direct = false, $sortByField = null, $direction = 'ASC')
  104. {
  105. $meta = $this->getClassMetadata();
  106. $config = $this->getConfiguration();
  107. $qb = $this->_em->createQueryBuilder();
  108. $qb->select('node')
  109. ->from($this->_entityName, 'node');
  110. if ($node !== null) {
  111. if ($direct) {
  112. $nodeId = $meta->getSingleIdentifierFieldName();
  113. $id = $meta->getReflectionProperty($nodeId)->getValue($node);
  114. $qb->where('node.' . $config['parent'] . ' = ' . $id);
  115. } else {
  116. $left = $meta->getReflectionProperty($config['left'])->getValue($node);
  117. $right = $meta->getReflectionProperty($config['right'])->getValue($node);
  118. if ($left && $right) {
  119. $qb->where('node.' . $config['right'] . " < {$right}")
  120. ->andWhere('node.' . $config['left'] . " > {$left}");
  121. }
  122. }
  123. } else {
  124. if ($direct) {
  125. $qb->where('node.' . $config['parent'] . ' IS NULL');
  126. }
  127. }
  128. if (!$sortByField) {
  129. $qb->orderBy('node.' . $config['left'], 'ASC');
  130. } else {
  131. if ($meta->hasField($sortByField) && in_array(strtolower($direction), array('asc', 'desc'))) {
  132. $qb->orderBy('node.' . $sortByField, $direction);
  133. } else {
  134. throw new \RuntimeException("Invalid sort options specified: field - {$sortByField}, direction - {$direction}");
  135. }
  136. }
  137. $q = $qb->getQuery();
  138. $q->useResultCache(false);
  139. $q->useQueryCache(false);
  140. return $q->getResult(Query::HYDRATE_OBJECT);
  141. }
  142. /**
  143. * Get list of leaf nodes of the tree
  144. *
  145. * @param string $sortByField - field name to sort by
  146. * @param string $direction - sort direction : "ASC" or "DESC"
  147. * @return array - list of given $node children, null on failure
  148. */
  149. public function getLeafs($sortByField = null, $direction = 'ASC')
  150. {
  151. $meta = $this->getClassMetadata();
  152. $config = $this->getConfiguration();
  153. $qb = $this->_em->createQueryBuilder();
  154. $qb->select('node')
  155. ->from($this->_entityName, 'node')
  156. ->where('node.' . $config['right'] . ' = 1 + node.' . $config['left']);
  157. if (!$sortByField) {
  158. $qb->orderBy('node.' . $config['left'], 'ASC');
  159. } else {
  160. if ($meta->hasField($sortByField) && in_array(strtolower($direction), array('asc', 'desc'))) {
  161. $qb->orderBy('node.' . $sortByField, $direction);
  162. } else {
  163. throw new \RuntimeException("Invalid sort options specified: field - {$sortByField}, direction - {$direction}");
  164. }
  165. }
  166. $q = $qb->getQuery();
  167. return $q->getResult(Query::HYDRATE_OBJECT);
  168. }
  169. /**
  170. * Move the node down in the same level
  171. *
  172. * @param object $node
  173. * @param mixed $number
  174. * integer - number of positions to shift
  175. * boolean - true shift till last position
  176. * @throws Exception if something fails in transaction
  177. * @return boolean - true if shifted
  178. */
  179. public function moveDown($node, $number = 1)
  180. {
  181. $meta = $this->getClassMetadata();
  182. $config = $this->getConfiguration();
  183. if (!$number) {
  184. return false;
  185. }
  186. $parent = $meta->getReflectionProperty($config['parent'])->getValue($node);
  187. $right = $meta->getReflectionProperty($config['right'])->getValue($node);
  188. if ($parent) {
  189. $this->_em->refresh($parent);
  190. $parentRight = $meta->getReflectionProperty($config['right'])->getValue($parent);
  191. if (($right + 1) == $parentRight) {
  192. return false;
  193. }
  194. }
  195. $dql = "SELECT node FROM {$this->_entityName} node";
  196. $dql .= ' WHERE node.' . $config['left'] . ' = ' . ($right + 1);
  197. $q = $this->_em->createQuery($dql);
  198. $q->setMaxResults(1);
  199. $result = $q->getResult(Query::HYDRATE_OBJECT);
  200. $nextSiblingNode = count($result) ? array_shift($result) : null;
  201. if (!$nextSiblingNode) {
  202. return false;
  203. }
  204. // this one is very important because if em is not cleared
  205. // it loads node from memory without refresh
  206. $this->_em->refresh($nextSiblingNode);
  207. $left = $meta->getReflectionProperty($config['left'])->getValue($node);
  208. $nextLeft = $meta->getReflectionProperty($config['left'])->getValue($nextSiblingNode);
  209. $nextRight = $meta->getReflectionProperty($config['right'])->getValue($nextSiblingNode);
  210. $edge = $this->_getTreeEdge($config);
  211. // process updates in transaction
  212. $this->_em->getConnection()->beginTransaction();
  213. try {
  214. $this->_sync($config, $edge - $left + 1, '+', 'BETWEEN ' . $left . ' AND ' . $right);
  215. $this->_sync($config, $nextLeft - $left, '-', 'BETWEEN ' . $nextLeft . ' AND ' . $nextRight);
  216. $this->_sync($config, $edge - $left - ($nextRight - $nextLeft), '-', ' > ' . $edge);
  217. $this->_em->getConnection()->commit();
  218. } catch (\Exception $e) {
  219. $this->_em->close();
  220. $this->_em->getConnection()->rollback();
  221. throw $e;
  222. }
  223. if (is_int($number)) {
  224. $number--;
  225. }
  226. if ($number) {
  227. $this->_em->refresh($node);
  228. $this->moveDown($node, $number);
  229. }
  230. return true;
  231. }
  232. /**
  233. * Move the node up in the same level
  234. *
  235. * @param object $node
  236. * @param mixed $number
  237. * integer - number of positions to shift
  238. * boolean - true shift till first position
  239. * @throws Exception if something fails in transaction
  240. * @return boolean - true if shifted
  241. */
  242. public function moveUp($node, $number = 1)
  243. {
  244. $meta = $this->getClassMetadata();
  245. $config = $this->getConfiguration();
  246. if (!$number) {
  247. return false;
  248. }
  249. $parent = $meta->getReflectionProperty($config['parent'])->getValue($node);
  250. $left = $meta->getReflectionProperty($config['left'])->getValue($node);
  251. if ($parent) {
  252. $this->_em->refresh($parent);
  253. $parentLeft = $meta->getReflectionProperty($config['left'])->getValue($parent);
  254. if (($left - 1) == $parentLeft) {
  255. return false;
  256. }
  257. }
  258. $dql = "SELECT node FROM {$this->_entityName} node";
  259. $dql .= ' WHERE node.' . $config['right'] . ' = ' . ($left - 1);
  260. $q = $this->_em->createQuery($dql);
  261. $q->setMaxResults(1);
  262. $result = $q->getResult(Query::HYDRATE_OBJECT);
  263. $previousSiblingNode = count($result) ? array_shift($result) : null;
  264. if (!$previousSiblingNode) {
  265. return false;
  266. }
  267. // this one is very important because if em is not cleared
  268. // it loads node from memory without refresh
  269. $this->_em->refresh($previousSiblingNode);
  270. $right = $meta->getReflectionProperty($config['right'])->getValue($node);
  271. $previousLeft = $meta->getReflectionProperty($config['left'])->getValue($previousSiblingNode);
  272. $previousRight = $meta->getReflectionProperty($config['right'])->getValue($previousSiblingNode);
  273. $edge = $this->_getTreeEdge($config);
  274. // process updates in transaction
  275. $this->_em->getConnection()->beginTransaction();
  276. try {
  277. $this->_sync($config, $edge - $previousLeft +1, '+', 'BETWEEN ' . $previousLeft . ' AND ' . $previousRight);
  278. $this->_sync($config, $left - $previousLeft, '-', 'BETWEEN ' .$left . ' AND ' . $right);
  279. $this->_sync($config, $edge - $previousLeft - ($right - $left), '-', '> ' . $edge);
  280. $this->_em->getConnection()->commit();
  281. } catch (\Exception $e) {
  282. $this->_em->close();
  283. $this->_em->getConnection()->rollback();
  284. throw $e;
  285. }
  286. if (is_int($number)) {
  287. $number--;
  288. }
  289. if ($number) {
  290. $this->_em->refresh($node);
  291. $this->moveUp($node, $number);
  292. }
  293. return true;
  294. }
  295. /**
  296. * Reorders the sibling nodes and child nodes by given $node,
  297. * according to the $sortByField and $direction specified
  298. *
  299. * @param object $node - null to reorder all tree
  300. * @param string $sortByField - field name to sort by
  301. * @param string $direction - sort direction : "ASC" or "DESC"
  302. * @param boolean $verify - true to verify tree first
  303. * @return boolean - true on success
  304. */
  305. public function reorder($node = null, $sortByField = null, $direction = 'ASC', $verify = true)
  306. {
  307. $meta = $this->getClassMetadata();
  308. $config = $this->getConfiguration();
  309. if ($verify && is_array($this->verify())) {
  310. return false;
  311. }
  312. $nodes = $this->children($node, true, $sortByField, $direction);
  313. foreach ($nodes as $node) {
  314. // this is overhead but had to be refreshed
  315. $this->_em->refresh($node);
  316. $right = $meta->getReflectionProperty($config['right'])->getValue($node);
  317. $left = $meta->getReflectionProperty($config['left'])->getValue($node);
  318. $this->moveDown($node, true);
  319. if ($left != ($right - 1)) {
  320. $this->reorder($node, $sortByField, $direction, false);
  321. }
  322. }
  323. return true;
  324. }
  325. /**
  326. * Removes given $node from the tree and reparents its descendants
  327. *
  328. * @param Node $node
  329. * @throws Exception if something fails in transaction
  330. * @return void
  331. */
  332. public function removeFromTree(Node $node)
  333. {
  334. $meta = $this->getClassMetadata();
  335. $config = $this->getConfiguration();
  336. $right = $meta->getReflectionProperty($config['right'])->getValue($node);
  337. $left = $meta->getReflectionProperty($config['left'])->getValue($node);
  338. $parent = $meta->getReflectionProperty($config['parent'])->getValue($node);
  339. if ($right == $left + 1) {
  340. $this->_em->remove($node);
  341. $this->_em->flush();
  342. return;
  343. }
  344. // process updates in transaction
  345. $this->_em->getConnection()->beginTransaction();
  346. try {
  347. $this->_em->refresh($parent);
  348. $pk = $meta->getSingleIdentifierFieldName();
  349. $parentId = $meta->getReflectionProperty($pk)->getValue($parent);
  350. $nodeId = $meta->getReflectionProperty($pk)->getValue($node);
  351. $dql = "UPDATE {$this->_entityName} node";
  352. $dql .= ' SET node.' . $config['parent'] . ' = ' . $parentId;
  353. $dql .= ' WHERE node.' . $config['parent'] . ' = ' . $nodeId;
  354. $q = $this->_em->createQuery($dql);
  355. $q->getSingleScalarResult();
  356. $this->_sync($config, 1, '-', 'BETWEEN ' . ($left + 1) . ' AND ' . ($right - 1));
  357. $this->_sync($config, 2, '-', '> ' . $right);
  358. $dql = "UPDATE {$this->_entityName} node";
  359. $dql .= ' SET node.' . $config['parent'] . ' = NULL,';
  360. $dql .= ' node.' . $config['left'] . ' = 0,';
  361. $dql .= ' node.' . $config['right'] . ' = 0';
  362. $dql .= ' WHERE node.' . $pk . ' = ' . $nodeId;
  363. $q = $this->_em->createQuery($dql);
  364. $q->getSingleScalarResult();
  365. $this->_em->getConnection()->commit();
  366. } catch (\Exception $e) {
  367. $this->_em->close();
  368. $this->_em->getConnection()->rollback();
  369. throw $e;
  370. }
  371. $this->_em->refresh($node);
  372. $this->_em->remove($node);
  373. $this->_em->flush();
  374. }
  375. /**
  376. * Verifies that current tree is valid.
  377. * If any error is detected it will return an array
  378. * with a list of errors found on tree
  379. *
  380. * @return mixed
  381. * boolean - true on success
  382. * array - error list on failure
  383. */
  384. public function verify()
  385. {
  386. if (!$this->childCount()) {
  387. return true; // tree is empty
  388. }
  389. $errors = array();
  390. $meta = $this->getClassMetadata();
  391. $config = $this->getConfiguration();
  392. $identifier = $meta->getSingleIdentifierFieldName();
  393. $leftField = $config['left'];
  394. $rightField = $config['right'];
  395. $parentField = $config['parent'];
  396. $q = $this->_em->createQuery("SELECT MIN(node.{$leftField}) FROM {$this->_entityName} node");
  397. $min = intval($q->getSingleScalarResult());
  398. $edge = $this->_getTreeEdge($config);
  399. for ($i = $min; $i <= $edge; $i++) {
  400. $dql = "SELECT COUNT(node.{$identifier}) FROM {$this->_entityName} node";
  401. $dql .= " WHERE (node.{$leftField} = {$i} OR node.{$rightField} = {$i})";
  402. $q = $this->_em->createQuery($dql);
  403. $count = intval($q->getSingleScalarResult());
  404. if ($count != 1) {
  405. if ($count == 0) {
  406. $errors[] = "index [{$i}], missing";
  407. } else {
  408. $errors[] = "index [{$i}], duplicate";
  409. }
  410. }
  411. }
  412. // check for missing parents
  413. $dql = "SELECT c FROM {$this->_entityName} c";
  414. $dql .= " LEFT JOIN c.{$parentField} p";
  415. $dql .= " WHERE c.{$parentField} IS NOT NULL";
  416. $dql .= " AND p.{$identifier} IS NULL";
  417. $q = $this->_em->createQuery($dql);
  418. $nodes = $q->getArrayResult();
  419. if (count($nodes)) {
  420. foreach ($nodes as $node) {
  421. $errors[] = "node [{$node[$identifier]}] has missing parent";
  422. }
  423. return $errors; // loading broken relation can cause infinite loop
  424. }
  425. $dql = "SELECT node FROM {$this->_entityName} node";
  426. $dql .= " WHERE node.{$rightField} < node.{$leftField}";
  427. $q = $this->_em->createQuery($dql);
  428. $q->setMaxResults(1);
  429. $result = $q->getResult(Query::HYDRATE_OBJECT);
  430. $node = count($result) ? array_shift($result) : null;
  431. if ($node) {
  432. $id = $meta->getReflectionProperty($identifier)->getValue($node);
  433. $errors[] = "node [{$id}], left is greater than right";
  434. }
  435. foreach ($this->findAll() as $node) {
  436. $right = $meta->getReflectionProperty($rightField)->getValue($node);
  437. $left = $meta->getReflectionProperty($leftField)->getValue($node);
  438. $id = $meta->getReflectionProperty($identifier)->getValue($node);
  439. $parent = $meta->getReflectionProperty($parentField)->getValue($node);
  440. if (!$right || !$left) {
  441. $errors[] = "node [{$id}] has invalid left or right values";
  442. } elseif ($right == $left) {
  443. $errors[] = "node [{$id}] has identical left and right values";
  444. } elseif ($parent) {
  445. $this->_em->refresh($parent);
  446. $parentRight = $meta->getReflectionProperty($rightField)->getValue($parent);
  447. $parentLeft = $meta->getReflectionProperty($leftField)->getValue($parent);
  448. $parentId = $meta->getReflectionProperty($identifier)->getValue($parent);
  449. if ($left < $parentLeft) {
  450. $errors[] = "node [{$id}] left is less than parent`s [{$parentId}] left value";
  451. } elseif ($right > $parentRight) {
  452. $errors[] = "node [{$id}] right is greater than parent`s [{$parentId}] right value";
  453. }
  454. } else {
  455. $dql = "SELECT COUNT(node.{$identifier}) FROM {$this->_entityName} node";
  456. $dql .= " WHERE node.{$leftField} < {$left}";
  457. $dql .= " AND node.{$rightField} > {$right}";
  458. $q = $this->_em->createQuery($dql);
  459. if ($count = intval($q->getSingleScalarResult())) {
  460. $errors[] = "node [{$id}] parent field is blank, but it has a parent";
  461. }
  462. }
  463. }
  464. return $errors ?: true;
  465. }
  466. /**
  467. * Tries to recover the tree
  468. *
  469. * @throws Exception if something fails in transaction
  470. * @return void
  471. */
  472. public function recover()
  473. {
  474. if ($this->verify() === true) {
  475. return;
  476. }
  477. $meta = $this->getClassMetadata();
  478. $config = $this->getConfiguration();
  479. $identifier = $meta->getSingleIdentifierFieldName();
  480. $leftField = $config['left'];
  481. $rightField = $config['right'];
  482. $parentField = $config['parent'];
  483. $count = 1;
  484. $dql = "SELECT node.{$identifier} FROM {$this->_entityName} node";
  485. $dql .= " ORDER BY node.{$leftField} ASC";
  486. $q = $this->_em->createQuery($dql);
  487. $nodes = $q->getArrayResult();
  488. // process updates in transaction
  489. $this->_em->getConnection()->beginTransaction();
  490. try {
  491. foreach ($nodes as $node) {
  492. $left = $count++;
  493. $right = $count++;
  494. $dql = "UPDATE {$this->_entityName} node";
  495. $dql .= " SET node.{$leftField} = {$left},";
  496. $dql .= " node.{$rightField} = {$right}";
  497. $dql .= " WHERE node.{$identifier} = {$node[$identifier]}";
  498. $q = $this->_em->createQuery($dql);
  499. $q->getSingleScalarResult();
  500. }
  501. foreach ($nodes as $node) {
  502. $node = $this->_em->getReference($this->_entityName, $node[$identifier]);
  503. $this->_em->refresh($node);
  504. $parent = $meta->getReflectionProperty($parentField)->getValue($node);
  505. $this->_adjustNodeWithParent($config, $parent, $node);
  506. }
  507. $this->_em->getConnection()->commit();
  508. } catch (\Exception $e) {
  509. $this->_em->close();
  510. $this->_em->getConnection()->rollback();
  511. throw $e;
  512. }
  513. }
  514. /**
  515. * Generally loads configuration from cache
  516. *
  517. * @throws RuntimeException if no configuration for class found
  518. * @return array
  519. */
  520. public function getConfiguration() {
  521. $config = array();
  522. if (isset($this->_configurations[$this->_entityName])) {
  523. $config = $this->_configurations[$this->_entityName];
  524. } else {
  525. $cacheDriver = $this->_em->getMetadataFactory()->getCacheDriver();
  526. if (($cached = $cacheDriver->fetch("{$this->_entityName}\$GEDMO_TREE_CLASSMETADATA")) !== false) {
  527. $this->_configurations[$this->_entityName] = $cached;
  528. $config = $cached;
  529. }
  530. }
  531. if (!$config) {
  532. throw new \RuntimeException("TreeNodeRepository: this repository cannot be used on {$this->_entityName} without Tree metadata");
  533. }
  534. return $config;
  535. }
  536. /**
  537. * Get the edge of tree
  538. *
  539. * @param array $config
  540. * @return integer
  541. */
  542. protected function _getTreeEdge($config)
  543. {
  544. $q = $this->_em->createQuery("SELECT MAX(node.{$config['right']}) FROM {$this->_entityName} node");
  545. $q->useResultCache(false);
  546. $q->useQueryCache(false);
  547. $right = $q->getSingleScalarResult();
  548. return intval($right);
  549. }
  550. /**
  551. * Synchronize the tree with given conditions
  552. *
  553. * @param array $config
  554. * @param integer $shift
  555. * @param string $dir
  556. * @param string $conditions
  557. * @param string $field
  558. * @return void
  559. */
  560. protected function _sync($config, $shift, $dir, $conditions, $field = 'both')
  561. {
  562. if ($field == 'both') {
  563. $this->_sync($config, $shift, $dir, $conditions, $config['left']);
  564. $field = $config['right'];
  565. }
  566. $dql = "UPDATE {$this->_entityName} node";
  567. $dql .= " SET node.{$field} = node.{$field} {$dir} {$shift}";
  568. $dql .= " WHERE node.{$field} {$conditions}";
  569. $q = $this->_em->createQuery($dql);
  570. return $q->getSingleScalarResult();
  571. }
  572. /**
  573. * Synchronize tree according to Node`s parent Node
  574. *
  575. * @param array $config
  576. * @param Node $parent
  577. * @param Node $node
  578. * @return void
  579. */
  580. protected function _adjustNodeWithParent($config, $parent, Node $node)
  581. {
  582. $edge = $this->_getTreeEdge($config);
  583. $meta = $this->getClassMetadata();
  584. $leftField = $config['left'];
  585. $rightField = $config['right'];
  586. $parentField = $config['parent'];
  587. $leftValue = $meta->getReflectionProperty($leftField)->getValue($node);
  588. $rightValue = $meta->getReflectionProperty($rightField)->getValue($node);
  589. if ($parent === null) {
  590. $this->_sync($config, $edge - $leftValue + 1, '+', 'BETWEEN ' . $leftValue . ' AND ' . $rightValue);
  591. $this->_sync($config, $rightValue - $leftValue + 1, '-', '> ' . $leftValue);
  592. } else {
  593. // need to refresh the parent to get up to date left and right
  594. $this->_em->refresh($parent);
  595. $parentLeftValue = $meta->getReflectionProperty($leftField)->getValue($parent);
  596. $parentRightValue = $meta->getReflectionProperty($rightField)->getValue($parent);
  597. if ($leftValue < $parentLeftValue && $parentRightValue < $rightValue) {
  598. return;
  599. }
  600. if (empty($leftValue) && empty($rightValue)) {
  601. $this->_sync($config, 2, '+', '>= ' . $parentRightValue);
  602. // cannot schedule this update if other Nodes pending
  603. $qb = $this->_em->createQueryBuilder();
  604. $qb->update($this->_entityName, 'node')
  605. ->set('node.' . $leftField, $parentRightValue)
  606. ->set('node.' . $rightField, $parentRightValue + 1);
  607. $entityIdentifiers = $meta->getIdentifierValues($node);
  608. foreach ($entityIdentifiers as $field => $value) {
  609. if (strlen($value)) {
  610. $qb->where('node.' . $field . ' = ' . $value);
  611. }
  612. }
  613. $q = $qb->getQuery();
  614. $q->getSingleScalarResult();
  615. } else {
  616. $this->_sync($config, $edge - $leftValue + 1, '+', 'BETWEEN ' . $leftValue . ' AND ' . $rightValue);
  617. $diff = $rightValue - $leftValue + 1;
  618. if ($leftValue > $parentLeftValue) {
  619. if ($rightValue < $parentRightValue) {
  620. $this->_sync($config, $diff, '-', 'BETWEEN ' . $rightValue . ' AND ' . ($parentRightValue - 1));
  621. $this->_sync($config, $edge - $parentRightValue + $diff + 1, '-', '> ' . $edge);
  622. } else {
  623. $this->_sync($config, $diff, '+', 'BETWEEN ' . $parentRightValue . ' AND ' . $rightValue);
  624. $this->_sync($config, $edge - $parentRightValue + 1, '-', '> ' . $edge);
  625. }
  626. } else {
  627. $this->_sync($config, $diff, '-', 'BETWEEN ' . $rightValue . ' AND ' . ($parentRightValue - 1));
  628. $this->_sync($config, $edge - $parentRightValue + $diff + 1, '-', '> ' . $edge);
  629. }
  630. }
  631. }
  632. }
  633. }