NestedTreeRepository.php 43 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. $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']} = ".$this->getIdExpression($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'] . ' = ' . $this->getIdExpression($id));
  184. if (isset($config['root'])) {
  185. $rootId = $wrapped->getPropertyValue($config['root']);
  186. $qb->andWhere("node.{$config['root']} = ".$this->getIdExpression($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'] . ' = ' . $this->getIdExpression($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']} = ".$this->getIdExpression($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']} = ".$this->getIdExpression($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']} = ".$this->getIdExpression($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']} = ".$this->getIdExpression($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. $nextSiblings = $this->getNextSiblings($node);
  515. if ($numSiblings = count($nextSiblings)) {
  516. $result = true;
  517. if ($number === true) {
  518. $number = $numSiblings;
  519. } elseif ($number > $numSiblings) {
  520. $number = $numSiblings;
  521. }
  522. $this->listener
  523. ->getStrategy($this->_em, $meta->name)
  524. ->updateNode($this->_em, $node, $nextSiblings[$number - 1], Nested::NEXT_SIBLING);
  525. }
  526. } else {
  527. throw new InvalidArgumentException("Node is not related to this repository");
  528. }
  529. return $result;
  530. }
  531. /**
  532. * Move the node up in the same level
  533. *
  534. * @param object $node
  535. * @param mixed $number
  536. * integer - number of positions to shift
  537. * boolean - true shift till first position
  538. * @throws RuntimeException - if something fails in transaction
  539. * @return boolean - true if shifted
  540. */
  541. public function moveUp($node, $number = 1)
  542. {
  543. $result = false;
  544. $meta = $this->getClassMetadata();
  545. if ($node instanceof $meta->name) {
  546. $prevSiblings = array_reverse($this->getPrevSiblings($node));
  547. if ($numSiblings = count($prevSiblings)) {
  548. $result = true;
  549. if ($number === true) {
  550. $number = $numSiblings;
  551. } elseif ($number > $numSiblings) {
  552. $number = $numSiblings;
  553. }
  554. $this->listener
  555. ->getStrategy($this->_em, $meta->name)
  556. ->updateNode($this->_em, $node, $prevSiblings[$number - 1], Nested::PREV_SIBLING);
  557. }
  558. } else {
  559. throw new InvalidArgumentException("Node is not related to this repository");
  560. }
  561. return $result;
  562. }
  563. /**
  564. * Removes given $node from the tree and reparents its descendants
  565. *
  566. * @param object $node
  567. * @throws RuntimeException - if something fails in transaction
  568. * @return void
  569. */
  570. public function removeFromTree($node)
  571. {
  572. $meta = $this->getClassMetadata();
  573. if ($node instanceof $meta->name) {
  574. $wrapped = new EntityWrapper($node, $this->_em);
  575. $config = $this->listener->getConfiguration($this->_em, $meta->name);
  576. $right = $wrapped->getPropertyValue($config['right']);
  577. $left = $wrapped->getPropertyValue($config['left']);
  578. $rootId = isset($config['root']) ? $wrapped->getPropertyValue($config['root']) : null;
  579. if ($right == $left + 1) {
  580. $this->removeSingle($wrapped);
  581. $this->listener
  582. ->getStrategy($this->_em, $meta->name)
  583. ->shiftRL($this->_em, $config['useObjectClass'], $right, -2, $rootId);
  584. return; // node was a leaf
  585. }
  586. // process updates in transaction
  587. $this->_em->getConnection()->beginTransaction();
  588. try {
  589. $parent = $wrapped->getPropertyValue($config['parent']);
  590. $parentId = null;
  591. if ($parent) {
  592. $wrappedParrent = new EntityWrapper($parent, $this->_em);
  593. $parentId = $wrappedParrent->getIdentifier();
  594. }
  595. $pk = $meta->getSingleIdentifierFieldName();
  596. $nodeId = $wrapped->getIdentifier();
  597. $shift = -1;
  598. // in case if root node is removed, childs become roots
  599. if (isset($config['root']) && !$parent) {
  600. $dql = "SELECT node.{$pk}, node.{$config['left']}, node.{$config['right']} FROM {$config['useObjectClass']} node";
  601. $dql .= " WHERE node.{$config['parent']} = ".$this->getIdExpression($nodeId);
  602. $nodes = $this->_em->createQuery($dql)->getArrayResult();
  603. foreach ($nodes as $newRoot) {
  604. $left = $newRoot[$config['left']];
  605. $right = $newRoot[$config['right']];
  606. $rootId = $newRoot[$pk];
  607. $shift = -($left - 1);
  608. $dql = "UPDATE {$config['useObjectClass']} node";
  609. $dql .= ' SET node.' . $config['root'] . ' = :rootId';
  610. $dql .= ' WHERE node.' . $config['root'] . ' = :nodeId';
  611. $dql .= " AND node.{$config['left']} >= :left";
  612. $dql .= " AND node.{$config['right']} <= :right";
  613. $q = $this->_em->createQuery($dql);
  614. $q->setParameters(compact('rootId', 'left', 'right', 'nodeId'));
  615. $q->getSingleScalarResult();
  616. $dql = "UPDATE {$config['useObjectClass']} node";
  617. $dql .= ' SET node.' . $config['parent'] . ' = ' . $this->getIdExpression($parentId);
  618. $dql .= ' WHERE node.' . $config['parent'] . ' = ' . $this->getIdExpression($nodeId);
  619. $dql .= ' AND node.' . $config['root'] . ' = ' . $this->getIdExpression($rootId);
  620. $q = $this->_em->createQuery($dql);
  621. $q->getSingleScalarResult();
  622. $this->listener
  623. ->getStrategy($this->_em, $meta->name)
  624. ->shiftRangeRL($this->_em, $config['useObjectClass'], $left, $right, $shift, $rootId, $rootId, - 1);
  625. $this->listener
  626. ->getStrategy($this->_em, $meta->name)
  627. ->shiftRL($this->_em, $config['useObjectClass'], $right, -2, $rootId);
  628. }
  629. } else {
  630. $dql = "UPDATE {$config['useObjectClass']} node";
  631. $dql .= ' SET node.' . $config['parent'] . ' = ' . $this->getIdExpression($parentId);
  632. $dql .= ' WHERE node.' . $config['parent'] . ' = ' . $this->getIdExpression($nodeId);
  633. if (isset($config['root'])) {
  634. $dql .= ' AND node.' . $config['root'] . ' = ' . $this->getIdExpression($rootId);
  635. }
  636. // @todo: update in memory nodes
  637. $q = $this->_em->createQuery($dql);
  638. $q->getSingleScalarResult();
  639. $this->listener
  640. ->getStrategy($this->_em, $meta->name)
  641. ->shiftRangeRL($this->_em, $config['useObjectClass'], $left, $right, $shift, $rootId, $rootId, - 1);
  642. $this->listener
  643. ->getStrategy($this->_em, $meta->name)
  644. ->shiftRL($this->_em, $config['useObjectClass'], $right, -2, $rootId);
  645. }
  646. $this->removeSingle($wrapped);
  647. $this->_em->getConnection()->commit();
  648. } catch (\Exception $e) {
  649. $this->_em->close();
  650. $this->_em->getConnection()->rollback();
  651. throw new \Gedmo\Exception\RuntimeException('Transaction failed', null, $e);
  652. }
  653. } else {
  654. throw new InvalidArgumentException("Node is not related to this repository");
  655. }
  656. }
  657. /**
  658. * Reorders the sibling nodes and child nodes by given $node,
  659. * according to the $sortByField and $direction specified
  660. *
  661. * @param object $node - from which node to start reordering the tree
  662. * @param string $sortByField - field name to sort by
  663. * @param string $direction - sort direction : "ASC" or "DESC"
  664. * @param boolean $verify - true to verify tree first
  665. * @return void
  666. */
  667. public function reorder($node, $sortByField = null, $direction = 'ASC', $verify = true)
  668. {
  669. $meta = $this->getClassMetadata();
  670. if ($node instanceof $meta->name) {
  671. $config = $this->listener->getConfiguration($this->_em, $meta->name);
  672. if ($verify && is_array($this->verify())) {
  673. return false;
  674. }
  675. $nodes = $this->children($node, true, $sortByField, $direction);
  676. foreach ($nodes as $node) {
  677. $wrapped = new EntityWrapper($node, $this->_em);
  678. $right = $wrapped->getPropertyValue($config['right']);
  679. $left = $wrapped->getPropertyValue($config['left']);
  680. $this->moveDown($node, true);
  681. if ($left != ($right - 1)) {
  682. $this->reorder($node, $sortByField, $direction, false);
  683. }
  684. }
  685. } else {
  686. throw new InvalidArgumentException("Node is not related to this repository");
  687. }
  688. }
  689. /**
  690. * Verifies that current tree is valid.
  691. * If any error is detected it will return an array
  692. * with a list of errors found on tree
  693. *
  694. * @return mixed
  695. * boolean - true on success
  696. * array - error list on failure
  697. */
  698. public function verify()
  699. {
  700. if (!$this->childCount()) {
  701. return true; // tree is empty
  702. }
  703. $errors = array();
  704. $meta = $this->getClassMetadata();
  705. $config = $this->listener->getConfiguration($this->_em, $meta->name);
  706. if (isset($config['root'])) {
  707. $trees = $this->getRootNodes();
  708. foreach ($trees as $tree) {
  709. $this->verifyTree($errors, $tree);
  710. }
  711. } else {
  712. $this->verifyTree($errors);
  713. }
  714. return $errors ?: true;
  715. }
  716. /**
  717. * Tries to recover the tree
  718. *
  719. * @todo implement
  720. * @throws RuntimeException - if something fails in transaction
  721. * @return void
  722. */
  723. public function recover()
  724. {
  725. if ($this->verify() === true) {
  726. return;
  727. }
  728. // not yet implemented
  729. }
  730. /**
  731. * Retrieves the nested array or the decorated output.
  732. * Uses @options to handle decorations
  733. *
  734. * @throws \Gedmo\Exception\InvalidArgumentException
  735. * @param object $node - from which node to start reordering the tree
  736. * @param boolean $direct - true to take only direct children
  737. * @param array $options :
  738. * decorate: boolean (false) - retrieves tree as UL->LI tree
  739. * nodeDecorator: Closure (null) - uses $node as argument and returns decorated item as string
  740. * rootOpen: string || Closure ('<ul>') - branch start, closure will be given $children as a parameter
  741. * rootClose: string ('</ul>') - branch close
  742. * childStart: string || Closure ('<li>') - start of node, closure will be given $node as a parameter
  743. * childClose: string ('</li>') - close of node
  744. *
  745. * @return array|string
  746. */
  747. public function childrenHierarchy($node = null, $direct = false, array $options = array())
  748. {
  749. $meta = $this->getClassMetadata();
  750. $config = $this->listener->getConfiguration($this->_em, $meta->name);
  751. if ($node !== null) {
  752. if ($node instanceof $meta->name) {
  753. $wrapped = new EntityWrapper($node, $this->_em);
  754. if (!$wrapped->hasValidIdentifier()) {
  755. throw new InvalidArgumentException("Node is not managed by UnitOfWork");
  756. }
  757. }
  758. }
  759. // Gets the array of $node results.
  760. // It must be order by 'root' and 'left' field
  761. $nodes = $this->childrenQuery(
  762. $node,
  763. $direct,
  764. isset($config['root']) ? array($config['root'], $config['left']) : $config['left'],
  765. 'ASC'
  766. )->getArrayResult();
  767. return $this->buildTree($nodes, $options);
  768. }
  769. /**
  770. * {@inheritdoc}
  771. */
  772. protected function validate()
  773. {
  774. return $this->listener->getStrategy($this->_em, $this->getClassMetadata()->name)->getName() === Strategy::NESTED;
  775. }
  776. /**
  777. * Collect errors on given tree if
  778. * where are any
  779. *
  780. * @param array $errors
  781. * @param object $root
  782. * @return void
  783. */
  784. private function verifyTree(&$errors, $root = null)
  785. {
  786. $meta = $this->getClassMetadata();
  787. $config = $this->listener->getConfiguration($this->_em, $meta->name);
  788. $identifier = $meta->getSingleIdentifierFieldName();
  789. $rootId = isset($config['root']) ? $meta->getReflectionProperty($config['root'])->getValue($root) : null;
  790. $dql = "SELECT MIN(node.{$config['left']}) FROM {$config['useObjectClass']} node";
  791. if ($root) {
  792. $dql .= " WHERE node.{$config['root']} = ".$this->getIdExpression($rootId);
  793. }
  794. $min = intval($this->_em->createQuery($dql)->getSingleScalarResult());
  795. $edge = $this->listener->getStrategy($this->_em, $meta->name)->max($this->_em, $config['useObjectClass'], $rootId);
  796. // check duplicate right and left values
  797. for ($i = $min; $i <= $edge; $i++) {
  798. $dql = "SELECT COUNT(node.{$identifier}) FROM {$config['useObjectClass']} node";
  799. $dql .= " WHERE (node.{$config['left']} = {$i} OR node.{$config['right']} = {$i})";
  800. if ($root) {
  801. $dql .= " AND node.{$config['root']} = ".$this->getIdExpression($rootId);
  802. }
  803. $count = intval($this->_em->createQuery($dql)->getSingleScalarResult());
  804. if ($count !== 1) {
  805. if ($count === 0) {
  806. $errors[] = "index [{$i}], missing" . ($root ? ' on tree root: ' . $rootId : '');
  807. } else {
  808. $errors[] = "index [{$i}], duplicate" . ($root ? ' on tree root: ' . $rootId : '');
  809. }
  810. }
  811. }
  812. // check for missing parents
  813. $dql = "SELECT node FROM {$config['useObjectClass']} node";
  814. $dql .= " LEFT JOIN node.{$config['parent']} parent";
  815. $dql .= " WHERE node.{$config['parent']} IS NOT NULL";
  816. $dql .= " AND parent.{$identifier} IS NULL";
  817. if ($root) {
  818. $dql .= " AND node.{$config['root']} = ".$this->getIdExpression($rootId);
  819. }
  820. $nodes = $this->_em->createQuery($dql)->getArrayResult();
  821. if (count($nodes)) {
  822. foreach ($nodes as $node) {
  823. $errors[] = "node [{$node[$identifier]}] has missing parent" . ($root ? ' on tree root: ' . $rootId : '');
  824. }
  825. return; // loading broken relation can cause infinite loop
  826. }
  827. $dql = "SELECT node FROM {$config['useObjectClass']} node";
  828. $dql .= " WHERE node.{$config['right']} < node.{$config['left']}";
  829. if ($root) {
  830. $dql .= " AND node.{$config['root']} = ".$this->getIdExpression($rootId);
  831. }
  832. $result = $this->_em->createQuery($dql)
  833. ->setMaxResults(1)
  834. ->getResult(Query::HYDRATE_ARRAY);
  835. $node = count($result) ? array_shift($result) : null;
  836. if ($node) {
  837. $id = $node[$identifier];
  838. $errors[] = "node [{$id}], left is greater than right" . ($root ? ' on tree root: ' . $rootId : '');
  839. }
  840. $dql = "SELECT node FROM {$config['useObjectClass']} node";
  841. if ($root) {
  842. $dql .= " WHERE node.{$config['root']} = ".$this->getIdExpression($rootId);
  843. }
  844. $nodes = $this->_em->createQuery($dql)->getResult(Query::HYDRATE_OBJECT);
  845. foreach ($nodes as $node) {
  846. $right = $meta->getReflectionProperty($config['right'])->getValue($node);
  847. $left = $meta->getReflectionProperty($config['left'])->getValue($node);
  848. $id = $meta->getReflectionProperty($identifier)->getValue($node);
  849. $parent = $meta->getReflectionProperty($config['parent'])->getValue($node);
  850. if (!$right || !$left) {
  851. $errors[] = "node [{$id}] has invalid left or right values";
  852. } elseif ($right == $left) {
  853. $errors[] = "node [{$id}] has identical left and right values";
  854. } elseif ($parent) {
  855. if ($parent instanceof Proxy && !$parent->__isInitialized__) {
  856. $this->_em->refresh($parent);
  857. }
  858. $parentRight = $meta->getReflectionProperty($config['right'])->getValue($parent);
  859. $parentLeft = $meta->getReflectionProperty($config['left'])->getValue($parent);
  860. $parentId = $meta->getReflectionProperty($identifier)->getValue($parent);
  861. if ($left < $parentLeft) {
  862. $errors[] = "node [{$id}] left is less than parent`s [{$parentId}] left value";
  863. } elseif ($right > $parentRight) {
  864. $errors[] = "node [{$id}] right is greater than parent`s [{$parentId}] right value";
  865. }
  866. } else {
  867. $dql = "SELECT COUNT(node.{$identifier}) FROM {$config['useObjectClass']} node";
  868. $dql .= " WHERE node.{$config['left']} < {$left}";
  869. $dql .= " AND node.{$config['right']} > {$right}";
  870. if ($root) {
  871. $dql .= " AND node.{$config['root']} = ".$this->getIdExpression($rootId);
  872. }
  873. $q = $this->_em->createQuery($dql);
  874. if ($count = intval($q->getSingleScalarResult())) {
  875. $errors[] = "node [{$id}] parent field is blank, but it has a parent";
  876. }
  877. }
  878. }
  879. }
  880. /**
  881. * Removes single node without touching children
  882. *
  883. * @internal
  884. * @param EntityWrapper $wrapped
  885. * @return void
  886. */
  887. private function removeSingle(EntityWrapper $wrapped)
  888. {
  889. $meta = $this->getClassMetadata();
  890. $config = $this->listener->getConfiguration($this->_em, $meta->name);
  891. $pk = $meta->getSingleIdentifierFieldName();
  892. $nodeId = $wrapped->getIdentifier();
  893. // prevent from deleting whole branch
  894. $dql = "UPDATE {$config['useObjectClass']} node";
  895. $dql .= ' SET node.' . $config['left'] . ' = 0,';
  896. $dql .= ' node.' . $config['right'] . ' = 0';
  897. $dql .= ' WHERE node.' . $pk . ' = ' . $this->getIdExpression($nodeId);
  898. $this->_em->createQuery($dql)->getSingleScalarResult();
  899. // remove the node from database
  900. $dql = "DELETE {$config['useObjectClass']} node";
  901. $dql .= " WHERE node.{$pk} = ".$this->getIdExpression($nodeId);
  902. $this->_em->createQuery($dql)->getSingleScalarResult();
  903. // remove from identity map
  904. $this->_em->getUnitOfWork()->removeFromIdentityMap($wrapped->getObject());
  905. }
  906. /**
  907. * Retrieves the nested array or the decorated output.
  908. * Uses @options to handle decorations
  909. * NOTE: @nodes should be fetched and hydrated as array
  910. *
  911. * @throws \Gedmo\Exception\InvalidArgumentException
  912. * @param array $nodes - list o nodes to build tree
  913. * @param array $options :
  914. * decorate: boolean (false) - retrieves tree as UL->LI tree
  915. * nodeDecorator: Closure (null) - uses $node as argument and returns decorated item as string
  916. * rootOpen: string || Closure ('<ul>') - branch start, closure will be given $children as a parameter
  917. * rootClose: string ('</ul>') - branch close
  918. * childStart: string || Closure ('<li>') - start of node, closure will be given $node as a parameter
  919. * childClose: string ('</li>') - close of node
  920. *
  921. * @return array|string
  922. */
  923. public function buildTree(array $nodes, array $options = array())
  924. {
  925. //process the nested tree into a nested array
  926. $meta = $this->getClassMetadata();
  927. $config = $this->listener->getConfiguration($this->_em, $meta->name);
  928. $nestedTree = array();
  929. $l = 0;
  930. if (count($nodes) > 0) {
  931. // Node Stack. Used to help building the hierarchy
  932. $stack = array();
  933. foreach ($nodes as $child) {
  934. $item = $child;
  935. $item['__children'] = array();
  936. // Number of stack items
  937. $l = count($stack);
  938. // Check if we're dealing with different levels
  939. while($l > 0 && $stack[$l - 1][$config['level']] >= $item[$config['level']]) {
  940. array_pop($stack);
  941. $l--;
  942. }
  943. // Stack is empty (we are inspecting the root)
  944. if ($l == 0) {
  945. // Assigning the root child
  946. $i = count($nestedTree);
  947. $nestedTree[$i] = $item;
  948. $stack[] = &$nestedTree[$i];
  949. } else {
  950. // Add child to parent
  951. $i = count($stack[$l - 1]['__children']);
  952. $stack[$l - 1]['__children'][$i] = $item;
  953. $stack[] = &$stack[$l - 1]['__children'][$i];
  954. }
  955. }
  956. }
  957. $default = array(
  958. 'decorate' => false,
  959. 'rootOpen' => '<ul>',
  960. 'rootClose' => '</ul>',
  961. 'childOpen' => '<li>',
  962. 'childClose' => '</li>',
  963. 'nodeDecorator' => function ($node) use ($meta) {
  964. // override and change it, guessing which field to use
  965. if ($meta->hasField('title')) {
  966. $field = 'title';
  967. } else if ($meta->hasField('name')) {
  968. $field = 'name';
  969. } else {
  970. throw new InvalidArgumentException("Cannot find any representation field");
  971. }
  972. return $node[$field];
  973. }
  974. );
  975. $options = array_merge($default, $options);
  976. // If you don't want any html output it will return the nested array
  977. if (!$options['decorate']) {
  978. return $nestedTree;
  979. } elseif (!count($nestedTree)) {
  980. return '';
  981. }
  982. $build = function($tree) use (&$build, &$options) {
  983. $output = is_string($options['rootOpen']) ? $options['rootOpen'] : $options['rootOpen']($tree);
  984. foreach ($tree as $node) {
  985. $output .= is_string($options['childOpen']) ? $options['childOpen'] : $options['childOpen']($node);
  986. $output .= $options['nodeDecorator']($node);
  987. if (count($node['__children']) > 0) {
  988. $output .= $build($node['__children']);
  989. }
  990. $output .= $options['childClose'];
  991. }
  992. return $output . $options['rootClose'];
  993. };
  994. return $build($nestedTree);
  995. }
  996. /**
  997. * get DQL expression for id value
  998. *
  999. * @param integer|string $id
  1000. * @return string
  1001. */
  1002. private function getIdExpression($id)
  1003. {
  1004. if (is_string($id)) {
  1005. $id = $this->_em->getExpressionBuilder()->literal($id);
  1006. }
  1007. if ($id === null) {
  1008. $id = 'NULL';
  1009. }
  1010. return (string)$id;
  1011. }
  1012. }