NestedTreeRepository.php 26 KB

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