NestedTreeRepository.php 41 KB

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