NestedTreeRepository.php 42 KB

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