NestedTreeRepository.php 33 KB

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