NestedTreeRepository.php 42 KB

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