NestedTreeRepository.php 41 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060
  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 ('<ul>') - branch start
  743. * rootClose: string ('</ul>') - branch close
  744. * childStart: string ('<li>') - start of node
  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 = self::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. * Builds the tree
  910. *
  911. * @param array $nodes - array result of ORM query
  912. * @param array $options
  913. * @return array|string
  914. */
  915. public function buildTree(array $nodes, array &$options = array())
  916. {
  917. //process the nested tree into a nested array
  918. $meta = $this->getClassMetadata();
  919. $config = $this->listener->getConfiguration($this->_em, $meta->name);
  920. $nestedTree = array();
  921. $l = 0;
  922. if (count($nodes) > 0) {
  923. // Node Stack. Used to help building the hierarchy
  924. $stack = array();
  925. foreach ($nodes as $child) {
  926. $item = $child;
  927. $item['__children'] = array();
  928. // Number of stack items
  929. $l = count($stack);
  930. // Check if we're dealing with different levels
  931. while($l > 0 && $stack[$l - 1][$config['level']] >= $item[$config['level']]) {
  932. array_pop($stack);
  933. $l--;
  934. }
  935. // Stack is empty (we are inspecting the root)
  936. if ($l == 0) {
  937. // Assigning the root child
  938. $i = count($nestedTree);
  939. $nestedTree[$i] = $item;
  940. $stack[] = &$nestedTree[$i];
  941. } else {
  942. // Add child to parent
  943. $i = count($stack[$l - 1]['__children']);
  944. $stack[$l - 1]['__children'][$i] = $item;
  945. $stack[] = &$stack[$l - 1]['__children'][$i];
  946. }
  947. }
  948. }
  949. $default = array(
  950. 'decorate' => false,
  951. 'rootOpen' => '<ul>',
  952. 'rootClose' => '</ul>',
  953. 'childOpen' => '<li>',
  954. 'childClose' => '</li>',
  955. 'nodeDecorator' => function ($node) use ($meta) {
  956. // override and change it, guessing which field to use
  957. if ($meta->hasField('title')) {
  958. $field = 'title';
  959. } else if ($meta->hasField('name')) {
  960. $field = 'name';
  961. } else {
  962. throw new InvalidArgumentException("Cannot find any representation field");
  963. }
  964. return $node[$field];
  965. }
  966. );
  967. $options = array_merge($default, $options);
  968. // If you don't want any html output it will return the nested array
  969. if (!$options['decorate'] || !count($nestedTree)) {
  970. return $nestedTree;
  971. }
  972. $build = function($tree) use (&$build, &$options) {
  973. $output = $options['rootOpen'];
  974. foreach ($tree as $node) {
  975. $output .= $options['childOpen'] . $options['nodeDecorator']($node);
  976. if (count($node['__children']) > 0) {
  977. $output .= $build($node['__children']);
  978. }
  979. $output .= $options['childClose'];
  980. }
  981. return $output . $options['rootClose'];
  982. };
  983. return $build($nestedTree);
  984. }
  985. }