NestedTreeRepository.php 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828
  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 query
  23. *
  24. * @return Query
  25. */
  26. public function getRootNodesQuery()
  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($config['useObjectClass'], 'node')
  33. ->where('node.' . $config['parent'] . " IS NULL")
  34. ->orderBy('node.' . $config['left'], 'ASC');
  35. return $qb->getQuery();
  36. }
  37. /**
  38. * Get all root nodes
  39. *
  40. * @return array
  41. */
  42. public function getRootNodes()
  43. {
  44. return $this->getRootNodesQuery()->getResult();
  45. }
  46. /**
  47. * Allows the following 'virtual' methods:
  48. * - persistAsFirstChild($node)
  49. * - persistAsFirstChildOf($node, $parent)
  50. * - persistAsLastChild($node)
  51. * - persistAsLastChildOf($node, $parent)
  52. * - persistAsNextSibling($node)
  53. * - persistAsNextSiblingOf($node, $sibling)
  54. * - persistAsPrevSibling($node)
  55. * - persistAsPrevSiblingOf($node, $sibling)
  56. * Inherited virtual methods:
  57. * - find*
  58. *
  59. * @see \Doctrine\ORM\EntityRepository
  60. * @throws InvalidArgumentException - If arguments are invalid
  61. * @throws BadMethodCallException - If the method called is an invalid find* or persistAs* method
  62. * or no find* either persistAs* method at all and therefore an invalid method call.
  63. * @return mixed - TreeNestedRepository if persistAs* is called
  64. */
  65. public function __call($method, $args)
  66. {
  67. if (substr($method, 0, 9) === 'persistAs') {
  68. if (!isset($args[0])) {
  69. throw new \Gedmo\Exception\InvalidArgumentException('Node to persist must be available as first argument');
  70. }
  71. $node = $args[0];
  72. $meta = $this->getClassMetadata();
  73. $config = $this->listener->getConfiguration($this->_em, $meta->name);
  74. $position = substr($method, 9);
  75. if (substr($method, -2) === 'Of') {
  76. if (!isset($args[1])) {
  77. throw new \Gedmo\Exception\InvalidArgumentException('If "Of" is specified you must provide parent or sibling as the second argument');
  78. }
  79. $parent = $args[1];
  80. $meta->getReflectionProperty($config['parent'])->setValue($node, $parent);
  81. $position = substr($position, 0, -2);
  82. }
  83. $oid = spl_object_hash($node);
  84. $this->listener
  85. ->getStrategy($this->_em, $meta->name)
  86. ->setNodePosition($oid, $position);
  87. $this->_em->persist($node);
  88. return $this;
  89. }
  90. return parent::__call($method, $args);
  91. }
  92. /**
  93. * Get the Tree path query by given $node
  94. *
  95. * @param object $node
  96. * @throws InvalidArgumentException - if input is not valid
  97. * @return Query
  98. */
  99. public function getPathQuery($node)
  100. {
  101. $meta = $this->getClassMetadata();
  102. if (!$node instanceof $meta->name) {
  103. throw new InvalidArgumentException("Node is not related to this repository");
  104. }
  105. if (!$this->_em->getUnitOfWork()->isInIdentityMap($node)) {
  106. throw new InvalidArgumentException("Node is not managed by UnitOfWork");
  107. }
  108. $config = $this->listener->getConfiguration($this->_em, $meta->name);
  109. $left = $meta->getReflectionProperty($config['left'])->getValue($node);
  110. $right = $meta->getReflectionProperty($config['right'])->getValue($node);
  111. $qb = $this->_em->createQueryBuilder();
  112. $qb->select('node')
  113. ->from($config['useObjectClass'], 'node')
  114. ->where('node.' . $config['left'] . " <= :left")
  115. ->andWhere('node.' . $config['right'] . " >= :right")
  116. ->orderBy('node.' . $config['left'], 'ASC');
  117. if (isset($config['root'])) {
  118. $rootId = $meta->getReflectionProperty($config['root'])->getValue($node);
  119. $qb->andWhere("node.{$config['root']} = {$rootId}");
  120. }
  121. $q = $qb->getQuery();
  122. $q->setParameters(compact('left', 'right'));
  123. return $q;
  124. }
  125. /**
  126. * Get the Tree path of Nodes by given $node
  127. *
  128. * @param object $node
  129. * @return array - list of Nodes in path
  130. */
  131. public function getPath($node)
  132. {
  133. return $this->getPathQuery($node)->getResult();
  134. }
  135. /**
  136. * Counts the children of given TreeNode
  137. *
  138. * @param object $node - if null counts all records in tree
  139. * @param boolean $direct - true to count only direct children
  140. * @throws InvalidArgumentException - if input is not valid
  141. * @return integer
  142. */
  143. public function childCount($node = null, $direct = false)
  144. {
  145. $count = 0;
  146. $meta = $this->getClassMetadata();
  147. $nodeId = $meta->getSingleIdentifierFieldName();
  148. $config = $this->listener->getConfiguration($this->_em, $meta->name);
  149. if (null !== $node) {
  150. if ($node instanceof $meta->name) {
  151. if (!$this->_em->getUnitOfWork()->isInIdentityMap($node)) {
  152. throw new InvalidArgumentException("Node is not managed by UnitOfWork");
  153. }
  154. if ($direct) {
  155. $id = $meta->getReflectionProperty($nodeId)->getValue($node);
  156. $qb = $this->_em->createQueryBuilder();
  157. $qb->select('COUNT(node.' . $nodeId . ')')
  158. ->from($config['useObjectClass'], 'node')
  159. ->where('node.' . $config['parent'] . ' = ' . $id);
  160. if (isset($config['root'])) {
  161. $rootId = $meta->getReflectionProperty($config['root'])->getValue($node);
  162. $qb->andWhere("node.{$config['root']} = {$rootId}");
  163. }
  164. $q = $qb->getQuery();
  165. $count = intval($q->getSingleScalarResult());
  166. } else {
  167. $left = $meta->getReflectionProperty($config['left'])->getValue($node);
  168. $right = $meta->getReflectionProperty($config['right'])->getValue($node);
  169. if ($left && $right) {
  170. $count = ($right - $left - 1) / 2;
  171. }
  172. }
  173. } else {
  174. throw new InvalidArgumentException("Node is not related to this repository");
  175. }
  176. } else {
  177. $dql = "SELECT COUNT(node.{$nodeId}) FROM " . $config['useObjectClass'] . " node";
  178. if ($direct) {
  179. $dql .= ' WHERE node.' . $config['parent'] . ' IS NULL';
  180. }
  181. $q = $this->_em->createQuery($dql);
  182. $count = intval($q->getSingleScalarResult());
  183. }
  184. return $count;
  185. }
  186. /**
  187. * Get tree children query followed by given $node
  188. *
  189. * @param object $node - if null, all tree nodes will be taken
  190. * @param boolean $direct - true to take only direct children
  191. * @param string $sortByField - field name to sort by
  192. * @param string $direction - sort direction : "ASC" or "DESC"
  193. * @throws InvalidArgumentException - if input is not valid
  194. * @return Query
  195. */
  196. public function childrenQuery($node = null, $direct = false, $sortByField = null, $direction = 'ASC')
  197. {
  198. $meta = $this->getClassMetadata();
  199. $config = $this->listener->getConfiguration($this->_em, $meta->name);
  200. $qb = $this->_em->createQueryBuilder();
  201. $qb->select('node')
  202. ->from($config['useObjectClass'], 'node');
  203. if ($node !== null) {
  204. if ($node instanceof $meta->name) {
  205. if (!$this->_em->getUnitOfWork()->isInIdentityMap($node)) {
  206. throw new InvalidArgumentException("Node is not managed by UnitOfWork");
  207. }
  208. if ($direct) {
  209. $nodeId = $meta->getSingleIdentifierFieldName();
  210. $id = $meta->getReflectionProperty($nodeId)->getValue($node);
  211. $qb->where('node.' . $config['parent'] . ' = ' . $id);
  212. } else {
  213. $left = $meta->getReflectionProperty($config['left'])->getValue($node);
  214. $right = $meta->getReflectionProperty($config['right'])->getValue($node);
  215. if ($left && $right) {
  216. $qb->where('node.' . $config['right'] . " < {$right}")
  217. ->andWhere('node.' . $config['left'] . " > {$left}");
  218. }
  219. }
  220. if (isset($config['root'])) {
  221. $rootId = $meta->getReflectionProperty($config['root'])->getValue($node);
  222. $qb->andWhere("node.{$config['root']} = {$rootId}");
  223. }
  224. } else {
  225. throw new \InvalidArgumentException("Node is not related to this repository");
  226. }
  227. } else {
  228. if ($direct) {
  229. $qb->where('node.' . $config['parent'] . ' IS NULL');
  230. }
  231. }
  232. if (!$sortByField) {
  233. $qb->orderBy('node.' . $config['left'], 'ASC');
  234. } else {
  235. if ($meta->hasField($sortByField) && in_array(strtolower($direction), array('asc', 'desc'))) {
  236. $qb->orderBy('node.' . $sortByField, $direction);
  237. } else {
  238. throw new InvalidArgumentException("Invalid sort options specified: field - {$sortByField}, direction - {$direction}");
  239. }
  240. }
  241. return $qb->getQuery();
  242. }
  243. /**
  244. * Get list of children followed by given $node
  245. *
  246. * @param object $node - if null, all tree nodes will be taken
  247. * @param boolean $direct - true to take only direct children
  248. * @param string $sortByField - field name to sort by
  249. * @param string $direction - sort direction : "ASC" or "DESC"
  250. * @return array - list of given $node children, null on failure
  251. */
  252. public function children($node = null, $direct = false, $sortByField = null, $direction = 'ASC')
  253. {
  254. $q = $this->childrenQuery($node, $direct, $sortByField, $direction);
  255. return $q->getResult();
  256. }
  257. /**
  258. * Get tree leafs query
  259. *
  260. * @param object $root - root node in case of root tree is required
  261. * @param string $sortByField - field name to sort by
  262. * @param string $direction - sort direction : "ASC" or "DESC"
  263. * @throws InvalidArgumentException - if input is not valid
  264. * @return Query
  265. */
  266. public function getLeafsQuery($root = null, $sortByField = null, $direction = 'ASC')
  267. {
  268. $meta = $this->getClassMetadata();
  269. $config = $this->listener->getConfiguration($this->_em, $meta->name);
  270. if (isset($config['root']) && is_null($root)) {
  271. if (is_null($root)) {
  272. throw new InvalidArgumentException("If tree has root, getLiefs method requires any node of this tree");
  273. }
  274. if (!$this->_em->getUnitOfWork()->isInIdentityMap($root)) {
  275. throw new InvalidArgumentException("Node is not managed by UnitOfWork");
  276. }
  277. }
  278. $qb = $this->_em->createQueryBuilder();
  279. $qb->select('node')
  280. ->from($config['useObjectClass'], 'node')
  281. ->where('node.' . $config['right'] . ' = 1 + node.' . $config['left']);
  282. if (isset($config['root'])) {
  283. if ($root instanceof $meta->name) {
  284. $rootId = $meta->getReflectionProperty($config['root'])->getValue($root);
  285. $qb->andWhere("node.{$config['root']} = {$rootId}");
  286. } else {
  287. throw new InvalidArgumentException("Node is not related to this repository");
  288. }
  289. }
  290. if (!$sortByField) {
  291. $qb->orderBy('node.' . $config['left'], 'ASC');
  292. } else {
  293. if ($meta->hasField($sortByField) && in_array(strtolower($direction), array('asc', 'desc'))) {
  294. $qb->orderBy('node.' . $sortByField, $direction);
  295. } else {
  296. throw new InvalidArgumentException("Invalid sort options specified: field - {$sortByField}, direction - {$direction}");
  297. }
  298. }
  299. return $qb->getQuery();
  300. }
  301. /**
  302. * Get list of leaf nodes of the tree
  303. *
  304. * @param object $root - root node in case of root tree is required
  305. * @param string $sortByField - field name to sort by
  306. * @param string $direction - sort direction : "ASC" or "DESC"
  307. * @return array
  308. */
  309. public function getLeafs($root = null, $sortByField = null, $direction = 'ASC')
  310. {
  311. return $this->getLeafsQuery($root, $sortByField, $direction)->getResult();
  312. }
  313. /**
  314. * Get the query for next siblings of the given $node
  315. *
  316. * @param object $node
  317. * @param bool $includeSelf - include the node itself
  318. * @throws \Gedmo\Exception\InvalidArgumentException - if input is invalid
  319. * @return Query
  320. */
  321. public function getNextSiblingsQuery($node, $includeSelf = false)
  322. {
  323. $meta = $this->getClassMetadata();
  324. if (!$node instanceof $meta->name) {
  325. throw new InvalidArgumentException("Node is not related to this repository");
  326. }
  327. if (!$this->_em->getUnitOfWork()->isInIdentityMap($node)) {
  328. throw new InvalidArgumentException("Node is not managed by UnitOfWork");
  329. }
  330. $config = $this->listener->getConfiguration($this->_em, $meta->name);
  331. $parent = $meta->getReflectionProperty($config['parent'])->getValue($node);
  332. if (!$parent) {
  333. throw new InvalidArgumentException("Cannot get siblings from tree root node");
  334. }
  335. $parentId = current($this->_em->getUnitOfWork()->getEntityIdentifier($parent));
  336. $left = $meta->getReflectionProperty($config['left'])->getValue($node);
  337. $sign = $includeSelf ? '>=' : '>';
  338. $dql = "SELECT node FROM {$config['useObjectClass']} node";
  339. $dql .= " WHERE node.{$config['parent']} = {$parentId}";
  340. $dql .= " AND node.{$config['left']} {$sign} {$left}";
  341. $dql .= " ORDER BY node.{$config['left']} ASC";
  342. return $this->_em->createQuery($dql);
  343. }
  344. /**
  345. * Find the next siblings of the given $node
  346. *
  347. * @param object $node
  348. * @param bool $includeSelf - include the node itself
  349. * @return array
  350. */
  351. public function getNextSiblings($node, $includeSelf = false)
  352. {
  353. return $this->getNextSiblingsQuery($node, $includeSelf)->getResult();
  354. }
  355. /**
  356. * Get query for previous siblings of the given $node
  357. *
  358. * @param object $node
  359. * @param bool $includeSelf - include the node itself
  360. * @throws \Gedmo\Exception\InvalidArgumentException - if input is invalid
  361. * @return Query
  362. */
  363. public function getPrevSiblingsQuery($node, $includeSelf = false)
  364. {
  365. $meta = $this->getClassMetadata();
  366. if (!$node instanceof $meta->name) {
  367. throw new InvalidArgumentException("Node is not related to this repository");
  368. }
  369. if (!$this->_em->getUnitOfWork()->isInIdentityMap($node)) {
  370. throw new InvalidArgumentException("Node is not managed by UnitOfWork");
  371. }
  372. $config = $this->listener->getConfiguration($this->_em, $meta->name);
  373. $parent = $meta->getReflectionProperty($config['parent'])->getValue($node);
  374. if (!$parent) {
  375. throw new InvalidArgumentException("Cannot get siblings from tree root node");
  376. }
  377. $parentId = current($this->_em->getUnitOfWork()->getEntityIdentifier($parent));
  378. $left = $meta->getReflectionProperty($config['left'])->getValue($node);
  379. $sign = $includeSelf ? '<=' : '<';
  380. $dql = "SELECT node FROM {$config['useObjectClass']} node";
  381. $dql .= " WHERE node.{$config['parent']} = {$parentId}";
  382. $dql .= " AND node.{$config['left']} {$sign} {$left}";
  383. $dql .= " ORDER BY node.{$config['left']} ASC";
  384. return $this->_em->createQuery($dql);
  385. }
  386. /**
  387. * Find the previous siblings of the given $node
  388. *
  389. * @param object $node
  390. * @param bool $includeSelf - include the node itself
  391. * @return array
  392. */
  393. public function getPrevSiblings($node, $includeSelf = false)
  394. {
  395. return $this->getPrevSiblingsQuery($node, $includeSelf)->getResult();
  396. }
  397. /**
  398. * Move the node down in the same level
  399. *
  400. * @param object $node
  401. * @param mixed $number
  402. * integer - number of positions to shift
  403. * boolean - if "true" - shift till last position
  404. * @throws RuntimeException - if something fails in transaction
  405. * @return boolean - true if shifted
  406. */
  407. public function moveDown($node, $number = 1)
  408. {
  409. $result = false;
  410. $meta = $this->getClassMetadata();
  411. if ($node instanceof $meta->name) {
  412. $config = $this->listener->getConfiguration($this->_em, $meta->name);
  413. $nextSiblings = $this->getNextSiblings($node);
  414. if ($numSiblings = count($nextSiblings)) {
  415. $result = true;
  416. if ($number === true) {
  417. $number = $numSiblings;
  418. } elseif ($number > $numSiblings) {
  419. $number = $numSiblings;
  420. }
  421. $this->listener
  422. ->getStrategy($this->_em, $meta->name)
  423. ->updateNode($this->_em, $node, $nextSiblings[$number - 1], Nested::NEXT_SIBLING);
  424. }
  425. } else {
  426. throw new InvalidArgumentException("Node is not related to this repository");
  427. }
  428. return $result;
  429. }
  430. /**
  431. * Move the node up in the same level
  432. *
  433. * @param object $node
  434. * @param mixed $number
  435. * integer - number of positions to shift
  436. * boolean - true shift till first position
  437. * @throws RuntimeException - if something fails in transaction
  438. * @return boolean - true if shifted
  439. */
  440. public function moveUp($node, $number = 1)
  441. {
  442. $result = false;
  443. $meta = $this->getClassMetadata();
  444. if ($node instanceof $meta->name) {
  445. $config = $this->listener->getConfiguration($this->_em, $meta->name);
  446. $prevSiblings = array_reverse($this->getPrevSiblings($node));
  447. if ($numSiblings = count($prevSiblings)) {
  448. $result = true;
  449. if ($number === true) {
  450. $number = $numSiblings;
  451. } elseif ($number > $numSiblings) {
  452. $number = $numSiblings;
  453. }
  454. $this->listener
  455. ->getStrategy($this->_em, $meta->name)
  456. ->updateNode($this->_em, $node, $prevSiblings[$number - 1], Nested::PREV_SIBLING);
  457. }
  458. } else {
  459. throw new InvalidArgumentException("Node is not related to this repository");
  460. }
  461. return $result;
  462. }
  463. /**
  464. * Removes given $node from the tree and reparents its descendants
  465. *
  466. * @param object $node
  467. * @throws RuntimeException - if something fails in transaction
  468. * @return void
  469. */
  470. public function removeFromTree($node)
  471. {
  472. $meta = $this->getClassMetadata();
  473. if ($node instanceof $meta->name) {
  474. $config = $this->listener->getConfiguration($this->_em, $meta->name);
  475. $right = $meta->getReflectionProperty($config['right'])->getValue($node);
  476. $left = $meta->getReflectionProperty($config['left'])->getValue($node);
  477. $rootId = isset($config['root']) ? $meta->getReflectionProperty($config['root'])->getValue($node) : null;
  478. if ($right == $left + 1) {
  479. $this->removeSingle($node);
  480. $this->listener
  481. ->getStrategy($this->_em, $meta->name)
  482. ->shiftRL($this->_em, $config['useObjectClass'], $right, -2, $rootId);
  483. return; // node was a leaf
  484. }
  485. // process updates in transaction
  486. $this->_em->getConnection()->beginTransaction();
  487. try {
  488. $parent = $meta->getReflectionProperty($config['parent'])->getValue($node);
  489. $parentId = 'NULL';
  490. if ($parent) {
  491. $parentId = current($this->_em->getUnitOfWork()->getEntityIdentifier($parent));
  492. }
  493. $pk = $meta->getSingleIdentifierFieldName();
  494. $nodeId = $meta->getReflectionProperty($pk)->getValue($node);
  495. $shift = -1;
  496. // in case if root node is removed, childs become roots
  497. if (isset($config['root']) && !$parent) {
  498. $dql = "SELECT node.{$pk}, node.{$config['left']}, node.{$config['right']} FROM {$config['useObjectClass']} node";
  499. $dql .= " WHERE node.{$config['parent']} = {$nodeId}";
  500. $nodes = $this->_em->createQuery($dql)->getArrayResult();
  501. foreach ($nodes as $newRoot) {
  502. $left = $newRoot[$config['left']];
  503. $right = $newRoot[$config['right']];
  504. $rootId = $newRoot[$pk];
  505. $shift = -($left - 1);
  506. $dql = "UPDATE {$config['useObjectClass']} node";
  507. $dql .= ' SET node.' . $config['root'] . ' = :rootId';
  508. $dql .= ' WHERE node.' . $config['root'] . ' = :nodeId';
  509. $dql .= " AND node.{$config['left']} >= :left";
  510. $dql .= " AND node.{$config['right']} <= :right";
  511. $q = $this->_em->createQuery($dql);
  512. $q->setParameters(compact('rootId', 'left', 'right', 'nodeId'));
  513. $q->getSingleScalarResult();
  514. $dql = "UPDATE {$config['useObjectClass']} node";
  515. $dql .= ' SET node.' . $config['parent'] . ' = ' . $parentId;
  516. $dql .= ' WHERE node.' . $config['parent'] . ' = ' . $nodeId;
  517. $dql .= ' AND node.' . $config['root'] . ' = ' . $rootId;
  518. $q = $this->_em->createQuery($dql);
  519. $q->getSingleScalarResult();
  520. $this->listener
  521. ->getStrategy($this->_em, $meta->name)
  522. ->shiftRangeRL($this->_em, $config['useObjectClass'], $left, $right, $shift, $rootId, $rootId, - 1);
  523. $this->listener
  524. ->getStrategy($this->_em, $meta->name)
  525. ->shiftRL($this->_em, $config['useObjectClass'], $right, -2, $rootId);
  526. }
  527. } else {
  528. $dql = "UPDATE {$config['useObjectClass']} node";
  529. $dql .= ' SET node.' . $config['parent'] . ' = ' . $parentId;
  530. $dql .= ' WHERE node.' . $config['parent'] . ' = ' . $nodeId;
  531. if (isset($config['root'])) {
  532. $dql .= ' AND node.' . $config['root'] . ' = ' . $rootId;
  533. }
  534. // @todo: update in memory nodes
  535. $q = $this->_em->createQuery($dql);
  536. $q->getSingleScalarResult();
  537. $this->listener
  538. ->getStrategy($this->_em, $meta->name)
  539. ->shiftRangeRL($this->_em, $config['useObjectClass'], $left, $right, $shift, $rootId, $rootId, - 1);
  540. $this->listener
  541. ->getStrategy($this->_em, $meta->name)
  542. ->shiftRL($this->_em, $config['useObjectClass'], $right, -2, $rootId);
  543. }
  544. $this->removeSingle($node);
  545. $this->_em->getConnection()->commit();
  546. } catch (\Exception $e) {
  547. $this->_em->close();
  548. $this->_em->getConnection()->rollback();
  549. throw new \Gedmo\Exception\RuntimeException('Transaction failed', null, $e);
  550. }
  551. } else {
  552. throw new InvalidArgumentException("Node is not related to this repository");
  553. }
  554. }
  555. /**
  556. * Reorders the sibling nodes and child nodes by given $node,
  557. * according to the $sortByField and $direction specified
  558. *
  559. * @param object $node - from which node to start reordering the tree
  560. * @param string $sortByField - field name to sort by
  561. * @param string $direction - sort direction : "ASC" or "DESC"
  562. * @param boolean $verify - true to verify tree first
  563. * @return void
  564. */
  565. public function reorder($node, $sortByField = null, $direction = 'ASC', $verify = true)
  566. {
  567. $meta = $this->getClassMetadata();
  568. if ($node instanceof $meta->name) {
  569. $config = $this->listener->getConfiguration($this->_em, $meta->name);
  570. if ($verify && is_array($this->verify())) {
  571. return false;
  572. }
  573. $nodes = $this->children($node, true, $sortByField, $direction);
  574. foreach ($nodes as $node) {
  575. // this is overhead but had to be refreshed
  576. if ($node instanceof Proxy && !$node->__isInitialized__) {
  577. $this->_em->refresh($node);
  578. }
  579. $right = $meta->getReflectionProperty($config['right'])->getValue($node);
  580. $left = $meta->getReflectionProperty($config['left'])->getValue($node);
  581. $this->moveDown($node, true);
  582. if ($left != ($right - 1)) {
  583. $this->reorder($node, $sortByField, $direction, false);
  584. }
  585. }
  586. } else {
  587. throw new InvalidArgumentException("Node is not related to this repository");
  588. }
  589. }
  590. /**
  591. * Verifies that current tree is valid.
  592. * If any error is detected it will return an array
  593. * with a list of errors found on tree
  594. *
  595. * @return mixed
  596. * boolean - true on success
  597. * array - error list on failure
  598. */
  599. public function verify()
  600. {
  601. if (!$this->childCount()) {
  602. return true; // tree is empty
  603. }
  604. $errors = array();
  605. $meta = $this->getClassMetadata();
  606. $config = $this->listener->getConfiguration($this->_em, $meta->name);
  607. if (isset($config['root'])) {
  608. $trees = $this->getRootNodes();
  609. foreach ($trees as $tree) {
  610. $this->verifyTree($errors, $tree);
  611. }
  612. } else {
  613. $this->verifyTree($errors);
  614. }
  615. return $errors ?: true;
  616. }
  617. /**
  618. * Tries to recover the tree
  619. *
  620. * @todo implement
  621. * @throws RuntimeException - if something fails in transaction
  622. * @return void
  623. */
  624. public function recover()
  625. {
  626. if ($this->verify() === true) {
  627. return;
  628. }
  629. // not yet implemented
  630. }
  631. /**
  632. * {@inheritdoc}
  633. */
  634. protected function validates()
  635. {
  636. return $this->listener->getStrategy($this->_em, $this->getClassMetadata()->name)->getName() === Strategy::NESTED;
  637. }
  638. /**
  639. * Collect errors on given tree if
  640. * where are any
  641. *
  642. * @param array $errors
  643. * @param object $root
  644. * @return void
  645. */
  646. private function verifyTree(&$errors, $root = null)
  647. {
  648. $meta = $this->getClassMetadata();
  649. $config = $this->listener->getConfiguration($this->_em, $meta->name);
  650. $identifier = $meta->getSingleIdentifierFieldName();
  651. $rootId = isset($config['root']) ? $meta->getReflectionProperty($config['root'])->getValue($root) : null;
  652. $dql = "SELECT MIN(node.{$config['left']}) FROM {$config['useObjectClass']} node";
  653. if ($root) {
  654. $dql .= " WHERE node.{$config['root']} = {$rootId}";
  655. }
  656. $min = intval($this->_em->createQuery($dql)->getSingleScalarResult());
  657. $edge = $this->listener->getStrategy($this->_em, $meta->name)->max($this->_em, $config['useObjectClass'], $rootId);
  658. // check duplicate right and left values
  659. for ($i = $min; $i <= $edge; $i++) {
  660. $dql = "SELECT COUNT(node.{$identifier}) FROM {$config['useObjectClass']} node";
  661. $dql .= " WHERE (node.{$config['left']} = {$i} OR node.{$config['right']} = {$i})";
  662. if ($root) {
  663. $dql .= " AND node.{$config['root']} = {$rootId}";
  664. }
  665. $count = intval($this->_em->createQuery($dql)->getSingleScalarResult());
  666. if ($count !== 1) {
  667. if ($count === 0) {
  668. $errors[] = "index [{$i}], missing" . ($root ? ' on tree root: ' . $rootId : '');
  669. } else {
  670. $errors[] = "index [{$i}], duplicate" . ($root ? ' on tree root: ' . $rootId : '');
  671. }
  672. }
  673. }
  674. // check for missing parents
  675. $dql = "SELECT node FROM {$config['useObjectClass']} node";
  676. $dql .= " LEFT JOIN node.{$config['parent']} parent";
  677. $dql .= " WHERE node.{$config['parent']} IS NOT NULL";
  678. $dql .= " AND parent.{$identifier} IS NULL";
  679. if ($root) {
  680. $dql .= " AND node.{$config['root']} = {$rootId}";
  681. }
  682. $nodes = $this->_em->createQuery($dql)->getArrayResult();
  683. if (count($nodes)) {
  684. foreach ($nodes as $node) {
  685. $errors[] = "node [{$node[$identifier]}] has missing parent" . ($root ? ' on tree root: ' . $rootId : '');
  686. }
  687. return; // loading broken relation can cause infinite loop
  688. }
  689. $dql = "SELECT node FROM {$config['useObjectClass']} node";
  690. $dql .= " WHERE node.{$config['right']} < node.{$config['left']}";
  691. if ($root) {
  692. $dql .= " AND node.{$config['root']} = {$rootId}";
  693. }
  694. $result = $this->_em->createQuery($dql)
  695. ->setMaxResults(1)
  696. ->getResult(Query::HYDRATE_ARRAY);
  697. $node = count($result) ? array_shift($result) : null;
  698. if ($node) {
  699. $id = $node[$identifier];
  700. $errors[] = "node [{$id}], left is greater than right" . ($root ? ' on tree root: ' . $rootId : '');
  701. }
  702. $dql = "SELECT node FROM {$config['useObjectClass']} node";
  703. if ($root) {
  704. $dql .= " WHERE node.{$config['root']} = {$rootId}";
  705. }
  706. $nodes = $this->_em->createQuery($dql)->getResult(Query::HYDRATE_OBJECT);
  707. foreach ($nodes as $node) {
  708. $right = $meta->getReflectionProperty($config['right'])->getValue($node);
  709. $left = $meta->getReflectionProperty($config['left'])->getValue($node);
  710. $id = $meta->getReflectionProperty($identifier)->getValue($node);
  711. $parent = $meta->getReflectionProperty($config['parent'])->getValue($node);
  712. if (!$right || !$left) {
  713. $errors[] = "node [{$id}] has invalid left or right values";
  714. } elseif ($right == $left) {
  715. $errors[] = "node [{$id}] has identical left and right values";
  716. } elseif ($parent) {
  717. if ($parent instanceof Proxy && !$parent->__isInitialized__) {
  718. $this->_em->refresh($parent);
  719. }
  720. $parentRight = $meta->getReflectionProperty($config['right'])->getValue($parent);
  721. $parentLeft = $meta->getReflectionProperty($config['left'])->getValue($parent);
  722. $parentId = $meta->getReflectionProperty($identifier)->getValue($parent);
  723. if ($left < $parentLeft) {
  724. $errors[] = "node [{$id}] left is less than parent`s [{$parentId}] left value";
  725. } elseif ($right > $parentRight) {
  726. $errors[] = "node [{$id}] right is greater than parent`s [{$parentId}] right value";
  727. }
  728. } else {
  729. $dql = "SELECT COUNT(node.{$identifier}) FROM {$config['useObjectClass']} node";
  730. $dql .= " WHERE node.{$config['left']} < {$left}";
  731. $dql .= " AND node.{$config['right']} > {$right}";
  732. if ($root) {
  733. $dql .= " AND node.{$config['root']} = {$rootId}";
  734. }
  735. $q = $this->_em->createQuery($dql);
  736. if ($count = intval($q->getSingleScalarResult())) {
  737. $errors[] = "node [{$id}] parent field is blank, but it has a parent";
  738. }
  739. }
  740. }
  741. }
  742. /**
  743. * Removes single node without touching children
  744. *
  745. * @internal
  746. * @param object $node
  747. * @return void
  748. */
  749. private function removeSingle($node)
  750. {
  751. $meta = $this->getClassMetadata();
  752. $config = $this->listener->getConfiguration($this->_em, $meta->name);
  753. $pk = $meta->getSingleIdentifierFieldName();
  754. $nodeId = $meta->getReflectionProperty($pk)->getValue($node);
  755. // prevent from deleting whole branch
  756. $dql = "UPDATE {$config['useObjectClass']} node";
  757. $dql .= ' SET node.' . $config['left'] . ' = 0,';
  758. $dql .= ' node.' . $config['right'] . ' = 0';
  759. $dql .= ' WHERE node.' . $pk . ' = ' . $nodeId;
  760. $this->_em->createQuery($dql)->getSingleScalarResult();
  761. // remove the node from database
  762. $dql = "DELETE {$config['useObjectClass']} node";
  763. $dql .= " WHERE node.{$pk} = {$nodeId}";
  764. $this->_em->createQuery($dql)->getSingleScalarResult();
  765. // remove from identity map
  766. $this->_em->getUnitOfWork()->removeFromIdentityMap($node);
  767. }
  768. }