AbstractTreeRepository.php 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215
  1. <?php
  2. namespace Gedmo\Tree\Entity\Repository;
  3. use Doctrine\ORM\EntityRepository,
  4. Doctrine\ORM\EntityManager,
  5. Doctrine\ORM\Mapping\ClassMetadata,
  6. Gedmo\Tool\Wrapper\EntityWrapper;
  7. abstract class AbstractTreeRepository extends EntityRepository
  8. {
  9. /**
  10. * Tree listener on event manager
  11. *
  12. * @var AbstractTreeListener
  13. */
  14. protected $listener = null;
  15. /**
  16. * {@inheritdoc}
  17. */
  18. public function __construct(EntityManager $em, ClassMetadata $class)
  19. {
  20. parent::__construct($em, $class);
  21. $treeListener = null;
  22. foreach ($em->getEventManager()->getListeners() as $listeners) {
  23. foreach ($listeners as $listener) {
  24. if ($listener instanceof \Gedmo\Tree\TreeListener) {
  25. $treeListener = $listener;
  26. break;
  27. }
  28. }
  29. if ($treeListener) {
  30. break;
  31. }
  32. }
  33. if (is_null($treeListener)) {
  34. throw new \Gedmo\Exception\InvalidMappingException('Tree listener was not found on your entity manager, it must be hooked into the event manager');
  35. }
  36. $this->listener = $treeListener;
  37. if (!$this->validate()) {
  38. throw new \Gedmo\Exception\InvalidMappingException('This repository cannot be used for tree type: ' . $treeListener->getStrategy($em, $class->name)->getName());
  39. }
  40. }
  41. /**
  42. * Retrieves the nested array or the decorated output.
  43. * Uses @options to handle decorations
  44. *
  45. * @throws \Gedmo\Exception\InvalidArgumentException
  46. * @param object $node - from which node to start reordering the tree
  47. * @param boolean $direct - true to take only direct children
  48. * @param array $options :
  49. * decorate: boolean (false) - retrieves tree as UL->LI tree
  50. * nodeDecorator: Closure (null) - uses $node as argument and returns decorated item as string
  51. * rootOpen: string || Closure ('<ul>') - branch start, closure will be given $children as a parameter
  52. * rootClose: string ('</ul>') - branch close
  53. * childStart: string || Closure ('<li>') - start of node, closure will be given $node as a parameter
  54. * childClose: string ('</li>') - close of node
  55. * childSort: array || keys allowed: field: field to sort on, dir: direction. 'asc' or 'desc'
  56. *
  57. * @return array|string
  58. */
  59. public function childrenHierarchy($node = null, $direct = false, array $options = array())
  60. {
  61. $meta = $this->getClassMetadata();
  62. $config = $this->listener->getConfiguration($this->_em, $meta->name);
  63. if ($node !== null) {
  64. if ($node instanceof $meta->name) {
  65. $wrapped = new EntityWrapper($node, $this->_em);
  66. if (!$wrapped->hasValidIdentifier()) {
  67. throw new InvalidArgumentException("Node is not managed by UnitOfWork");
  68. }
  69. }
  70. }
  71. // Gets the array of $node results. It must be ordered by depth
  72. $nodes = $this->getNodesHierarchy($node, $direct, $config, $options);
  73. return $this->buildTree($nodes, $options);
  74. }
  75. /**
  76. * Retrieves the nested array or the decorated output.
  77. * Uses @options to handle decorations
  78. * NOTE: @nodes should be fetched and hydrated as array
  79. *
  80. * @throws \Gedmo\Exception\InvalidArgumentException
  81. * @param array $nodes - list o nodes to build tree
  82. * @param array $options :
  83. * decorate: boolean (false) - retrieves tree as UL->LI tree
  84. * nodeDecorator: Closure (null) - uses $node as argument and returns decorated item as string
  85. * rootOpen: string || Closure ('<ul>') - branch start, closure will be given $children as a parameter
  86. * rootClose: string ('</ul>') - branch close
  87. * childStart: string || Closure ('<li>') - start of node, closure will be given $node as a parameter
  88. * childClose: string ('</li>') - close of node
  89. *
  90. * @return array|string
  91. */
  92. public function buildTree(array $nodes, array $options = array())
  93. {
  94. $meta = $this->getClassMetadata();
  95. $nestedTree = $this->buildTreeArray($nodes);
  96. $default = array(
  97. 'decorate' => false,
  98. 'rootOpen' => '<ul>',
  99. 'rootClose' => '</ul>',
  100. 'childOpen' => '<li>',
  101. 'childClose' => '</li>',
  102. 'nodeDecorator' => function ($node) use ($meta) {
  103. // override and change it, guessing which field to use
  104. if ($meta->hasField('title')) {
  105. $field = 'title';
  106. } else if ($meta->hasField('name')) {
  107. $field = 'name';
  108. } else {
  109. throw new InvalidArgumentException("Cannot find any representation field");
  110. }
  111. return $node[$field];
  112. }
  113. );
  114. $options = array_merge($default, $options);
  115. // If you don't want any html output it will return the nested array
  116. if (!$options['decorate']) {
  117. return $nestedTree;
  118. } elseif (!count($nestedTree)) {
  119. return '';
  120. }
  121. $build = function($tree) use (&$build, &$options) {
  122. $output = is_string($options['rootOpen']) ? $options['rootOpen'] : $options['rootOpen']($tree);
  123. foreach ($tree as $node) {
  124. $output .= is_string($options['childOpen']) ? $options['childOpen'] : $options['childOpen']($node);
  125. $output .= $options['nodeDecorator']($node);
  126. if (count($node['__children']) > 0) {
  127. $output .= $build($node['__children']);
  128. }
  129. $output .= is_string($options['childClose']) ? $options['childClose'] : $options['childClose']($node);
  130. }
  131. return $output . (is_string($options['rootClose']) ? $options['rootClose'] : $options['rootClose']($tree));
  132. };
  133. return $build($nestedTree);
  134. }
  135. /**
  136. * Process nodes and produce an array with the
  137. * structure of the tree
  138. *
  139. * @param array - Array of nodes
  140. *
  141. * @return array - Array with tree structure
  142. */
  143. public function buildTreeArray(array $nodes)
  144. {
  145. $meta = $this->getClassMetadata();
  146. $config = $this->listener->getConfiguration($this->_em, $meta->name);
  147. $nestedTree = array();
  148. $l = 0;
  149. if (count($nodes) > 0) {
  150. // Node Stack. Used to help building the hierarchy
  151. $stack = array();
  152. foreach ($nodes as $child) {
  153. $item = $child;
  154. $item['__children'] = array();
  155. // Number of stack items
  156. $l = count($stack);
  157. // Check if we're dealing with different levels
  158. while($l > 0 && $stack[$l - 1][$config['level']] >= $item[$config['level']]) {
  159. array_pop($stack);
  160. $l--;
  161. }
  162. // Stack is empty (we are inspecting the root)
  163. if ($l == 0) {
  164. // Assigning the root child
  165. $i = count($nestedTree);
  166. $nestedTree[$i] = $item;
  167. $stack[] = &$nestedTree[$i];
  168. } else {
  169. // Add child to parent
  170. $i = count($stack[$l - 1]['__children']);
  171. $stack[$l - 1]['__children'][$i] = $item;
  172. $stack[] = &$stack[$l - 1]['__children'][$i];
  173. }
  174. }
  175. }
  176. return $nestedTree;
  177. }
  178. /**
  179. * Checks if current repository is right
  180. * for currently used tree strategy
  181. *
  182. * @return bool
  183. */
  184. abstract protected function validate();
  185. /**
  186. * Returns an array of nodes suitable for method buildTree
  187. *
  188. * @param object - Root node
  189. * @param bool - Obtain direct children?
  190. * @param array - Metadata configuration
  191. * @param array - Options
  192. *
  193. * @return array - Array of nodes
  194. */
  195. abstract public function getNodesHierarchy($node, $direct, array $config, array $options = array());
  196. }