TreeNodeRepository.php 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652
  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. * Move the node down in the same level
  144. *
  145. * @param object $node
  146. * @param mixed $number
  147. * integer - number of positions to shift
  148. * boolean - true shift till last position
  149. * @throws Exception if something fails in transaction
  150. * @return boolean - true if shifted
  151. */
  152. public function moveDown($node, $number = 1)
  153. {
  154. $meta = $this->getClassMetadata();
  155. $config = $this->getConfiguration();
  156. if (!$number) {
  157. return false;
  158. }
  159. $parent = $meta->getReflectionProperty($config['parent'])->getValue($node);
  160. $right = $meta->getReflectionProperty($config['right'])->getValue($node);
  161. if ($parent) {
  162. $this->_em->refresh($parent);
  163. $parentRight = $meta->getReflectionProperty($config['right'])->getValue($parent);
  164. if (($right + 1) == $parentRight) {
  165. return false;
  166. }
  167. }
  168. $dql = "SELECT node FROM {$this->_entityName} node";
  169. $dql .= ' WHERE node.' . $config['left'] . ' = ' . ($right + 1);
  170. $q = $this->_em->createQuery($dql);
  171. $q->setMaxResults(1);
  172. $result = $q->getResult(Query::HYDRATE_OBJECT);
  173. $nextSiblingNode = count($result) ? array_shift($result) : null;
  174. if (!$nextSiblingNode) {
  175. return false;
  176. }
  177. // this one is very important because if em is not cleared
  178. // it loads node from memory without refresh
  179. $this->_em->refresh($nextSiblingNode);
  180. $left = $meta->getReflectionProperty($config['left'])->getValue($node);
  181. $nextLeft = $meta->getReflectionProperty($config['left'])->getValue($nextSiblingNode);
  182. $nextRight = $meta->getReflectionProperty($config['right'])->getValue($nextSiblingNode);
  183. $edge = $this->_getTreeEdge($config);
  184. // process updates in transaction
  185. $this->_em->getConnection()->beginTransaction();
  186. try {
  187. $this->_sync($config, $edge - $left + 1, '+', 'BETWEEN ' . $left . ' AND ' . $right);
  188. $this->_sync($config, $nextLeft - $left, '-', 'BETWEEN ' . $nextLeft . ' AND ' . $nextRight);
  189. $this->_sync($config, $edge - $left - ($nextRight - $nextLeft), '-', ' > ' . $edge);
  190. $this->_em->getConnection()->commit();
  191. } catch (\Exception $e) {
  192. $this->_em->close();
  193. $this->_em->getConnection()->rollback();
  194. throw $e;
  195. }
  196. if (is_int($number)) {
  197. $number--;
  198. }
  199. if ($number) {
  200. $this->_em->refresh($node);
  201. $this->moveDown($node, $number);
  202. }
  203. return true;
  204. }
  205. /**
  206. * Move the node up in the same level
  207. *
  208. * @param object $node
  209. * @param mixed $number
  210. * integer - number of positions to shift
  211. * boolean - true shift till first position
  212. * @throws Exception if something fails in transaction
  213. * @return boolean - true if shifted
  214. */
  215. public function moveUp($node, $number = 1)
  216. {
  217. $meta = $this->getClassMetadata();
  218. $config = $this->getConfiguration();
  219. if (!$number) {
  220. return false;
  221. }
  222. $parent = $meta->getReflectionProperty($config['parent'])->getValue($node);
  223. $left = $meta->getReflectionProperty($config['left'])->getValue($node);
  224. if ($parent) {
  225. $this->_em->refresh($parent);
  226. $parentLeft = $meta->getReflectionProperty($config['left'])->getValue($parent);
  227. if (($left - 1) == $parentLeft) {
  228. return false;
  229. }
  230. }
  231. $dql = "SELECT node FROM {$this->_entityName} node";
  232. $dql .= ' WHERE node.' . $config['right'] . ' = ' . ($left - 1);
  233. $q = $this->_em->createQuery($dql);
  234. $q->setMaxResults(1);
  235. $result = $q->getResult(Query::HYDRATE_OBJECT);
  236. $previousSiblingNode = count($result) ? array_shift($result) : null;
  237. if (!$previousSiblingNode) {
  238. return false;
  239. }
  240. // this one is very important because if em is not cleared
  241. // it loads node from memory without refresh
  242. $this->_em->refresh($previousSiblingNode);
  243. $right = $meta->getReflectionProperty($config['right'])->getValue($node);
  244. $previousLeft = $meta->getReflectionProperty($config['left'])->getValue($previousSiblingNode);
  245. $previousRight = $meta->getReflectionProperty($config['right'])->getValue($previousSiblingNode);
  246. $edge = $this->_getTreeEdge($config);
  247. // process updates in transaction
  248. $this->_em->getConnection()->beginTransaction();
  249. try {
  250. $this->_sync($config, $edge - $previousLeft +1, '+', 'BETWEEN ' . $previousLeft . ' AND ' . $previousRight);
  251. $this->_sync($config, $left - $previousLeft, '-', 'BETWEEN ' .$left . ' AND ' . $right);
  252. $this->_sync($config, $edge - $previousLeft - ($right - $left), '-', '> ' . $edge);
  253. $this->_em->getConnection()->commit();
  254. } catch (\Exception $e) {
  255. $this->_em->close();
  256. $this->_em->getConnection()->rollback();
  257. throw $e;
  258. }
  259. if (is_int($number)) {
  260. $number--;
  261. }
  262. if ($number) {
  263. $this->_em->refresh($node);
  264. $this->moveUp($node, $number);
  265. }
  266. return true;
  267. }
  268. /**
  269. * Reorders the sibling nodes and child nodes by given $node,
  270. * according to the $sortByField and $direction specified
  271. *
  272. * @param object $node - null to reorder all tree
  273. * @param string $sortByField - field name to sort by
  274. * @param string $direction - sort direction : "ASC" or "DESC"
  275. * @param boolean $verify - true to verify tree first
  276. * @return boolean - true on success
  277. */
  278. public function reorder($node = null, $sortByField = null, $direction = 'ASC', $verify = true)
  279. {
  280. $meta = $this->getClassMetadata();
  281. $config = $this->getConfiguration();
  282. if ($verify && is_array($this->verify())) {
  283. return false;
  284. }
  285. $nodes = $this->children($node, true, $sortByField, $direction);
  286. foreach ($nodes as $node) {
  287. // this is overhead but had to be refreshed
  288. $this->_em->refresh($node);
  289. $right = $meta->getReflectionProperty($config['right'])->getValue($node);
  290. $left = $meta->getReflectionProperty($config['left'])->getValue($node);
  291. $this->moveDown($node, true);
  292. if ($left != ($right - 1)) {
  293. $this->reorder($node, $sortByField, $direction, false);
  294. }
  295. }
  296. return true;
  297. }
  298. /**
  299. * Removes given $node from the tree and reparents its descendants
  300. *
  301. * @param Node $node
  302. * @throws Exception if something fails in transaction
  303. * @return void
  304. */
  305. public function removeFromTree(Node $node)
  306. {
  307. $meta = $this->getClassMetadata();
  308. $config = $this->getConfiguration();
  309. $right = $meta->getReflectionProperty($config['right'])->getValue($node);
  310. $left = $meta->getReflectionProperty($config['left'])->getValue($node);
  311. $parent = $meta->getReflectionProperty($config['parent'])->getValue($node);
  312. if ($right == $left + 1) {
  313. $this->_em->remove($node);
  314. $this->_em->flush();
  315. return;
  316. }
  317. // process updates in transaction
  318. $this->_em->getConnection()->beginTransaction();
  319. try {
  320. $this->_em->refresh($parent);
  321. $pk = $meta->getSingleIdentifierFieldName();
  322. $parentId = $meta->getReflectionProperty($pk)->getValue($parent);
  323. $nodeId = $meta->getReflectionProperty($pk)->getValue($node);
  324. $dql = "UPDATE {$this->_entityName} node";
  325. $dql .= ' SET node.' . $config['parent'] . ' = ' . $parentId;
  326. $dql .= ' WHERE node.' . $config['parent'] . ' = ' . $nodeId;
  327. $q = $this->_em->createQuery($dql);
  328. $q->getSingleScalarResult();
  329. $this->_sync($config, 1, '-', 'BETWEEN ' . ($left + 1) . ' AND ' . ($right - 1));
  330. $this->_sync($config, 2, '-', '> ' . $right);
  331. $dql = "UPDATE {$this->_entityName} node";
  332. $dql .= ' SET node.' . $config['parent'] . ' = NULL,';
  333. $dql .= ' node.' . $config['left'] . ' = 0,';
  334. $dql .= ' node.' . $config['right'] . ' = 0';
  335. $dql .= ' WHERE node.' . $pk . ' = ' . $nodeId;
  336. $q = $this->_em->createQuery($dql);
  337. $q->getSingleScalarResult();
  338. $this->_em->getConnection()->commit();
  339. } catch (\Exception $e) {
  340. $this->_em->close();
  341. $this->_em->getConnection()->rollback();
  342. throw $e;
  343. }
  344. $this->_em->refresh($node);
  345. $this->_em->remove($node);
  346. $this->_em->flush();
  347. }
  348. /**
  349. * Verifies that current tree is valid.
  350. * If any error is detected it will return an array
  351. * with a list of errors found on tree
  352. *
  353. * @return mixed
  354. * boolean - true on success
  355. * array - error list on failure
  356. */
  357. public function verify()
  358. {
  359. if (!$this->childCount()) {
  360. return true; // tree is empty
  361. }
  362. $errors = array();
  363. $meta = $this->getClassMetadata();
  364. $config = $this->getConfiguration();
  365. $identifier = $meta->getSingleIdentifierFieldName();
  366. $leftField = $config['left'];
  367. $rightField = $config['right'];
  368. $parentField = $config['parent'];
  369. $q = $this->_em->createQuery("SELECT MIN(node.{$leftField}) FROM {$this->_entityName} node");
  370. $min = intval($q->getSingleScalarResult());
  371. $edge = $this->_getTreeEdge($config);
  372. for ($i = $min; $i <= $edge; $i++) {
  373. $dql = "SELECT COUNT(node.{$identifier}) FROM {$this->_entityName} node";
  374. $dql .= " WHERE (node.{$leftField} = {$i} OR node.{$rightField} = {$i})";
  375. $q = $this->_em->createQuery($dql);
  376. $count = intval($q->getSingleScalarResult());
  377. if ($count != 1) {
  378. if ($count == 0) {
  379. $errors[] = "index [{$i}], missing";
  380. } else {
  381. $errors[] = "index [{$i}], duplicate";
  382. }
  383. }
  384. }
  385. // check for missing parents
  386. $dql = "SELECT c FROM {$this->_entityName} c";
  387. $dql .= " LEFT JOIN c.{$parentField} p";
  388. $dql .= " WHERE c.{$parentField} IS NOT NULL";
  389. $dql .= " AND p.{$identifier} IS NULL";
  390. $q = $this->_em->createQuery($dql);
  391. $nodes = $q->getArrayResult();
  392. if (count($nodes)) {
  393. foreach ($nodes as $node) {
  394. $errors[] = "node [{$node[$identifier]}] has missing parent";
  395. }
  396. return $errors; // loading broken relation can cause infinite loop
  397. }
  398. $dql = "SELECT node FROM {$this->_entityName} node";
  399. $dql .= " WHERE node.{$rightField} < node.{$leftField}";
  400. $q = $this->_em->createQuery($dql);
  401. $q->setMaxResults(1);
  402. $result = $q->getResult(Query::HYDRATE_OBJECT);
  403. $node = count($result) ? array_shift($result) : null;
  404. if ($node) {
  405. $id = $meta->getReflectionProperty($identifier)->getValue($node);
  406. $errors[] = "node [{$id}], left is greater than right";
  407. }
  408. foreach ($this->findAll() as $node) {
  409. $right = $meta->getReflectionProperty($rightField)->getValue($node);
  410. $left = $meta->getReflectionProperty($leftField)->getValue($node);
  411. $id = $meta->getReflectionProperty($identifier)->getValue($node);
  412. $parent = $meta->getReflectionProperty($parentField)->getValue($node);
  413. if (!$right || !$left) {
  414. $errors[] = "node [{$id}] has invalid left or right values";
  415. } elseif ($right == $left) {
  416. $errors[] = "node [{$id}] has identical left and right values";
  417. } elseif ($parent) {
  418. $this->_em->refresh($parent);
  419. $parentRight = $meta->getReflectionProperty($rightField)->getValue($parent);
  420. $parentLeft = $meta->getReflectionProperty($leftField)->getValue($parent);
  421. $parentId = $meta->getReflectionProperty($identifier)->getValue($parent);
  422. if ($left < $parentLeft) {
  423. $errors[] = "node [{$id}] left is less than parent`s [{$parentId}] left value";
  424. } elseif ($right > $parentRight) {
  425. $errors[] = "node [{$id}] right is greater than parent`s [{$parentId}] right value";
  426. }
  427. } else {
  428. $dql = "SELECT COUNT(node.{$identifier}) FROM {$this->_entityName} node";
  429. $dql .= " WHERE node.{$leftField} < {$left}";
  430. $dql .= " AND node.{$rightField} > {$right}";
  431. $q = $this->_em->createQuery($dql);
  432. if ($count = intval($q->getSingleScalarResult())) {
  433. $errors[] = "node [{$id}] parent field is blank, but it has a parent";
  434. }
  435. }
  436. }
  437. return $errors ?: true;
  438. }
  439. /**
  440. * Tries to recover the tree
  441. *
  442. * @throws Exception if something fails in transaction
  443. * @return void
  444. */
  445. public function recover()
  446. {
  447. if ($this->verify() === true) {
  448. return;
  449. }
  450. $meta = $this->getClassMetadata();
  451. $config = $this->getConfiguration();
  452. $identifier = $meta->getSingleIdentifierFieldName();
  453. $leftField = $config['left'];
  454. $rightField = $config['right'];
  455. $parentField = $config['parent'];
  456. $count = 1;
  457. $dql = "SELECT node.{$identifier} FROM {$this->_entityName} node";
  458. $dql .= " ORDER BY node.{$leftField} ASC";
  459. $q = $this->_em->createQuery($dql);
  460. $nodes = $q->getArrayResult();
  461. // process updates in transaction
  462. $this->_em->getConnection()->beginTransaction();
  463. try {
  464. foreach ($nodes as $node) {
  465. $left = $count++;
  466. $right = $count++;
  467. $dql = "UPDATE {$this->_entityName} node";
  468. $dql .= " SET node.{$leftField} = {$left},";
  469. $dql .= " node.{$rightField} = {$right}";
  470. $dql .= " WHERE node.{$identifier} = {$node[$identifier]}";
  471. $q = $this->_em->createQuery($dql);
  472. $q->getSingleScalarResult();
  473. }
  474. foreach ($nodes as $node) {
  475. $node = $this->_em->getReference($this->_entityName, $node[$identifier]);
  476. $this->_em->refresh($node);
  477. $parent = $meta->getReflectionProperty($parentField)->getValue($node);
  478. $this->_adjustNodeWithParent($config, $parent, $node);
  479. }
  480. $this->_em->getConnection()->commit();
  481. } catch (\Exception $e) {
  482. $this->_em->close();
  483. $this->_em->getConnection()->rollback();
  484. throw $e;
  485. }
  486. }
  487. /**
  488. * Generally loads configuration from cache
  489. *
  490. * @throws RuntimeException if no configuration for class found
  491. * @return array
  492. */
  493. public function getConfiguration() {
  494. $config = array();
  495. if (isset($this->_configurations[$this->_entityName])) {
  496. $config = $this->_configurations[$this->_entityName];
  497. } else {
  498. $cacheDriver = $this->_em->getMetadataFactory()->getCacheDriver();
  499. if (($cached = $cacheDriver->fetch("{$this->_entityName}\$GEDMO_TREE_CLASSMETADATA")) !== false) {
  500. $this->_configurations[$this->_entityName] = $cached;
  501. $config = $cached;
  502. }
  503. }
  504. if (!$config) {
  505. throw new \RuntimeException("TreeNodeRepository: this repository cannot be used on {$this->_entityName} without Tree metadata");
  506. }
  507. return $config;
  508. }
  509. /**
  510. * Get the edge of tree
  511. *
  512. * @param array $config
  513. * @return integer
  514. */
  515. protected function _getTreeEdge($config)
  516. {
  517. $q = $this->_em->createQuery("SELECT MAX(node.{$config['right']}) FROM {$this->_entityName} node");
  518. $q->useResultCache(false);
  519. $q->useQueryCache(false);
  520. $right = $q->getSingleScalarResult();
  521. return intval($right);
  522. }
  523. /**
  524. * Synchronize the tree with given conditions
  525. *
  526. * @param array $config
  527. * @param integer $shift
  528. * @param string $dir
  529. * @param string $conditions
  530. * @param string $field
  531. * @return void
  532. */
  533. protected function _sync($config, $shift, $dir, $conditions, $field = 'both')
  534. {
  535. if ($field == 'both') {
  536. $this->_sync($config, $shift, $dir, $conditions, $config['left']);
  537. $field = $config['right'];
  538. }
  539. $dql = "UPDATE {$this->_entityName} node";
  540. $dql .= " SET node.{$field} = node.{$field} {$dir} {$shift}";
  541. $dql .= " WHERE node.{$field} {$conditions}";
  542. $q = $this->_em->createQuery($dql);
  543. return $q->getSingleScalarResult();
  544. }
  545. /**
  546. * Synchronize tree according to Node`s parent Node
  547. *
  548. * @param array $config
  549. * @param Node $parent
  550. * @param Node $node
  551. * @return void
  552. */
  553. protected function _adjustNodeWithParent($config, $parent, Node $node)
  554. {
  555. $edge = $this->_getTreeEdge($config);
  556. $meta = $this->getClassMetadata();
  557. $leftField = $config['left'];
  558. $rightField = $config['right'];
  559. $parentField = $config['parent'];
  560. $leftValue = $meta->getReflectionProperty($leftField)->getValue($node);
  561. $rightValue = $meta->getReflectionProperty($rightField)->getValue($node);
  562. if ($parent === null) {
  563. $this->_sync($config, $edge - $leftValue + 1, '+', 'BETWEEN ' . $leftValue . ' AND ' . $rightValue);
  564. $this->_sync($config, $rightValue - $leftValue + 1, '-', '> ' . $leftValue);
  565. } else {
  566. // need to refresh the parent to get up to date left and right
  567. $this->_em->refresh($parent);
  568. $parentLeftValue = $meta->getReflectionProperty($leftField)->getValue($parent);
  569. $parentRightValue = $meta->getReflectionProperty($rightField)->getValue($parent);
  570. if ($leftValue < $parentLeftValue && $parentRightValue < $rightValue) {
  571. return;
  572. }
  573. if (empty($leftValue) && empty($rightValue)) {
  574. $this->_sync($config, 2, '+', '>= ' . $parentRightValue);
  575. // cannot schedule this update if other Nodes pending
  576. $qb = $this->_em->createQueryBuilder();
  577. $qb->update($this->_entityName, 'node')
  578. ->set('node.' . $leftField, $parentRightValue)
  579. ->set('node.' . $rightField, $parentRightValue + 1);
  580. $entityIdentifiers = $meta->getIdentifierValues($node);
  581. foreach ($entityIdentifiers as $field => $value) {
  582. if (strlen($value)) {
  583. $qb->where('node.' . $field . ' = ' . $value);
  584. }
  585. }
  586. $q = $qb->getQuery();
  587. $q->getSingleScalarResult();
  588. } else {
  589. $this->_sync($config, $edge - $leftValue + 1, '+', 'BETWEEN ' . $leftValue . ' AND ' . $rightValue);
  590. $diff = $rightValue - $leftValue + 1;
  591. if ($leftValue > $parentLeftValue) {
  592. if ($rightValue < $parentRightValue) {
  593. $this->_sync($config, $diff, '-', 'BETWEEN ' . $rightValue . ' AND ' . ($parentRightValue - 1));
  594. $this->_sync($config, $edge - $parentRightValue + $diff + 1, '-', '> ' . $edge);
  595. } else {
  596. $this->_sync($config, $diff, '+', 'BETWEEN ' . $parentRightValue . ' AND ' . $rightValue);
  597. $this->_sync($config, $edge - $parentRightValue + 1, '-', '> ' . $edge);
  598. }
  599. } else {
  600. $this->_sync($config, $diff, '-', 'BETWEEN ' . $rightValue . ' AND ' . ($parentRightValue - 1));
  601. $this->_sync($config, $edge - $parentRightValue + $diff + 1, '-', '> ' . $edge);
  602. }
  603. }
  604. }
  605. }
  606. }