NestedTreeRepository.php 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635
  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. $identifierField = $meta->getSingleIdentifierFieldName();
  226. if ($parent instanceof Proxy) {
  227. $this->_em->refresh($parent);
  228. }
  229. $parentId = $meta->getReflectionProperty($identifierField)->getValue($parent);
  230. $left = $meta->getReflectionProperty($config['left'])->getValue($node);
  231. $sign = $includeSelf ? '>=' : '>';
  232. $dql = "SELECT node FROM {$meta->rootEntityName} node";
  233. $dql .= " WHERE node.{$config['parent']} = {$parentId}";
  234. $dql .= " AND node.{$config['left']} {$sign} {$left}";
  235. $dql .= " ORDER BY node.{$config['left']} ASC";
  236. $result = $this->_em->createQuery($dql)->getResult(Query::HYDRATE_OBJECT);
  237. } else {
  238. throw new InvalidArgumentException("Node is not related to this repository");
  239. }
  240. return $result;
  241. }
  242. /**
  243. * Find the previous siblings of the given $node
  244. *
  245. * @param object $node
  246. * @param bool $includeSelf - include the node itself
  247. * @throws \Gedmo\Exception\InvalidArgumentException - if input is invalid
  248. * @return array
  249. */
  250. public function getPrevSiblings($node, $includeSelf = false)
  251. {
  252. $result = array();
  253. $meta = $this->getClassMetadata();
  254. if ($node instanceof $meta->rootEntityName) {
  255. $config = $this->listener->getConfiguration($this->_em, $meta->name);
  256. $parent = $meta->getReflectionProperty($config['parent'])->getValue($node);
  257. if (!$parent) {
  258. throw new InvalidArgumentException("Cannot get siblings from tree root node");
  259. }
  260. $identifierField = $meta->getSingleIdentifierFieldName();
  261. if ($parent instanceof Proxy) {
  262. $this->_em->refresh($parent);
  263. }
  264. $parentId = $meta->getReflectionProperty($identifierField)->getValue($parent);
  265. $left = $meta->getReflectionProperty($config['left'])->getValue($node);
  266. $sign = $includeSelf ? '<=' : '<';
  267. $dql = "SELECT node FROM {$meta->rootEntityName} node";
  268. $dql .= " WHERE node.{$config['parent']} = {$parentId}";
  269. $dql .= " AND node.{$config['left']} {$sign} {$left}";
  270. $dql .= " ORDER BY node.{$config['left']} ASC";
  271. $result = $this->_em->createQuery($dql)->getResult(Query::HYDRATE_OBJECT);
  272. } else {
  273. throw new InvalidArgumentException("Node is not related to this repository");
  274. }
  275. return $result;
  276. }
  277. /**
  278. * Move the node down in the same level
  279. *
  280. * @param object $node
  281. * @param mixed $number
  282. * integer - number of positions to shift
  283. * boolean - if "true" - shift till last position
  284. * @throws RuntimeException - if something fails in transaction
  285. * @return boolean - true if shifted
  286. */
  287. public function moveDown($node, $number = 1)
  288. {
  289. $result = false;
  290. $meta = $this->getClassMetadata();
  291. if ($node instanceof $meta->rootEntityName) {
  292. $config = $this->listener->getConfiguration($this->_em, $meta->name);
  293. $nextSiblings = $this->getNextSiblings($node);
  294. if ($numSiblings = count($nextSiblings)) {
  295. $result = true;
  296. if ($number === true) {
  297. $number = $numSiblings;
  298. } elseif ($number > $numSiblings) {
  299. $number = $numSiblings;
  300. }
  301. $this->listener
  302. ->getStrategy($this->_em, $meta->name)
  303. ->updateNode($this->_em, $node, $nextSiblings[$number - 1], Nested::NEXT_SIBLING);
  304. }
  305. } else {
  306. throw new InvalidArgumentException("Node is not related to this repository");
  307. }
  308. return $result;
  309. }
  310. /**
  311. * Move the node up in the same level
  312. *
  313. * @param object $node
  314. * @param mixed $number
  315. * integer - number of positions to shift
  316. * boolean - true shift till first position
  317. * @throws RuntimeException - if something fails in transaction
  318. * @return boolean - true if shifted
  319. */
  320. public function moveUp($node, $number = 1)
  321. {
  322. $result = false;
  323. $meta = $this->getClassMetadata();
  324. if ($node instanceof $meta->rootEntityName) {
  325. $config = $this->listener->getConfiguration($this->_em, $meta->name);
  326. $prevSiblings = array_reverse($this->getPrevSiblings($node));
  327. if ($numSiblings = count($prevSiblings)) {
  328. $result = true;
  329. if ($number === true) {
  330. $number = $numSiblings;
  331. } elseif ($number > $numSiblings) {
  332. $number = $numSiblings;
  333. }
  334. $this->listener
  335. ->getStrategy($this->_em, $meta->name)
  336. ->updateNode($this->_em, $node, $prevSiblings[$number - 1], Nested::PREV_SIBLING);
  337. }
  338. } else {
  339. throw new InvalidArgumentException("Node is not related to this repository");
  340. }
  341. return $result;
  342. }
  343. /**
  344. * Removes given $node from the tree and reparents its descendants
  345. *
  346. * @param object $node
  347. * @param bool $autoFlush - flush after removing
  348. * @throws RuntimeException - if something fails in transaction
  349. * @return void
  350. */
  351. public function removeFromTree($node, $autoFlush = true)
  352. {
  353. $meta = $this->getClassMetadata();
  354. if ($node instanceof $meta->rootEntityName) {
  355. $config = $this->listener->getConfiguration($this->_em, $meta->name);
  356. $right = $meta->getReflectionProperty($config['right'])->getValue($node);
  357. $left = $meta->getReflectionProperty($config['left'])->getValue($node);
  358. if ($right == $left + 1) {
  359. $this->_em->remove($node);
  360. $autoFlush && $this->_em->flush();
  361. return;
  362. }
  363. // process updates in transaction
  364. $this->_em->getConnection()->beginTransaction();
  365. try {
  366. $parent = $meta->getReflectionProperty($config['parent'])->getValue($node);
  367. if ($parent instanceof Proxy) {
  368. $this->_em->refresh($parent);
  369. }
  370. $pk = $meta->getSingleIdentifierFieldName();
  371. $parentId = $meta->getReflectionProperty($pk)->getValue($parent);
  372. $nodeId = $meta->getReflectionProperty($pk)->getValue($node);
  373. $dql = "UPDATE {$meta->rootEntityName} node";
  374. $dql .= ' SET node.' . $config['parent'] . ' = ' . $parentId;
  375. $dql .= ' WHERE node.' . $config['parent'] . ' = ' . $nodeId;
  376. $rootId = isset($config['root']) ? $meta->getReflectionProperty($config['root'])->getValue($node) : null;
  377. if (isset($config['root'])) {
  378. $dql .= ' AND node.' . $config['root'] . ' = ' . $rootId;
  379. }
  380. $q = $this->_em->createQuery($dql);
  381. $q->getSingleScalarResult();
  382. $this->listener
  383. ->getStrategy($this->_em, $meta->name)
  384. ->shiftRangeRL($this->_em, $meta->rootEntityName, $left, $right, -1, $rootId, $rootId, - 1);
  385. $this->listener
  386. ->getStrategy($this->_em, $meta->name)
  387. ->shiftRL($this->_em, $meta->rootEntityName, $right, -2, $rootId);
  388. $dql = "UPDATE {$meta->rootEntityName} node";
  389. $dql .= ' SET node.' . $config['parent'] . ' = NULL,';
  390. $dql .= ' node.' . $config['left'] . ' = 0,';
  391. $dql .= ' node.' . $config['right'] . ' = 0';
  392. $dql .= ' WHERE node.' . $pk . ' = ' . $nodeId;
  393. $q = $this->_em->createQuery($dql);
  394. $q->getSingleScalarResult();
  395. $this->_em->getConnection()->commit();
  396. } catch (\Exception $e) {
  397. $this->_em->close();
  398. $this->_em->getConnection()->rollback();
  399. throw new \Gedmo\Exception\RuntimeException('Transaction failed', null, $e);
  400. }
  401. $this->_em->refresh($node);
  402. $this->_em->remove($node);
  403. $autoFlush && $this->_em->flush();
  404. } else {
  405. throw new InvalidArgumentException("Node is not related to this repository");
  406. }
  407. }
  408. /**
  409. * Reorders the sibling nodes and child nodes by given $node,
  410. * according to the $sortByField and $direction specified
  411. *
  412. * @param object $node - from which node to start reordering the tree
  413. * @param string $sortByField - field name to sort by
  414. * @param string $direction - sort direction : "ASC" or "DESC"
  415. * @param boolean $verify - true to verify tree first
  416. * @return void
  417. */
  418. public function reorder($node, $sortByField = null, $direction = 'ASC', $verify = true)
  419. {
  420. $meta = $this->getClassMetadata();
  421. if ($node instanceof $meta->rootEntityName) {
  422. $config = $this->listener->getConfiguration($this->_em, $meta->name);
  423. if ($verify && is_array($this->verify())) {
  424. return false;
  425. }
  426. $nodes = $this->children($node, true, $sortByField, $direction);
  427. foreach ($nodes as $node) {
  428. // this is overhead but had to be refreshed
  429. if ($node instanceof Proxy) {
  430. $this->_em->refresh($node);
  431. }
  432. $right = $meta->getReflectionProperty($config['right'])->getValue($node);
  433. $left = $meta->getReflectionProperty($config['left'])->getValue($node);
  434. $this->moveDown($node, true);
  435. if ($left != ($right - 1)) {
  436. $this->reorder($node, $sortByField, $direction, false);
  437. }
  438. }
  439. } else {
  440. throw new InvalidArgumentException("Node is not related to this repository");
  441. }
  442. }
  443. /**
  444. * Verifies that current tree is valid.
  445. * If any error is detected it will return an array
  446. * with a list of errors found on tree
  447. *
  448. * @return mixed
  449. * boolean - true on success
  450. * array - error list on failure
  451. */
  452. public function verify()
  453. {
  454. if (!$this->childCount()) {
  455. return true; // tree is empty
  456. }
  457. $errors = array();
  458. $meta = $this->getClassMetadata();
  459. $config = $this->listener->getConfiguration($this->_em, $meta->name);
  460. if (isset($config['root'])) {
  461. $trees = $this->getRootNodes();
  462. foreach ($trees as $tree) {
  463. $this->verifyTree($errors, $tree);
  464. }
  465. } else {
  466. $this->verifyTree($errors);
  467. }
  468. return $errors ?: true;
  469. }
  470. /**
  471. * Tries to recover the tree
  472. *
  473. * @todo implement
  474. * @throws RuntimeException - if something fails in transaction
  475. * @return void
  476. */
  477. public function recover()
  478. {
  479. if ($this->verify() === true) {
  480. return;
  481. }
  482. }
  483. /**
  484. * {@inheritdoc}
  485. */
  486. protected function validates()
  487. {
  488. return $this->listener->getStrategy($this->_em, $this->getClassMetadata()->name)->getName() === Strategy::NESTED;
  489. }
  490. /**
  491. * Collect errors on given tree if
  492. * where are any
  493. *
  494. * @param array $errors
  495. * @param object $root
  496. * @return void
  497. */
  498. private function verifyTree(&$errors, $root = null)
  499. {
  500. $meta = $this->getClassMetadata();
  501. $config = $this->listener->getConfiguration($this->_em, $meta->name);
  502. $identifier = $meta->getSingleIdentifierFieldName();
  503. $rootId = isset($config['root']) ? $meta->getReflectionProperty($config['root'])->getValue($root) : null;
  504. $dql = "SELECT MIN(node.{$config['left']}) FROM {$meta->rootEntityName} node";
  505. if ($root) {
  506. $dql .= " WHERE node.{$config['root']} = {$rootId}";
  507. }
  508. $min = intval($this->_em->createQuery($dql)->getSingleScalarResult());
  509. $edge = $this->listener->getStrategy($this->_em, $meta->name)->max($this->_em, $meta->name, $rootId);
  510. // check duplicate right and left values
  511. for ($i = $min; $i <= $edge; $i++) {
  512. $dql = "SELECT COUNT(node.{$identifier}) FROM {$meta->rootEntityName} node";
  513. $dql .= " WHERE (node.{$config['left']} = {$i} OR node.{$config['right']} = {$i})";
  514. if ($root) {
  515. $dql .= " AND node.{$config['root']} = {$rootId}";
  516. }
  517. $count = intval($this->_em->createQuery($dql)->getSingleScalarResult());
  518. if ($count !== 1) {
  519. if ($count === 0) {
  520. $errors[] = "index [{$i}], missing" . ($root ? ' on tree root: ' . $rootId : '');
  521. } else {
  522. $errors[] = "index [{$i}], duplicate" . ($root ? ' on tree root: ' . $rootId : '');
  523. }
  524. }
  525. }
  526. // check for missing parents
  527. $dql = "SELECT node FROM {$meta->rootEntityName} node";
  528. $dql .= " LEFT JOIN node.{$config['parent']} parent";
  529. $dql .= " WHERE node.{$config['parent']} IS NOT NULL";
  530. $dql .= " AND parent.{$identifier} IS NULL";
  531. if ($root) {
  532. $dql .= " AND node.{$config['root']} = {$rootId}";
  533. }
  534. $nodes = $this->_em->createQuery($dql)->getArrayResult();
  535. if (count($nodes)) {
  536. foreach ($nodes as $node) {
  537. $errors[] = "node [{$node[$identifier]}] has missing parent" . ($root ? ' on tree root: ' . $rootId : '');
  538. }
  539. return; // loading broken relation can cause infinite loop
  540. }
  541. $dql = "SELECT node FROM {$meta->rootEntityName} node";
  542. $dql .= " WHERE node.{$config['right']} < node.{$config['left']}";
  543. if ($root) {
  544. $dql .= " AND node.{$config['root']} = {$rootId}";
  545. }
  546. $result = $this->_em->createQuery($dql)
  547. ->setMaxResults(1)
  548. ->getResult(Query::HYDRATE_ARRAY);
  549. $node = count($result) ? array_shift($result) : null;
  550. if ($node) {
  551. $id = $node[$identifier];
  552. $errors[] = "node [{$id}], left is greater than right" . ($root ? ' on tree root: ' . $rootId : '');
  553. }
  554. $dql = "SELECT node FROM {$meta->rootEntityName} node";
  555. if ($root) {
  556. $dql .= " WHERE node.{$config['root']} = {$rootId}";
  557. }
  558. $nodes = $this->_em->createQuery($dql)->getResult(Query::HYDRATE_OBJECT);
  559. foreach ($nodes as $node) {
  560. $right = $meta->getReflectionProperty($config['right'])->getValue($node);
  561. $left = $meta->getReflectionProperty($config['left'])->getValue($node);
  562. $id = $meta->getReflectionProperty($identifier)->getValue($node);
  563. $parent = $meta->getReflectionProperty($config['parent'])->getValue($node);
  564. if (!$right || !$left) {
  565. $errors[] = "node [{$id}] has invalid left or right values";
  566. } elseif ($right == $left) {
  567. $errors[] = "node [{$id}] has identical left and right values";
  568. } elseif ($parent) {
  569. $parent instanceof Proxy && $this->_em->refresh($parent);
  570. $parentRight = $meta->getReflectionProperty($config['right'])->getValue($parent);
  571. $parentLeft = $meta->getReflectionProperty($config['left'])->getValue($parent);
  572. $parentId = $meta->getReflectionProperty($identifier)->getValue($parent);
  573. if ($left < $parentLeft) {
  574. $errors[] = "node [{$id}] left is less than parent`s [{$parentId}] left value";
  575. } elseif ($right > $parentRight) {
  576. $errors[] = "node [{$id}] right is greater than parent`s [{$parentId}] right value";
  577. }
  578. } else {
  579. $dql = "SELECT COUNT(node.{$identifier}) FROM {$meta->rootEntityName} node";
  580. $dql .= " WHERE node.{$config['left']} < {$left}";
  581. $dql .= " AND node.{$config['right']} > {$right}";
  582. if ($root) {
  583. $dql .= " AND node.{$config['root']} = {$rootId}";
  584. }
  585. $q = $this->_em->createQuery($dql);
  586. if ($count = intval($q->getSingleScalarResult())) {
  587. $errors[] = "node [{$id}] parent field is blank, but it has a parent";
  588. }
  589. }
  590. }
  591. }
  592. }