@@ -15,6 +15,9 @@ class Builder {
1515 protected $ CSRF ;
1616 protected $ Auth ;
1717
18+ // Properties
19+ protected $ Routes ;
20+
1821 /**
1922 * Constructor
2023 */
@@ -33,94 +36,186 @@ public function __construct()
3336 }
3437
3538 /**
36- * Get a menu
39+ * Get the routes
3740 *
38- * @param string $location
39- * @param string $parent
4041 * @return array
4142 */
42- public function menu ( $ location = ' sidebar ' , $ parent = null )
43+ public function routes (): array
4344 {
44- // Import Global Variables
45- global $ AUTH ;
46- $ menu = [];
47- $ routes = $ this ->Config ->get ('routes ' );
48-
49- // Load Plugins Routes
50- $ pluginsPath = $ this ->Config ->root () . DIRECTORY_SEPARATOR . 'lib ' . DIRECTORY_SEPARATOR . 'plugins ' ;
51- if (is_dir ($ pluginsPath )){
52- foreach (array_diff (scandir ($ pluginsPath ), array ('.. ' , '. ' )) as $ plugin ){
53- $ pluginPath = $ pluginsPath . DIRECTORY_SEPARATOR . $ plugin ;
54- if (is_file ($ pluginPath . DIRECTORY_SEPARATOR . 'routes.cfg ' )){
55- foreach (json_decode (file_get_contents ($ pluginPath . DIRECTORY_SEPARATOR . 'routes.cfg ' ),true ) as $ route => $ param ){
56- if (!isset ($ routes [$ route ])){
57- $ routes [$ route ] = $ param ;
45+ if ($ this ->Routes === null ) {
46+ $ this ->Routes = $ this ->Config ->get ('routes ' ) ?? [];
47+
48+ $ pluginsPath = $ this ->Config ->root () . DIRECTORY_SEPARATOR . 'lib ' . DIRECTORY_SEPARATOR . 'plugins ' ;
49+ if (is_dir ($ pluginsPath )) {
50+ foreach (array_diff (scandir ($ pluginsPath ), ['. ' , '.. ' , '.DS_Store ' ]) as $ plugin ) {
51+ $ cfg = $ pluginsPath . DIRECTORY_SEPARATOR . $ plugin . DIRECTORY_SEPARATOR . 'routes.cfg ' ;
52+ if (is_file ($ cfg )) {
53+ $ pluginRoutes = json_decode (file_get_contents ($ cfg ), true ) ?: [];
54+ foreach ($ pluginRoutes as $ r => $ p ) {
55+ if (!isset ($ this ->Routes [$ r ])) $ this ->Routes [$ r ] = $ p ;
5856 }
5957 }
6058 }
6159 }
60+
61+ ksort ($ this ->Routes , SORT_NATURAL | SORT_FLAG_CASE );
6262 }
6363
64- // Sort the routes
65- ksort ($ routes );
66-
67- foreach ($ routes as $ route => $ param ) {
68- if (!isset ($ param ['parent ' ]) || is_null ($ param ['parent ' ])) $ param ['parent ' ] = [];
69- if (!is_array ($ param ['parent ' ])) $ param ['parent ' ] = [$ param ['parent ' ]];
70- if ($ parent && !in_array ($ parent ,$ param ['parent ' ])) continue ;
71- if (!isset ($ param ['location ' ])) continue ;
72- if (is_string ($ param ['location ' ]) && $ param ['location ' ] !== $ location ) continue ;
73- if (is_array ($ param ['location ' ]) && !in_array ($ location ,$ param ['location ' ])) continue ;
74- if (!$ param ['public ' ] && !$ AUTH ->isAuthenticated ()) continue ;
75- if (!$ param ['public ' ] && !$ AUTH ->isAuthorized ("Route> " . $ route , $ param ['level ' ])) continue ;
76-
77- $ parts = array_filter (explode ('/ ' , $ route ));
78- if (empty ($ parts )) $ parts = ["" ];
79-
80- $ param ['items ' ] = [];
81- $ param ['link ' ] = $ route ;
82-
83- if (!empty ($ param ['parent ' ])){
84- foreach ($ param ['parent ' ] as $ par ){
85- if (!array_key_exists ($ par , $ menu )){
86- $ menu [$ par ] = [];
87- }
88- if (!array_key_exists ('items ' , $ menu [$ par ])){
89- $ menu [$ par ]['items ' ] = [];
90- }
91- $ menu [$ par ]['items ' ][$ route ] = $ param ;
64+ return $ this ->Routes ;
65+ }
66+
67+ /**
68+ * Build a menu from routes with depth control.
69+ *
70+ * Behavior:
71+ * - $maxDepth === 1: return a flat list of ALL descendants under the chosen parent(s)
72+ * (or global roots if $parent is null); no nesting, no wrapper key.
73+ * - $maxDepth >= 2: return a nested tree up to $maxDepth levels; items deeper than $maxDepth are omitted.
74+ *
75+ * Output node fields: label, icon, color, items, link
76+ *
77+ * @param string $location e.g. 'sidebar-main'
78+ * @param string|array|null $parent e.g. '/crm'. If null, build from global roots (no eligible parent).
79+ * @param int $maxDepth depth cap (1 = flat)
80+ * @return array
81+ */
82+ public function menu ($ location = 'sidebar ' , $ parent = null , int $ maxDepth = 2 ): array
83+ {
84+ global $ AUTH ;
85+
86+ $ maxDepth = max (1 , (int )$ maxDepth );
87+
88+ // 1) Collect eligible routes
89+ $ all = $ this ->routes ();
90+ if (!$ all ) return [];
91+
92+ $ eligible = []; // route => ['label','icon','color','link','parents'=>[]]
93+ foreach ($ all as $ route => $ p ) {
94+ // normalize parents
95+ $ parents = [];
96+ if (isset ($ p ['parent ' ]) && $ p ['parent ' ] !== null ) {
97+ $ parents = is_array ($ p ['parent ' ]) ? $ p ['parent ' ] : [$ p ['parent ' ]];
98+ $ parents = array_values (array_filter ($ parents , fn ($ x ) => is_string ($ x ) && $ x !== '' ));
99+ }
100+
101+ // location filter
102+ if (!isset ($ p ['location ' ])) continue ;
103+ if (is_string ($ p ['location ' ])) {
104+ if ($ p ['location ' ] !== $ location ) continue ;
105+ } elseif (is_array ($ p ['location ' ])) {
106+ if (!in_array ($ location , $ p ['location ' ], true )) continue ;
107+ } else continue ;
108+
109+ // auth/public filter
110+ $ isPublic = $ p ['public ' ] ?? false ;
111+ $ level = $ p ['level ' ] ?? 0 ;
112+ if (!$ isPublic && (!$ AUTH || !$ AUTH ->isAuthenticated ())) continue ;
113+ if (!$ isPublic && (!$ AUTH || !$ AUTH ->isAuthorized ('Route> ' . $ route , $ level ))) continue ;
114+
115+ $ eligible [$ route ] = [
116+ 'label ' => $ p ['label ' ] ?? '' ,
117+ 'icon ' => $ p ['icon ' ] ?? null ,
118+ 'color ' => $ p ['color ' ] ?? null ,
119+ 'link ' => $ route ,
120+ 'parents ' => $ parents ,
121+ ];
122+ }
123+ if (!$ eligible ) return [];
124+
125+ // 2) Build parent->children map among eligible routes
126+ $ children = []; // parentRoute => [childRoute...]
127+ foreach ($ eligible as $ r => $ _ ) $ children [$ r ] = [];
128+ foreach ($ eligible as $ child => $ node ) {
129+ foreach ($ node ['parents ' ] as $ par ) {
130+ if (isset ($ eligible [$ par ])) $ children [$ par ][] = $ child ;
131+ }
132+ }
133+ $ sortKeys = function (array &$ arr ) { ksort ($ arr , SORT_NATURAL | SORT_FLAG_CASE ); };
134+ $ sortList = function (array &$ list ) { sort ($ list , SORT_NATURAL | SORT_FLAG_CASE ); };
135+ foreach ($ children as &$ lst ) $ sortList ($ lst );
136+ unset($ lst );
137+
138+ // 3) Determine start nodes
139+ $ starts = [];
140+ if ($ parent === null ) {
141+ // global roots = nodes that have no eligible parent
142+ foreach ($ eligible as $ route => $ node ) {
143+ $ hasEligibleParent = false ;
144+ foreach ($ node ['parents ' ] as $ p ) {
145+ if (isset ($ eligible [$ p ])) { $ hasEligibleParent = true ; break ; }
92146 }
93- } else {
94- if (array_key_exists ($ route , $ menu )){
95- $ menu [$ route ] = array_merge_recursive ($ menu [$ route ], $ param );
147+ if (!$ hasEligibleParent ) $ starts [] = $ route ;
148+ }
149+ $ sortList ($ starts );
150+ } else {
151+ $ want = is_array ($ parent ) ? $ parent : [$ parent ];
152+ $ seen = [];
153+ foreach ($ want as $ p ) {
154+ if (isset ($ children [$ p ])) {
155+ foreach ($ children [$ p ] as $ c ) { $ seen [$ c ] = true ; }
96156 } else {
97- $ menu [$ route ] = $ param ;
157+ // if parent isn't itself eligible, include any eligible that declares it as parent
158+ foreach ($ eligible as $ route => $ node ) {
159+ if (in_array ($ p , $ node ['parents ' ], true )) $ seen [$ route ] = true ;
160+ }
98161 }
99162 }
163+ $ starts = array_keys ($ seen );
164+ $ sortList ($ starts );
100165 }
101166
102- foreach ($ menu as $ route => $ param ) {
103- if ((!array_key_exists ('link ' ,$ param ) || is_null ($ param ['link ' ])) && array_key_exists ('items ' ,$ param )){
104- foreach ($ param ['items ' ] as $ item => $ parameters ){
105- if (!is_null ($ parameters ['link ' ]) && !array_key_exists ($ parameters ['link ' ],$ menu )){
106- $ menu [$ parameters ['link ' ]] = $ parameters ;
107- unset($ menu [$ route ]['items ' ][$ item ]);
108- }
109- }
167+ // Helpers
168+ $ makeLeaf = function (string $ route ) use ($ eligible ): array {
169+ return [
170+ 'label ' => $ eligible [$ route ]['label ' ],
171+ 'icon ' => $ eligible [$ route ]['icon ' ],
172+ 'color ' => $ eligible [$ route ]['color ' ],
173+ 'items ' => [],
174+ 'link ' => $ eligible [$ route ]['link ' ],
175+ ];
176+ };
177+
178+ // 4a) Depth = 1: FLAT list of ALL descendants of the start set
179+ if ($ maxDepth === 1 ) {
180+ $ out = [];
181+ $ queue = $ starts ;
182+ $ visited = [];
183+ while ($ queue ) {
184+ $ cur = array_shift ($ queue );
185+ if (isset ($ visited [$ cur ])) continue ;
186+ $ visited [$ cur ] = true ;
187+ $ out [$ cur ] = $ makeLeaf ($ cur );
188+ // enqueue children (we want *all* descendants in flat mode)
189+ foreach ($ children [$ cur ] ?? [] as $ ch ) $ queue [] = $ ch ;
110190 }
191+ $ sortKeys ($ out );
192+ return $ out ;
111193 }
112194
113- foreach ($ menu as $ route => $ param ) {
114- if ((!array_key_exists ('link ' ,$ param ) || is_null ($ param ['link ' ])) && array_key_exists ('items ' ,$ param )){
115- if (empty ($ param ['items ' ])){
116- unset($ menu [$ route ]);
117- }
195+ // 4b) Depth >= 2: build nested tree up to $maxDepth; deeper nodes are omitted
196+ $ buildTree = function (string $ route , int $ depth ) use (&$ buildTree , $ maxDepth , $ children , $ makeLeaf ): array {
197+ $ node = $ makeLeaf ($ route );
198+ if ($ depth >= $ maxDepth ) return $ node ; // reached cap; omit deeper nodes
199+ $ items = [];
200+ foreach ($ children [$ route ] ?? [] as $ ch ) {
201+ $ items [$ ch ] = $ buildTree ($ ch , $ depth + 1 );
118202 }
119- }
203+ if ($ items ) {
204+ ksort ($ items , SORT_NATURAL | SORT_FLAG_CASE );
205+ $ node ['items ' ] = $ items ;
206+ }
207+ return $ node ;
208+ };
120209
121- return $ menu ;
210+ $ result = [];
211+ foreach ($ starts as $ s ) {
212+ $ result [$ s ] = $ buildTree ($ s , 1 );
213+ }
214+ $ sortKeys ($ result );
215+ return $ result ;
122216 }
123217
218+
124219 /**
125220 * Create Crumbs
126221 *
0 commit comments