-
-
Notifications
You must be signed in to change notification settings - Fork 236
Expand file tree
/
Copy pathNavigationManager.php
More file actions
796 lines (699 loc) · 25.3 KB
/
NavigationManager.php
File metadata and controls
796 lines (699 loc) · 25.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
<?php namespace Backend\Classes;
use BackendAuth;
use Config;
use Event;
use Log;
use System\Classes\Extensions\PluginManager;
use SystemException;
use Validator;
/**
* Manages the backend navigation.
*
* @package winter\wn-backend-module
* @author Alexey Bobkov, Samuel Georges
*/
class NavigationManager
{
use \Winter\Storm\Support\Traits\Singleton;
use \System\Traits\LazyOwnerAlias;
/**
* @var array Cache of registration callbacks.
*/
protected $callbacks = [];
/**
* @var array List of owner aliases. ['Aliased.Owner' => 'Real.Owner']
*/
protected $aliases = [];
/**
* @var MainMenuItem[] List of registered items.
*/
protected $items;
/**
* @var QuickActionItem[] List of registered quick actions.
*/
protected $quickActions;
protected $contextSidenavPartials = [];
protected $contextOwner;
protected $contextMainMenuItemCode;
protected $contextSideMenuItemCode;
/**
* @var PluginManager
*/
protected $pluginManager;
/**
* Initialize this singleton.
*/
protected function init()
{
foreach (static::$lazyAliases as $alias => $owner) {
$this->registerOwnerAlias($owner, $alias);
}
$this->pluginManager = PluginManager::instance();
}
/**
* Loads the menu items from modules and plugins
* @return void
* @throws SystemException
*/
protected function loadItems()
{
$this->items = [];
$this->quickActions = [];
/*
* Load module items
*/
foreach ($this->callbacks as $callback) {
$callback($this);
}
/*
* Load plugin items
*/
$plugins = $this->pluginManager->getPlugins();
foreach ($plugins as $id => $plugin) {
$items = $plugin->registerNavigation();
$quickActions = $plugin->registerQuickActions();
if (!is_array($items) && !is_array($quickActions)) {
continue;
}
if (is_array($items)) {
$this->registerMenuItems($id, $items);
}
if (is_array($quickActions)) {
$this->registerQuickActions($id, $quickActions);
}
}
/**
* @event backend.menu.extendItems
* Provides an opportunity to manipulate the backend navigation
*
* Example usage:
*
* Event::listen('backend.menu.extendItems', function ((\Backend\Classes\NavigationManager) $navigationManager) {
* $navigationManager->addMainMenuItems(...)
* $navigationManager->addSideMenuItems(...)
* $navigationManager->removeMainMenuItem(...)
* });
*
*/
Event::fire('backend.menu.extendItems', [$this]);
/*
* Sort menu items and quick actions
*/
$this->applyDefaultOrders($this->items);
uasort($this->items, static function ($a, $b) {
return $a->order - $b->order;
});
$this->applyDefaultOrders($this->quickActions);
uasort($this->quickActions, static function ($a, $b) {
return $a->order - $b->order;
});
/*
* Filter items and quick actions that the user lacks permission for
*/
$user = BackendAuth::getUser();
$this->items = $this->filterItemPermissions($user, $this->items);
$this->quickActions = $this->filterItemPermissions($user, $this->quickActions);
foreach ($this->items as $item) {
if (!$item->sideMenu || !count($item->sideMenu)) {
continue;
}
$this->applyDefaultOrders($item->sideMenu);
/*
* Sort side menu items
*/
uasort($item->sideMenu, static function ($a, $b) {
return $a->order - $b->order;
});
/*
* Filter items user lacks permission for
*/
$item->sideMenu = $this->filterItemPermissions($user, $item->sideMenu);
}
}
/**
* Apply incremental default orders to items with the explicit auto-order value (-1)
* or that have invalid order values (non-integer).
*
* @param array $items Array of MainMenuItem, SideMenuItem, or QuickActionItem objects
* @return void
*/
protected function applyDefaultOrders(array $items)
{
$orderCount = 0;
foreach ($items as $item) {
if ($item->order !== -1 && is_integer($item->order)) {
continue;
}
$item->order = ($orderCount += 100);
}
}
/**
* Registers a callback function that defines menu items.
* The callback function should register menu items by calling the manager's
* `registerMenuItems` method. The manager instance is passed to the callback
* function as an argument. Usage:
*
* BackendMenu::registerCallback(function ($manager) {
* $manager->registerMenuItems([...]);
* });
*
* @param callable $callback A callable function.
*/
public function registerCallback(callable $callback)
{
$this->callbacks[] = $callback;
}
/**
* Registers the back-end menu items.
* The argument is an array of the main menu items. The array keys represent the
* menu item codes, specific for the plugin/module. Each element in the
* array should be an associative array with the following keys:
* - label - specifies the menu label localization string key, required.
* - icon - an icon name from the Font Awesome icon collection, required.
* - url - the back-end relative URL the menu item should point to, required.
* - permissions - an array of permissions the back-end user should have, optional.
* The item will be displayed if the user has any of the specified permissions.
* - order - a position of the item in the menu, optional.
* - counter - an optional numeric value to output near the menu icon. The value should be
* a number or a callable returning a number.
* - counterLabel - an optional string value to describe the numeric reference in counter.
* - sideMenu - an array of side menu items, optional. If provided, the array items
* should represent the side menu item code, and each value should be an associative
* array with the following keys:
* - label - specifies the menu label localization string key, required.
* - icon - an icon name from the Font Awesome icon collection, required.
* - url - the back-end relative URL the menu item should point to, required.
* - attributes - an array of attributes and values to apply to the menu item, optional.
* - permissions - an array of permissions the back-end user should have, optional.
* - counter - an optional numeric value to output near the menu icon. The value should be
* a number or a callable returning a number.
* - counterLabel - an optional string value to describe the numeric reference in counter.
* - badge - an optional string value to output near the menu icon. The value should be
* a string. This value will override the counter if set.
* @param string $owner Specifies the menu items owner plugin or module in the format Author.Plugin.
* @param array $definitions An array of the menu item definitions.
* @throws SystemException
*/
public function registerMenuItems($owner, array $definitions)
{
$validator = Validator::make($definitions, [
'*.label' => 'required',
'*.icon' => 'required_without:*.iconSvg',
'*.url' => 'required',
'*.sideMenu.*.label' => 'nullable|required',
'*.sideMenu.*.icon' => 'nullable|required_without:*.sideMenu.*.iconSvg',
'*.sideMenu.*.url' => 'nullable|required',
]);
if ($validator->fails()) {
$errorMessage = 'Invalid menu item detected in ' . $owner . '. Contact the plugin author to fix (' . $validator->errors()->first() . ')';
if (Config::get('app.debug', false)) {
throw new SystemException($errorMessage);
}
Log::error($errorMessage);
}
$this->addMainMenuItems($owner, $definitions);
}
/**
* Register an owner alias
*
* @param string $owner The owner to register an alias for. Example: Real.Owner
* @param string $alias The alias to register. Example: Aliased.Owner
* @return void
*/
public function registerOwnerAlias(string $owner, string $alias)
{
$this->aliases[strtoupper($alias)] = strtoupper($owner);
}
/**
* Dynamically add an array of main menu items
* @param string $owner
* @param array $definitions
*/
public function addMainMenuItems($owner, array $definitions)
{
foreach ($definitions as $code => $definition) {
$this->addMainMenuItem($owner, $code, $definition);
}
}
/**
* Dynamically add a single main menu item
* @param string $owner
* @param string $code
* @param array $definition
*/
public function addMainMenuItem($owner, $code, array $definition)
{
$itemKey = $this->makeItemKey($owner, $code);
if (isset($this->items[$itemKey])) {
$definition = array_merge((array) $this->items[$itemKey], $definition);
}
$item = array_merge($definition, [
'code' => $code,
'owner' => $owner
]);
$this->items[$itemKey] = MainMenuItem::createFromArray($item);
if (array_key_exists('sideMenu', $item)) {
$this->addSideMenuItems($owner, $code, $item['sideMenu']);
}
}
/**
* @param string $owner
* @param string $code
* @return MainMenuItem
* @throws SystemException
*/
public function getMainMenuItem(string $owner, string $code)
{
$itemKey = $this->makeItemKey($owner, $code);
if (!array_key_exists($itemKey, $this->items)) {
throw new SystemException('No main menu item found with key ' . $itemKey);
}
return $this->items[$itemKey];
}
/**
* Removes a single main menu item
* @param $owner
* @param $code
*/
public function removeMainMenuItem($owner, $code)
{
$itemKey = $this->makeItemKey($owner, $code);
unset($this->items[$itemKey]);
}
/**
* Dynamically add an array of side menu items
* @param string $owner
* @param string $code
* @param array $definitions
*/
public function addSideMenuItems($owner, $code, array $definitions)
{
foreach ($definitions as $sideCode => $definition) {
$this->addSideMenuItem($owner, $code, $sideCode, (array) $definition);
}
}
/**
* Dynamically add a single side menu item
* @param string $owner
* @param string $code
* @param string $sideCode
* @param array $definition
* @return bool
*/
public function addSideMenuItem($owner, $code, $sideCode, array $definition)
{
$itemKey = $this->makeItemKey($owner, $code);
if (!isset($this->items[$itemKey])) {
return false;
}
$mainItem = $this->items[$itemKey];
$definition = array_merge($definition, [
'code' => $sideCode,
'owner' => $owner
]);
if (isset($mainItem->sideMenu[$sideCode])) {
$definition = array_merge((array) $mainItem->sideMenu[$sideCode], $definition);
}
$item = SideMenuItem::createFromArray($definition);
$this->items[$itemKey]->addSideMenuItem($item);
return true;
}
/**
* Remove multiple side menu items
*
* @param string $owner
* @param string $code
* @param array $sideCodes
* @return void
*/
public function removeSideMenuItems($owner, $code, $sideCodes)
{
foreach ($sideCodes as $sideCode) {
$this->removeSideMenuItem($owner, $code, $sideCode);
}
}
/**
* Removes a single main menu item
* @param string $owner
* @param string $code
* @param string $sideCode
* @return bool
*/
public function removeSideMenuItem($owner, $code, $sideCode)
{
$itemKey = $this->makeItemKey($owner, $code);
if (!isset($this->items[$itemKey])) {
return false;
}
$mainItem = $this->items[$itemKey];
$mainItem->removeSideMenuItem($sideCode);
return true;
}
/**
* Returns a list of the main menu items.
* @return array
* @throws SystemException
*/
public function listMainMenuItems()
{
if ($this->items === null && $this->quickActions === null) {
$this->loadItems();
}
if ($this->items === null) {
return [];
}
foreach ($this->items as $item) {
if ($item->badge) {
$item->counter = (string) $item->badge;
continue;
}
if ($item->counter === false) {
continue;
}
if ($item->counter !== null && is_callable($item->counter)) {
$item->counter = call_user_func($item->counter, $item);
} elseif (!empty((int) $item->counter)) {
$item->counter = (int) $item->counter;
} elseif (!empty($sideItems = $this->listSideMenuItems($item->owner, $item->code))) {
$item->counter = 0;
foreach ($sideItems as $sideItem) {
if ($sideItem->badge) {
continue;
}
$item->counter += $sideItem->counter;
}
}
if (empty($item->counter) || !is_numeric($item->counter)) {
$item->counter = null;
}
}
return $this->items;
}
/**
* Returns a list of side menu items for the currently active main menu item.
* The currently active main menu item is set with the setContext methods.
* @param null $owner
* @param null $code
* @return SideMenuItem[]
* @throws SystemException
*/
public function listSideMenuItems($owner = null, $code = null)
{
$activeItem = null;
if ($owner !== null && $code !== null) {
$activeItem = @$this->items[$this->makeItemKey($owner, $code)];
} else {
foreach ($this->listMainMenuItems() as $item) {
if ($this->isMainMenuItemActive($item)) {
$activeItem = $item;
break;
}
}
}
if (!$activeItem) {
return [];
}
$items = $activeItem->sideMenu;
foreach ($items as $item) {
if ($item->badge) {
$item->counter = (string) $item->badge;
continue;
}
if ($item->counter !== null && is_callable($item->counter)) {
$item->counter = call_user_func($item->counter, $item);
if (empty($item->counter)) {
$item->counter = null;
}
}
if (!is_null($item->counter) && !is_numeric($item->counter)) {
throw new SystemException("The menu item {$activeItem->code}.{$item->code}'s counter property is invalid. Check to make sure it's numeric or callable. Value: " . var_export($item->counter, true));
}
}
return $items;
}
/**
* Registers quick actions in the main navigation.
*
* Quick actions are single purpose links displayed to the left of the user menu in the
* backend main navigation.
*
* The argument is an array of the quick action items. The array keys represent the
* quick action item codes, specific for the plugin/module. Each element in the
* array should be an associative array with the following keys:
* - label - specifies the action label localization string key, used as a tooltip, required.
* - icon - an icon name from the Font Awesome icon collection, required if iconSvg is unspecified.
* - iconSvg - a custom SVG icon to use for the icon, required if icon is unspecified.
* - url - the back-end relative URL the quick action item should point to, required.
* - permissions - an array of permissions the back-end user should have, optional.
* The item will be displayed if the user has any of the specified permissions.
* - order - a position of the item in the menu, optional.
*
* @param string $owner Specifies the quick action items owner plugin or module in the format Author.Plugin.
* @param array $definitions An array of the quick action item definitions.
* @return void
* @throws SystemException If the validation of the quick action configuration fails
*/
public function registerQuickActions($owner, array $definitions)
{
$validator = Validator::make($definitions, [
'*.label' => 'required',
'*.icon' => 'required_without:*.iconSvg',
'*.url' => 'required'
]);
if ($validator->fails()) {
$errorMessage = 'Invalid quick action item detected in ' . $owner . '. Contact the plugin author to fix (' . $validator->errors()->first() . ')';
if (Config::get('app.debug', false)) {
throw new SystemException($errorMessage);
}
Log::error($errorMessage);
}
$this->addQuickActionItems($owner, $definitions);
}
/**
* Dynamically add an array of quick action items
*
* @param string $owner
* @param array $definitions
* @return void
*/
public function addQuickActionItems($owner, array $definitions)
{
foreach ($definitions as $code => $definition) {
$this->addQuickActionItem($owner, $code, $definition);
}
}
/**
* Dynamically add a single quick action item
*
* @param string $owner
* @param string $code
* @param array $definition
* @return void
*/
public function addQuickActionItem($owner, $code, array $definition)
{
$itemKey = $this->makeItemKey($owner, $code);
if (isset($this->quickActions[$itemKey])) {
$definition = array_merge((array) $this->quickActions[$itemKey], $definition);
}
$item = array_merge($definition, [
'code' => $code,
'owner' => $owner
]);
$this->quickActions[$itemKey] = QuickActionItem::createFromArray($item);
}
/**
* Gets the instance of a specified quick action item.
*
* @param string $owner
* @param string $code
* @return QuickActionItem
* @throws SystemException
*/
public function getQuickActionItem(string $owner, string $code)
{
$itemKey = $this->makeItemKey($owner, $code);
if (!array_key_exists($itemKey, $this->quickActions)) {
throw new SystemException('No quick action item found with key ' . $itemKey);
}
return $this->quickActions[$itemKey];
}
/**
* Removes a single quick action item
*
* @param $owner
* @param $code
* @return void
*/
public function removeQuickActionItem($owner, $code)
{
$itemKey = $this->makeItemKey($owner, $code);
unset($this->quickActions[$itemKey]);
}
/**
* Returns a list of quick action items.
*
* @return array
* @throws SystemException
*/
public function listQuickActionItems()
{
if ($this->items === null && $this->quickActions === null) {
$this->loadItems();
}
if ($this->quickActions === null) {
return [];
}
return $this->quickActions;
}
/**
* Sets the navigation context.
* The function sets the navigation owner, main menu item code and the side menu item code.
* @param string $owner Specifies the navigation owner in the format Vendor/Module
* @param string $mainMenuItemCode Specifies the main menu item code
* @param string $sideMenuItemCode Specifies the side menu item code
*/
public function setContext($owner, $mainMenuItemCode, $sideMenuItemCode = null)
{
$this->setContextOwner($owner);
$this->setContextMainMenu($mainMenuItemCode);
$this->setContextSideMenu($sideMenuItemCode);
}
/**
* Sets the navigation context owner.
*
* @param string $owner Specifies the navigation owner in the format Vendor/Module
*/
public function setContextOwner($owner)
{
$this->contextOwner = strtoupper($owner);
}
/**
* Gets the navigation context owner
*/
public function getContextOwner()
{
return $this->aliases[$this->contextOwner] ?? $this->contextOwner;
}
/**
* Specifies a code of the main menu item in the current navigation context.
* @param string $mainMenuItemCode Specifies the main menu item code
*/
public function setContextMainMenu($mainMenuItemCode)
{
$this->contextMainMenuItemCode = $mainMenuItemCode;
}
/**
* Returns information about the current navigation context.
* @return mixed Returns an object with the following fields:
* - mainMenuCode
* - sideMenuCode
* - owner
*/
public function getContext()
{
return (object)[
'mainMenuCode' => $this->contextMainMenuItemCode,
'sideMenuCode' => $this->contextSideMenuItemCode,
'owner' => $this->getContextOwner(),
];
}
/**
* Specifies a code of the side menu item in the current navigation context.
* If the code is set to TRUE, the first item will be flagged as active.
* @param string $sideMenuItemCode Specifies the side menu item code
*/
public function setContextSideMenu($sideMenuItemCode)
{
$this->contextSideMenuItemCode = $sideMenuItemCode;
}
/**
* Determines if a main menu item is active.
* @param MainMenuItem $item Specifies the item object.
* @return boolean Returns true if the menu item is active.
*/
public function isMainMenuItemActive($item)
{
return $this->getContextOwner() === strtoupper($item->owner) && $this->contextMainMenuItemCode === $item->code;
}
/**
* Returns the currently active main menu item
* @return null|MainMenuItem $item Returns the item object or null.
* @throws SystemException
*/
public function getActiveMainMenuItem()
{
foreach ($this->listMainMenuItems() as $item) {
if ($this->isMainMenuItemActive($item)) {
return $item;
}
}
return null;
}
/**
* Determines if a side menu item is active.
* @param SideMenuItem $item Specifies the item object.
* @return boolean Returns true if the side item is active.
*/
public function isSideMenuItemActive($item)
{
if ($this->contextSideMenuItemCode === true) {
$this->contextSideMenuItemCode = null;
return true;
}
return $this->getContextOwner() === strtoupper($item->owner) && $this->contextSideMenuItemCode === $item->code;
}
/**
* Registers a special side navigation partial for a specific main menu.
* The sidenav partial replaces the standard side navigation.
* @param string $owner Specifies the navigation owner in the format Vendor/Module.
* @param string $mainMenuItemCode Specifies the main menu item code.
* @param string $partial Specifies the partial name.
*/
public function registerContextSidenavPartial($owner, $mainMenuItemCode, $partial)
{
$this->contextSidenavPartials[$this->makeItemKey($owner, $mainMenuItemCode)] = $partial;
}
/**
* Returns the side navigation partial for a specific main menu previously registered
* with the registerContextSidenavPartial() method.
*
* @param string $owner Specifies the navigation owner in the format Vendor/Module.
* @param string $mainMenuItemCode Specifies the main menu item code.
* @return mixed Returns the partial name or null.
*/
public function getContextSidenavPartial($owner, $mainMenuItemCode)
{
return $this->contextSidenavPartials[$this->makeItemKey($owner, $mainMenuItemCode)] ?? null;
}
/**
* Removes menu items from an array if the supplied user lacks permission.
* @param \Backend\Models\User $user A user object
* @param MainMenuItem[]|SideMenuItem[] $items A collection of menu items
* @return array The filtered menu items
*/
protected function filterItemPermissions($user, array $items)
{
if (!$user) {
return $items;
}
$items = array_filter($items, static function ($item) use ($user) {
if (!$item->permissions || !count($item->permissions)) {
return true;
}
return $user->hasAnyAccess($item->permissions);
});
return $items;
}
/**
* Internal method to make a unique key for an item.
* @param string $owner
* @param string $code
* @return string
*/
protected function makeItemKey($owner, $code)
{
$owner = strtoupper($owner);
return ($this->aliases[$owner] ?? $owner) . '.' . strtoupper($code);
}
}