NestedTreeRepository.php 42 KB

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