NestedTreeRepository.php 42 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073
  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 builder
  24. *
  25. * @return Doctrine\ORM\QueryBuilder
  26. */
  27. public function getRootNodesQueryBuilder()
  28. {
  29. $meta = $this->getClassMetadata();
  30. $config = $this->listener->getConfiguration($this->_em, $meta->name);
  31. return $this->_em->createQueryBuilder()
  32. ->select('node')
  33. ->from($config['useObjectClass'], 'node')
  34. ->where('node.' . $config['parent'] . " IS NULL")
  35. ->orderBy('node.' . $config['left'], 'ASC')
  36. ;
  37. }
  38. /**
  39. * Get all root nodes query
  40. *
  41. * @return Doctrine\ORM\Query
  42. */
  43. public function getRootNodesQuery()
  44. {
  45. return $this->getRootNodesQueryBuilder()->getQuery();
  46. }
  47. /**
  48. * Get all root nodes
  49. *
  50. * @return array
  51. */
  52. public function getRootNodes()
  53. {
  54. return $this->getRootNodesQuery()->getResult();
  55. }
  56. /**
  57. * Allows the following 'virtual' methods:
  58. * - persistAsFirstChild($node)
  59. * - persistAsFirstChildOf($node, $parent)
  60. * - persistAsLastChild($node)
  61. * - persistAsLastChildOf($node, $parent)
  62. * - persistAsNextSibling($node)
  63. * - persistAsNextSiblingOf($node, $sibling)
  64. * - persistAsPrevSibling($node)
  65. * - persistAsPrevSiblingOf($node, $sibling)
  66. * Inherited virtual methods:
  67. * - find*
  68. *
  69. * @see \Doctrine\ORM\EntityRepository
  70. * @throws InvalidArgumentException - If arguments are invalid
  71. * @throws BadMethodCallException - If the method called is an invalid find* or persistAs* method
  72. * or no find* either persistAs* method at all and therefore an invalid method call.
  73. * @return mixed - TreeNestedRepository if persistAs* is called
  74. */
  75. public function __call($method, $args)
  76. {
  77. if (substr($method, 0, 9) === 'persistAs') {
  78. if (!isset($args[0])) {
  79. throw new \Gedmo\Exception\InvalidArgumentException('Node to persist must be available as first argument');
  80. }
  81. $node = $args[0];
  82. $wrapped = new EntityWrapper($node, $this->_em);
  83. $meta = $this->getClassMetadata();
  84. $config = $this->listener->getConfiguration($this->_em, $meta->name);
  85. $position = substr($method, 9);
  86. if (substr($method, -2) === 'Of') {
  87. if (!isset($args[1])) {
  88. throw new \Gedmo\Exception\InvalidArgumentException('If "Of" is specified you must provide parent or sibling as the second argument');
  89. }
  90. $parentOrSibling = $args[1];
  91. $wrapped->setPropertyValue($config['parent'], $parentOrSibling);
  92. $position = substr($position, 0, -2);
  93. }
  94. $wrapped->setPropertyValue($config['left'], 0); // simulate changeset
  95. $oid = spl_object_hash($node);
  96. $this->listener
  97. ->getStrategy($this->_em, $meta->name)
  98. ->setNodePosition($oid, $position)
  99. ;
  100. $this->_em->persist($node);
  101. return $this;
  102. }
  103. return parent::__call($method, $args);
  104. }
  105. /**
  106. * Get the Tree path query builder by given $node
  107. *
  108. * @param object $node
  109. * @throws InvalidArgumentException - if input is not valid
  110. * @return Doctrine\ORM\QueryBuilder
  111. */
  112. public function getPathQueryBuilder($node)
  113. {
  114. $meta = $this->getClassMetadata();
  115. if (!$node instanceof $meta->name) {
  116. throw new InvalidArgumentException("Node is not related to this repository");
  117. }
  118. $config = $this->listener->getConfiguration($this->_em, $meta->name);
  119. $wrapped = new EntityWrapper($node, $this->_em);
  120. if (!$wrapped->hasValidIdentifier()) {
  121. throw new InvalidArgumentException("Node is not managed by UnitOfWork");
  122. }
  123. $left = $wrapped->getPropertyValue($config['left']);
  124. $right = $wrapped->getPropertyValue($config['right']);
  125. $qb = $this->_em->createQueryBuilder();
  126. $qb->select('node')
  127. ->from($config['useObjectClass'], 'node')
  128. ->where('node.' . $config['left'] . " <= {$left}")
  129. ->andWhere('node.' . $config['right'] . " >= {$right}")
  130. ->orderBy('node.' . $config['left'], 'ASC')
  131. ;
  132. if (isset($config['root'])) {
  133. $rootId = $wrapped->getPropertyValue($config['root']);
  134. $qb->andWhere("node.{$config['root']} = {$rootId}");
  135. }
  136. return $qb;
  137. }
  138. /**
  139. * Get the Tree path query by given $node
  140. *
  141. * @param object $node
  142. * @return Doctrine\ORM\Query
  143. */
  144. public function getPathQuery($node)
  145. {
  146. return $this->getPathQueryBuilder($node)->getQuery();
  147. }
  148. /**
  149. * Get the Tree path of Nodes by given $node
  150. *
  151. * @param object $node
  152. * @return array - list of Nodes in path
  153. */
  154. public function getPath($node)
  155. {
  156. return $this->getPathQuery($node)->getResult();
  157. }
  158. /**
  159. * Counts the children of given TreeNode
  160. *
  161. * @param object $node - if null counts all records in tree
  162. * @param boolean $direct - true to count only direct children
  163. * @throws InvalidArgumentException - if input is not valid
  164. * @return integer
  165. */
  166. public function childCount($node = null, $direct = false)
  167. {
  168. $count = 0;
  169. $meta = $this->getClassMetadata();
  170. $nodeId = $meta->getSingleIdentifierFieldName();
  171. $config = $this->listener->getConfiguration($this->_em, $meta->name);
  172. if (null !== $node) {
  173. if ($node instanceof $meta->name) {
  174. $wrapped = new EntityWrapper($node, $this->_em);
  175. if (!$wrapped->hasValidIdentifier()) {
  176. throw new InvalidArgumentException("Node is not managed by UnitOfWork");
  177. }
  178. if ($direct) {
  179. $id = $wrapped->getIdentifier();
  180. $qb = $this->_em->createQueryBuilder();
  181. $qb->select('COUNT(node.' . $nodeId . ')')
  182. ->from($config['useObjectClass'], 'node')
  183. ->where('node.' . $config['parent'] . ' = ' . $id);
  184. if (isset($config['root'])) {
  185. $rootId = $wrapped->getPropertyValue($config['root']);
  186. $qb->andWhere("node.{$config['root']} = {$rootId}");
  187. }
  188. $q = $qb->getQuery();
  189. $count = intval($q->getSingleScalarResult());
  190. } else {
  191. $left = $wrapped->getPropertyValue($config['left']);
  192. $right = $wrapped->getPropertyValue($config['right']);
  193. if ($left && $right) {
  194. $count = ($right - $left - 1) / 2;
  195. }
  196. }
  197. } else {
  198. throw new InvalidArgumentException("Node is not related to this repository");
  199. }
  200. } else {
  201. $dql = "SELECT COUNT(node.{$nodeId}) FROM " . $config['useObjectClass'] . " node";
  202. if ($direct) {
  203. $dql .= ' WHERE node.' . $config['parent'] . ' IS NULL';
  204. }
  205. $q = $this->_em->createQuery($dql);
  206. $count = intval($q->getSingleScalarResult());
  207. }
  208. return $count;
  209. }
  210. /**
  211. * Get tree children query builder followed by given $node
  212. *
  213. * @param object $node - if null, all tree nodes will be taken
  214. * @param boolean $direct - true to take only direct children
  215. * @param string $sortByField - field name to sort by
  216. * @param string $direction - sort direction : "ASC" or "DESC"
  217. * @throws InvalidArgumentException - if input is not valid
  218. * @return Doctrine\ORM\QueryBuilder
  219. */
  220. public function childrenQueryBuilder($node = null, $direct = false, $sortByField = null, $direction = 'ASC')
  221. {
  222. $meta = $this->getClassMetadata();
  223. $config = $this->listener->getConfiguration($this->_em, $meta->name);
  224. $qb = $this->_em->createQueryBuilder();
  225. $qb->select('node')
  226. ->from($config['useObjectClass'], 'node')
  227. ;
  228. if ($node !== null) {
  229. if ($node instanceof $meta->name) {
  230. $wrapped = new EntityWrapper($node, $this->_em);
  231. if (!$wrapped->hasValidIdentifier()) {
  232. throw new InvalidArgumentException("Node is not managed by UnitOfWork");
  233. }
  234. if ($direct) {
  235. $id = $wrapped->getIdentifier();
  236. $qb->where('node.' . $config['parent'] . ' = ' . $id);
  237. } else {
  238. $left = $wrapped->getPropertyValue($config['left']);
  239. $right = $wrapped->getPropertyValue($config['right']);
  240. if ($left && $right) {
  241. $qb
  242. ->where('node.' . $config['right'] . " < {$right}")
  243. ->andWhere('node.' . $config['left'] . " > {$left}")
  244. ;
  245. }
  246. }
  247. if (isset($config['root'])) {
  248. $rootId = $wrapped->getPropertyValue($config['root']);
  249. $qb->andWhere("node.{$config['root']} = {$rootId}");
  250. }
  251. } else {
  252. throw new \InvalidArgumentException("Node is not related to this repository");
  253. }
  254. } else {
  255. if ($direct) {
  256. $qb->where('node.' . $config['parent'] . ' IS NULL');
  257. }
  258. }
  259. if (!$sortByField) {
  260. $qb->orderBy('node.' . $config['left'], 'ASC');
  261. } elseif (is_array($sortByField)) {
  262. $fields = '';
  263. foreach ($sortByField as $field) {
  264. $fields .= 'node.'.$field.',';
  265. }
  266. $fields = rtrim($fields,',');
  267. $qb->orderBy($fields,$direction);
  268. } else {
  269. if ($meta->hasField($sortByField) && in_array(strtolower($direction), array('asc', 'desc'))) {
  270. $qb->orderBy('node.' . $sortByField, $direction);
  271. } else {
  272. throw new InvalidArgumentException("Invalid sort options specified: field - {$sortByField}, direction - {$direction}");
  273. }
  274. }
  275. return $qb;
  276. }
  277. /**
  278. * Get tree children query followed by given $node
  279. *
  280. * @param object $node - if null, all tree nodes will be taken
  281. * @param boolean $direct - true to take only direct children
  282. * @param string|array $sortByField - field names to sort by
  283. * @param string $direction - sort direction : "ASC" or "DESC"
  284. * @return Doctrine\ORM\Query
  285. */
  286. public function childrenQuery($node = null, $direct = false, $sortByField = null, $direction = 'ASC')
  287. {
  288. return $this->childrenQueryBuilder($node, $direct, $sortByField, $direction)->getQuery();
  289. }
  290. /**
  291. * Get list of children followed by given $node
  292. *
  293. * @param object $node - if null, all tree nodes will be taken
  294. * @param boolean $direct - true to take only direct children
  295. * @param string $sortByField - field name to sort by
  296. * @param string $direction - sort direction : "ASC" or "DESC"
  297. * @return array - list of given $node children, null on failure
  298. */
  299. public function children($node = null, $direct = false, $sortByField = null, $direction = 'ASC')
  300. {
  301. $q = $this->childrenQuery($node, $direct, $sortByField, $direction);
  302. return $q->getResult();
  303. }
  304. /**
  305. * Get tree leafs query builder
  306. *
  307. * @param object $root - root node in case of root tree is required
  308. * @param string $sortByField - field name to sort by
  309. * @param string $direction - sort direction : "ASC" or "DESC"
  310. * @throws InvalidArgumentException - if input is not valid
  311. * @return Doctrine\ORM\QueryBuilder
  312. */
  313. public function getLeafsQueryBuilder($root = null, $sortByField = null, $direction = 'ASC')
  314. {
  315. $meta = $this->getClassMetadata();
  316. $config = $this->listener->getConfiguration($this->_em, $meta->name);
  317. if (isset($config['root']) && is_null($root)) {
  318. if (is_null($root)) {
  319. throw new InvalidArgumentException("If tree has root, getLeafs method requires any node of this tree");
  320. }
  321. }
  322. $qb = $this->_em->createQueryBuilder();
  323. $qb->select('node')
  324. ->from($config['useObjectClass'], 'node')
  325. ->where('node.' . $config['right'] . ' = 1 + node.' . $config['left'])
  326. ;
  327. if (isset($config['root'])) {
  328. if ($root instanceof $meta->name) {
  329. $wrapped = new EntityWrapper($root, $this->_em);
  330. $rootId = $wrapped->getPropertyValue($config['root']);
  331. if (!$rootId) {
  332. throw new InvalidArgumentException("Root node must be managed");
  333. }
  334. $qb->andWhere("node.{$config['root']} = {$rootId}");
  335. } else {
  336. throw new InvalidArgumentException("Node is not related to this repository");
  337. }
  338. }
  339. if (!$sortByField) {
  340. $qb->orderBy('node.' . $config['left'], 'ASC');
  341. } else {
  342. if ($meta->hasField($sortByField) && in_array(strtolower($direction), array('asc', 'desc'))) {
  343. $qb->orderBy('node.' . $sortByField, $direction);
  344. } else {
  345. throw new InvalidArgumentException("Invalid sort options specified: field - {$sortByField}, direction - {$direction}");
  346. }
  347. }
  348. return $qb;
  349. }
  350. /**
  351. * Get tree leafs query
  352. *
  353. * @param object $root - root node in case of root tree is required
  354. * @param string $sortByField - field name to sort by
  355. * @param string $direction - sort direction : "ASC" or "DESC"
  356. * @return Doctrine\ORM\Query
  357. */
  358. public function getLeafsQuery($root = null, $sortByField = null, $direction = 'ASC')
  359. {
  360. return $this->getLeafsQueryBuilder($root, $sortByField, $direction)->getQuery();
  361. }
  362. /**
  363. * Get list of leaf nodes of the tree
  364. *
  365. * @param object $root - root node in case of root tree is required
  366. * @param string $sortByField - field name to sort by
  367. * @param string $direction - sort direction : "ASC" or "DESC"
  368. * @return array
  369. */
  370. public function getLeafs($root = null, $sortByField = null, $direction = 'ASC')
  371. {
  372. return $this->getLeafsQuery($root, $sortByField, $direction)->getResult();
  373. }
  374. /**
  375. * Get the query builder for next siblings of the given $node
  376. *
  377. * @param object $node
  378. * @param bool $includeSelf - include the node itself
  379. * @throws \Gedmo\Exception\InvalidArgumentException - if input is invalid
  380. * @return Doctrine\ORM\QueryBuilder
  381. */
  382. public function getNextSiblingsQueryBuilder($node, $includeSelf = false)
  383. {
  384. $meta = $this->getClassMetadata();
  385. if (!$node instanceof $meta->name) {
  386. throw new InvalidArgumentException("Node is not related to this repository");
  387. }
  388. $wrapped = new EntityWrapper($node, $this->_em);
  389. if (!$wrapped->hasValidIdentifier()) {
  390. throw new InvalidArgumentException("Node is not managed by UnitOfWork");
  391. }
  392. $config = $this->listener->getConfiguration($this->_em, $meta->name);
  393. $parent = $wrapped->getPropertyValue($config['parent']);
  394. if (isset($config['root']) && !$parent) {
  395. throw new InvalidArgumentException("Cannot get siblings from tree root node");
  396. }
  397. $left = $wrapped->getPropertyValue($config['left']);
  398. $sign = $includeSelf ? '>=' : '>';
  399. $qb = $this->_em->createQueryBuilder();
  400. $qb->select('node')
  401. ->from($config['useObjectClass'], 'node')
  402. ->where("node.{$config['left']} {$sign} {$left}")
  403. ->orderBy("node.{$config['left']}", 'ASC')
  404. ;
  405. if ($parent) {
  406. $wrappedParent = new EntityWrapper($parent, $this->_em);
  407. $parentId = $wrappedParent->getIdentifier();
  408. $qb->andWhere("node.{$config['parent']} = {$parentId}");
  409. } else {
  410. $qb->andWhere($qb->expr()->isNull('node.'.$config['parent']));
  411. }
  412. return $qb;
  413. }
  414. /**
  415. * Get the query for next siblings of the given $node
  416. *
  417. * @param object $node
  418. * @param bool $includeSelf - include the node itself
  419. * @return Doctrine\ORM\Query
  420. */
  421. public function getNextSiblingsQuery($node, $includeSelf = false)
  422. {
  423. return $this->getNextSiblingsQueryBuilder($node, $includeSelf)->getQuery();
  424. }
  425. /**
  426. * Find the next siblings of the given $node
  427. *
  428. * @param object $node
  429. * @param bool $includeSelf - include the node itself
  430. * @return array
  431. */
  432. public function getNextSiblings($node, $includeSelf = false)
  433. {
  434. return $this->getNextSiblingsQuery($node, $includeSelf)->getResult();
  435. }
  436. /**
  437. * Get query builder for previous siblings of the given $node
  438. *
  439. * @param object $node
  440. * @param bool $includeSelf - include the node itself
  441. * @throws \Gedmo\Exception\InvalidArgumentException - if input is invalid
  442. * @return Doctrine\ORM\QueryBuilder
  443. */
  444. public function getPrevSiblingsQueryBuilder($node, $includeSelf = false)
  445. {
  446. $meta = $this->getClassMetadata();
  447. if (!$node instanceof $meta->name) {
  448. throw new InvalidArgumentException("Node is not related to this repository");
  449. }
  450. $wrapped = new EntityWrapper($node, $this->_em);
  451. if (!$wrapped->hasValidIdentifier()) {
  452. throw new InvalidArgumentException("Node is not managed by UnitOfWork");
  453. }
  454. $config = $this->listener->getConfiguration($this->_em, $meta->name);
  455. $parent = $wrapped->getPropertyValue($config['parent']);
  456. if (isset($config['root']) && !$parent) {
  457. throw new InvalidArgumentException("Cannot get siblings from tree root node");
  458. }
  459. $left = $wrapped->getPropertyValue($config['left']);
  460. $sign = $includeSelf ? '<=' : '<';
  461. $qb = $this->_em->createQueryBuilder();
  462. $qb->select('node')
  463. ->from($config['useObjectClass'], 'node')
  464. ->where("node.{$config['left']} {$sign} {$left}")
  465. ->orderBy("node.{$config['left']}", 'ASC')
  466. ;
  467. if ($parent) {
  468. $wrappedParent = new EntityWrapper($parent, $this->_em);
  469. $parentId = $wrappedParent->getIdentifier();
  470. $qb->andWhere("node.{$config['parent']} = {$parentId}");
  471. } else {
  472. $qb->andWhere($qb->expr()->isNull('node.'.$config['parent']));
  473. }
  474. return $qb;
  475. }
  476. /**
  477. * Get query for previous siblings of the given $node
  478. *
  479. * @param object $node
  480. * @param bool $includeSelf - include the node itself
  481. * @throws \Gedmo\Exception\InvalidArgumentException - if input is invalid
  482. * @return Doctrine\ORM\Query
  483. */
  484. public function getPrevSiblingsQuery($node, $includeSelf = false)
  485. {
  486. return $this->getPrevSiblingsQueryBuilder($node, $includeSelf)->getQuery();
  487. }
  488. /**
  489. * Find the previous siblings of the given $node
  490. *
  491. * @param object $node
  492. * @param bool $includeSelf - include the node itself
  493. * @return array
  494. */
  495. public function getPrevSiblings($node, $includeSelf = false)
  496. {
  497. return $this->getPrevSiblingsQuery($node, $includeSelf)->getResult();
  498. }
  499. /**
  500. * Move the node down in the same level
  501. *
  502. * @param object $node
  503. * @param mixed $number
  504. * integer - number of positions to shift
  505. * boolean - if "true" - shift till last position
  506. * @throws RuntimeException - if something fails in transaction
  507. * @return boolean - true if shifted
  508. */
  509. public function moveDown($node, $number = 1)
  510. {
  511. $result = false;
  512. $meta = $this->getClassMetadata();
  513. if ($node instanceof $meta->name) {
  514. $config = $this->listener->getConfiguration($this->_em, $meta->name);
  515. $nextSiblings = $this->getNextSiblings($node);
  516. if ($numSiblings = count($nextSiblings)) {
  517. $result = true;
  518. if ($number === true) {
  519. $number = $numSiblings;
  520. } elseif ($number > $numSiblings) {
  521. $number = $numSiblings;
  522. }
  523. $this->listener
  524. ->getStrategy($this->_em, $meta->name)
  525. ->updateNode($this->_em, $node, $nextSiblings[$number - 1], Nested::NEXT_SIBLING);
  526. }
  527. } else {
  528. throw new InvalidArgumentException("Node is not related to this repository");
  529. }
  530. return $result;
  531. }
  532. /**
  533. * Move the node up in the same level
  534. *
  535. * @param object $node
  536. * @param mixed $number
  537. * integer - number of positions to shift
  538. * boolean - true shift till first position
  539. * @throws RuntimeException - if something fails in transaction
  540. * @return boolean - true if shifted
  541. */
  542. public function moveUp($node, $number = 1)
  543. {
  544. $result = false;
  545. $meta = $this->getClassMetadata();
  546. if ($node instanceof $meta->name) {
  547. $config = $this->listener->getConfiguration($this->_em, $meta->name);
  548. $prevSiblings = array_reverse($this->getPrevSiblings($node));
  549. if ($numSiblings = count($prevSiblings)) {
  550. $result = true;
  551. if ($number === true) {
  552. $number = $numSiblings;
  553. } elseif ($number > $numSiblings) {
  554. $number = $numSiblings;
  555. }
  556. $this->listener
  557. ->getStrategy($this->_em, $meta->name)
  558. ->updateNode($this->_em, $node, $prevSiblings[$number - 1], Nested::PREV_SIBLING);
  559. }
  560. } else {
  561. throw new InvalidArgumentException("Node is not related to this repository");
  562. }
  563. return $result;
  564. }
  565. /**
  566. * Removes given $node from the tree and reparents its descendants
  567. *
  568. * @param object $node
  569. * @throws RuntimeException - if something fails in transaction
  570. * @return void
  571. */
  572. public function removeFromTree($node)
  573. {
  574. $meta = $this->getClassMetadata();
  575. if ($node instanceof $meta->name) {
  576. $wrapped = new EntityWrapper($node, $this->_em);
  577. $config = $this->listener->getConfiguration($this->_em, $meta->name);
  578. $right = $wrapped->getPropertyValue($config['right']);
  579. $left = $wrapped->getPropertyValue($config['left']);
  580. $rootId = isset($config['root']) ? $wrapped->getPropertyValue($config['root']) : null;
  581. if ($right == $left + 1) {
  582. $this->removeSingle($wrapped);
  583. $this->listener
  584. ->getStrategy($this->_em, $meta->name)
  585. ->shiftRL($this->_em, $config['useObjectClass'], $right, -2, $rootId);
  586. return; // node was a leaf
  587. }
  588. // process updates in transaction
  589. $this->_em->getConnection()->beginTransaction();
  590. try {
  591. $parent = $wrapped->getPropertyValue($config['parent']);
  592. $parentId = 'NULL';
  593. if ($parent) {
  594. $wrappedParrent = new EntityWrapper($parent, $this->_em);
  595. $parentId = $wrappedParrent->getIdentifier();
  596. }
  597. $pk = $meta->getSingleIdentifierFieldName();
  598. $nodeId = $wrapped->getIdentifier();
  599. $shift = -1;
  600. // in case if root node is removed, childs become roots
  601. if (isset($config['root']) && !$parent) {
  602. $dql = "SELECT node.{$pk}, node.{$config['left']}, node.{$config['right']} FROM {$config['useObjectClass']} node";
  603. $dql .= " WHERE node.{$config['parent']} = {$nodeId}";
  604. $nodes = $this->_em->createQuery($dql)->getArrayResult();
  605. foreach ($nodes as $newRoot) {
  606. $left = $newRoot[$config['left']];
  607. $right = $newRoot[$config['right']];
  608. $rootId = $newRoot[$pk];
  609. $shift = -($left - 1);
  610. $dql = "UPDATE {$config['useObjectClass']} node";
  611. $dql .= ' SET node.' . $config['root'] . ' = :rootId';
  612. $dql .= ' WHERE node.' . $config['root'] . ' = :nodeId';
  613. $dql .= " AND node.{$config['left']} >= :left";
  614. $dql .= " AND node.{$config['right']} <= :right";
  615. $q = $this->_em->createQuery($dql);
  616. $q->setParameters(compact('rootId', 'left', 'right', 'nodeId'));
  617. $q->getSingleScalarResult();
  618. $dql = "UPDATE {$config['useObjectClass']} node";
  619. $dql .= ' SET node.' . $config['parent'] . ' = ' . $parentId;
  620. $dql .= ' WHERE node.' . $config['parent'] . ' = ' . $nodeId;
  621. $dql .= ' AND node.' . $config['root'] . ' = ' . $rootId;
  622. $q = $this->_em->createQuery($dql);
  623. $q->getSingleScalarResult();
  624. $this->listener
  625. ->getStrategy($this->_em, $meta->name)
  626. ->shiftRangeRL($this->_em, $config['useObjectClass'], $left, $right, $shift, $rootId, $rootId, - 1);
  627. $this->listener
  628. ->getStrategy($this->_em, $meta->name)
  629. ->shiftRL($this->_em, $config['useObjectClass'], $right, -2, $rootId);
  630. }
  631. } else {
  632. $dql = "UPDATE {$config['useObjectClass']} node";
  633. $dql .= ' SET node.' . $config['parent'] . ' = ' . $parentId;
  634. $dql .= ' WHERE node.' . $config['parent'] . ' = ' . $nodeId;
  635. if (isset($config['root'])) {
  636. $dql .= ' AND node.' . $config['root'] . ' = ' . $rootId;
  637. }
  638. // @todo: update in memory nodes
  639. $q = $this->_em->createQuery($dql);
  640. $q->getSingleScalarResult();
  641. $this->listener
  642. ->getStrategy($this->_em, $meta->name)
  643. ->shiftRangeRL($this->_em, $config['useObjectClass'], $left, $right, $shift, $rootId, $rootId, - 1);
  644. $this->listener
  645. ->getStrategy($this->_em, $meta->name)
  646. ->shiftRL($this->_em, $config['useObjectClass'], $right, -2, $rootId);
  647. }
  648. $this->removeSingle($wrapped);
  649. $this->_em->getConnection()->commit();
  650. } catch (\Exception $e) {
  651. $this->_em->close();
  652. $this->_em->getConnection()->rollback();
  653. throw new \Gedmo\Exception\RuntimeException('Transaction failed', null, $e);
  654. }
  655. } else {
  656. throw new InvalidArgumentException("Node is not related to this repository");
  657. }
  658. }
  659. /**
  660. * Reorders the sibling nodes and child nodes by given $node,
  661. * according to the $sortByField and $direction specified
  662. *
  663. * @param object $node - from which node to start reordering the tree
  664. * @param string $sortByField - field name to sort by
  665. * @param string $direction - sort direction : "ASC" or "DESC"
  666. * @param boolean $verify - true to verify tree first
  667. * @return void
  668. */
  669. public function reorder($node, $sortByField = null, $direction = 'ASC', $verify = true)
  670. {
  671. $meta = $this->getClassMetadata();
  672. if ($node instanceof $meta->name) {
  673. $config = $this->listener->getConfiguration($this->_em, $meta->name);
  674. if ($verify && is_array($this->verify())) {
  675. return false;
  676. }
  677. $nodes = $this->children($node, true, $sortByField, $direction);
  678. foreach ($nodes as $node) {
  679. $wrapped = new EntityWrapper($node, $this->_em);
  680. $right = $wrapped->getPropertyValue($config['right']);
  681. $left = $wrapped->getPropertyValue($config['left']);
  682. $this->moveDown($node, true);
  683. if ($left != ($right - 1)) {
  684. $this->reorder($node, $sortByField, $direction, false);
  685. }
  686. }
  687. } else {
  688. throw new InvalidArgumentException("Node is not related to this repository");
  689. }
  690. }
  691. /**
  692. * Verifies that current tree is valid.
  693. * If any error is detected it will return an array
  694. * with a list of errors found on tree
  695. *
  696. * @return mixed
  697. * boolean - true on success
  698. * array - error list on failure
  699. */
  700. public function verify()
  701. {
  702. if (!$this->childCount()) {
  703. return true; // tree is empty
  704. }
  705. $errors = array();
  706. $meta = $this->getClassMetadata();
  707. $config = $this->listener->getConfiguration($this->_em, $meta->name);
  708. if (isset($config['root'])) {
  709. $trees = $this->getRootNodes();
  710. foreach ($trees as $tree) {
  711. $this->verifyTree($errors, $tree);
  712. }
  713. } else {
  714. $this->verifyTree($errors);
  715. }
  716. return $errors ?: true;
  717. }
  718. /**
  719. * Tries to recover the tree
  720. *
  721. * @todo implement
  722. * @throws RuntimeException - if something fails in transaction
  723. * @return void
  724. */
  725. public function recover()
  726. {
  727. if ($this->verify() === true) {
  728. return;
  729. }
  730. // not yet implemented
  731. }
  732. /**
  733. * Retrieves the nested array or the decorated output.
  734. * Uses @options to handle decorations
  735. *
  736. * @throws \Gedmo\Exception\InvalidArgumentException
  737. * @param object $node - from which node to start reordering the tree
  738. * @param boolean $direct - true to take only direct children
  739. * @param array $options :
  740. * decorate: boolean (false) - retrieves tree as UL->LI tree
  741. * nodeDecorator: Closure (null) - uses $node as argument and returns decorated item as string
  742. * rootOpen: string || Closure ('<ul>') - branch start, closure will be given $children as a parameter
  743. * rootClose: string ('</ul>') - branch close
  744. * childStart: string || Closure ('<li>') - start of node, closure will be given $node as a parameter
  745. * childClose: string ('</li>') - close of node
  746. *
  747. * @return array|string
  748. */
  749. public function childrenHierarchy($node = null, $direct = false, array $options = array())
  750. {
  751. $meta = $this->getClassMetadata();
  752. $config = $this->listener->getConfiguration($this->_em, $meta->name);
  753. if ($node !== null) {
  754. if ($node instanceof $meta->name) {
  755. $wrapped = new EntityWrapper($node, $this->_em);
  756. if (!$wrapped->hasValidIdentifier()) {
  757. throw new InvalidArgumentException("Node is not managed by UnitOfWork");
  758. }
  759. }
  760. }
  761. // Gets the array of $node results.
  762. // It must be order by 'root' and 'left' field
  763. $nodes = $this->childrenQuery(
  764. $node,
  765. $direct,
  766. isset($config['root']) ? array($config['root'], $config['left']) : $config['left'],
  767. 'ASC'
  768. )->getArrayResult();
  769. return $this->buildTree($nodes, $options);
  770. }
  771. /**
  772. * {@inheritdoc}
  773. */
  774. protected function validates()
  775. {
  776. return $this->listener->getStrategy($this->_em, $this->getClassMetadata()->name)->getName() === Strategy::NESTED;
  777. }
  778. /**
  779. * Collect errors on given tree if
  780. * where are any
  781. *
  782. * @param array $errors
  783. * @param object $root
  784. * @return void
  785. */
  786. private function verifyTree(&$errors, $root = null)
  787. {
  788. $meta = $this->getClassMetadata();
  789. $config = $this->listener->getConfiguration($this->_em, $meta->name);
  790. $identifier = $meta->getSingleIdentifierFieldName();
  791. $rootId = isset($config['root']) ? $meta->getReflectionProperty($config['root'])->getValue($root) : null;
  792. $dql = "SELECT MIN(node.{$config['left']}) FROM {$config['useObjectClass']} node";
  793. if ($root) {
  794. $dql .= " WHERE node.{$config['root']} = {$rootId}";
  795. }
  796. $min = intval($this->_em->createQuery($dql)->getSingleScalarResult());
  797. $edge = $this->listener->getStrategy($this->_em, $meta->name)->max($this->_em, $config['useObjectClass'], $rootId);
  798. // check duplicate right and left values
  799. for ($i = $min; $i <= $edge; $i++) {
  800. $dql = "SELECT COUNT(node.{$identifier}) FROM {$config['useObjectClass']} node";
  801. $dql .= " WHERE (node.{$config['left']} = {$i} OR node.{$config['right']} = {$i})";
  802. if ($root) {
  803. $dql .= " AND node.{$config['root']} = {$rootId}";
  804. }
  805. $count = intval($this->_em->createQuery($dql)->getSingleScalarResult());
  806. if ($count !== 1) {
  807. if ($count === 0) {
  808. $errors[] = "index [{$i}], missing" . ($root ? ' on tree root: ' . $rootId : '');
  809. } else {
  810. $errors[] = "index [{$i}], duplicate" . ($root ? ' on tree root: ' . $rootId : '');
  811. }
  812. }
  813. }
  814. // check for missing parents
  815. $dql = "SELECT node FROM {$config['useObjectClass']} node";
  816. $dql .= " LEFT JOIN node.{$config['parent']} parent";
  817. $dql .= " WHERE node.{$config['parent']} IS NOT NULL";
  818. $dql .= " AND parent.{$identifier} IS NULL";
  819. if ($root) {
  820. $dql .= " AND node.{$config['root']} = {$rootId}";
  821. }
  822. $nodes = $this->_em->createQuery($dql)->getArrayResult();
  823. if (count($nodes)) {
  824. foreach ($nodes as $node) {
  825. $errors[] = "node [{$node[$identifier]}] has missing parent" . ($root ? ' on tree root: ' . $rootId : '');
  826. }
  827. return; // loading broken relation can cause infinite loop
  828. }
  829. $dql = "SELECT node FROM {$config['useObjectClass']} node";
  830. $dql .= " WHERE node.{$config['right']} < node.{$config['left']}";
  831. if ($root) {
  832. $dql .= " AND node.{$config['root']} = {$rootId}";
  833. }
  834. $result = $this->_em->createQuery($dql)
  835. ->setMaxResults(1)
  836. ->getResult(Query::HYDRATE_ARRAY);
  837. $node = count($result) ? array_shift($result) : null;
  838. if ($node) {
  839. $id = $node[$identifier];
  840. $errors[] = "node [{$id}], left is greater than right" . ($root ? ' on tree root: ' . $rootId : '');
  841. }
  842. $dql = "SELECT node FROM {$config['useObjectClass']} node";
  843. if ($root) {
  844. $dql .= " WHERE node.{$config['root']} = {$rootId}";
  845. }
  846. $nodes = $this->_em->createQuery($dql)->getResult(Query::HYDRATE_OBJECT);
  847. foreach ($nodes as $node) {
  848. $right = $meta->getReflectionProperty($config['right'])->getValue($node);
  849. $left = $meta->getReflectionProperty($config['left'])->getValue($node);
  850. $id = $meta->getReflectionProperty($identifier)->getValue($node);
  851. $parent = $meta->getReflectionProperty($config['parent'])->getValue($node);
  852. if (!$right || !$left) {
  853. $errors[] = "node [{$id}] has invalid left or right values";
  854. } elseif ($right == $left) {
  855. $errors[] = "node [{$id}] has identical left and right values";
  856. } elseif ($parent) {
  857. if ($parent instanceof Proxy && !$parent->__isInitialized__) {
  858. $this->_em->refresh($parent);
  859. }
  860. $parentRight = $meta->getReflectionProperty($config['right'])->getValue($parent);
  861. $parentLeft = $meta->getReflectionProperty($config['left'])->getValue($parent);
  862. $parentId = $meta->getReflectionProperty($identifier)->getValue($parent);
  863. if ($left < $parentLeft) {
  864. $errors[] = "node [{$id}] left is less than parent`s [{$parentId}] left value";
  865. } elseif ($right > $parentRight) {
  866. $errors[] = "node [{$id}] right is greater than parent`s [{$parentId}] right value";
  867. }
  868. } else {
  869. $dql = "SELECT COUNT(node.{$identifier}) FROM {$config['useObjectClass']} node";
  870. $dql .= " WHERE node.{$config['left']} < {$left}";
  871. $dql .= " AND node.{$config['right']} > {$right}";
  872. if ($root) {
  873. $dql .= " AND node.{$config['root']} = {$rootId}";
  874. }
  875. $q = $this->_em->createQuery($dql);
  876. if ($count = intval($q->getSingleScalarResult())) {
  877. $errors[] = "node [{$id}] parent field is blank, but it has a parent";
  878. }
  879. }
  880. }
  881. }
  882. /**
  883. * Removes single node without touching children
  884. *
  885. * @internal
  886. * @param EntityWrapper $wrapped
  887. * @return void
  888. */
  889. private function removeSingle(EntityWrapper $wrapped)
  890. {
  891. $meta = $this->getClassMetadata();
  892. $config = $this->listener->getConfiguration($this->_em, $meta->name);
  893. $pk = $meta->getSingleIdentifierFieldName();
  894. $nodeId = $wrapped->getIdentifier();
  895. // prevent from deleting whole branch
  896. $dql = "UPDATE {$config['useObjectClass']} node";
  897. $dql .= ' SET node.' . $config['left'] . ' = 0,';
  898. $dql .= ' node.' . $config['right'] . ' = 0';
  899. $dql .= ' WHERE node.' . $pk . ' = ' . $nodeId;
  900. $this->_em->createQuery($dql)->getSingleScalarResult();
  901. // remove the node from database
  902. $dql = "DELETE {$config['useObjectClass']} node";
  903. $dql .= " WHERE node.{$pk} = {$nodeId}";
  904. $this->_em->createQuery($dql)->getSingleScalarResult();
  905. // remove from identity map
  906. $this->_em->getUnitOfWork()->removeFromIdentityMap($wrapped->getObject());
  907. }
  908. /**
  909. * Retrieves the nested array or the decorated output.
  910. * Uses @options to handle decorations
  911. * NOTE: @nodes should be fetched and hydrated as array
  912. *
  913. * @throws \Gedmo\Exception\InvalidArgumentException
  914. * @param array $nodes - list o nodes to build tree
  915. * @param array $options :
  916. * decorate: boolean (false) - retrieves tree as UL->LI tree
  917. * nodeDecorator: Closure (null) - uses $node as argument and returns decorated item as string
  918. * rootOpen: string || Closure ('<ul>') - branch start, closure will be given $children as a parameter
  919. * rootClose: string ('</ul>') - branch close
  920. * childStart: string || Closure ('<li>') - start of node, closure will be given $node as a parameter
  921. * childClose: string ('</li>') - close of node
  922. *
  923. * @return array|string
  924. */
  925. public function buildTree(array $nodes, array $options = array())
  926. {
  927. //process the nested tree into a nested array
  928. $meta = $this->getClassMetadata();
  929. $config = $this->listener->getConfiguration($this->_em, $meta->name);
  930. $nestedTree = array();
  931. $l = 0;
  932. if (count($nodes) > 0) {
  933. // Node Stack. Used to help building the hierarchy
  934. $stack = array();
  935. foreach ($nodes as $child) {
  936. $item = $child;
  937. $item['__children'] = array();
  938. // Number of stack items
  939. $l = count($stack);
  940. // Check if we're dealing with different levels
  941. while($l > 0 && $stack[$l - 1][$config['level']] >= $item[$config['level']]) {
  942. array_pop($stack);
  943. $l--;
  944. }
  945. // Stack is empty (we are inspecting the root)
  946. if ($l == 0) {
  947. // Assigning the root child
  948. $i = count($nestedTree);
  949. $nestedTree[$i] = $item;
  950. $stack[] = &$nestedTree[$i];
  951. } else {
  952. // Add child to parent
  953. $i = count($stack[$l - 1]['__children']);
  954. $stack[$l - 1]['__children'][$i] = $item;
  955. $stack[] = &$stack[$l - 1]['__children'][$i];
  956. }
  957. }
  958. }
  959. $default = array(
  960. 'decorate' => false,
  961. 'rootOpen' => '<ul>',
  962. 'rootClose' => '</ul>',
  963. 'childOpen' => '<li>',
  964. 'childClose' => '</li>',
  965. 'nodeDecorator' => function ($node) use ($meta) {
  966. // override and change it, guessing which field to use
  967. if ($meta->hasField('title')) {
  968. $field = 'title';
  969. } else if ($meta->hasField('name')) {
  970. $field = 'name';
  971. } else {
  972. throw new InvalidArgumentException("Cannot find any representation field");
  973. }
  974. return $node[$field];
  975. }
  976. );
  977. $options = array_merge($default, $options);
  978. // If you don't want any html output it will return the nested array
  979. if (!$options['decorate']) {
  980. return $nestedTree;
  981. } elseif (!count($nestedTree)) {
  982. return '';
  983. }
  984. $build = function($tree) use (&$build, &$options) {
  985. $output = is_string($options['rootOpen']) ? $options['rootOpen'] : $options['rootOpen']($tree);
  986. foreach ($tree as $node) {
  987. $output .= is_string($options['childOpen']) ? $options['childOpen'] : $options['childOpen']($node);
  988. $output .= $options['nodeDecorator']($node);
  989. if (count($node['__children']) > 0) {
  990. $output .= $build($node['__children']);
  991. }
  992. $output .= $options['childClose'];
  993. }
  994. return $output . $options['rootClose'];
  995. };
  996. return $build($nestedTree);
  997. }
  998. }