cache ??= cache(); } public function getMenuForRoles(array $roles): array { $roles = array_map(fn($r)=>strtolower(trim((string)$r)), $roles); $cacheKey = 'navbar_' . md5(json_encode($roles)); if ($menu = $this->cache->get($cacheKey)) { return $menu; } // Which items this user can see $allowedIds = $this->mapModel->getNavItemIdsForRoles($roles); if (empty($allowedIds)) return []; // Load all enabled items, then filter. // The displayed sidebar is alphabetized after the tree is built so // new menu items fall into place without relying on manual sort_order. $rows = $this->navModel->where('is_enabled', 1) ->orderBy('sort_order', 'ASC') ->orderBy('label', 'ASC') ->orderBy('id', 'ASC') ->findAll(); // index by id $byId = []; foreach ($rows as $r) { if (in_array($r['id'], $allowedIds, true)) { $r['children'] = []; $byId[$r['id']] = $r; } } // Build tree $tree = []; foreach ($byId as $id => &$node) { $pid = $node['menu_parent_id']; if ($pid && isset($byId[$pid])) { $byId[$pid]['children'][] = &$node; } else { $tree[] = &$node; } } unset($node); $this->sortTreeAlphabetically($tree); $this->cache->save($cacheKey, $tree, 300); // 5 minutes return $tree; } private function sortTreeAlphabetically(array &$nodes): void { usort($nodes, fn ($a, $b) => strnatcasecmp( $this->labelSortKey((string) ($a['label'] ?? '')), $this->labelSortKey((string) ($b['label'] ?? '')) )); foreach ($nodes as &$node) { if (!empty($node['children']) && is_array($node['children'])) { $this->sortTreeAlphabetically($node['children']); } } unset($node); } private function labelSortKey(string $label): string { $label = trim(preg_replace('/\s+/', ' ', $label) ?? ''); $label = preg_replace('/^[^[:alnum:]]+/u', '', $label) ?? $label; $label = preg_replace('/^(the|an|a)\s+/iu', '', $label) ?? $label; return function_exists('mb_strtolower') ? mb_strtolower($label, 'UTF-8') : strtolower($label); } public function clearCache(): void { // simplest: flush; or if you have a tagged cache, clear only keys cache()->clean(); } }