TreeNodeRepository.php 28 KB

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