NestedTreeRepository.php 28 KB

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