NestedTreeRepository.php 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844
  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 (isset($config['root']) && !$parent) {
  339. throw new InvalidArgumentException("Cannot get siblings from tree root node");
  340. }
  341. $left = $wrapped->getPropertyValue($config['left']);
  342. $sign = $includeSelf ? '>=' : '>';
  343. $dql = "SELECT node FROM {$config['useObjectClass']} node";
  344. if ($parent) {
  345. $wrappedParent = new EntityWrapper($parent, $this->_em);
  346. $parentId = $wrappedParent->getIdentifier();
  347. $dql .= " WHERE node.{$config['parent']} = {$parentId}";
  348. } else {
  349. $dql .= " WHERE node.{$config['parent']} IS NULL";
  350. }
  351. $dql .= " AND node.{$config['left']} {$sign} {$left}";
  352. $dql .= " ORDER BY node.{$config['left']} ASC";
  353. return $this->_em->createQuery($dql);
  354. }
  355. /**
  356. * Find the next siblings of the given $node
  357. *
  358. * @param object $node
  359. * @param bool $includeSelf - include the node itself
  360. * @return array
  361. */
  362. public function getNextSiblings($node, $includeSelf = false)
  363. {
  364. return $this->getNextSiblingsQuery($node, $includeSelf)->getResult();
  365. }
  366. /**
  367. * Get query for previous siblings of the given $node
  368. *
  369. * @param object $node
  370. * @param bool $includeSelf - include the node itself
  371. * @throws \Gedmo\Exception\InvalidArgumentException - if input is invalid
  372. * @return Query
  373. */
  374. public function getPrevSiblingsQuery($node, $includeSelf = false)
  375. {
  376. $meta = $this->getClassMetadata();
  377. if (!$node instanceof $meta->name) {
  378. throw new InvalidArgumentException("Node is not related to this repository");
  379. }
  380. $wrapped = new EntityWrapper($node, $this->_em);
  381. if (!$wrapped->hasValidIdentifier()) {
  382. throw new InvalidArgumentException("Node is not managed by UnitOfWork");
  383. }
  384. $config = $this->listener->getConfiguration($this->_em, $meta->name);
  385. $parent = $wrapped->getPropertyValue($config['parent']);
  386. if (isset($config['root']) && !$parent) {
  387. throw new InvalidArgumentException("Cannot get siblings from tree root node");
  388. }
  389. $left = $wrapped->getPropertyValue($config['left']);
  390. $sign = $includeSelf ? '<=' : '<';
  391. $dql = "SELECT node FROM {$config['useObjectClass']} node";
  392. if ($parent) {
  393. $wrappedParent = new EntityWrapper($parent, $this->_em);
  394. $parentId = $wrappedParent->getIdentifier();
  395. $dql .= " WHERE node.{$config['parent']} = {$parentId}";
  396. } else {
  397. $dql .= " WHERE node.{$config['parent']} IS NULL";
  398. }
  399. $dql .= " AND node.{$config['left']} {$sign} {$left}";
  400. $dql .= " ORDER BY node.{$config['left']} ASC";
  401. return $this->_em->createQuery($dql);
  402. }
  403. /**
  404. * Find the previous siblings of the given $node
  405. *
  406. * @param object $node
  407. * @param bool $includeSelf - include the node itself
  408. * @return array
  409. */
  410. public function getPrevSiblings($node, $includeSelf = false)
  411. {
  412. return $this->getPrevSiblingsQuery($node, $includeSelf)->getResult();
  413. }
  414. /**
  415. * Move the node down in the same level
  416. *
  417. * @param object $node
  418. * @param mixed $number
  419. * integer - number of positions to shift
  420. * boolean - if "true" - shift till last position
  421. * @throws RuntimeException - if something fails in transaction
  422. * @return boolean - true if shifted
  423. */
  424. public function moveDown($node, $number = 1)
  425. {
  426. $result = false;
  427. $meta = $this->getClassMetadata();
  428. if ($node instanceof $meta->name) {
  429. $config = $this->listener->getConfiguration($this->_em, $meta->name);
  430. $nextSiblings = $this->getNextSiblings($node);
  431. if ($numSiblings = count($nextSiblings)) {
  432. $result = true;
  433. if ($number === true) {
  434. $number = $numSiblings;
  435. } elseif ($number > $numSiblings) {
  436. $number = $numSiblings;
  437. }
  438. $this->listener
  439. ->getStrategy($this->_em, $meta->name)
  440. ->updateNode($this->_em, $node, $nextSiblings[$number - 1], Nested::NEXT_SIBLING);
  441. }
  442. } else {
  443. throw new InvalidArgumentException("Node is not related to this repository");
  444. }
  445. return $result;
  446. }
  447. /**
  448. * Move the node up in the same level
  449. *
  450. * @param object $node
  451. * @param mixed $number
  452. * integer - number of positions to shift
  453. * boolean - true shift till first position
  454. * @throws RuntimeException - if something fails in transaction
  455. * @return boolean - true if shifted
  456. */
  457. public function moveUp($node, $number = 1)
  458. {
  459. $result = false;
  460. $meta = $this->getClassMetadata();
  461. if ($node instanceof $meta->name) {
  462. $config = $this->listener->getConfiguration($this->_em, $meta->name);
  463. $prevSiblings = array_reverse($this->getPrevSiblings($node));
  464. if ($numSiblings = count($prevSiblings)) {
  465. $result = true;
  466. if ($number === true) {
  467. $number = $numSiblings;
  468. } elseif ($number > $numSiblings) {
  469. $number = $numSiblings;
  470. }
  471. $this->listener
  472. ->getStrategy($this->_em, $meta->name)
  473. ->updateNode($this->_em, $node, $prevSiblings[$number - 1], Nested::PREV_SIBLING);
  474. }
  475. } else {
  476. throw new InvalidArgumentException("Node is not related to this repository");
  477. }
  478. return $result;
  479. }
  480. /**
  481. * Removes given $node from the tree and reparents its descendants
  482. *
  483. * @param object $node
  484. * @throws RuntimeException - if something fails in transaction
  485. * @return void
  486. */
  487. public function removeFromTree($node)
  488. {
  489. $meta = $this->getClassMetadata();
  490. if ($node instanceof $meta->name) {
  491. $wrapped = new EntityWrapper($node, $this->_em);
  492. $config = $this->listener->getConfiguration($this->_em, $meta->name);
  493. $right = $wrapped->getPropertyValue($config['right']);
  494. $left = $wrapped->getPropertyValue($config['left']);
  495. $rootId = isset($config['root']) ? $wrapped->getPropertyValue($config['root']) : null;
  496. if ($right == $left + 1) {
  497. $this->removeSingle($wrapped);
  498. $this->listener
  499. ->getStrategy($this->_em, $meta->name)
  500. ->shiftRL($this->_em, $config['useObjectClass'], $right, -2, $rootId);
  501. return; // node was a leaf
  502. }
  503. // process updates in transaction
  504. $this->_em->getConnection()->beginTransaction();
  505. try {
  506. $parent = $wrapped->getPropertyValue($config['parent']);
  507. $parentId = 'NULL';
  508. if ($parent) {
  509. $wrappedParrent = new EntityWrapper($parent, $this->_em);
  510. $parentId = $wrappedParrent->getIdentifier();
  511. }
  512. $pk = $meta->getSingleIdentifierFieldName();
  513. $nodeId = $wrapped->getIdentifier();
  514. $shift = -1;
  515. // in case if root node is removed, childs become roots
  516. if (isset($config['root']) && !$parent) {
  517. $dql = "SELECT node.{$pk}, node.{$config['left']}, node.{$config['right']} FROM {$config['useObjectClass']} node";
  518. $dql .= " WHERE node.{$config['parent']} = {$nodeId}";
  519. $nodes = $this->_em->createQuery($dql)->getArrayResult();
  520. foreach ($nodes as $newRoot) {
  521. $left = $newRoot[$config['left']];
  522. $right = $newRoot[$config['right']];
  523. $rootId = $newRoot[$pk];
  524. $shift = -($left - 1);
  525. $dql = "UPDATE {$config['useObjectClass']} node";
  526. $dql .= ' SET node.' . $config['root'] . ' = :rootId';
  527. $dql .= ' WHERE node.' . $config['root'] . ' = :nodeId';
  528. $dql .= " AND node.{$config['left']} >= :left";
  529. $dql .= " AND node.{$config['right']} <= :right";
  530. $q = $this->_em->createQuery($dql);
  531. $q->setParameters(compact('rootId', 'left', 'right', 'nodeId'));
  532. $q->getSingleScalarResult();
  533. $dql = "UPDATE {$config['useObjectClass']} node";
  534. $dql .= ' SET node.' . $config['parent'] . ' = ' . $parentId;
  535. $dql .= ' WHERE node.' . $config['parent'] . ' = ' . $nodeId;
  536. $dql .= ' AND node.' . $config['root'] . ' = ' . $rootId;
  537. $q = $this->_em->createQuery($dql);
  538. $q->getSingleScalarResult();
  539. $this->listener
  540. ->getStrategy($this->_em, $meta->name)
  541. ->shiftRangeRL($this->_em, $config['useObjectClass'], $left, $right, $shift, $rootId, $rootId, - 1);
  542. $this->listener
  543. ->getStrategy($this->_em, $meta->name)
  544. ->shiftRL($this->_em, $config['useObjectClass'], $right, -2, $rootId);
  545. }
  546. } else {
  547. $dql = "UPDATE {$config['useObjectClass']} node";
  548. $dql .= ' SET node.' . $config['parent'] . ' = ' . $parentId;
  549. $dql .= ' WHERE node.' . $config['parent'] . ' = ' . $nodeId;
  550. if (isset($config['root'])) {
  551. $dql .= ' AND node.' . $config['root'] . ' = ' . $rootId;
  552. }
  553. // @todo: update in memory nodes
  554. $q = $this->_em->createQuery($dql);
  555. $q->getSingleScalarResult();
  556. $this->listener
  557. ->getStrategy($this->_em, $meta->name)
  558. ->shiftRangeRL($this->_em, $config['useObjectClass'], $left, $right, $shift, $rootId, $rootId, - 1);
  559. $this->listener
  560. ->getStrategy($this->_em, $meta->name)
  561. ->shiftRL($this->_em, $config['useObjectClass'], $right, -2, $rootId);
  562. }
  563. $this->removeSingle($wrapped);
  564. $this->_em->getConnection()->commit();
  565. } catch (\Exception $e) {
  566. $this->_em->close();
  567. $this->_em->getConnection()->rollback();
  568. throw new \Gedmo\Exception\RuntimeException('Transaction failed', null, $e);
  569. }
  570. } else {
  571. throw new InvalidArgumentException("Node is not related to this repository");
  572. }
  573. }
  574. /**
  575. * Reorders the sibling nodes and child nodes by given $node,
  576. * according to the $sortByField and $direction specified
  577. *
  578. * @param object $node - from which node to start reordering the tree
  579. * @param string $sortByField - field name to sort by
  580. * @param string $direction - sort direction : "ASC" or "DESC"
  581. * @param boolean $verify - true to verify tree first
  582. * @return void
  583. */
  584. public function reorder($node, $sortByField = null, $direction = 'ASC', $verify = true)
  585. {
  586. $meta = $this->getClassMetadata();
  587. if ($node instanceof $meta->name) {
  588. $config = $this->listener->getConfiguration($this->_em, $meta->name);
  589. if ($verify && is_array($this->verify())) {
  590. return false;
  591. }
  592. $nodes = $this->children($node, true, $sortByField, $direction);
  593. foreach ($nodes as $node) {
  594. $wrapped = new EntityWrapper($node, $this->_em);
  595. $right = $wrapped->getPropertyValue($config['right']);
  596. $left = $wrapped->getPropertyValue($config['left']);
  597. $this->moveDown($node, true);
  598. if ($left != ($right - 1)) {
  599. $this->reorder($node, $sortByField, $direction, false);
  600. }
  601. }
  602. } else {
  603. throw new InvalidArgumentException("Node is not related to this repository");
  604. }
  605. }
  606. /**
  607. * Verifies that current tree is valid.
  608. * If any error is detected it will return an array
  609. * with a list of errors found on tree
  610. *
  611. * @return mixed
  612. * boolean - true on success
  613. * array - error list on failure
  614. */
  615. public function verify()
  616. {
  617. if (!$this->childCount()) {
  618. return true; // tree is empty
  619. }
  620. $errors = array();
  621. $meta = $this->getClassMetadata();
  622. $config = $this->listener->getConfiguration($this->_em, $meta->name);
  623. if (isset($config['root'])) {
  624. $trees = $this->getRootNodes();
  625. foreach ($trees as $tree) {
  626. $this->verifyTree($errors, $tree);
  627. }
  628. } else {
  629. $this->verifyTree($errors);
  630. }
  631. return $errors ?: true;
  632. }
  633. /**
  634. * Tries to recover the tree
  635. *
  636. * @todo implement
  637. * @throws RuntimeException - if something fails in transaction
  638. * @return void
  639. */
  640. public function recover()
  641. {
  642. if ($this->verify() === true) {
  643. return;
  644. }
  645. // not yet implemented
  646. }
  647. /**
  648. * {@inheritdoc}
  649. */
  650. protected function validates()
  651. {
  652. return $this->listener->getStrategy($this->_em, $this->getClassMetadata()->name)->getName() === Strategy::NESTED;
  653. }
  654. /**
  655. * Collect errors on given tree if
  656. * where are any
  657. *
  658. * @param array $errors
  659. * @param object $root
  660. * @return void
  661. */
  662. private function verifyTree(&$errors, $root = null)
  663. {
  664. $meta = $this->getClassMetadata();
  665. $config = $this->listener->getConfiguration($this->_em, $meta->name);
  666. $identifier = $meta->getSingleIdentifierFieldName();
  667. $rootId = isset($config['root']) ? $meta->getReflectionProperty($config['root'])->getValue($root) : null;
  668. $dql = "SELECT MIN(node.{$config['left']}) FROM {$config['useObjectClass']} node";
  669. if ($root) {
  670. $dql .= " WHERE node.{$config['root']} = {$rootId}";
  671. }
  672. $min = intval($this->_em->createQuery($dql)->getSingleScalarResult());
  673. $edge = $this->listener->getStrategy($this->_em, $meta->name)->max($this->_em, $config['useObjectClass'], $rootId);
  674. // check duplicate right and left values
  675. for ($i = $min; $i <= $edge; $i++) {
  676. $dql = "SELECT COUNT(node.{$identifier}) FROM {$config['useObjectClass']} node";
  677. $dql .= " WHERE (node.{$config['left']} = {$i} OR node.{$config['right']} = {$i})";
  678. if ($root) {
  679. $dql .= " AND node.{$config['root']} = {$rootId}";
  680. }
  681. $count = intval($this->_em->createQuery($dql)->getSingleScalarResult());
  682. if ($count !== 1) {
  683. if ($count === 0) {
  684. $errors[] = "index [{$i}], missing" . ($root ? ' on tree root: ' . $rootId : '');
  685. } else {
  686. $errors[] = "index [{$i}], duplicate" . ($root ? ' on tree root: ' . $rootId : '');
  687. }
  688. }
  689. }
  690. // check for missing parents
  691. $dql = "SELECT node FROM {$config['useObjectClass']} node";
  692. $dql .= " LEFT JOIN node.{$config['parent']} parent";
  693. $dql .= " WHERE node.{$config['parent']} IS NOT NULL";
  694. $dql .= " AND parent.{$identifier} IS NULL";
  695. if ($root) {
  696. $dql .= " AND node.{$config['root']} = {$rootId}";
  697. }
  698. $nodes = $this->_em->createQuery($dql)->getArrayResult();
  699. if (count($nodes)) {
  700. foreach ($nodes as $node) {
  701. $errors[] = "node [{$node[$identifier]}] has missing parent" . ($root ? ' on tree root: ' . $rootId : '');
  702. }
  703. return; // loading broken relation can cause infinite loop
  704. }
  705. $dql = "SELECT node FROM {$config['useObjectClass']} node";
  706. $dql .= " WHERE node.{$config['right']} < node.{$config['left']}";
  707. if ($root) {
  708. $dql .= " AND node.{$config['root']} = {$rootId}";
  709. }
  710. $result = $this->_em->createQuery($dql)
  711. ->setMaxResults(1)
  712. ->getResult(Query::HYDRATE_ARRAY);
  713. $node = count($result) ? array_shift($result) : null;
  714. if ($node) {
  715. $id = $node[$identifier];
  716. $errors[] = "node [{$id}], left is greater than right" . ($root ? ' on tree root: ' . $rootId : '');
  717. }
  718. $dql = "SELECT node FROM {$config['useObjectClass']} node";
  719. if ($root) {
  720. $dql .= " WHERE node.{$config['root']} = {$rootId}";
  721. }
  722. $nodes = $this->_em->createQuery($dql)->getResult(Query::HYDRATE_OBJECT);
  723. foreach ($nodes as $node) {
  724. $right = $meta->getReflectionProperty($config['right'])->getValue($node);
  725. $left = $meta->getReflectionProperty($config['left'])->getValue($node);
  726. $id = $meta->getReflectionProperty($identifier)->getValue($node);
  727. $parent = $meta->getReflectionProperty($config['parent'])->getValue($node);
  728. if (!$right || !$left) {
  729. $errors[] = "node [{$id}] has invalid left or right values";
  730. } elseif ($right == $left) {
  731. $errors[] = "node [{$id}] has identical left and right values";
  732. } elseif ($parent) {
  733. if ($parent instanceof Proxy && !$parent->__isInitialized__) {
  734. $this->_em->refresh($parent);
  735. }
  736. $parentRight = $meta->getReflectionProperty($config['right'])->getValue($parent);
  737. $parentLeft = $meta->getReflectionProperty($config['left'])->getValue($parent);
  738. $parentId = $meta->getReflectionProperty($identifier)->getValue($parent);
  739. if ($left < $parentLeft) {
  740. $errors[] = "node [{$id}] left is less than parent`s [{$parentId}] left value";
  741. } elseif ($right > $parentRight) {
  742. $errors[] = "node [{$id}] right is greater than parent`s [{$parentId}] right value";
  743. }
  744. } else {
  745. $dql = "SELECT COUNT(node.{$identifier}) FROM {$config['useObjectClass']} node";
  746. $dql .= " WHERE node.{$config['left']} < {$left}";
  747. $dql .= " AND node.{$config['right']} > {$right}";
  748. if ($root) {
  749. $dql .= " AND node.{$config['root']} = {$rootId}";
  750. }
  751. $q = $this->_em->createQuery($dql);
  752. if ($count = intval($q->getSingleScalarResult())) {
  753. $errors[] = "node [{$id}] parent field is blank, but it has a parent";
  754. }
  755. }
  756. }
  757. }
  758. /**
  759. * Removes single node without touching children
  760. *
  761. * @internal
  762. * @param EntityWrapper $wrapped
  763. * @return void
  764. */
  765. private function removeSingle(EntityWrapper $wrapped)
  766. {
  767. $meta = $this->getClassMetadata();
  768. $config = $this->listener->getConfiguration($this->_em, $meta->name);
  769. $pk = $meta->getSingleIdentifierFieldName();
  770. $nodeId = $wrapped->getIdentifier();
  771. // prevent from deleting whole branch
  772. $dql = "UPDATE {$config['useObjectClass']} node";
  773. $dql .= ' SET node.' . $config['left'] . ' = 0,';
  774. $dql .= ' node.' . $config['right'] . ' = 0';
  775. $dql .= ' WHERE node.' . $pk . ' = ' . $nodeId;
  776. $this->_em->createQuery($dql)->getSingleScalarResult();
  777. // remove the node from database
  778. $dql = "DELETE {$config['useObjectClass']} node";
  779. $dql .= " WHERE node.{$pk} = {$nodeId}";
  780. $this->_em->createQuery($dql)->getSingleScalarResult();
  781. // remove from identity map
  782. $this->_em->getUnitOfWork()->removeFromIdentityMap($wrapped->getObject());
  783. }
  784. }