TreeNodeRepository.php 28 KB

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