Chaos Stop the madness!

How to change Backend Controller Menu Items in OctoberCMS

This will cover how to change the navigation context (i.e. the currently active menu item) for a given controller in the OctoberCMS backend. For the purposes of this post, we will be moving the Backend User management from its current location under the Settings menu into its own top level navigation item.

In October, navigation items are registered by hooking into the Backend\Classes\NavigationManager singleton and calling registerMenuItems(string $owner, array $items). For built in navigation items, each Module (Backend, System, & CMS) will do this by utilizing the BackendMenu facade to registerCallback(callable $callback) that setup that module’s navigation items.

Additionally, the System module will auto register all navigation items defined by all plugins that return them from the Author\Plugin\Plugin::registerNavigation() : array method. (see docs). This is the method primarily used by plugin authors to register their navigation items.

Once the navigation items are registered, a given controller must set their context within all registered navigation items. This is achieved by accessing the NavigationManager singleton through the BackendMenu facade once again and calling the ::setContext(string $owner, string $mainItem, string $sideItem = null) method. After setting the context, when the backend view is rendered, the selected navigation context will be displayed, placing the controller in the correct context.

However, because the NavigationManager is a singleton, and because the navigation context is usually set in the constructor of the controller in question, we can override the selected navigation context with our own code as long as our code runs after the original controller sets the context. For this example, I will be moving the backend users management out of the Settings context and into it’s own top level navigation item.

To do this, we will first need to register our own top level menu item that we can then set the context to. Keeping things simple, we toss this into a plugin with the following code:

/**
 * Registers back-end navigation items for this plugin.
 *
 * @return array
 */
public function registerNavigation()
{
    return [
        'users' => [
            // We utilize the same label lang key used by the original side item within the Settings main item
            'label' => 'backend::lang.user.menu_label',

            // We set the URL to point to the controller we want to affect
            'url' => Backend::url('backend/users'),

            // We set the icon to the same icon used by the original side item within the Settings main item
            'icon' => 'icon-user',
            // We require the same permissions that the controller in question requires to ensure displaying to privileged users only
            'permissions' => ['backend.manage_users'],
        ],
    ];
}

Now that we’ve registered the navigation item that we’re going to use, it’s time to override the navigation context for our chosen controller:

public function boot()
{
    // Hook into the backend.page.beforeDisplay since that event will be run  right before the view is rendered which makes it perfect for our use case
    Event::listen('backend.page.beforeDisplay', function ($controller, $action, $params) {
        // We only want our changes to affect our chosen controller
        if (!($controller instanceof UsersController)) {
            return;
        }
        // We now override the existing navigation context by setting it once again and pointing to our navigation item that we created earlier
        BackendMenu::setContext('LukeTowers.MenuChangerExample', 'users');
    });
}

And boom! Just like that you’ve “moved” the Backend users management out of Settings and into your own custom top level navigation item.

There is still one more thing to take care of now however. Since the controller in question is actually initially added to the NavigationManager through the use of the System\Classes\SettingsManager by registering the controller as a Setting Item (very similar in overall structure to navigation items), and then the SettingsManager rendering it in the custom sidenav partial used by the Settings top level navigation item, doing the above will not remove the original navigation item of the Users controller from the navigation system. To finish this up, we will need to remove the original Settings item for the Users controller with the below code, which will again go in the boot method of our plugin:

SettingsManager::instance()->registerCallback(function ($manager) {
    $manager->removeSettingItem('October.System', 'administrators');
});

Note: If you were wanting to remove a main or side navigation item, you would use the same as above, but on the NavigationManager and the methods are removeMainMenuItem($owner, $code) and removeSideMenuItem($owner, $code, $sideCode).

Congratulations! You’ve now successfully “moved” a controller to a different navigation context within the OctoberCMS backend!

The code in this post is available on GitHub at luketowers/oc-menuchangerexample-plugin

Get in touch today!