ElggMenuBuilder.php 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335
  1. <?php
  2. /**
  3. * Elgg Menu Builder
  4. *
  5. * @package Elgg.Core
  6. * @subpackage Navigation
  7. * @since 1.8.0
  8. */
  9. class ElggMenuBuilder {
  10. /**
  11. * @var \ElggMenuItem[]
  12. */
  13. protected $menu = array();
  14. protected $selected = null;
  15. /**
  16. * \ElggMenuBuilder constructor
  17. *
  18. * @param \ElggMenuItem[] $menu Array of \ElggMenuItem objects
  19. */
  20. public function __construct(array $menu) {
  21. $this->menu = $menu;
  22. }
  23. /**
  24. * Get a prepared menu array
  25. *
  26. * @param mixed $sort_by Method to sort the menu by. @see \ElggMenuBuilder::sort()
  27. * @return array
  28. */
  29. public function getMenu($sort_by = 'text') {
  30. $this->selectFromContext();
  31. $this->selected = $this->findSelected();
  32. $this->setupSections();
  33. $this->setupTrees();
  34. $this->sort($sort_by);
  35. return $this->menu;
  36. }
  37. /**
  38. * Get the selected menu item
  39. *
  40. * @return \ElggMenuItem
  41. */
  42. public function getSelected() {
  43. return $this->selected;
  44. }
  45. /**
  46. * Select menu items for the current context
  47. *
  48. * @return void
  49. */
  50. protected function selectFromContext() {
  51. if (!isset($this->menu)) {
  52. $this->menu = array();
  53. return;
  54. }
  55. // get menu items for this context
  56. $selected_menu = array();
  57. foreach ($this->menu as $menu_item) {
  58. if (!is_object($menu_item)) {
  59. _elgg_services()->logger->error("A non-object was passed to \ElggMenuBuilder");
  60. continue;
  61. }
  62. if ($menu_item->inContext()) {
  63. $selected_menu[] = $menu_item;
  64. }
  65. }
  66. $this->menu = $selected_menu;
  67. }
  68. /**
  69. * Group the menu items into sections
  70. *
  71. * @return void
  72. */
  73. protected function setupSections() {
  74. $sectioned_menu = array();
  75. foreach ($this->menu as $menu_item) {
  76. if (!isset($sectioned_menu[$menu_item->getSection()])) {
  77. $sectioned_menu[$menu_item->getSection()] = array();
  78. }
  79. $sectioned_menu[$menu_item->getSection()][] = $menu_item;
  80. }
  81. $this->menu = $sectioned_menu;
  82. }
  83. /**
  84. * Create trees for each menu section
  85. *
  86. * @internal The tree is doubly linked (parent and children links)
  87. * @return void
  88. */
  89. protected function setupTrees() {
  90. $menu_tree = array();
  91. foreach ($this->menu as $key => $section) {
  92. $parents = array();
  93. $children = array();
  94. $all_menu_items = array();
  95. // divide base nodes from children
  96. foreach ($section as $menu_item) {
  97. /* @var \ElggMenuItem $menu_item */
  98. $parent_name = $menu_item->getParentName();
  99. $menu_item_name = $menu_item->getName();
  100. if (!$parent_name) {
  101. // no parents so top level menu items
  102. $parents[$menu_item_name] = $menu_item;
  103. } else {
  104. $children[$menu_item_name] = $menu_item;
  105. }
  106. $all_menu_items[$menu_item_name] = $menu_item;
  107. }
  108. if (empty($all_menu_items)) {
  109. // empty sections can be skipped
  110. continue;
  111. }
  112. if (empty($parents)) {
  113. // menu items without parents? That is sad.. report to the log
  114. $message = _elgg_services()->translator->translate('ElggMenuBuilder:Trees:NoParents');
  115. _elgg_services()->logger->notice($message);
  116. // skip section as without parents menu can not be drawn
  117. continue;
  118. }
  119. foreach ($children as $menu_item_name => $menu_item) {
  120. $parent_name = $menu_item->getParentName();
  121. if (!array_key_exists($parent_name, $all_menu_items)) {
  122. // orphaned child, inform authorities and skip to next item
  123. $message = _elgg_services()->translator->translate('ElggMenuBuilder:Trees:OrphanedChild', array($menu_item_name, $parent_name));
  124. _elgg_services()->logger->notice($message);
  125. continue;
  126. }
  127. if (!in_array($menu_item, $all_menu_items[$parent_name]->getData('children'))) {
  128. $all_menu_items[$parent_name]->addChild($menu_item);
  129. $menu_item->setParent($all_menu_items[$parent_name]);
  130. } else {
  131. // menu item already existed in parents children, report the duplicate registration
  132. $message = _elgg_services()->translator->translate('ElggMenuBuilder:Trees:DuplicateChild', array($menu_item_name));
  133. _elgg_services()->logger->notice($message);
  134. continue;
  135. }
  136. }
  137. // convert keys to indexes for first level of tree
  138. $parents = array_values($parents);
  139. $menu_tree[$key] = $parents;
  140. }
  141. $this->menu = $menu_tree;
  142. }
  143. /**
  144. * Find the menu item that is currently selected
  145. *
  146. * @return \ElggMenuItem
  147. */
  148. protected function findSelected() {
  149. // do we have a selected menu item already
  150. foreach ($this->menu as $menu_item) {
  151. if ($menu_item->getSelected()) {
  152. return $menu_item;
  153. }
  154. }
  155. // scan looking for a selected item
  156. foreach ($this->menu as $menu_item) {
  157. if ($menu_item->getHref()) {
  158. if (elgg_http_url_is_identical(current_page_url(), $menu_item->getHref())) {
  159. $menu_item->setSelected(true);
  160. return $menu_item;
  161. }
  162. }
  163. }
  164. return null;
  165. }
  166. /**
  167. * Sort the menu sections and trees
  168. *
  169. * @param mixed $sort_by Sort type as string or php callback
  170. * @return void
  171. */
  172. protected function sort($sort_by) {
  173. // sort sections
  174. ksort($this->menu);
  175. switch ($sort_by) {
  176. case 'text':
  177. $sort_callback = array('\ElggMenuBuilder', 'compareByText');
  178. break;
  179. case 'name':
  180. $sort_callback = array('\ElggMenuBuilder', 'compareByName');
  181. break;
  182. case 'priority':
  183. $sort_callback = array('\ElggMenuBuilder', 'compareByPriority');
  184. break;
  185. case 'register':
  186. // use registration order - usort breaks this
  187. return;
  188. break;
  189. default:
  190. if (is_callable($sort_by)) {
  191. $sort_callback = $sort_by;
  192. } else {
  193. return;
  194. }
  195. break;
  196. }
  197. // sort each section
  198. foreach ($this->menu as $index => $section) {
  199. foreach ($section as $key => $node) {
  200. $section[$key]->setData('original_order', $key);
  201. }
  202. usort($section, $sort_callback);
  203. $this->menu[$index] = $section;
  204. // depth first traversal of tree
  205. foreach ($section as $root) {
  206. $stack = array();
  207. array_push($stack, $root);
  208. while (!empty($stack)) {
  209. $node = array_pop($stack);
  210. /* @var \ElggMenuItem $node */
  211. $node->sortChildren($sort_callback);
  212. $children = $node->getChildren();
  213. if ($children) {
  214. $stack = array_merge($stack, $children);
  215. }
  216. }
  217. }
  218. }
  219. }
  220. /**
  221. * Compare two menu items by their display text
  222. * HTML tags are stripped before comparison
  223. *
  224. * @param \ElggMenuItem $a Menu item
  225. * @param \ElggMenuItem $b Menu item
  226. * @return bool
  227. */
  228. public static function compareByText($a, $b) {
  229. $at = strip_tags($a->getText());
  230. $bt = strip_tags($b->getText());
  231. $result = strnatcmp($at, $bt);
  232. if ($result === 0) {
  233. return $a->getData('original_order') - $b->getData('original_order');
  234. }
  235. return $result;
  236. }
  237. /**
  238. * Compare two menu items by their identifiers
  239. *
  240. * @param \ElggMenuItem $a Menu item
  241. * @param \ElggMenuItem $b Menu item
  242. * @return bool
  243. */
  244. public static function compareByName($a, $b) {
  245. $an = $a->getName();
  246. $bn = $b->getName();
  247. $result = strnatcmp($an, $bn);
  248. if ($result === 0) {
  249. return $a->getData('original_order') - $b->getData('original_order');
  250. }
  251. return $result;
  252. }
  253. /**
  254. * Compare two menu items by their priority
  255. *
  256. * @param \ElggMenuItem $a Menu item
  257. * @param \ElggMenuItem $b Menu item
  258. * @return bool
  259. * @since 1.9.0
  260. */
  261. public static function compareByPriority($a, $b) {
  262. $aw = $a->getPriority();
  263. $bw = $b->getPriority();
  264. if ($aw == $bw) {
  265. return $a->getData('original_order') - $b->getData('original_order');
  266. }
  267. return $aw - $bw;
  268. }
  269. /**
  270. * Compare two menu items by their priority
  271. *
  272. * @param \ElggMenuItem $a Menu item
  273. * @param \ElggMenuItem $b Menu item
  274. * @return bool
  275. * @deprecated 1.9 Use compareByPriority()
  276. */
  277. public static function compareByWeight($a, $b) {
  278. elgg_deprecated_notice("\ElggMenuBuilder::compareByWeight() deprecated by \ElggMenuBuilder::compareByPriority", 1.9);
  279. $aw = $a->getPriority();
  280. $bw = $b->getPriority();
  281. if ($aw == $bw) {
  282. return $a->getData('original_order') - $b->getData('original_order');
  283. }
  284. return $aw - $bw;
  285. }
  286. }