Menu.php 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276
  1. <?php
  2. namespace Webkul\Core;
  3. use Illuminate\Support\Arr;
  4. use Illuminate\Support\Collection;
  5. use Webkul\Core\Menu\MenuItem;
  6. class Menu
  7. {
  8. /**
  9. * Menu items.
  10. */
  11. private array $items = [];
  12. /**
  13. * Config menu.
  14. */
  15. private array $configMenu = [];
  16. /**
  17. * Contains current item key.
  18. */
  19. private string $currentKey = '';
  20. /**
  21. * Menu area for admin.
  22. */
  23. const ADMIN = 'admin';
  24. /**
  25. * Menu area for customer.
  26. */
  27. const CUSTOMER = 'customer';
  28. /**
  29. * Add a new menu item.
  30. */
  31. public function addItem(MenuItem $menuItem): void
  32. {
  33. $this->items[] = $menuItem;
  34. }
  35. /**
  36. * Get all menu items.
  37. */
  38. public function getItems(?string $area = null): Collection
  39. {
  40. if (! $area) {
  41. throw new \Exception('Area must be provided to get menu items.');
  42. }
  43. $configMenu = collect(config("menu.$area"));
  44. switch ($area) {
  45. case self::ADMIN:
  46. $this->configMenu = $configMenu
  47. ->filter(fn ($item) => bouncer()->hasPermission($item['key']))
  48. ->toArray();
  49. break;
  50. case self::CUSTOMER:
  51. $canShowWishlist = ! (bool) core()->getConfigData('customer.settings.wishlist.wishlist_option');
  52. $canShowGdpr = ! (bool) core()->getConfigData('general.gdpr.settings.enabled');
  53. $this->configMenu = $configMenu
  54. ->reject(fn ($item) => ($item['key'] == 'account.wishlist' && $canShowWishlist) || ($item['key'] == 'account.gdpr_data_request' && $canShowGdpr))
  55. ->toArray();
  56. break;
  57. default:
  58. $this->configMenu = $configMenu->toArray();
  59. break;
  60. }
  61. if (! $this->items) {
  62. $this->prepareMenuItems();
  63. }
  64. return collect($this->removeUnauthorizedMenuItem())
  65. ->sortBy('sort');
  66. }
  67. /**
  68. * Prepare menu items.
  69. */
  70. private function prepareMenuItems(): void
  71. {
  72. $menuWithDotNotation = [];
  73. foreach ($this->configMenu as $item) {
  74. // 验证菜单项必须有必要的字段
  75. if (!isset($item['key']) || !isset($item['name']) || !isset($item['route'])) {
  76. continue;
  77. }
  78. // 安全获取路由参数
  79. $routeParameters = $item['route_parameters'] ?? [];
  80. // 检查是否有路由参数
  81. if (!empty($routeParameters)) {
  82. try {
  83. $url = route($item['route'], $routeParameters);
  84. if (strpos(request()->url(), $url) !== false) {
  85. $this->currentKey = $item['key'];
  86. }
  87. } catch (\Exception $e) {
  88. // 如果路由生成失败,回退到原来的逻辑
  89. try {
  90. if (strpos(request()->url(), route($item['route'])) !== false) {
  91. $this->currentKey = $item['key'];
  92. }
  93. } catch (\Exception $e2) {
  94. // 忽略错误
  95. }
  96. }
  97. } else {
  98. try {
  99. if (strpos(request()->url(), route($item['route'])) !== false) {
  100. $this->currentKey = $item['key'];
  101. }
  102. } catch (\Exception $e) {
  103. // 忽略错误
  104. }
  105. }
  106. $menuWithDotNotation[$item['key']] = $item;
  107. }
  108. $menu = Arr::undot(Arr::dot($menuWithDotNotation));
  109. foreach ($menu as $menuItemKey => $menuItem) {
  110. // 验证顶级菜单项
  111. if (!is_array($menuItem) || !isset($menuItem['key'])) {
  112. continue;
  113. }
  114. $subMenuItems = $this->processSubMenuItems($menuItem);
  115. // 安全解析路由参数
  116. $routeParameters = $this->parseRouteParameters($menuItem['route_parameters'] ?? []);
  117. $this->addItem(new MenuItem(
  118. key: $menuItemKey,
  119. name: trans($menuItem['name']),
  120. route: $menuItem['route'],
  121. sort: $menuItem['sort'] ?? 0,
  122. icon: $menuItem['icon'] ?? '',
  123. children: $subMenuItems,
  124. routeParameters: $routeParameters,
  125. ));
  126. }
  127. }
  128. /**
  129. * Process sub menu items.
  130. */
  131. private function processSubMenuItems($menuItem): Collection
  132. {
  133. return collect($menuItem)
  134. ->sortBy('sort')
  135. ->filter(fn ($value, $key) => is_array($value) && isset($value['key']))
  136. ->map(function ($subMenuItem) {
  137. // 确保子菜单项有必要的字段
  138. if (!isset($subMenuItem['key']) || !isset($subMenuItem['name']) || !isset($subMenuItem['route'])) {
  139. return null;
  140. }
  141. $subSubMenuItems = $this->processSubMenuItems($subMenuItem);
  142. // 安全解析子菜单的路由参数
  143. $routeParameters = $this->parseRouteParameters($subMenuItem['route_parameters'] ?? []);
  144. return new MenuItem(
  145. key: $subMenuItem['key'],
  146. name: trans($subMenuItem['name']),
  147. route: $subMenuItem['route'],
  148. sort: $subMenuItem['sort'] ?? 0,
  149. icon: $subMenuItem['icon'] ?? '',
  150. children: $subSubMenuItems,
  151. routeParameters: $routeParameters,
  152. );
  153. })
  154. ->filter(); // 过滤掉 null 值
  155. }
  156. /**
  157. * Parse route parameters from various formats.
  158. *
  159. * @param mixed $parameters
  160. * @return array
  161. */
  162. private function parseRouteParameters($parameters): array
  163. {
  164. if (empty($parameters)) {
  165. return [];
  166. }
  167. // 如果已经是数组,直接返回
  168. if (is_array($parameters)) {
  169. // 过滤空值
  170. return array_values(array_filter($parameters));
  171. }
  172. // 如果是 JSON 字符串,解码
  173. if (is_string($parameters)) {
  174. $decoded = json_decode($parameters, true);
  175. if (is_array($decoded)) {
  176. return array_values(array_filter($decoded));
  177. }
  178. }
  179. return [];
  180. }
  181. /**
  182. * Get current active menu.
  183. */
  184. public function getCurrentActiveMenu(?string $area = null): ?MenuItem
  185. {
  186. $currentKey = implode('.', array_slice(explode('.', $this->currentKey), 0, 2));
  187. return $this->findMatchingItem($this->getItems($area), $currentKey);
  188. }
  189. /**
  190. * Finding the matching item.
  191. */
  192. private function findMatchingItem($items, $currentKey): ?MenuItem
  193. {
  194. foreach ($items as $item) {
  195. if ($item->key == $currentKey) {
  196. return $item;
  197. }
  198. if ($item->haveChildren()) {
  199. $matchingChild = $this->findMatchingItem($item->getChildren(), $currentKey);
  200. if ($matchingChild) {
  201. return $matchingChild;
  202. }
  203. }
  204. }
  205. return null;
  206. }
  207. /**
  208. * Remove unauthorized menu item.
  209. */
  210. private function removeUnauthorizedMenuItem(): array
  211. {
  212. return collect($this->items)->map(function ($item) {
  213. $this->removeChildrenUnauthorizedMenuItem($item);
  214. return $item;
  215. })->toArray();
  216. }
  217. /**
  218. * Remove unauthorized menuItem's children. This will handle all levels.
  219. */
  220. private function removeChildrenUnauthorizedMenuItem(MenuItem &$menuItem): void
  221. {
  222. if ($menuItem->haveChildren()) {
  223. $firstChildrenItem = $menuItem->getChildren()->first();
  224. $menuItem->route = $firstChildrenItem->getRoute();
  225. $this->removeChildrenUnauthorizedMenuItem($firstChildrenItem);
  226. }
  227. }
  228. }